From 11f127c1a3820d8df5d5e5b878b0198dd9a55850 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 30 Sep 2021 19:32:25 +0200 Subject: [PATCH 0001/1038] Bump version to 2021.11.0dev0 (#56835) --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index f0aec3aaa79..ccb1fd02986 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Final MAJOR_VERSION: Final = 2021 -MINOR_VERSION: Final = 10 +MINOR_VERSION: Final = 11 PATCH_VERSION: Final = "0.dev0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" From b00822f93ad6a55cbb398e2c960955f229b9b8b0 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 30 Sep 2021 16:32:17 -0400 Subject: [PATCH 0002/1038] Add strings for new zwave_js config flow keys (#56844) --- homeassistant/components/zwave_js/strings.json | 10 ++++++++-- .../components/zwave_js/translations/en.json | 13 +++++++++---- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json index 75c1ea76e9d..1446c1fc7aa 100644 --- a/homeassistant/components/zwave_js/strings.json +++ b/homeassistant/components/zwave_js/strings.json @@ -24,7 +24,10 @@ "title": "Enter the Z-Wave JS add-on configuration", "data": { "usb_path": "[%key:common::config_flow::data::usb_path%]", - "network_key": "Network Key" + "s0_legacy_key": "S0 Key (Legacy)", + "s2_authenticated_key": "S2 Authenticated Key", + "s2_unauthenticated_key": "S2 Unauthenticated Key", + "s2_access_control_key": "S2 Access Control Key" } }, "start_addon": { @@ -78,7 +81,10 @@ "title": "Enter the Z-Wave JS add-on configuration", "data": { "usb_path": "[%key:common::config_flow::data::usb_path%]", - "network_key": "Network Key", + "s0_legacy_key": "S0 Key (Legacy)", + "s2_authenticated_key": "S2 Authenticated Key", + "s2_unauthenticated_key": "S2 Unauthenticated Key", + "s2_access_control_key": "S2 Access Control Key", "log_level": "Log level", "emulate_hardware": "Emulate Hardware" } diff --git a/homeassistant/components/zwave_js/translations/en.json b/homeassistant/components/zwave_js/translations/en.json index abe37c4da04..b24d4f31b06 100644 --- a/homeassistant/components/zwave_js/translations/en.json +++ b/homeassistant/components/zwave_js/translations/en.json @@ -26,7 +26,10 @@ "step": { "configure_addon": { "data": { - "network_key": "Network Key", + "s0_legacy_key": "S0 Key (Legacy)", + "s2_access_control_key": "S2 Access Control Key", + "s2_authenticated_key": "S2 Authenticated Key", + "s2_unauthenticated_key": "S2 Unauthenticated Key", "usb_path": "USB Device Path" }, "title": "Enter the Z-Wave JS add-on configuration" @@ -108,7 +111,10 @@ "data": { "emulate_hardware": "Emulate Hardware", "log_level": "Log level", - "network_key": "Network Key", + "s0_legacy_key": "S0 Key (Legacy)", + "s2_access_control_key": "S2 Access Control Key", + "s2_authenticated_key": "S2 Authenticated Key", + "s2_unauthenticated_key": "S2 Unauthenticated Key", "usb_path": "USB Device Path" }, "title": "Enter the Z-Wave JS add-on configuration" @@ -132,6 +138,5 @@ "title": "The Z-Wave JS add-on is starting." } } - }, - "title": "Z-Wave JS" + } } \ No newline at end of file From efeee27be1bfdca42e15dfe20e8d47187fafe940 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Thu, 30 Sep 2021 23:04:09 +0200 Subject: [PATCH 0003/1038] Upgrade aionanoleaf to 0.0.2 (#56845) --- homeassistant/components/nanoleaf/light.py | 4 ---- homeassistant/components/nanoleaf/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py index f5537d3dc1c..5902beba226 100644 --- a/homeassistant/components/nanoleaf/light.py +++ b/homeassistant/components/nanoleaf/light.py @@ -1,7 +1,6 @@ """Support for Nanoleaf Lights.""" from __future__ import annotations -from aiohttp import ServerDisconnectedError from aionanoleaf import Nanoleaf, Unavailable import voluptuous as vol @@ -176,9 +175,6 @@ class NanoleafLight(LightEntity): """Fetch new state data for this light.""" try: await self._nanoleaf.get_info() - except ServerDisconnectedError: - # Retry the request once if the device disconnected - await self._nanoleaf.get_info() except Unavailable: self._attr_available = False return diff --git a/homeassistant/components/nanoleaf/manifest.json b/homeassistant/components/nanoleaf/manifest.json index 31576fd73a7..133257dc7fe 100644 --- a/homeassistant/components/nanoleaf/manifest.json +++ b/homeassistant/components/nanoleaf/manifest.json @@ -3,7 +3,7 @@ "name": "Nanoleaf", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/nanoleaf", - "requirements": ["aionanoleaf==0.0.1"], + "requirements": ["aionanoleaf==0.0.2"], "zeroconf": ["_nanoleafms._tcp.local.", "_nanoleafapi._tcp.local."], "homekit" : { "models": [ diff --git a/requirements_all.txt b/requirements_all.txt index fdb66ea982e..d77d32b2939 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -219,7 +219,7 @@ aiomodernforms==0.1.8 aiomusiccast==0.9.2 # homeassistant.components.nanoleaf -aionanoleaf==0.0.1 +aionanoleaf==0.0.2 # homeassistant.components.keyboard_remote aionotify==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 03556b3659d..e44eaba5719 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -146,7 +146,7 @@ aiomodernforms==0.1.8 aiomusiccast==0.9.2 # homeassistant.components.nanoleaf -aionanoleaf==0.0.1 +aionanoleaf==0.0.2 # homeassistant.components.notion aionotion==3.0.2 From dfb3a0c52847a8de325413bbf9f30377161e5a09 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 30 Sep 2021 23:11:00 +0200 Subject: [PATCH 0004/1038] Correct database migration to schema version 22 (#56848) --- homeassistant/components/recorder/migration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 0d40707d825..1ced8b73207 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -567,7 +567,7 @@ def _apply_update(instance, session, new_version, old_version): # noqa: C901 sum_statistics = get_metadata_with_session( instance.hass, session, None, statistic_type="sum" ) - for metadata_id in sum_statistics: + for metadata_id, _ in sum_statistics.values(): last_statistic = ( session.query(Statistics) .filter_by(metadata_id=metadata_id) From e757cb2ab457c2c4addb244b81193911420df2c4 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Thu, 30 Sep 2021 23:48:28 +0200 Subject: [PATCH 0005/1038] Strictly type Nanoleaf (#56852) --- .strict-typing | 1 + homeassistant/components/nanoleaf/light.py | 22 ++++++++++++---------- mypy.ini | 11 +++++++++++ 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/.strict-typing b/.strict-typing index b5be241ac00..50841c49f2f 100644 --- a/.strict-typing +++ b/.strict-typing @@ -68,6 +68,7 @@ homeassistant.components.media_player.* homeassistant.components.modbus.* homeassistant.components.mysensors.* homeassistant.components.nam.* +homeassistant.components.nanoleaf.* homeassistant.components.neato.* homeassistant.components.nest.* homeassistant.components.netatmo.* diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py index 5902beba226..15a4ed75e07 100644 --- a/homeassistant/components/nanoleaf/light.py +++ b/homeassistant/components/nanoleaf/light.py @@ -1,6 +1,8 @@ """Support for Nanoleaf Lights.""" from __future__ import annotations +from typing import Any + from aionanoleaf import Nanoleaf, Unavailable import voluptuous as vol @@ -79,19 +81,19 @@ class NanoleafLight(LightEntity): self._attr_max_mireds = 833 @property - def brightness(self): + def brightness(self) -> int: """Return the brightness of the light.""" return int(self._nanoleaf.brightness * 2.55) @property - def color_temp(self): + def color_temp(self) -> int: """Return the current color temperature.""" return color_util.color_temperature_kelvin_to_mired( self._nanoleaf.color_temperature ) @property - def effect(self): + def effect(self) -> str | None: """Return the current effect.""" # The API returns the *Solid* effect if the Nanoleaf is in HS or CT mode. # The effects *Static* and *Dynamic* are not supported by Home Assistant. @@ -102,27 +104,27 @@ class NanoleafLight(LightEntity): ) @property - def effect_list(self): + def effect_list(self) -> list[str]: """Return the list of supported effects.""" return self._nanoleaf.effects_list @property - def icon(self): + def icon(self) -> str: """Return the icon to use in the frontend, if any.""" return "mdi:triangle-outline" @property - def is_on(self): + def is_on(self) -> bool: """Return true if light is on.""" return self._nanoleaf.is_on @property - def hs_color(self): + def hs_color(self) -> tuple[int, int]: """Return the color in HS.""" return self._nanoleaf.hue, self._nanoleaf.saturation @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return ( SUPPORT_BRIGHTNESS @@ -132,7 +134,7 @@ class NanoleafLight(LightEntity): | SUPPORT_TRANSITION ) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" brightness = kwargs.get(ATTR_BRIGHTNESS) hs_color = kwargs.get(ATTR_HS_COLOR) @@ -166,7 +168,7 @@ class NanoleafLight(LightEntity): ) await self._nanoleaf.set_effect(effect) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" transition = kwargs.get(ATTR_TRANSITION) await self._nanoleaf.turn_off(transition) diff --git a/mypy.ini b/mypy.ini index a9c4ed4e3d5..afaf7dc6c21 100644 --- a/mypy.ini +++ b/mypy.ini @@ -759,6 +759,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.nanoleaf.*] +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.neato.*] check_untyped_defs = true disallow_incomplete_defs = true From 25b76964a5cb51b414bf1508b384f25738713605 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Fri, 1 Oct 2021 01:25:57 +0200 Subject: [PATCH 0006/1038] Add Device Info to Nanoleaf (#56856) --- homeassistant/components/nanoleaf/light.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py index 15a4ed75e07..1bc1e4a69b9 100644 --- a/homeassistant/components/nanoleaf/light.py +++ b/homeassistant/components/nanoleaf/light.py @@ -24,6 +24,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import color as color_util @@ -77,6 +78,13 @@ class NanoleafLight(LightEntity): self._nanoleaf = nanoleaf self._attr_unique_id = self._nanoleaf.serial_no self._attr_name = self._nanoleaf.name + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._nanoleaf.serial_no)}, + name=self._nanoleaf.name, + manufacturer=self._nanoleaf.manufacturer, + model=self._nanoleaf.model, + sw_version=self._nanoleaf.firmware_version, + ) self._attr_min_mireds = 154 self._attr_max_mireds = 833 From 7560c7b3de28b4cb7d567e3aa22f4a0fddd1d2a4 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 1 Oct 2021 07:24:55 +0200 Subject: [PATCH 0007/1038] Add long-term statistics support for rain sensors (#56847) --- homeassistant/components/netatmo/sensor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index a1f7b2ac079..8a4078d3d22 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -9,6 +9,7 @@ import pyatmo from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, SensorEntityDescription, ) @@ -143,6 +144,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( netatmo_name="Rain", entity_registry_enabled_default=True, native_unit_of_measurement=LENGTH_MILLIMETERS, + state_class=STATE_CLASS_MEASUREMENT, icon="mdi:weather-rainy", ), NetatmoSensorEntityDescription( @@ -151,6 +153,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( netatmo_name="sum_rain_1", entity_registry_enabled_default=False, native_unit_of_measurement=LENGTH_MILLIMETERS, + state_class=STATE_CLASS_TOTAL_INCREASING, icon="mdi:weather-rainy", ), NetatmoSensorEntityDescription( @@ -159,6 +162,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( netatmo_name="sum_rain_24", entity_registry_enabled_default=True, native_unit_of_measurement=LENGTH_MILLIMETERS, + state_class=STATE_CLASS_TOTAL_INCREASING, icon="mdi:weather-rainy", ), NetatmoSensorEntityDescription( From 3f5725c6eab9fb9a42f1bc447ded8ec088090c71 Mon Sep 17 00:00:00 2001 From: Ian Foster Date: Thu, 30 Sep 2021 22:25:28 -0700 Subject: [PATCH 0008/1038] removing excess variable (#56849) --- homeassistant/components/keyboard_remote/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/keyboard_remote/__init__.py b/homeassistant/components/keyboard_remote/__init__.py index 1c62dcb7575..a3b43013519 100644 --- a/homeassistant/components/keyboard_remote/__init__.py +++ b/homeassistant/components/keyboard_remote/__init__.py @@ -18,7 +18,6 @@ DEVICE_DESCRIPTOR = "device_descriptor" DEVICE_ID_GROUP = "Device description" DEVICE_NAME = "device_name" DOMAIN = "keyboard_remote" -VALUE = "value" ICON = "mdi:remote" From d4201eaa23aeb45f931a1c0881357953c8611fbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Fri, 1 Oct 2021 09:12:45 +0200 Subject: [PATCH 0009/1038] Opengarage bug fix (#56869) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Opengarage bug fix Signed-off-by: Daniel Hjelseth Høyer * Opengarage bug fix Signed-off-by: Daniel Hjelseth Høyer * Deprecated open garage config Signed-off-by: Daniel Hjelseth Høyer * Deprecated open garage config Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/opengarage/config_flow.py | 13 +++++++------ homeassistant/components/opengarage/cover.py | 9 +++++---- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/opengarage/config_flow.py b/homeassistant/components/opengarage/config_flow.py index 9121391b4e0..6ddc186cb9c 100644 --- a/homeassistant/components/opengarage/config_flow.py +++ b/homeassistant/components/opengarage/config_flow.py @@ -60,13 +60,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, import_info): """Set the config entry up from yaml.""" - import_info[CONF_HOST] = ( - f"{'https' if import_info[CONF_SSL] else 'http'}://" - f"{import_info.get(CONF_HOST)}" - ) - del import_info[CONF_SSL] - return await self.async_step_user(import_info) + user_input = { + CONF_DEVICE_KEY: import_info[CONF_DEVICE_KEY], + CONF_HOST: f"{'https' if import_info.get(CONF_SSL, False) else 'http'}://{import_info[CONF_HOST]}", + CONF_PORT: import_info.get(CONF_PORT, DEFAULT_PORT), + CONF_VERIFY_SSL: import_info.get(CONF_VERIFY_SSL, False), + } + return await self.async_step_user(user_input) async def async_step_user( self, user_input: dict[str, Any] | None = None diff --git a/homeassistant/components/opengarage/cover.py b/homeassistant/components/opengarage/cover.py index bf23d3286ad..12a1103f7df 100644 --- a/homeassistant/components/opengarage/cover.py +++ b/homeassistant/components/opengarage/cover.py @@ -50,15 +50,16 @@ COVER_SCHEMA = vol.Schema( ) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - vol.All( - cv.deprecated(DOMAIN), - {vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA)}, - ), + {vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA)} ) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the OpenGarage covers.""" + _LOGGER.warning( + "Open Garage YAML configuration is deprecated, " + "it has been imported into the UI automatically and can be safely removed" + ) devices = config.get(CONF_COVERS) for device_config in devices.values(): hass.async_create_task( From acda3afe63fd051d035192a3983d2d48ff35289c Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 1 Oct 2021 11:50:49 +0200 Subject: [PATCH 0010/1038] Fix check_control_message short description (#56876) --- homeassistant/components/bmw_connected_drive/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index a7fd72fc1a7..225ec5f7f99 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -122,7 +122,7 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity): if has_check_control_messages: cbs_list = [] for message in check_control_messages: - cbs_list.append(message["ccmDescriptionShort"]) + cbs_list.append(message.description_short) result["check_control_messages"] = cbs_list else: result["check_control_messages"] = "OK" From dc40de6b622175cbcc6d5a9a21b970a346aa453b Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 1 Oct 2021 12:11:06 +0200 Subject: [PATCH 0011/1038] Bump aioesphomeapi from 9.1.0 to 9.1.2 (#56879) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 857aebdc4dd..28371e89d8e 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==9.1.0"], + "requirements": ["aioesphomeapi==9.1.2"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], diff --git a/requirements_all.txt b/requirements_all.txt index d77d32b2939..dd2447b9bda 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -164,7 +164,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==9.1.0 +aioesphomeapi==9.1.2 # homeassistant.components.flo aioflo==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e44eaba5719..8ba707f471c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -109,7 +109,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==9.1.0 +aioesphomeapi==9.1.2 # homeassistant.components.flo aioflo==0.4.1 From 954bd49849f9f63c7bea5fb0ebbbb3593859ffa4 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 1 Oct 2021 15:21:43 +0200 Subject: [PATCH 0012/1038] Rename state to value_fn - picnic sensor (#56889) --- homeassistant/components/picnic/const.py | 28 +++++++++++------------ homeassistant/components/picnic/sensor.py | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/picnic/const.py b/homeassistant/components/picnic/const.py index 5aa21fd671b..228983d8189 100644 --- a/homeassistant/components/picnic/const.py +++ b/homeassistant/components/picnic/const.py @@ -42,7 +42,7 @@ class PicnicRequiredKeysMixin: """Mixin for required keys.""" data_type: Literal["cart_data", "slot_data", "last_order_data"] - state: Callable[[Any], StateType] + value_fn: Callable[[Any], StateType] @dataclass @@ -57,7 +57,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( key=SENSOR_CART_ITEMS_COUNT, icon="mdi:format-list-numbered", data_type="cart_data", - state=lambda cart: cart.get("total_count", 0), + value_fn=lambda cart: cart.get("total_count", 0), ), PicnicSensorEntityDescription( key=SENSOR_CART_TOTAL_PRICE, @@ -65,7 +65,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( icon="mdi:currency-eur", entity_registry_enabled_default=True, data_type="cart_data", - state=lambda cart: cart.get("total_price", 0) / 100, + value_fn=lambda cart: cart.get("total_price", 0) / 100, ), PicnicSensorEntityDescription( key=SENSOR_SELECTED_SLOT_START, @@ -73,7 +73,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( icon="mdi:calendar-start", entity_registry_enabled_default=True, data_type="slot_data", - state=lambda slot: slot.get("window_start"), + value_fn=lambda slot: slot.get("window_start"), ), PicnicSensorEntityDescription( key=SENSOR_SELECTED_SLOT_END, @@ -81,7 +81,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( icon="mdi:calendar-end", entity_registry_enabled_default=True, data_type="slot_data", - state=lambda slot: slot.get("window_end"), + value_fn=lambda slot: slot.get("window_end"), ), PicnicSensorEntityDescription( key=SENSOR_SELECTED_SLOT_MAX_ORDER_TIME, @@ -89,7 +89,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( icon="mdi:clock-alert-outline", entity_registry_enabled_default=True, data_type="slot_data", - state=lambda slot: slot.get("cut_off_time"), + value_fn=lambda slot: slot.get("cut_off_time"), ), PicnicSensorEntityDescription( key=SENSOR_SELECTED_SLOT_MIN_ORDER_VALUE, @@ -97,7 +97,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( icon="mdi:currency-eur", entity_registry_enabled_default=True, data_type="slot_data", - state=lambda slot: ( + value_fn=lambda slot: ( slot["minimum_order_value"] / 100 if slot.get("minimum_order_value") else None @@ -108,20 +108,20 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( device_class=DEVICE_CLASS_TIMESTAMP, icon="mdi:calendar-start", data_type="last_order_data", - state=lambda last_order: last_order.get("slot", {}).get("window_start"), + value_fn=lambda last_order: last_order.get("slot", {}).get("window_start"), ), PicnicSensorEntityDescription( key=SENSOR_LAST_ORDER_SLOT_END, device_class=DEVICE_CLASS_TIMESTAMP, icon="mdi:calendar-end", data_type="last_order_data", - state=lambda last_order: last_order.get("slot", {}).get("window_end"), + value_fn=lambda last_order: last_order.get("slot", {}).get("window_end"), ), PicnicSensorEntityDescription( key=SENSOR_LAST_ORDER_STATUS, icon="mdi:list-status", data_type="last_order_data", - state=lambda last_order: last_order.get("status"), + value_fn=lambda last_order: last_order.get("status"), ), PicnicSensorEntityDescription( key=SENSOR_LAST_ORDER_ETA_START, @@ -129,7 +129,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( icon="mdi:clock-start", entity_registry_enabled_default=True, data_type="last_order_data", - state=lambda last_order: last_order.get("eta", {}).get("start"), + value_fn=lambda last_order: last_order.get("eta", {}).get("start"), ), PicnicSensorEntityDescription( key=SENSOR_LAST_ORDER_ETA_END, @@ -137,7 +137,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( icon="mdi:clock-end", entity_registry_enabled_default=True, data_type="last_order_data", - state=lambda last_order: last_order.get("eta", {}).get("end"), + value_fn=lambda last_order: last_order.get("eta", {}).get("end"), ), PicnicSensorEntityDescription( key=SENSOR_LAST_ORDER_DELIVERY_TIME, @@ -145,13 +145,13 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = ( icon="mdi:timeline-clock", entity_registry_enabled_default=True, data_type="last_order_data", - state=lambda last_order: last_order.get("delivery_time", {}).get("start"), + value_fn=lambda last_order: last_order.get("delivery_time", {}).get("start"), ), PicnicSensorEntityDescription( key=SENSOR_LAST_ORDER_TOTAL_PRICE, native_unit_of_measurement=CURRENCY_EURO, icon="mdi:cash-marker", data_type="last_order_data", - state=lambda last_order: last_order.get("total_price", 0) / 100, + value_fn=lambda last_order: last_order.get("total_price", 0) / 100, ), ) diff --git a/homeassistant/components/picnic/sensor.py b/homeassistant/components/picnic/sensor.py index 34ad2943d8e..e0f0d943663 100644 --- a/homeassistant/components/picnic/sensor.py +++ b/homeassistant/components/picnic/sensor.py @@ -68,7 +68,7 @@ class PicnicSensor(SensorEntity, CoordinatorEntity): if self.coordinator.data is not None else {} ) - return self.entity_description.state(data_set) + return self.entity_description.value_fn(data_set) @property def available(self) -> bool: From 0916322a438b49e649904f5144ffa69c92f62504 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 1 Oct 2021 15:59:29 +0200 Subject: [PATCH 0013/1038] Deduplicate controls of UniFi services (#56834) * Fix left over comments from #56717 - no need to keep UNIFI_SERVICES if we control it is only called while UNIFI_DOMAIN is empty * Fix late comments as well * Improve service tests * mock.called_with was not reliable --- homeassistant/components/unifi/__init__.py | 6 +- homeassistant/components/unifi/services.py | 19 ++---- tests/components/unifi/test_controller.py | 3 +- tests/components/unifi/test_services.py | 69 ++++++++-------------- 4 files changed, 38 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 2394dfe92d8..03816c03df7 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -43,8 +43,10 @@ async def async_setup_entry(hass, config_entry): config_entry, unique_id=controller.site_id ) + if not hass.data[UNIFI_DOMAIN]: + async_setup_services(hass) + hass.data[UNIFI_DOMAIN][config_entry.entry_id] = controller - await async_setup_services(hass) config_entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, controller.shutdown) @@ -72,7 +74,7 @@ async def async_unload_entry(hass, config_entry): controller = hass.data[UNIFI_DOMAIN].pop(config_entry.entry_id) if not hass.data[UNIFI_DOMAIN]: - await async_unload_services(hass) + async_unload_services(hass) return await controller.async_reset() diff --git a/homeassistant/components/unifi/services.py b/homeassistant/components/unifi/services.py index dca95a764c3..2dce6f829b0 100644 --- a/homeassistant/components/unifi/services.py +++ b/homeassistant/components/unifi/services.py @@ -1,18 +1,15 @@ """UniFi services.""" -from .const import DOMAIN as UNIFI_DOMAIN +from homeassistant.core import callback -UNIFI_SERVICES = "unifi_services" +from .const import DOMAIN as UNIFI_DOMAIN SERVICE_REMOVE_CLIENTS = "remove_clients" -async def async_setup_services(hass) -> None: +@callback +def async_setup_services(hass) -> None: """Set up services for UniFi integration.""" - if hass.data.get(UNIFI_SERVICES, False): - return - - hass.data[UNIFI_SERVICES] = True async def async_call_unifi_service(service_call) -> None: """Call correct UniFi service.""" @@ -31,13 +28,9 @@ async def async_setup_services(hass) -> None: ) -async def async_unload_services(hass) -> None: +@callback +def async_unload_services(hass) -> None: """Unload UniFi services.""" - if not hass.data.get(UNIFI_SERVICES): - return - - hass.data[UNIFI_SERVICES] = False - hass.services.async_remove(UNIFI_DOMAIN, SERVICE_REMOVE_CLIENTS) diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index ec666ff27b9..02745dd60fb 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -166,6 +166,7 @@ async def setup_unifi_integration( known_wireless_clients=None, controllers=None, unique_id="1", + config_entry_id=DEFAULT_CONFIG_ENTRY_ID, ): """Create the UniFi controller.""" assert await async_setup_component(hass, UNIFI_DOMAIN, {}) @@ -175,7 +176,7 @@ async def setup_unifi_integration( data=deepcopy(config), options=deepcopy(options), unique_id=unique_id, - entry_id=DEFAULT_CONFIG_ENTRY_ID, + entry_id=config_entry_id, version=1, ) config_entry.add_to_hass(hass) diff --git a/tests/components/unifi/test_services.py b/tests/components/unifi/test_services.py index 388a33a4c64..d9989e8733a 100644 --- a/tests/components/unifi/test_services.py +++ b/tests/components/unifi/test_services.py @@ -1,58 +1,39 @@ """deCONZ service tests.""" -from unittest.mock import Mock, patch +from unittest.mock import patch from homeassistant.components.unifi.const import DOMAIN as UNIFI_DOMAIN -from homeassistant.components.unifi.services import ( - SERVICE_REMOVE_CLIENTS, - UNIFI_SERVICES, - async_setup_services, - async_unload_services, -) +from homeassistant.components.unifi.services import SERVICE_REMOVE_CLIENTS from .test_controller import setup_unifi_integration -async def test_service_setup(hass): +async def test_service_setup_and_unload(hass, aioclient_mock): """Verify service setup works.""" - assert UNIFI_SERVICES not in hass.data - with patch( - "homeassistant.core.ServiceRegistry.async_register", return_value=Mock(True) - ) as async_register: - await async_setup_services(hass) - assert hass.data[UNIFI_SERVICES] is True - assert async_register.call_count == 1 + config_entry = await setup_unifi_integration(hass, aioclient_mock) + assert hass.services.has_service(UNIFI_DOMAIN, SERVICE_REMOVE_CLIENTS) + + assert await hass.config_entries.async_unload(config_entry.entry_id) + assert not hass.services.has_service(UNIFI_DOMAIN, SERVICE_REMOVE_CLIENTS) -async def test_service_setup_already_registered(hass): - """Make sure that services are only registered once.""" - hass.data[UNIFI_SERVICES] = True - with patch( - "homeassistant.core.ServiceRegistry.async_register", return_value=Mock(True) - ) as async_register: - await async_setup_services(hass) - async_register.assert_not_called() +@patch("homeassistant.core.ServiceRegistry.async_remove") +@patch("homeassistant.core.ServiceRegistry.async_register") +async def test_service_setup_and_unload_not_called_if_multiple_integrations_detected( + register_service_mock, remove_service_mock, hass, aioclient_mock +): + """Make sure that services are only setup and removed once.""" + config_entry = await setup_unifi_integration(hass, aioclient_mock) + register_service_mock.reset_mock() + config_entry_2 = await setup_unifi_integration( + hass, aioclient_mock, config_entry_id=2 + ) + register_service_mock.assert_not_called() - -async def test_service_unload(hass): - """Verify service unload works.""" - hass.data[UNIFI_SERVICES] = True - with patch( - "homeassistant.core.ServiceRegistry.async_remove", return_value=Mock(True) - ) as async_remove: - await async_unload_services(hass) - assert hass.data[UNIFI_SERVICES] is False - assert async_remove.call_count == 1 - - -async def test_service_unload_not_registered(hass): - """Make sure that services can only be unloaded once.""" - with patch( - "homeassistant.core.ServiceRegistry.async_remove", return_value=Mock(True) - ) as async_remove: - await async_unload_services(hass) - assert UNIFI_SERVICES not in hass.data - async_remove.assert_not_called() + assert await hass.config_entries.async_unload(config_entry_2.entry_id) + remove_service_mock.assert_not_called() + assert await hass.config_entries.async_unload(config_entry.entry_id) + remove_service_mock.assert_called_once() async def test_remove_clients(hass, aioclient_mock): @@ -103,6 +84,8 @@ async def test_remove_clients(hass, aioclient_mock): "macs": ["00:00:00:00:00:01"], } + assert await hass.config_entries.async_unload(config_entry.entry_id) + async def test_remove_clients_controller_unavailable(hass, aioclient_mock): """Verify no call is made if controller is unavailable.""" From 735c9f8f0b7f4e31acecd21bb8dc027e3e883e44 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 1 Oct 2021 16:18:49 +0200 Subject: [PATCH 0014/1038] Revert fritz pref_disable_new_entities handling (#56891) --- homeassistant/components/fritz/common.py | 3 --- homeassistant/components/fritz/device_tracker.py | 9 ++------- homeassistant/components/fritz/switch.py | 13 +++---------- 3 files changed, 5 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index acb733709a3..0fb062af2d7 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -54,7 +54,6 @@ def _is_tracked(mac: str, current_devices: ValuesView) -> bool: def device_filter_out_from_trackers( mac: str, device: FritzDevice, - pref_disable_new_entities: bool, current_devices: ValuesView, ) -> bool: """Check if device should be filtered out from trackers.""" @@ -63,8 +62,6 @@ def device_filter_out_from_trackers( reason = "Missing IP" elif _is_tracked(mac, current_devices): reason = "Already tracked" - elif pref_disable_new_entities: - reason = "Disabled System Options" if reason: _LOGGER.debug( diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index f3134f32a27..9483d8163e0 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -80,9 +80,7 @@ async def async_setup_entry( @callback def update_router() -> None: """Update the values of the router.""" - _async_add_entities( - router, async_add_entities, data_fritz, entry.pref_disable_new_entities - ) + _async_add_entities(router, async_add_entities, data_fritz) entry.async_on_unload( async_dispatcher_connect(hass, router.signal_device_new, update_router) @@ -96,7 +94,6 @@ def _async_add_entities( router: FritzBoxTools, async_add_entities: AddEntitiesCallback, data_fritz: FritzData, - pref_disable_new_entities: bool, ) -> None: """Add new tracker entities from the router.""" @@ -105,9 +102,7 @@ def _async_add_entities( data_fritz.tracked[router.unique_id] = set() for mac, device in router.devices.items(): - if device_filter_out_from_trackers( - mac, device, pref_disable_new_entities, data_fritz.tracked.values() - ): + if device_filter_out_from_trackers(mac, device, data_fritz.tracked.values()): continue new_tracked.append(FritzBoxTracker(router, device)) diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index c337c568d18..a53d0867a3c 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -277,7 +277,6 @@ def wifi_entities_list( def profile_entities_list( router: FritzBoxTools, data_fritz: FritzData, - pref_disable_new_entities: bool, ) -> list[FritzBoxProfileSwitch]: """Add new tracker entities from the router.""" @@ -291,7 +290,7 @@ def profile_entities_list( for mac, device in router.devices.items(): if device_filter_out_from_trackers( - mac, device, pref_disable_new_entities, data_fritz.profile_switches.values() + mac, device, data_fritz.profile_switches.values() ): continue @@ -306,14 +305,13 @@ def all_entities_list( device_friendly_name: str, data_fritz: FritzData, local_ip: str, - pref_disable_new_entities: bool, ) -> list[Entity]: """Get a list of all entities.""" return [ *deflection_entities_list(fritzbox_tools, device_friendly_name), *port_entities_list(fritzbox_tools, device_friendly_name, local_ip), *wifi_entities_list(fritzbox_tools, device_friendly_name), - *profile_entities_list(fritzbox_tools, data_fritz, pref_disable_new_entities), + *profile_entities_list(fritzbox_tools, data_fritz), ] @@ -337,7 +335,6 @@ async def async_setup_entry( entry.title, data_fritz, local_ip, - entry.pref_disable_new_entities, ) async_add_entities(entities_list) @@ -345,11 +342,7 @@ async def async_setup_entry( @callback def update_router() -> None: """Update the values of the router.""" - async_add_entities( - profile_entities_list( - fritzbox_tools, data_fritz, entry.pref_disable_new_entities - ) - ) + async_add_entities(profile_entities_list(fritzbox_tools, data_fritz)) entry.async_on_unload( async_dispatcher_connect(hass, fritzbox_tools.signal_device_new, update_router) From ab037383ed1ac2ad4e7e501d9de4d94787511eaf Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 1 Oct 2021 16:42:42 +0200 Subject: [PATCH 0015/1038] Adjust state class of solarlog yield and consumption sensors (#56824) --- homeassistant/components/solarlog/const.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/solarlog/const.py b/homeassistant/components/solarlog/const.py index 3ee767f1513..0e9e5e8e5e0 100644 --- a/homeassistant/components/solarlog/const.py +++ b/homeassistant/components/solarlog/const.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + STATE_CLASS_TOTAL, SensorEntityDescription, ) from homeassistant.const import ( @@ -19,7 +19,6 @@ from homeassistant.const import ( PERCENTAGE, POWER_WATT, ) -from homeassistant.util import dt DOMAIN = "solarlog" @@ -102,7 +101,7 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( name="yield total", icon="mdi:solar-power", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=STATE_CLASS_TOTAL_INCREASING, + state_class=STATE_CLASS_TOTAL, factor=0.001, ), SolarLogSensorEntityDescription( @@ -145,8 +144,7 @@ SENSOR_TYPES: tuple[SolarLogSensorEntityDescription, ...] = ( name="consumption total", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, - state_class=STATE_CLASS_MEASUREMENT, - last_reset=dt.utc_from_timestamp(0), + state_class=STATE_CLASS_TOTAL, factor=0.001, ), SolarLogSensorEntityDescription( From 369412547a8f7b80444a5906dbff39aac8138694 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 1 Oct 2021 16:50:09 +0200 Subject: [PATCH 0016/1038] Use native unit of measurement in deCONZ sensors (#56897) --- homeassistant/components/deconz/sensor.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index e0e979ccf0b..b486f6b8677 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -70,13 +70,13 @@ ENTITY_DESCRIPTIONS = { key="battery", device_class=DEVICE_CLASS_BATTERY, state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, ), Consumption: SensorEntityDescription( key="consumption", device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), Daylight: SensorEntityDescription( key="daylight", @@ -87,30 +87,30 @@ ENTITY_DESCRIPTIONS = { key="humidity", device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, ), LightLevel: SensorEntityDescription( key="lightlevel", device_class=DEVICE_CLASS_ILLUMINANCE, - unit_of_measurement=LIGHT_LUX, + native_unit_of_measurement=LIGHT_LUX, ), Power: SensorEntityDescription( key="power", device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, ), Pressure: SensorEntityDescription( key="pressure", device_class=DEVICE_CLASS_PRESSURE, state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=PRESSURE_HPA, + native_unit_of_measurement=PRESSURE_HPA, ), Temperature: SensorEntityDescription( key="temperature", device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, - unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=TEMP_CELSIUS, ), } From e4f15c42e0fa7f837d4bf8d921ab1d428825b2e0 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Fri, 1 Oct 2021 11:08:04 -0400 Subject: [PATCH 0017/1038] Add kPa as a pressure unit (#56885) --- homeassistant/const.py | 1 + homeassistant/util/pressure.py | 3 +++ tests/util/test_pressure.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/homeassistant/const.py b/homeassistant/const.py index ccb1fd02986..2f408491d29 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -470,6 +470,7 @@ FREQUENCY_GIGAHERTZ: Final = "GHz" # Pressure units PRESSURE_PA: Final = "Pa" PRESSURE_HPA: Final = "hPa" +PRESSURE_KPA: Final = "kPa" PRESSURE_BAR: Final = "bar" PRESSURE_MBAR: Final = "mbar" PRESSURE_INHG: Final = "inHg" diff --git a/homeassistant/util/pressure.py b/homeassistant/util/pressure.py index 95b32a69643..53bbbffc01e 100644 --- a/homeassistant/util/pressure.py +++ b/homeassistant/util/pressure.py @@ -8,6 +8,7 @@ from homeassistant.const import ( PRESSURE_BAR, PRESSURE_HPA, PRESSURE_INHG, + PRESSURE_KPA, PRESSURE_MBAR, PRESSURE_PA, PRESSURE_PSI, @@ -17,6 +18,7 @@ from homeassistant.const import ( VALID_UNITS: tuple[str, ...] = ( PRESSURE_PA, PRESSURE_HPA, + PRESSURE_KPA, PRESSURE_BAR, PRESSURE_MBAR, PRESSURE_INHG, @@ -26,6 +28,7 @@ VALID_UNITS: tuple[str, ...] = ( UNIT_CONVERSION: dict[str, float] = { PRESSURE_PA: 1, PRESSURE_HPA: 1 / 100, + PRESSURE_KPA: 1 / 1000, PRESSURE_BAR: 1 / 100000, PRESSURE_MBAR: 1 / 100, PRESSURE_INHG: 1 / 3386.389, diff --git a/tests/util/test_pressure.py b/tests/util/test_pressure.py index d92cc32542f..d6211fa5343 100644 --- a/tests/util/test_pressure.py +++ b/tests/util/test_pressure.py @@ -4,6 +4,7 @@ import pytest from homeassistant.const import ( PRESSURE_HPA, PRESSURE_INHG, + PRESSURE_KPA, PRESSURE_MBAR, PRESSURE_PA, PRESSURE_PSI, @@ -20,6 +21,7 @@ def test_convert_same_unit(): assert pressure_util.convert(3, PRESSURE_HPA, PRESSURE_HPA) == 3 assert pressure_util.convert(4, PRESSURE_MBAR, PRESSURE_MBAR) == 4 assert pressure_util.convert(5, PRESSURE_INHG, PRESSURE_INHG) == 5 + assert pressure_util.convert(6, PRESSURE_KPA, PRESSURE_KPA) == 6 def test_convert_invalid_unit(): @@ -49,17 +51,43 @@ def test_convert_from_hpascals(): assert pressure_util.convert(hpascals, PRESSURE_HPA, PRESSURE_PA) == pytest.approx( 100000 ) + assert pressure_util.convert(hpascals, PRESSURE_HPA, PRESSURE_KPA) == pytest.approx( + 100 + ) assert pressure_util.convert( hpascals, PRESSURE_HPA, PRESSURE_MBAR ) == pytest.approx(1000) +def test_convert_from_kpascals(): + """Test conversion from hPA to other units.""" + kpascals = 100 + assert pressure_util.convert(kpascals, PRESSURE_KPA, PRESSURE_PSI) == pytest.approx( + 14.5037743897 + ) + assert pressure_util.convert( + kpascals, PRESSURE_KPA, PRESSURE_INHG + ) == pytest.approx(29.5299801647) + assert pressure_util.convert(kpascals, PRESSURE_KPA, PRESSURE_PA) == pytest.approx( + 100000 + ) + assert pressure_util.convert(kpascals, PRESSURE_KPA, PRESSURE_HPA) == pytest.approx( + 1000 + ) + assert pressure_util.convert( + kpascals, PRESSURE_KPA, PRESSURE_MBAR + ) == pytest.approx(1000) + + def test_convert_from_inhg(): """Test conversion from inHg to other units.""" inhg = 30 assert pressure_util.convert(inhg, PRESSURE_INHG, PRESSURE_PSI) == pytest.approx( 14.7346266155 ) + assert pressure_util.convert(inhg, PRESSURE_INHG, PRESSURE_KPA) == pytest.approx( + 101.59167 + ) assert pressure_util.convert(inhg, PRESSURE_INHG, PRESSURE_HPA) == pytest.approx( 1015.9167 ) From 061c33567398e580c1007581f84abc3264596b01 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 1 Oct 2021 17:08:43 +0200 Subject: [PATCH 0018/1038] Remove some redundant code from trace (#56883) --- homeassistant/components/trace/__init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/trace/__init__.py b/homeassistant/components/trace/__init__.py index c1113467661..0ecdb610698 100644 --- a/homeassistant/components/trace/__init__.py +++ b/homeassistant/components/trace/__init__.py @@ -109,8 +109,7 @@ class ActionTrace: "context": self.context, } ) - if self._error is not None: - result["error"] = str(self._error) + return result def as_short_dict(self) -> dict[str, Any]: @@ -135,7 +134,5 @@ class ActionTrace: } if self._error is not None: result["error"] = str(self._error) - if last_step is not None: - result["last_step"] = last_step return result From 1c1bb057d79133bc376941ed36fde04108c51565 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 1 Oct 2021 17:10:01 +0200 Subject: [PATCH 0019/1038] CLIPGenericFlag should be deCONZ sensor not binary sensor (#56901) --- .../components/deconz/binary_sensor.py | 2 -- homeassistant/components/deconz/sensor.py | 2 ++ tests/components/deconz/test_binary_sensor.py | 18 ++++++++++++++++-- tests/components/deconz/test_sensor.py | 18 ++++++++++++++++-- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 96de780c137..33b68f25cab 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -4,7 +4,6 @@ from pydeconz.sensor import ( CarbonMonoxide, Fire, GenericFlag, - GenericStatus, OpenClose, Presence, Vibration, @@ -36,7 +35,6 @@ DECONZ_BINARY_SENSORS = ( CarbonMonoxide, Fire, GenericFlag, - GenericStatus, OpenClose, Presence, Vibration, diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index b486f6b8677..8b82c2fa7bf 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -4,6 +4,7 @@ from pydeconz.sensor import ( Battery, Consumption, Daylight, + GenericStatus, Humidity, LightLevel, Power, @@ -52,6 +53,7 @@ DECONZ_SENSORS = ( AirQuality, Consumption, Daylight, + GenericStatus, Humidity, LightLevel, Power, diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 2a1b3c154f0..7f986ce4b81 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -181,6 +181,17 @@ async def test_allow_clip_sensor(hass, aioclient_mock): "config": {}, "uniqueid": "00:00:00:00:00:00:00:02-00", }, + "3": { + "config": {"on": True, "reachable": True}, + "etag": "fda064fca03f17389d0799d7cb1883ee", + "manufacturername": "Philips", + "modelid": "CLIPGenericFlag", + "name": "Clip Flag Boot Time", + "state": {"flag": True, "lastupdated": "2021-09-30T07:09:06.281"}, + "swversion": "1.0", + "type": "CLIPGenericFlag", + "uniqueid": "/sensors/3", + }, } } @@ -189,9 +200,10 @@ async def test_allow_clip_sensor(hass, aioclient_mock): hass, aioclient_mock, options={CONF_ALLOW_CLIP_SENSOR: True} ) - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_all()) == 3 assert hass.states.get("binary_sensor.presence_sensor").state == STATE_OFF assert hass.states.get("binary_sensor.clip_presence_sensor").state == STATE_OFF + assert hass.states.get("binary_sensor.clip_flag_boot_time").state == STATE_ON # Disallow clip sensors @@ -202,6 +214,7 @@ async def test_allow_clip_sensor(hass, aioclient_mock): assert len(hass.states.async_all()) == 1 assert not hass.states.get("binary_sensor.clip_presence_sensor") + assert not hass.states.get("binary_sensor.clip_flag_boot_time") # Allow clip sensors @@ -210,8 +223,9 @@ async def test_allow_clip_sensor(hass, aioclient_mock): ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_all()) == 3 assert hass.states.get("binary_sensor.clip_presence_sensor").state == STATE_OFF + assert hass.states.get("binary_sensor.clip_flag_boot_time").state == STATE_ON async def test_add_new_binary_sensor(hass, aioclient_mock, mock_deconz_websocket): diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 33f9c8c6a2c..624a1bec7ff 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -196,6 +196,17 @@ async def test_allow_clip_sensors(hass, aioclient_mock): "config": {"reachable": True}, "uniqueid": "00:00:00:00:00:00:00:01-00", }, + "3": { + "config": {"on": True, "reachable": True}, + "etag": "a5ed309124d9b7a21ef29fc278f2625e", + "manufacturername": "Philips", + "modelid": "CLIPGenericStatus", + "name": "CLIP Flur", + "state": {"lastupdated": "2021-10-01T10:23:06.779", "status": 0}, + "swversion": "1.0", + "type": "CLIPGenericStatus", + "uniqueid": "/sensors/3", + }, } } with patch.dict(DECONZ_WEB_REQUEST, data): @@ -205,8 +216,9 @@ async def test_allow_clip_sensors(hass, aioclient_mock): options={CONF_ALLOW_CLIP_SENSOR: True}, ) - assert len(hass.states.async_all()) == 3 + assert len(hass.states.async_all()) == 4 assert hass.states.get("sensor.clip_light_level_sensor").state == "999.8" + assert hass.states.get("sensor.clip_flur").state == "0" # Disallow clip sensors @@ -217,6 +229,7 @@ async def test_allow_clip_sensors(hass, aioclient_mock): assert len(hass.states.async_all()) == 2 assert not hass.states.get("sensor.clip_light_level_sensor") + assert not hass.states.get("sensor.clip_flur") # Allow clip sensors @@ -225,8 +238,9 @@ async def test_allow_clip_sensors(hass, aioclient_mock): ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 3 + assert len(hass.states.async_all()) == 4 assert hass.states.get("sensor.clip_light_level_sensor").state == "999.8" + assert hass.states.get("sensor.clip_flur").state == "0" async def test_add_new_sensor(hass, aioclient_mock, mock_deconz_websocket): From c0016415aad81bfb90b6aa7a19f5074f26188c5a Mon Sep 17 00:00:00 2001 From: Ricardo Steijn <61013287+RicArch97@users.noreply.github.com> Date: Fri, 1 Oct 2021 17:42:32 +0200 Subject: [PATCH 0020/1038] Handle missing serial extended parameters in crownstone (#56864) --- .../components/crownstone/helpers.py | 4 +- .../components/crownstone/test_config_flow.py | 41 +++++++++++++++---- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/crownstone/helpers.py b/homeassistant/components/crownstone/helpers.py index ad12c28d464..58b4dcdba47 100644 --- a/homeassistant/components/crownstone/helpers.py +++ b/homeassistant/components/crownstone/helpers.py @@ -31,8 +31,8 @@ def list_ports_as_str( port.serial_number, port.manufacturer, port.description, - f"{hex(port.vid)[2:]:0>4}".upper(), - f"{hex(port.pid)[2:]:0>4}".upper(), + f"{hex(port.vid)[2:]:0>4}".upper() if port.vid else None, + f"{hex(port.pid)[2:]:0>4}".upper() if port.pid else None, ) ) ports_as_string.append(MANUAL_PATH) diff --git a/tests/components/crownstone/test_config_flow.py b/tests/components/crownstone/test_config_flow.py index 7b05c8ba530..05fde6109e7 100644 --- a/tests/components/crownstone/test_config_flow.py +++ b/tests/components/crownstone/test_config_flow.py @@ -51,6 +51,16 @@ def usb_comports() -> MockFixture: yield comports_mock +@pytest.fixture(name="pyserial_comports_none_types") +def usb_comports_none_types() -> MockFixture: + """Mock pyserial comports.""" + with patch( + "serial.tools.list_ports.comports", + MagicMock(return_value=[get_mocked_com_port_none_types()]), + ) as comports_mock: + yield comports_mock + + @pytest.fixture(name="usb_path") def usb_path() -> MockFixture: """Mock usb serial path.""" @@ -104,6 +114,19 @@ def get_mocked_com_port(): return port +def get_mocked_com_port_none_types(): + """Mock of a serial port with NoneTypes.""" + port = ListPortInfo("/dev/ttyUSB1234") + port.device = "/dev/ttyUSB1234" + port.serial_number = None + port.manufacturer = None + port.description = "crownstone dongle - crownstone dongle" + port.vid = None + port.pid = None + + return port + + def create_mocked_entry_data_conf(email: str, password: str): """Set a result for the entry data for comparison.""" mock_data: dict[str, str | None] = {} @@ -262,7 +285,7 @@ async def test_successful_login_no_usb( async def test_successful_login_with_usb( crownstone_setup: MockFixture, - pyserial_comports: MockFixture, + pyserial_comports_none_types: MockFixture, usb_path: MockFixture, hass: HomeAssistant, ): @@ -282,17 +305,18 @@ async def test_successful_login_with_usb( # should show usb form assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "usb_config" - assert pyserial_comports.call_count == 1 + assert pyserial_comports_none_types.call_count == 1 - # create a mocked port - port = get_mocked_com_port() + # create a mocked port which should be in + # the list returned from list_ports_as_str, from .helpers + port = get_mocked_com_port_none_types() port_select = usb.human_readable_device_name( port.device, port.serial_number, port.manufacturer, port.description, - f"{hex(port.vid)[2:]:0>4}".upper(), - f"{hex(port.pid)[2:]:0>4}".upper(), + port.vid, + port.pid, ) # select a port from the list @@ -301,7 +325,7 @@ async def test_successful_login_with_usb( ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "usb_sphere_config" - assert pyserial_comports.call_count == 2 + assert pyserial_comports_none_types.call_count == 2 assert usb_path.call_count == 1 # select a sphere @@ -406,7 +430,8 @@ async def test_options_flow_setup_usb( assert result["step_id"] == "usb_config" assert pyserial_comports.call_count == 1 - # create a mocked port + # create a mocked port which should be in + # the list returned from list_ports_as_str, from .helpers port = get_mocked_com_port() port_select = usb.human_readable_device_name( port.device, From 451199338cf284fd9e6602a904169c43b7eeefab Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 1 Oct 2021 18:27:32 +0200 Subject: [PATCH 0021/1038] Fix bmw_connected_drive battery icon (#56884) --- homeassistant/components/bmw_connected_drive/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 76d183bf8e8..104a2eb78d9 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -513,6 +513,9 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): self._attr_entity_registry_enabled_default = attribute_info.get( attribute, [None, None, None, True] )[3] + self._attr_icon = self._attribute_info.get( + self._attribute, [None, None, None, None] + )[0] self._attr_device_class = attribute_info.get( attribute, [None, None, None, None] )[1] @@ -570,6 +573,3 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): self._attr_icon = icon_for_battery_level( battery_level=vehicle_state.charging_level_hv, charging=charging_state ) - self._attr_icon = self._attribute_info.get( - self._attribute, [None, None, None, None] - )[0] From cc97502a0c379b6ba4846251aaf7c15806cd6efa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 1 Oct 2021 19:27:44 +0300 Subject: [PATCH 0022/1038] Use HTTPStatus instead of HTTP_* consts in aiohttp web response statuses (#56541) --- homeassistant/components/auth/__init__.py | 7 +++---- homeassistant/components/auth/login_flow.py | 3 +-- homeassistant/components/calendar/__init__.py | 7 ++++--- homeassistant/components/config/zwave.py | 3 +-- homeassistant/components/doorbird/__init__.py | 3 ++- homeassistant/components/geofency/__init__.py | 7 +++++-- homeassistant/components/gpslogger/__init__.py | 13 ++++++------- homeassistant/components/hassio/addon_panel.py | 5 +++-- homeassistant/components/hassio/auth.py | 6 +++--- homeassistant/components/hassio/http.py | 4 ++-- homeassistant/components/http/view.py | 4 ++-- homeassistant/components/locative/__init__.py | 8 +++++--- homeassistant/components/mailbox/__init__.py | 6 +++--- homeassistant/components/media_player/__init__.py | 6 +++--- homeassistant/components/motioneye/__init__.py | 15 +++++---------- homeassistant/components/traccar/__init__.py | 8 ++++++-- homeassistant/components/tts/__init__.py | 3 +-- homeassistant/components/webhook/__init__.py | 8 ++++---- 18 files changed, 59 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index 49c18b4737a..4076cace2a2 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -134,7 +134,6 @@ from homeassistant.components.http.auth import async_sign_path from homeassistant.components.http.ban import log_invalid_auth from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView -from homeassistant.const import HTTP_OK from homeassistant.core import HomeAssistant, callback from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util @@ -274,15 +273,15 @@ class TokenView(HomeAssistantView): token = data.get("token") if token is None: - return web.Response(status=HTTP_OK) + 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=HTTP_OK) + return web.Response(status=HTTPStatus.OK) await hass.auth.async_remove_refresh_token(refresh_token) - return web.Response(status=HTTP_OK) + return web.Response(status=HTTPStatus.OK) async def _async_handle_auth_code(self, hass, data, remote_addr): """Handle authorization code request.""" diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index f15eeee2f16..7975e220acb 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -81,7 +81,6 @@ from homeassistant.components.http.ban import ( ) from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView -from homeassistant.const import HTTP_METHOD_NOT_ALLOWED from . import indieauth @@ -155,7 +154,7 @@ class LoginFlowIndexView(HomeAssistantView): async def get(self, request): """Do not allow index of flows in progress.""" # pylint: disable=no-self-use - return web.Response(status=HTTP_METHOD_NOT_ALLOWED) + return web.Response(status=HTTPStatus.METHOD_NOT_ALLOWED) @RequestDataValidator( vol.Schema( diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 8809e05d25b..10580339329 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import timedelta +from http import HTTPStatus import logging import re from typing import cast, final @@ -10,7 +11,7 @@ from aiohttp import web from homeassistant.components import http from homeassistant.config_entries import ConfigEntry -from homeassistant.const import HTTP_BAD_REQUEST, STATE_OFF, STATE_ON +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -200,12 +201,12 @@ class CalendarEventView(http.HomeAssistantView): start = request.query.get("start") end = request.query.get("end") if None in (start, end, entity): - return web.Response(status=HTTP_BAD_REQUEST) + return web.Response(status=HTTPStatus.BAD_REQUEST) try: start_date = dt.parse_datetime(start) end_date = dt.parse_datetime(end) except (ValueError, AttributeError): - return web.Response(status=HTTP_BAD_REQUEST) + return web.Response(status=HTTPStatus.BAD_REQUEST) event_list = await entity.async_get_events( request.app["hass"], start_date, end_date ) diff --git a/homeassistant/components/config/zwave.py b/homeassistant/components/config/zwave.py index f8a3ac0cd9f..6817b230fb8 100644 --- a/homeassistant/components/config/zwave.py +++ b/homeassistant/components/config/zwave.py @@ -7,7 +7,6 @@ from aiohttp.web import Response from homeassistant.components.http import HomeAssistantView from homeassistant.components.zwave import DEVICE_CONFIG_SCHEMA_ENTRY, const -from homeassistant.const import HTTP_BAD_REQUEST import homeassistant.core as ha import homeassistant.helpers.config_validation as cv @@ -52,7 +51,7 @@ class ZWaveLogView(HomeAssistantView): try: lines = int(request.query.get("lines", 0)) except ValueError: - return Response(text="Invalid datetime", status=HTTP_BAD_REQUEST) + return Response(text="Invalid datetime", status=HTTPStatus.BAD_REQUEST) hass = request.app["hass"] response = await hass.async_add_executor_job(self._get_log, hass, lines) diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index f1addbf477b..255b9e54636 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -1,4 +1,5 @@ """Support for DoorBird devices.""" +from http import HTTPStatus import logging from aiohttp import web @@ -332,7 +333,7 @@ class DoorBirdRequestView(HomeAssistantView): if device is None: return web.Response( - status=HTTP_UNAUTHORIZED, text="Invalid token provided." + status=HTTPStatus.UNAUTHORIZED, text="Invalid token provided." ) if device: diff --git a/homeassistant/components/geofency/__init__.py b/homeassistant/components/geofency/__init__.py index 6d604ec6e30..1e8e3eb1f04 100644 --- a/homeassistant/components/geofency/__init__.py +++ b/homeassistant/components/geofency/__init__.py @@ -1,4 +1,6 @@ """Support for Geofency.""" +from http import HTTPStatus + from aiohttp import web import voluptuous as vol @@ -8,7 +10,6 @@ from homeassistant.const import ( ATTR_LONGITUDE, ATTR_NAME, CONF_WEBHOOK_ID, - HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME, ) from homeassistant.helpers import config_entry_flow @@ -88,7 +89,9 @@ async def handle_webhook(hass, webhook_id, request): try: data = WEBHOOK_SCHEMA(dict(await request.post())) except vol.MultipleInvalid as error: - return web.Response(text=error.error_message, status=HTTP_UNPROCESSABLE_ENTITY) + return web.Response( + text=error.error_message, status=HTTPStatus.UNPROCESSABLE_ENTITY + ) if _is_mobile_beacon(data, hass.data[DOMAIN]["beacons"]): return _set_location(hass, data, None) diff --git a/homeassistant/components/gpslogger/__init__.py b/homeassistant/components/gpslogger/__init__.py index 0c475872093..715119448b5 100644 --- a/homeassistant/components/gpslogger/__init__.py +++ b/homeassistant/components/gpslogger/__init__.py @@ -1,4 +1,6 @@ """Support for GPSLogger.""" +from http import HTTPStatus + from aiohttp import web import voluptuous as vol @@ -6,12 +8,7 @@ from homeassistant.components.device_tracker import ( ATTR_BATTERY, DOMAIN as DEVICE_TRACKER, ) -from homeassistant.const import ( - ATTR_LATITUDE, - ATTR_LONGITUDE, - CONF_WEBHOOK_ID, - HTTP_UNPROCESSABLE_ENTITY, -) +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_WEBHOOK_ID from homeassistant.helpers import config_entry_flow import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -68,7 +65,9 @@ async def handle_webhook(hass, webhook_id, request): try: data = WEBHOOK_SCHEMA(dict(await request.post())) except vol.MultipleInvalid as error: - return web.Response(text=error.error_message, status=HTTP_UNPROCESSABLE_ENTITY) + return web.Response( + text=error.error_message, status=HTTPStatus.UNPROCESSABLE_ENTITY + ) attrs = { ATTR_SPEED: data.get(ATTR_SPEED), diff --git a/homeassistant/components/hassio/addon_panel.py b/homeassistant/components/hassio/addon_panel.py index d540479d779..d6240896c84 100644 --- a/homeassistant/components/hassio/addon_panel.py +++ b/homeassistant/components/hassio/addon_panel.py @@ -1,11 +1,12 @@ """Implement the Ingress Panel feature for Hass.io Add-ons.""" import asyncio +from http import HTTPStatus import logging from aiohttp import web from homeassistant.components.http import HomeAssistantView -from homeassistant.const import ATTR_ICON, HTTP_BAD_REQUEST +from homeassistant.const import ATTR_ICON from homeassistant.core import HomeAssistant from .const import ATTR_ADMIN, ATTR_ENABLE, ATTR_PANELS, ATTR_TITLE @@ -53,7 +54,7 @@ class HassIOAddonPanel(HomeAssistantView): # Panel exists for add-on slug if addon not in panels or not panels[addon][ATTR_ENABLE]: _LOGGER.error("Panel is not enable for %s", addon) - return web.Response(status=HTTP_BAD_REQUEST) + return web.Response(status=HTTPStatus.BAD_REQUEST) data = panels[addon] # Register panel diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index 78925ed73fc..2d76a758096 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -1,4 +1,5 @@ """Implement the auth feature from Hass.io for Add-ons.""" +from http import HTTPStatus from ipaddress import ip_address import logging import os @@ -12,7 +13,6 @@ from homeassistant.auth.providers import homeassistant as auth_ha from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.const import KEY_HASS_USER from homeassistant.components.http.data_validator import RequestDataValidator -from homeassistant.const import HTTP_OK from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -83,7 +83,7 @@ class HassIOAuth(HassIOBaseAuth): except auth_ha.InvalidAuth: raise HTTPNotFound() from None - return web.Response(status=HTTP_OK) + return web.Response(status=HTTPStatus.OK) class HassIOPasswordReset(HassIOBaseAuth): @@ -113,4 +113,4 @@ class HassIOPasswordReset(HassIOBaseAuth): except auth_ha.InvalidUser as err: raise HTTPNotFound() from err - return web.Response(status=HTTP_OK) + return web.Response(status=HTTPStatus.OK) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index fe01cbe3197..d25a65afee1 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from http import HTTPStatus import logging import os import re @@ -20,7 +21,6 @@ from aiohttp.web_exceptions import HTTPBadGateway from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView from homeassistant.components.onboarding import async_is_onboarded -from homeassistant.const import HTTP_UNAUTHORIZED from .const import X_HASS_IS_ADMIN, X_HASS_USER_ID, X_HASSIO @@ -73,7 +73,7 @@ class HassIOView(HomeAssistantView): """Route data to Hass.io.""" hass = request.app["hass"] if _need_auth(hass, path) and not request[KEY_AUTHENTICATED]: - return web.Response(status=HTTP_UNAUTHORIZED) + return web.Response(status=HTTPStatus.UNAUTHORIZED) return await self._command_proxy(path, request) diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 39225c918e5..adebf2bb46a 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -19,7 +19,7 @@ from aiohttp.web_urldispatcher import AbstractRoute import voluptuous as vol from homeassistant import exceptions -from homeassistant.const import CONTENT_TYPE_JSON, HTTP_OK, HTTP_SERVICE_UNAVAILABLE +from homeassistant.const import CONTENT_TYPE_JSON, HTTP_OK from homeassistant.core import Context, is_callback from homeassistant.helpers.json import JSONEncoder @@ -115,7 +115,7 @@ def request_handler_factory( async def handle(request: web.Request) -> web.StreamResponse: """Handle incoming request.""" if request.app[KEY_HASS].is_stopping: - return web.Response(status=HTTP_SERVICE_UNAVAILABLE) + return web.Response(status=HTTPStatus.SERVICE_UNAVAILABLE) authenticated = request.get(KEY_AUTHENTICATED, False) diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index 52cfc7900ba..548b0a2d1fe 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -1,6 +1,7 @@ """Support for Locative.""" from __future__ import annotations +from http import HTTPStatus import logging from aiohttp import web @@ -12,7 +13,6 @@ from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, CONF_WEBHOOK_ID, - HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME, ) from homeassistant.helpers import config_entry_flow @@ -68,7 +68,9 @@ async def handle_webhook(hass, webhook_id, request): try: data = WEBHOOK_SCHEMA(dict(await request.post())) except vol.MultipleInvalid as error: - return web.Response(text=error.error_message, status=HTTP_UNPROCESSABLE_ENTITY) + return web.Response( + text=error.error_message, status=HTTPStatus.UNPROCESSABLE_ENTITY + ) device = data[ATTR_DEVICE_ID] location_name = data.get(ATTR_ID, data[ATTR_TRIGGER]).lower() @@ -105,7 +107,7 @@ async def handle_webhook(hass, webhook_id, request): _LOGGER.error("Received unidentified message from Locative: %s", direction) return web.Response( text=f"Received unidentified message: {direction}", - status=HTTP_UNPROCESSABLE_ENTITY, + status=HTTPStatus.UNPROCESSABLE_ENTITY, ) diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index 307bd54195f..0c473367fe9 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -2,6 +2,7 @@ import asyncio from contextlib import suppress from datetime import timedelta +from http import HTTPStatus import logging from aiohttp import web @@ -9,7 +10,6 @@ from aiohttp.web_exceptions import HTTPNotFound import async_timeout from homeassistant.components.http import HomeAssistantView -from homeassistant.const import HTTP_INTERNAL_SERVER_ERROR from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery @@ -255,8 +255,8 @@ class MailboxMediaView(MailboxView): stream = await mailbox.async_get_media(msgid) except StreamError as err: _LOGGER.error("Error getting media: %s", err) - return web.Response(status=HTTP_INTERNAL_SERVER_ERROR) + return web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR) if stream: return web.Response(body=stream, content_type=mailbox.media_type) - return web.Response(status=HTTP_INTERNAL_SERVER_ERROR) + return web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 9ac350a5714..dbe7c0ef04d 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -9,6 +9,7 @@ from dataclasses import dataclass import datetime as dt import functools as ft import hashlib +from http import HTTPStatus import logging import secrets from typing import final @@ -30,7 +31,6 @@ from homeassistant.components.websocket_api.const import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - HTTP_INTERNAL_SERVER_ERROR, HTTP_NOT_FOUND, HTTP_OK, HTTP_UNAUTHORIZED, @@ -1045,7 +1045,7 @@ class MediaPlayerImageView(HomeAssistantView): ) if not authenticated: - return web.Response(status=HTTP_UNAUTHORIZED) + return web.Response(status=HTTPStatus.UNAUTHORIZED) if media_content_type and media_content_id: media_image_id = request.query.get("media_image_id") @@ -1056,7 +1056,7 @@ class MediaPlayerImageView(HomeAssistantView): data, content_type = await player.async_get_media_image() if data is None: - return web.Response(status=HTTP_INTERNAL_SERVER_ERROR) + return web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR) headers: LooseHeaders = {CACHE_CONTROL: "max-age=3600"} return web.Response(body=data, content_type=content_type, headers=headers) diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index 07385f24216..51f1e316a87 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable +from http import HTTPStatus import json import logging from types import MappingProxyType @@ -38,13 +39,7 @@ from homeassistant.components.webhook import ( async_unregister as webhook_unregister, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_DEVICE_ID, - ATTR_NAME, - CONF_URL, - CONF_WEBHOOK_ID, - HTTP_BAD_REQUEST, -) +from homeassistant.const import ATTR_DEVICE_ID, ATTR_NAME, CONF_URL, CONF_WEBHOOK_ID from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -412,14 +407,14 @@ async def handle_webhook( except (json.decoder.JSONDecodeError, UnicodeDecodeError): return Response( text="Could not decode request", - status=HTTP_BAD_REQUEST, + status=HTTPStatus.BAD_REQUEST, ) for key in (ATTR_DEVICE_ID, ATTR_EVENT_TYPE): if key not in data: return Response( text=f"Missing webhook parameter: {key}", - status=HTTP_BAD_REQUEST, + status=HTTPStatus.BAD_REQUEST, ) event_type = data[ATTR_EVENT_TYPE] @@ -430,7 +425,7 @@ async def handle_webhook( if not device: return Response( text=f"Device not found: {device_id}", - status=HTTP_BAD_REQUEST, + status=HTTPStatus.BAD_REQUEST, ) hass.bus.async_fire( diff --git a/homeassistant/components/traccar/__init__.py b/homeassistant/components/traccar/__init__.py index 916b0f71169..e2605cdfd54 100644 --- a/homeassistant/components/traccar/__init__.py +++ b/homeassistant/components/traccar/__init__.py @@ -1,9 +1,11 @@ """Support for Traccar.""" +from http import HTTPStatus + from aiohttp import web import voluptuous as vol from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER -from homeassistant.const import ATTR_ID, CONF_WEBHOOK_ID, HTTP_UNPROCESSABLE_ENTITY +from homeassistant.const import ATTR_ID, CONF_WEBHOOK_ID from homeassistant.helpers import config_entry_flow import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -61,7 +63,9 @@ async def handle_webhook(hass, webhook_id, request): try: data = WEBHOOK_SCHEMA(dict(request.query)) except vol.MultipleInvalid as error: - return web.Response(text=error.error_message, status=HTTP_UNPROCESSABLE_ENTITY) + return web.Response( + text=error.error_message, status=HTTPStatus.UNPROCESSABLE_ENTITY + ) attrs = { ATTR_ALTITUDE: data.get(ATTR_ALTITUDE), diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 59dfaf484b4..52b7c4aa034 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -30,7 +30,6 @@ from homeassistant.const import ( CONF_DESCRIPTION, CONF_NAME, CONF_PLATFORM, - HTTP_NOT_FOUND, PLATFORM_FORMAT, ) from homeassistant.core import HomeAssistant, callback @@ -641,7 +640,7 @@ class TextToSpeechView(HomeAssistantView): content, data = await self.tts.async_read_tts(filename) except HomeAssistantError as err: _LOGGER.error("Error on load tts: %s", err) - return web.Response(status=HTTP_NOT_FOUND) + return web.Response(status=HTTPStatus.NOT_FOUND) return web.Response(body=data, content_type=content) diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py index 656f12950ea..52778600147 100644 --- a/homeassistant/components/webhook/__init__.py +++ b/homeassistant/components/webhook/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Awaitable, Callable +from http import HTTPStatus import logging import secrets @@ -10,7 +11,6 @@ import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.components.http.view import HomeAssistantView -from homeassistant.const import HTTP_OK from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.network import get_url from homeassistant.loader import bind_hass @@ -99,16 +99,16 @@ async def async_handle_webhook(hass, webhook_id, request): # Limit to 64 chars to avoid flooding the log content = await request.content.read(64) _LOGGER.debug("%s", content) - return Response(status=HTTP_OK) + return Response(status=HTTPStatus.OK) try: response = await webhook["handler"](hass, webhook_id, request) if response is None: - response = Response(status=HTTP_OK) + response = Response(status=HTTPStatus.OK) return response except Exception: # pylint: disable=broad-except _LOGGER.exception("Error processing webhook %s", webhook_id) - return Response(status=HTTP_OK) + return Response(status=HTTPStatus.OK) async def async_setup(hass, config): From 15a8f6741bdf852b6f542be61aacb4896cd9e1ff Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Fri, 1 Oct 2021 09:35:44 -0700 Subject: [PATCH 0023/1038] Enable template icons for template numbers (#56154) --- homeassistant/components/template/number.py | 15 ++- tests/components/template/test_number.py | 138 ++++++++++++++++++-- 2 files changed, 142 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/template/number.py b/homeassistant/components/template/number.py index a7737c31246..86cc4886430 100644 --- a/homeassistant/components/template/number.py +++ b/homeassistant/components/template/number.py @@ -18,7 +18,13 @@ from homeassistant.components.number.const import ( DOMAIN as NUMBER_DOMAIN, ) from homeassistant.components.template import TriggerUpdateCoordinator -from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, CONF_UNIQUE_ID +from homeassistant.const import ( + CONF_ICON, + CONF_NAME, + CONF_OPTIMISTIC, + CONF_STATE, + CONF_UNIQUE_ID, +) from homeassistant.core import Config, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -47,6 +53,7 @@ NUMBER_SCHEMA = vol.Schema( vol.Optional(CONF_AVAILABILITY): cv.template, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_ICON): cv.template, } ) @@ -72,6 +79,7 @@ async def _async_create_entities( definition[ATTR_MAX], definition[CONF_OPTIMISTIC], unique_id, + definition.get(CONF_ICON), ) ) return entities @@ -119,9 +127,12 @@ class TemplateNumber(TemplateEntity, NumberEntity): maximum_template: Template | None, optimistic: bool, unique_id: str | None, + icon_template: Template | None, ) -> None: """Initialize the number.""" - super().__init__(availability_template=availability_template) + super().__init__( + availability_template=availability_template, icon_template=icon_template + ) self._attr_name = DEFAULT_NAME self._name_template = name_template name_template.hass = hass diff --git a/tests/components/template/test_number.py b/tests/components/template/test_number.py index f297307fd0e..1f317c06330 100644 --- a/tests/components/template/test_number.py +++ b/tests/components/template/test_number.py @@ -15,7 +15,7 @@ from homeassistant.components.number.const import ( DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE as NUMBER_SERVICE_SET_VALUE, ) -from homeassistant.const import CONF_ENTITY_ID, STATE_UNKNOWN +from homeassistant.const import ATTR_ICON, CONF_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import Context from homeassistant.helpers.entity_registry import async_get @@ -35,6 +35,17 @@ _MAXIMUM_INPUT_NUMBER = "input_number.maximum" # Represent for number's step _STEP_INPUT_NUMBER = "input_number.step" +# Config for `_VALUE_INPUT_NUMBER` +_VALUE_INPUT_NUMBER_CONFIG = { + "value": { + "min": 0.0, + "max": 100.0, + "name": "Value", + "step": 1.0, + "mode": "slider", + } +} + @pytest.fixture def calls(hass): @@ -128,20 +139,14 @@ async def test_all_optional_config(hass, calls): async def test_templates_with_entities(hass, calls): - """Test tempalates with values from other entities.""" + """Test templates with values from other entities.""" with assert_setup_component(4, "input_number"): assert await setup.async_setup_component( hass, "input_number", { "input_number": { - "value": { - "min": 0.0, - "max": 100.0, - "name": "Value", - "step": 1.0, - "mode": "slider", - }, + **_VALUE_INPUT_NUMBER_CONFIG, "step": { "min": 0.0, "max": 100.0, @@ -334,3 +339,118 @@ def _verify( assert attributes.get(ATTR_STEP) == float(expected_step) assert attributes.get(ATTR_MAX) == float(expected_maximum) assert attributes.get(ATTR_MIN) == float(expected_minimum) + + +async def test_icon_template(hass): + """Test template numbers with icon templates.""" + with assert_setup_component(1, "input_number"): + assert await setup.async_setup_component( + hass, + "input_number", + {"input_number": _VALUE_INPUT_NUMBER_CONFIG}, + ) + + with assert_setup_component(1, "template"): + assert await setup.async_setup_component( + hass, + "template", + { + "template": { + "unique_id": "b", + "number": { + "state": f"{{{{ states('{_VALUE_INPUT_NUMBER}') }}}}", + "step": 1, + "min": 0, + "max": 100, + "set_value": { + "service": "input_number.set_value", + "data_template": { + "entity_id": _VALUE_INPUT_NUMBER, + "value": "{{ value }}", + }, + }, + "icon": "{% if ((states.input_number.value.state or 0) | int) > 50 %}mdi:greater{% else %}mdi:less{% endif %}", + }, + } + }, + ) + + hass.states.async_set(_VALUE_INPUT_NUMBER, 49) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get(_TEST_NUMBER) + assert float(state.state) == 49 + assert state.attributes[ATTR_ICON] == "mdi:less" + + await hass.services.async_call( + INPUT_NUMBER_DOMAIN, + INPUT_NUMBER_SERVICE_SET_VALUE, + {CONF_ENTITY_ID: _VALUE_INPUT_NUMBER, INPUT_NUMBER_ATTR_VALUE: 51}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(_TEST_NUMBER) + assert float(state.state) == 51 + assert state.attributes[ATTR_ICON] == "mdi:greater" + + +async def test_icon_template_with_trigger(hass): + """Test template numbers with icon templates.""" + with assert_setup_component(1, "input_number"): + assert await setup.async_setup_component( + hass, + "input_number", + {"input_number": _VALUE_INPUT_NUMBER_CONFIG}, + ) + + with assert_setup_component(1, "template"): + assert await setup.async_setup_component( + hass, + "template", + { + "template": { + "trigger": {"platform": "state", "entity_id": _VALUE_INPUT_NUMBER}, + "unique_id": "b", + "number": { + "state": "{{ trigger.to_state.state }}", + "step": 1, + "min": 0, + "max": 100, + "set_value": { + "service": "input_number.set_value", + "data_template": { + "entity_id": _VALUE_INPUT_NUMBER, + "value": "{{ value }}", + }, + }, + "icon": "{% if ((trigger.to_state.state or 0) | int) > 50 %}mdi:greater{% else %}mdi:less{% endif %}", + }, + } + }, + ) + + hass.states.async_set(_VALUE_INPUT_NUMBER, 49) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get(_TEST_NUMBER) + assert float(state.state) == 49 + assert state.attributes[ATTR_ICON] == "mdi:less" + + await hass.services.async_call( + INPUT_NUMBER_DOMAIN, + INPUT_NUMBER_SERVICE_SET_VALUE, + {CONF_ENTITY_ID: _VALUE_INPUT_NUMBER, INPUT_NUMBER_ATTR_VALUE: 51}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(_TEST_NUMBER) + assert float(state.state) == 51 + assert state.attributes[ATTR_ICON] == "mdi:greater" From 8d06527cb157488bea2dfa7edee5dbb297c03e99 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 1 Oct 2021 20:31:38 +0200 Subject: [PATCH 0024/1038] Improve deCONZ services code quality (#56904) * setup and unload services does not need to be async * Only use DECONZ_DOMAIN to decide if service should be setup * Consolidation of functionality * Make a service to schema dictionary --- homeassistant/components/deconz/__init__.py | 7 ++- homeassistant/components/deconz/services.py | 60 +++++++++---------- tests/components/deconz/test_services.py | 65 ++++++++------------- 3 files changed, 55 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 1b9a418fb29..47a70a43ae2 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -32,12 +32,13 @@ async def async_setup_entry(hass, config_entry): if not await gateway.async_setup(): return False + if not hass.data[DOMAIN]: + async_setup_services(hass) + hass.data[DOMAIN][config_entry.entry_id] = gateway await gateway.async_update_device_registry() - await async_setup_services(hass) - config_entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gateway.shutdown) ) @@ -50,7 +51,7 @@ async def async_unload_entry(hass, config_entry): gateway = hass.data[DOMAIN].pop(config_entry.entry_id) if not hass.data[DOMAIN]: - await async_unload_services(hass) + async_unload_services(hass) elif gateway.master: await async_update_master_gateway(hass, config_entry) diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index 361ab1715c0..5ae2875305d 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -5,6 +5,7 @@ import asyncio from pydeconz.utils import normalize_bridge_id import voluptuous as vol +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity_registry import ( @@ -46,13 +47,22 @@ SERVICE_DEVICE_REFRESH = "device_refresh" SERVICE_REMOVE_ORPHANED_ENTRIES = "remove_orphaned_entries" SELECT_GATEWAY_SCHEMA = vol.All(vol.Schema({vol.Optional(CONF_BRIDGE_ID): str})) +SUPPORTED_SERVICES = ( + SERVICE_CONFIGURE_DEVICE, + SERVICE_DEVICE_REFRESH, + SERVICE_REMOVE_ORPHANED_ENTRIES, +) -async def async_setup_services(hass): +SERVICE_TO_SCHEMA = { + SERVICE_CONFIGURE_DEVICE: SERVICE_CONFIGURE_DEVICE_SCHEMA, + SERVICE_DEVICE_REFRESH: SELECT_GATEWAY_SCHEMA, + SERVICE_REMOVE_ORPHANED_ENTRIES: SELECT_GATEWAY_SCHEMA, +} + + +@callback +def async_setup_services(hass): """Set up services for deCONZ integration.""" - if hass.data.get(DECONZ_SERVICES, False): - return - - hass.data[DECONZ_SERVICES] = True async def async_call_deconz_service(service_call): """Call correct deCONZ service.""" @@ -83,38 +93,20 @@ async def async_setup_services(hass): elif service == SERVICE_REMOVE_ORPHANED_ENTRIES: await async_remove_orphaned_entries_service(gateway) - hass.services.async_register( - DOMAIN, - SERVICE_CONFIGURE_DEVICE, - async_call_deconz_service, - schema=SERVICE_CONFIGURE_DEVICE_SCHEMA, - ) - - hass.services.async_register( - DOMAIN, - SERVICE_DEVICE_REFRESH, - async_call_deconz_service, - schema=SELECT_GATEWAY_SCHEMA, - ) - - hass.services.async_register( - DOMAIN, - SERVICE_REMOVE_ORPHANED_ENTRIES, - async_call_deconz_service, - schema=SELECT_GATEWAY_SCHEMA, - ) + for service in SUPPORTED_SERVICES: + hass.services.async_register( + DOMAIN, + service, + async_call_deconz_service, + schema=SERVICE_TO_SCHEMA[service], + ) -async def async_unload_services(hass): +@callback +def async_unload_services(hass): """Unload deCONZ services.""" - if not hass.data.get(DECONZ_SERVICES): - return - - hass.data[DECONZ_SERVICES] = False - - hass.services.async_remove(DOMAIN, SERVICE_CONFIGURE_DEVICE) - hass.services.async_remove(DOMAIN, SERVICE_DEVICE_REFRESH) - hass.services.async_remove(DOMAIN, SERVICE_REMOVE_ORPHANED_ENTRIES) + for service in SUPPORTED_SERVICES: + hass.services.async_remove(DOMAIN, service) async def async_configure_service(gateway, data): diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py index 8a696da9eb4..c27530b012e 100644 --- a/tests/components/deconz/test_services.py +++ b/tests/components/deconz/test_services.py @@ -1,5 +1,5 @@ """deCONZ service tests.""" -from unittest.mock import Mock, patch +from unittest.mock import patch import pytest import voluptuous as vol @@ -10,15 +10,13 @@ from homeassistant.components.deconz.const import ( ) from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT from homeassistant.components.deconz.services import ( - DECONZ_SERVICES, SERVICE_CONFIGURE_DEVICE, SERVICE_DATA, SERVICE_DEVICE_REFRESH, SERVICE_ENTITY, SERVICE_FIELD, SERVICE_REMOVE_ORPHANED_ENTRIES, - async_setup_services, - async_unload_services, + SUPPORTED_SERVICES, ) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -35,46 +33,33 @@ from .test_gateway import ( from tests.common import async_capture_events -async def test_service_setup(hass): +async def test_service_setup_and_unload(hass, aioclient_mock): """Verify service setup works.""" - assert DECONZ_SERVICES not in hass.data - with patch( - "homeassistant.core.ServiceRegistry.async_register", return_value=Mock(True) - ) as async_register: - await async_setup_services(hass) - assert hass.data[DECONZ_SERVICES] is True - assert async_register.call_count == 3 + config_entry = await setup_deconz_integration(hass, aioclient_mock) + for service in SUPPORTED_SERVICES: + assert hass.services.has_service(DECONZ_DOMAIN, service) + + assert await hass.config_entries.async_unload(config_entry.entry_id) + for service in SUPPORTED_SERVICES: + assert not hass.services.has_service(DECONZ_DOMAIN, service) -async def test_service_setup_already_registered(hass): - """Make sure that services are only registered once.""" - hass.data[DECONZ_SERVICES] = True - with patch( - "homeassistant.core.ServiceRegistry.async_register", return_value=Mock(True) - ) as async_register: - await async_setup_services(hass) - async_register.assert_not_called() +@patch("homeassistant.core.ServiceRegistry.async_remove") +@patch("homeassistant.core.ServiceRegistry.async_register") +async def test_service_setup_and_unload_not_called_if_multiple_integrations_detected( + register_service_mock, remove_service_mock, hass, aioclient_mock +): + """Make sure that services are only setup and removed once.""" + config_entry = await setup_deconz_integration(hass, aioclient_mock) + register_service_mock.reset_mock() + config_entry_2 = await setup_deconz_integration(hass, aioclient_mock, entry_id=2) + register_service_mock.assert_not_called() - -async def test_service_unload(hass): - """Verify service unload works.""" - hass.data[DECONZ_SERVICES] = True - with patch( - "homeassistant.core.ServiceRegistry.async_remove", return_value=Mock(True) - ) as async_remove: - await async_unload_services(hass) - assert hass.data[DECONZ_SERVICES] is False - assert async_remove.call_count == 3 - - -async def test_service_unload_not_registered(hass): - """Make sure that services can only be unloaded once.""" - with patch( - "homeassistant.core.ServiceRegistry.async_remove", return_value=Mock(True) - ) as async_remove: - await async_unload_services(hass) - assert DECONZ_SERVICES not in hass.data - async_remove.assert_not_called() + register_service_mock.assert_not_called() + assert await hass.config_entries.async_unload(config_entry_2.entry_id) + remove_service_mock.assert_not_called() + assert await hass.config_entries.async_unload(config_entry.entry_id) + assert remove_service_mock.call_count == 3 async def test_configure_service_with_field(hass, aioclient_mock): From 316070f1e9b5777d03af6c1d95f9087bfb431a35 Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 1 Oct 2021 21:46:44 +0200 Subject: [PATCH 0025/1038] Fix vicare binary sensor (#56912) --- homeassistant/components/vicare/binary_sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/vicare/binary_sensor.py b/homeassistant/components/vicare/binary_sensor.py index 9897b38ccf5..88d6e3ac06a 100644 --- a/homeassistant/components/vicare/binary_sensor.py +++ b/homeassistant/components/vicare/binary_sensor.py @@ -124,6 +124,7 @@ class ViCareBinarySensor(BinarySensorEntity): def __init__(self, name, api, description: DescriptionT): """Initialize the sensor.""" + self.entity_description = description self._attr_name = f"{name} {description.name}" self._api = api self._state = None From e5b0bbcca61e90177f40b16eac9138ac19564fc6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 Oct 2021 15:38:49 -0700 Subject: [PATCH 0026/1038] Bump netdisco to 3.0.0 (#56903) --- homeassistant/components/discovery/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/discovery/manifest.json b/homeassistant/components/discovery/manifest.json index 558c727c62c..1b7d51c1716 100644 --- a/homeassistant/components/discovery/manifest.json +++ b/homeassistant/components/discovery/manifest.json @@ -2,7 +2,7 @@ "domain": "discovery", "name": "Discovery", "documentation": "https://www.home-assistant.io/integrations/discovery", - "requirements": ["netdisco==2.9.0"], + "requirements": ["netdisco==3.0.0"], "after_dependencies": ["zeroconf"], "codeowners": [], "quality_scale": "internal" diff --git a/requirements_all.txt b/requirements_all.txt index dd2447b9bda..a20a0f16c80 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1047,7 +1047,7 @@ nessclient==0.9.15 netdata==0.2.0 # homeassistant.components.discovery -netdisco==2.9.0 +netdisco==3.0.0 # homeassistant.components.nmap_tracker netmap==0.7.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8ba707f471c..a65048a5b83 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -612,7 +612,7 @@ ndms2_client==0.1.1 nessclient==0.9.15 # homeassistant.components.discovery -netdisco==2.9.0 +netdisco==3.0.0 # homeassistant.components.nmap_tracker netmap==0.7.0.2 From 2730a27fd0d6e59bb178d92b2b17b48116d0783d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 2 Oct 2021 01:52:45 +0300 Subject: [PATCH 0027/1038] Use HTTPStatus instead of HTTP_* constants in various test mocks (#56543) --- tests/components/buienradar/test_camera.py | 11 ++++----- tests/components/cloud/test_http_api.py | 3 ++- tests/components/directv/__init__.py | 16 ++++++------- tests/components/rest/test_switch.py | 24 +++++++++---------- .../smartthings/test_config_flow.py | 16 ++++--------- tests/components/smartthings/test_init.py | 24 ++++++++++++------- tests/components/startca/test_sensor.py | 12 ++++------ tests/components/yandextts/test_tts.py | 7 ++++-- tests/test_util/aiohttp.py | 5 ++-- 9 files changed, 59 insertions(+), 59 deletions(-) diff --git a/tests/components/buienradar/test_camera.py b/tests/components/buienradar/test_camera.py index 3d0c63d972b..1688dd83d2c 100644 --- a/tests/components/buienradar/test_camera.py +++ b/tests/components/buienradar/test_camera.py @@ -2,15 +2,12 @@ import asyncio from contextlib import suppress import copy +from http import HTTPStatus from aiohttp.client_exceptions import ClientResponseError from homeassistant.components.buienradar.const import CONF_COUNTRY, CONF_DELTA, DOMAIN -from homeassistant.const import ( - CONF_LATITUDE, - CONF_LONGITUDE, - HTTP_INTERNAL_SERVER_ERROR, -) +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.helpers.entity_registry import async_get from homeassistant.util import dt as dt_util @@ -216,7 +213,9 @@ async def test_retries_after_error(aioclient_mock, hass, hass_client): client = await hass_client() - aioclient_mock.get(radar_map_url(), text=None, status=HTTP_INTERNAL_SERVER_ERROR) + aioclient_mock.get( + radar_map_url(), text=None, status=HTTPStatus.INTERNAL_SERVER_ERROR + ) # A 404 should not return data and throw: with suppress(ClientResponseError): diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 4116e97be92..a7181ea4a73 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -1,5 +1,6 @@ """Tests for the HTTP API for the cloud component.""" import asyncio +from http import HTTPStatus from ipaddress import ip_network from unittest.mock import AsyncMock, MagicMock, Mock, patch @@ -412,7 +413,7 @@ async def test_websocket_subscription_fail( hass, hass_ws_client, aioclient_mock, mock_auth, mock_cloud_login ): """Test querying the status.""" - aioclient_mock.get(SUBSCRIPTION_INFO_URL, status=HTTP_INTERNAL_SERVER_ERROR) + aioclient_mock.get(SUBSCRIPTION_INFO_URL, status=HTTPStatus.INTERNAL_SERVER_ERROR) client = await hass_ws_client(hass) await client.send_json({"id": 5, "type": "cloud/subscription"}) response = await client.receive_json() diff --git a/tests/components/directv/__init__.py b/tests/components/directv/__init__.py index 1bdfbeea823..790121ddca1 100644 --- a/tests/components/directv/__init__.py +++ b/tests/components/directv/__init__.py @@ -1,12 +1,9 @@ """Tests for the DirecTV component.""" +from http import HTTPStatus + from homeassistant.components.directv.const import CONF_RECEIVER_ID, DOMAIN from homeassistant.components.ssdp import ATTR_SSDP_LOCATION -from homeassistant.const import ( - CONF_HOST, - CONTENT_TYPE_JSON, - HTTP_FORBIDDEN, - HTTP_INTERNAL_SERVER_ERROR, -) +from homeassistant.const import CONF_HOST, CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture @@ -46,7 +43,7 @@ def mock_connection(aioclient_mock: AiohttpClientMocker) -> None: aioclient_mock.get( f"http://{HOST}:8080/info/mode", params={"clientAddr": "9XXXXXXXXXX9"}, - status=HTTP_INTERNAL_SERVER_ERROR, + status=HTTPStatus.INTERNAL_SERVER_ERROR, text=load_fixture("directv/info-mode-error.json"), headers={"Content-Type": CONTENT_TYPE_JSON}, ) @@ -86,7 +83,7 @@ def mock_connection(aioclient_mock: AiohttpClientMocker) -> None: aioclient_mock.get( f"http://{HOST}:8080/tv/getTuned", params={"clientAddr": "C01234567890"}, - status=HTTP_FORBIDDEN, + status=HTTPStatus.FORBIDDEN, text=load_fixture("directv/tv-get-tuned-restricted.json"), headers={"Content-Type": CONTENT_TYPE_JSON}, ) @@ -107,7 +104,8 @@ async def setup_integration( """Set up the DirecTV integration in Home Assistant.""" if setup_error: aioclient_mock.get( - f"http://{HOST}:8080/info/getVersion", status=HTTP_INTERNAL_SERVER_ERROR + f"http://{HOST}:8080/info/getVersion", + status=HTTPStatus.INTERNAL_SERVER_ERROR, ) else: mock_connection(aioclient_mock) diff --git a/tests/components/rest/test_switch.py b/tests/components/rest/test_switch.py index 62fc30d9e4b..48f63ddabc7 100644 --- a/tests/components/rest/test_switch.py +++ b/tests/components/rest/test_switch.py @@ -1,5 +1,6 @@ """The tests for the REST switch platform.""" import asyncio +from http import HTTPStatus import aiohttp @@ -13,9 +14,6 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_RESOURCE, CONTENT_TYPE_JSON, - HTTP_INTERNAL_SERVER_ERROR, - HTTP_NOT_FOUND, - HTTP_OK, ) from homeassistant.helpers.template import Template from homeassistant.setup import async_setup_component @@ -69,7 +67,7 @@ async def test_setup_timeout(hass, aioclient_mock): async def test_setup_minimum(hass, aioclient_mock): """Test setup with minimum configuration.""" - aioclient_mock.get("http://localhost", status=HTTP_OK) + aioclient_mock.get("http://localhost", status=HTTPStatus.OK) with assert_setup_component(1, SWITCH_DOMAIN): assert await async_setup_component( hass, @@ -87,7 +85,7 @@ async def test_setup_minimum(hass, aioclient_mock): async def test_setup_query_params(hass, aioclient_mock): """Test setup with query params.""" - aioclient_mock.get("http://localhost/?search=something", status=HTTP_OK) + aioclient_mock.get("http://localhost/?search=something", status=HTTPStatus.OK) with assert_setup_component(1, SWITCH_DOMAIN): assert await async_setup_component( hass, @@ -108,7 +106,7 @@ async def test_setup_query_params(hass, aioclient_mock): async def test_setup(hass, aioclient_mock): """Test setup with valid configuration.""" - aioclient_mock.get("http://localhost", status=HTTP_OK) + aioclient_mock.get("http://localhost", status=HTTPStatus.OK) assert await async_setup_component( hass, SWITCH_DOMAIN, @@ -130,8 +128,8 @@ async def test_setup(hass, aioclient_mock): async def test_setup_with_state_resource(hass, aioclient_mock): """Test setup with valid configuration.""" - aioclient_mock.get("http://localhost", status=HTTP_NOT_FOUND) - aioclient_mock.get("http://localhost/state", status=HTTP_OK) + aioclient_mock.get("http://localhost", status=HTTPStatus.NOT_FOUND) + aioclient_mock.get("http://localhost/state", status=HTTPStatus.OK) assert await async_setup_component( hass, SWITCH_DOMAIN, @@ -190,7 +188,7 @@ def test_is_on_before_update(hass): async def test_turn_on_success(hass, aioclient_mock): """Test turn_on.""" - aioclient_mock.post(RESOURCE, status=HTTP_OK) + aioclient_mock.post(RESOURCE, status=HTTPStatus.OK) switch, body_on, body_off = _setup_test_switch(hass) await switch.async_turn_on() @@ -200,7 +198,7 @@ async def test_turn_on_success(hass, aioclient_mock): async def test_turn_on_status_not_ok(hass, aioclient_mock): """Test turn_on when error status returned.""" - aioclient_mock.post(RESOURCE, status=HTTP_INTERNAL_SERVER_ERROR) + aioclient_mock.post(RESOURCE, status=HTTPStatus.INTERNAL_SERVER_ERROR) switch, body_on, body_off = _setup_test_switch(hass) await switch.async_turn_on() @@ -210,7 +208,7 @@ async def test_turn_on_status_not_ok(hass, aioclient_mock): async def test_turn_on_timeout(hass, aioclient_mock): """Test turn_on when timeout occurs.""" - aioclient_mock.post(RESOURCE, status=HTTP_INTERNAL_SERVER_ERROR) + aioclient_mock.post(RESOURCE, status=HTTPStatus.INTERNAL_SERVER_ERROR) switch, body_on, body_off = _setup_test_switch(hass) await switch.async_turn_on() @@ -219,7 +217,7 @@ async def test_turn_on_timeout(hass, aioclient_mock): async def test_turn_off_success(hass, aioclient_mock): """Test turn_off.""" - aioclient_mock.post(RESOURCE, status=HTTP_OK) + aioclient_mock.post(RESOURCE, status=HTTPStatus.OK) switch, body_on, body_off = _setup_test_switch(hass) await switch.async_turn_off() @@ -229,7 +227,7 @@ async def test_turn_off_success(hass, aioclient_mock): async def test_turn_off_status_not_ok(hass, aioclient_mock): """Test turn_off when error status returned.""" - aioclient_mock.post(RESOURCE, status=HTTP_INTERNAL_SERVER_ERROR) + aioclient_mock.post(RESOURCE, status=HTTPStatus.INTERNAL_SERVER_ERROR) switch, body_on, body_off = _setup_test_switch(hass) await switch.async_turn_off() diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index 15e045338af..be6385ccdd9 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the SmartThings config flow module.""" +from http import HTTPStatus from unittest.mock import AsyncMock, Mock, patch from uuid import uuid4 @@ -15,14 +16,7 @@ from homeassistant.components.smartthings.const import ( DOMAIN, ) from homeassistant.config import async_process_ha_core_config -from homeassistant.const import ( - CONF_ACCESS_TOKEN, - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, - HTTP_FORBIDDEN, - HTTP_NOT_FOUND, - HTTP_UNAUTHORIZED, -) +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET from tests.common import MockConfigEntry @@ -482,7 +476,7 @@ async def test_unauthorized_token_shows_error(hass, smartthings_mock): token = str(uuid4()) request_info = Mock(real_url="http://example.com") smartthings_mock.apps.side_effect = ClientResponseError( - request_info=request_info, history=None, status=HTTP_UNAUTHORIZED + request_info=request_info, history=None, status=HTTPStatus.UNAUTHORIZED ) # Webhook confirmation shown @@ -519,7 +513,7 @@ async def test_forbidden_token_shows_error(hass, smartthings_mock): token = str(uuid4()) request_info = Mock(real_url="http://example.com") smartthings_mock.apps.side_effect = ClientResponseError( - request_info=request_info, history=None, status=HTTP_FORBIDDEN + request_info=request_info, history=None, status=HTTPStatus.FORBIDDEN ) # Webhook confirmation shown @@ -635,7 +629,7 @@ async def test_unknown_response_error_shows_error(hass, smartthings_mock): token = str(uuid4()) request_info = Mock(real_url="http://example.com") error = ClientResponseError( - request_info=request_info, history=None, status=HTTP_NOT_FOUND + request_info=request_info, history=None, status=HTTPStatus.NOT_FOUND ) smartthings_mock.apps.side_effect = error diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index e38c123829c..7504a1536d1 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -1,4 +1,5 @@ """Tests for the SmartThings component init module.""" +from http import HTTPStatus from unittest.mock import Mock, patch from uuid import uuid4 @@ -19,7 +20,6 @@ from homeassistant.components.smartthings.const import ( SIGNAL_SMARTTHINGS_UPDATE, ) from homeassistant.config import async_process_ha_core_config -from homeassistant.const import HTTP_FORBIDDEN, HTTP_INTERNAL_SERVER_ERROR from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.setup import async_setup_component @@ -83,7 +83,9 @@ async def test_recoverable_api_errors_raise_not_ready( config_entry.add_to_hass(hass) request_info = Mock(real_url="http://example.com") smartthings_mock.app.side_effect = ClientResponseError( - request_info=request_info, history=None, status=HTTP_INTERNAL_SERVER_ERROR + request_info=request_info, + history=None, + status=HTTPStatus.INTERNAL_SERVER_ERROR, ) with pytest.raises(ConfigEntryNotReady): @@ -99,7 +101,9 @@ async def test_scenes_api_errors_raise_not_ready( smartthings_mock.app.return_value = app smartthings_mock.installed_app.return_value = installed_app smartthings_mock.scenes.side_effect = ClientResponseError( - request_info=request_info, history=None, status=HTTP_INTERNAL_SERVER_ERROR + request_info=request_info, + history=None, + status=HTTPStatus.INTERNAL_SERVER_ERROR, ) with pytest.raises(ConfigEntryNotReady): await smartthings.async_setup_entry(hass, config_entry) @@ -160,7 +164,7 @@ async def test_scenes_unauthorized_loads_platforms( smartthings_mock.installed_app.return_value = installed_app smartthings_mock.devices.return_value = [device] smartthings_mock.scenes.side_effect = ClientResponseError( - request_info=request_info, history=None, status=HTTP_FORBIDDEN + request_info=request_info, history=None, status=HTTPStatus.FORBIDDEN ) mock_token = Mock() mock_token.access_token = str(uuid4()) @@ -311,10 +315,10 @@ async def test_remove_entry_already_deleted(hass, config_entry, smartthings_mock request_info = Mock(real_url="http://example.com") # Arrange smartthings_mock.delete_installed_app.side_effect = ClientResponseError( - request_info=request_info, history=None, status=HTTP_FORBIDDEN + request_info=request_info, history=None, status=HTTPStatus.FORBIDDEN ) smartthings_mock.delete_app.side_effect = ClientResponseError( - request_info=request_info, history=None, status=HTTP_FORBIDDEN + request_info=request_info, history=None, status=HTTPStatus.FORBIDDEN ) # Act await smartthings.async_remove_entry(hass, config_entry) @@ -330,7 +334,9 @@ async def test_remove_entry_installedapp_api_error( request_info = Mock(real_url="http://example.com") # Arrange smartthings_mock.delete_installed_app.side_effect = ClientResponseError( - request_info=request_info, history=None, status=HTTP_INTERNAL_SERVER_ERROR + request_info=request_info, + history=None, + status=HTTPStatus.INTERNAL_SERVER_ERROR, ) # Act with pytest.raises(ClientResponseError): @@ -359,7 +365,9 @@ async def test_remove_entry_app_api_error(hass, config_entry, smartthings_mock): # Arrange request_info = Mock(real_url="http://example.com") smartthings_mock.delete_app.side_effect = ClientResponseError( - request_info=request_info, history=None, status=HTTP_INTERNAL_SERVER_ERROR + request_info=request_info, + history=None, + status=HTTPStatus.INTERNAL_SERVER_ERROR, ) # Act with pytest.raises(ClientResponseError): diff --git a/tests/components/startca/test_sensor.py b/tests/components/startca/test_sensor.py index 511061933cb..11fc3120b5b 100644 --- a/tests/components/startca/test_sensor.py +++ b/tests/components/startca/test_sensor.py @@ -1,12 +1,9 @@ """Tests for the Start.ca sensor platform.""" +from http import HTTPStatus + from homeassistant.bootstrap import async_setup_component from homeassistant.components.startca.sensor import StartcaData -from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, - DATA_GIGABYTES, - HTTP_NOT_FOUND, - PERCENTAGE, -) +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, DATA_GIGABYTES, PERCENTAGE from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -205,7 +202,8 @@ async def test_unlimited_setup(hass, aioclient_mock): async def test_bad_return_code(hass, aioclient_mock): """Test handling a return code that isn't HTTP OK.""" aioclient_mock.get( - "https://www.start.ca/support/usage/api?key=NOTAKEY", status=HTTP_NOT_FOUND + "https://www.start.ca/support/usage/api?key=NOTAKEY", + status=HTTPStatus.NOT_FOUND, ) scd = StartcaData(hass.loop, async_get_clientsession(hass), "NOTAKEY", 400) diff --git a/tests/components/yandextts/test_tts.py b/tests/components/yandextts/test_tts.py index d13ba867cd8..3c5cc967c6b 100644 --- a/tests/components/yandextts/test_tts.py +++ b/tests/components/yandextts/test_tts.py @@ -1,5 +1,6 @@ """The tests for the Yandex SpeechKit speech platform.""" import asyncio +from http import HTTPStatus import os import shutil @@ -9,7 +10,6 @@ from homeassistant.components.media_player.const import ( ) import homeassistant.components.tts as tts from homeassistant.config import async_process_ha_core_config -from homeassistant.const import HTTP_FORBIDDEN from homeassistant.setup import setup_component from tests.common import assert_setup_component, get_test_home_assistant, mock_service @@ -207,7 +207,10 @@ class TestTTSYandexPlatform: "speed": 1, } aioclient_mock.get( - self._base_url, status=HTTP_FORBIDDEN, content=b"test", params=url_param + self._base_url, + status=HTTPStatus.FORBIDDEN, + content=b"test", + params=url_param, ) config = {tts.DOMAIN: {"platform": "yandextts", "api_key": "1234567xx"}} diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index 077ed292d10..31a8a5c71e3 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -1,6 +1,7 @@ """Aiohttp test utils.""" import asyncio from contextlib import contextmanager +from http import HTTPStatus import json as _json import re from unittest import mock @@ -41,7 +42,7 @@ class AiohttpClientMocker: url, *, auth=None, - status=200, + status=HTTPStatus.OK, text=None, data=None, content=None, @@ -157,7 +158,7 @@ class AiohttpClientMockResponse: self, method, url, - status=200, + status=HTTPStatus.OK, response=None, json=None, text=None, From 4cdbd3c5763cbbc0d435dcfa27901efc1357516d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 2 Oct 2021 09:05:49 +0200 Subject: [PATCH 0028/1038] Fix `Unable to serialize to JSON` error in Xiaomi Miio (#56929) --- homeassistant/components/xiaomi_miio/fan.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 1b275ea2d6e..04cdc4573db 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -1,6 +1,7 @@ """Support for Xiaomi Mi Air Purifier and Xiaomi Mi Air Humidifier.""" from abc import abstractmethod import asyncio +from enum import Enum import logging import math @@ -363,13 +364,21 @@ class XiaomiGenericAirPurifier(XiaomiGenericDevice): return None + @staticmethod + def _extract_value_from_attribute(state, attribute): + value = getattr(state, attribute) + if isinstance(value, Enum): + return value.value + + return value + @callback def _handle_coordinator_update(self): """Fetch state from the device.""" self._state = self.coordinator.data.is_on self._state_attrs.update( { - key: getattr(self.coordinator.data, value) + key: self._extract_value_from_attribute(self.coordinator.data, value) for key, value in self._available_attributes.items() } ) @@ -434,7 +443,7 @@ class XiaomiAirPurifier(XiaomiGenericAirPurifier): self._state = self.coordinator.data.is_on self._state_attrs.update( { - key: getattr(self.coordinator.data, value) + key: self._extract_value_from_attribute(self.coordinator.data, value) for key, value in self._available_attributes.items() } ) From 8258443a9ee093aaff7d21aaf2823e6eada053ef Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 2 Oct 2021 09:08:01 +0200 Subject: [PATCH 0029/1038] Replace strings with library constants in deCONZ climate platform --- homeassistant/components/deconz/climate.py | 66 ++++++++++++++-------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 86c19be3cd2..d80ecc6cc77 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -1,7 +1,27 @@ """Support for deCONZ climate devices.""" from __future__ import annotations -from pydeconz.sensor import Thermostat +from pydeconz.sensor import ( + THERMOSTAT_FAN_MODE_AUTO, + THERMOSTAT_FAN_MODE_HIGH, + THERMOSTAT_FAN_MODE_LOW, + THERMOSTAT_FAN_MODE_MEDIUM, + THERMOSTAT_FAN_MODE_OFF, + THERMOSTAT_FAN_MODE_ON, + THERMOSTAT_FAN_MODE_SMART, + THERMOSTAT_MODE_AUTO, + THERMOSTAT_MODE_COOL, + THERMOSTAT_MODE_HEAT, + THERMOSTAT_MODE_OFF, + THERMOSTAT_PRESET_AUTO, + THERMOSTAT_PRESET_BOOST, + THERMOSTAT_PRESET_COMFORT, + THERMOSTAT_PRESET_COMPLEX, + THERMOSTAT_PRESET_ECO, + THERMOSTAT_PRESET_HOLIDAY, + THERMOSTAT_PRESET_MANUAL, + Thermostat, +) from homeassistant.components.climate import DOMAIN, ClimateEntity from homeassistant.components.climate.const import ( @@ -33,22 +53,22 @@ from .gateway import get_gateway_from_config_entry DECONZ_FAN_SMART = "smart" FAN_MODE_TO_DECONZ = { - DECONZ_FAN_SMART: "smart", - FAN_AUTO: "auto", - FAN_HIGH: "high", - FAN_MEDIUM: "medium", - FAN_LOW: "low", - FAN_ON: "on", - FAN_OFF: "off", + DECONZ_FAN_SMART: THERMOSTAT_FAN_MODE_SMART, + FAN_AUTO: THERMOSTAT_FAN_MODE_AUTO, + FAN_HIGH: THERMOSTAT_FAN_MODE_HIGH, + FAN_MEDIUM: THERMOSTAT_FAN_MODE_MEDIUM, + FAN_LOW: THERMOSTAT_FAN_MODE_LOW, + FAN_ON: THERMOSTAT_FAN_MODE_ON, + FAN_OFF: THERMOSTAT_FAN_MODE_OFF, } DECONZ_TO_FAN_MODE = {value: key for key, value in FAN_MODE_TO_DECONZ.items()} HVAC_MODE_TO_DECONZ = { - HVAC_MODE_AUTO: "auto", - HVAC_MODE_COOL: "cool", - HVAC_MODE_HEAT: "heat", - HVAC_MODE_OFF: "off", + HVAC_MODE_AUTO: THERMOSTAT_MODE_AUTO, + HVAC_MODE_COOL: THERMOSTAT_MODE_COOL, + HVAC_MODE_HEAT: THERMOSTAT_MODE_HEAT, + HVAC_MODE_OFF: THERMOSTAT_MODE_OFF, } DECONZ_PRESET_AUTO = "auto" @@ -57,13 +77,13 @@ DECONZ_PRESET_HOLIDAY = "holiday" DECONZ_PRESET_MANUAL = "manual" PRESET_MODE_TO_DECONZ = { - DECONZ_PRESET_AUTO: "auto", - PRESET_BOOST: "boost", - PRESET_COMFORT: "comfort", - DECONZ_PRESET_COMPLEX: "complex", - PRESET_ECO: "eco", - DECONZ_PRESET_HOLIDAY: "holiday", - DECONZ_PRESET_MANUAL: "manual", + DECONZ_PRESET_AUTO: THERMOSTAT_PRESET_AUTO, + PRESET_BOOST: THERMOSTAT_PRESET_BOOST, + PRESET_COMFORT: THERMOSTAT_PRESET_COMFORT, + DECONZ_PRESET_COMPLEX: THERMOSTAT_PRESET_COMPLEX, + PRESET_ECO: THERMOSTAT_PRESET_ECO, + DECONZ_PRESET_HOLIDAY: THERMOSTAT_PRESET_HOLIDAY, + DECONZ_PRESET_MANUAL: THERMOSTAT_PRESET_MANUAL, } DECONZ_TO_PRESET_MODE = {value: key for key, value in PRESET_MODE_TO_DECONZ.items()} @@ -116,7 +136,7 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): super().__init__(device, gateway) self._hvac_mode_to_deconz = dict(HVAC_MODE_TO_DECONZ) - if "mode" not in device.raw["config"]: + if not device.mode: self._hvac_mode_to_deconz = { HVAC_MODE_HEAT: True, HVAC_MODE_OFF: False, @@ -129,10 +149,10 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): self._attr_supported_features = SUPPORT_TARGET_TEMPERATURE - if "fanmode" in device.raw["config"]: + if device.fan_mode: self._attr_supported_features |= SUPPORT_FAN_MODE - if "preset" in device.raw["config"]: + if device.preset: self._attr_supported_features |= SUPPORT_PRESET_MODE # Fan control @@ -214,7 +234,7 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): @property def target_temperature(self) -> float: """Return the target temperature.""" - if self._device.mode == "cool": + if self._device.mode == THERMOSTAT_MODE_COOL: return self._device.cooling_setpoint return self._device.heating_setpoint From 818f695227753bba0ad8c96a1dc8384523e48fee Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 2 Oct 2021 09:09:26 +0200 Subject: [PATCH 0030/1038] Replace strings with library constants in deCONZ fan platform --- homeassistant/components/deconz/fan.py | 38 ++++++++++++++++++++------ 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/deconz/fan.py b/homeassistant/components/deconz/fan.py index 38fc087cdfd..3b1f3bd256d 100644 --- a/homeassistant/components/deconz/fan.py +++ b/homeassistant/components/deconz/fan.py @@ -1,7 +1,14 @@ """Support for deCONZ fans.""" from __future__ import annotations -from pydeconz.light import Fan +from pydeconz.light import ( + FAN_SPEED_25_PERCENT, + FAN_SPEED_50_PERCENT, + FAN_SPEED_75_PERCENT, + FAN_SPEED_100_PERCENT, + FAN_SPEED_OFF, + Fan, +) from homeassistant.components.fan import ( DOMAIN, @@ -23,10 +30,25 @@ from .const import NEW_LIGHT from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry -ORDERED_NAMED_FAN_SPEEDS = [1, 2, 3, 4] +ORDERED_NAMED_FAN_SPEEDS = [ + FAN_SPEED_25_PERCENT, + FAN_SPEED_50_PERCENT, + FAN_SPEED_75_PERCENT, + FAN_SPEED_100_PERCENT, +] -LEGACY_SPEED_TO_DECONZ = {SPEED_OFF: 0, SPEED_LOW: 1, SPEED_MEDIUM: 2, SPEED_HIGH: 4} -LEGACY_DECONZ_TO_SPEED = {0: SPEED_OFF, 1: SPEED_LOW, 2: SPEED_MEDIUM, 4: SPEED_HIGH} +LEGACY_SPEED_TO_DECONZ = { + SPEED_OFF: FAN_SPEED_OFF, + SPEED_LOW: FAN_SPEED_25_PERCENT, + SPEED_MEDIUM: FAN_SPEED_50_PERCENT, + SPEED_HIGH: FAN_SPEED_100_PERCENT, +} +LEGACY_DECONZ_TO_SPEED = { + FAN_SPEED_OFF: SPEED_OFF, + FAN_SPEED_25_PERCENT: SPEED_LOW, + FAN_SPEED_50_PERCENT: SPEED_MEDIUM, + FAN_SPEED_100_PERCENT: SPEED_HIGH, +} async def async_setup_entry(hass, config_entry, async_add_entities) -> None: @@ -68,7 +90,7 @@ class DeconzFan(DeconzDevice, FanEntity): """Set up fan.""" super().__init__(device, gateway) - self._default_on_speed = 2 + self._default_on_speed = FAN_SPEED_50_PERCENT if self._device.speed in ORDERED_NAMED_FAN_SPEEDS: self._default_on_speed = self._device.speed @@ -77,12 +99,12 @@ class DeconzFan(DeconzDevice, FanEntity): @property def is_on(self) -> bool: """Return true if fan is on.""" - return self._device.speed != 0 + return self._device.speed != FAN_SPEED_OFF @property def percentage(self) -> int | None: """Return the current speed percentage.""" - if self._device.speed == 0: + if self._device.speed == FAN_SPEED_OFF: return 0 if self._device.speed not in ORDERED_NAMED_FAN_SPEEDS: return None @@ -177,4 +199,4 @@ class DeconzFan(DeconzDevice, FanEntity): async def async_turn_off(self, **kwargs) -> None: """Turn off fan.""" - await self._device.set_speed(0) + await self._device.set_speed(FAN_SPEED_OFF) From 11690bed58ea7124c2ffef73ac5789f7fa491b8d Mon Sep 17 00:00:00 2001 From: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> Date: Sat, 2 Oct 2021 03:11:31 -0400 Subject: [PATCH 0031/1038] Bump pynws: fix unit code bug (#56923) --- homeassistant/components/nws/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nws/manifest.json b/homeassistant/components/nws/manifest.json index d1e7158ab20..30b00fde15a 100644 --- a/homeassistant/components/nws/manifest.json +++ b/homeassistant/components/nws/manifest.json @@ -3,7 +3,7 @@ "name": "National Weather Service (NWS)", "documentation": "https://www.home-assistant.io/integrations/nws", "codeowners": ["@MatthewFlamm"], - "requirements": ["pynws==1.3.0"], + "requirements": ["pynws==1.3.1"], "quality_scale": "platinum", "config_flow": true, "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index a20a0f16c80..82cc9603b46 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1670,7 +1670,7 @@ pynuki==1.4.1 pynut2==2.1.2 # homeassistant.components.nws -pynws==1.3.0 +pynws==1.3.1 # homeassistant.components.nx584 pynx584==0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a65048a5b83..00d7f17003d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -986,7 +986,7 @@ pynuki==1.4.1 pynut2==2.1.2 # homeassistant.components.nws -pynws==1.3.0 +pynws==1.3.1 # homeassistant.components.nx584 pynx584==0.5 From 73e58c8c6268ebf43c6c36a19baea261aece063d Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sat, 2 Oct 2021 09:13:40 +0200 Subject: [PATCH 0032/1038] Bump fritzconnection to 1.7.0 (#56924) --- homeassistant/components/fritz/manifest.json | 2 +- homeassistant/components/fritzbox_callmonitor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index 46531183afd..e6ae95e3eb4 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -3,7 +3,7 @@ "name": "AVM FRITZ!Box Tools", "documentation": "https://www.home-assistant.io/integrations/fritz", "requirements": [ - "fritzconnection==1.6.0", + "fritzconnection==1.7.0", "xmltodict==0.12.0" ], "dependencies": ["network"], diff --git a/homeassistant/components/fritzbox_callmonitor/manifest.json b/homeassistant/components/fritzbox_callmonitor/manifest.json index 0a1f7330c6d..3d58f27e950 100644 --- a/homeassistant/components/fritzbox_callmonitor/manifest.json +++ b/homeassistant/components/fritzbox_callmonitor/manifest.json @@ -3,7 +3,7 @@ "name": "AVM FRITZ!Box Call Monitor", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/fritzbox_callmonitor", - "requirements": ["fritzconnection==1.6.0"], + "requirements": ["fritzconnection==1.7.0"], "codeowners": [], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 82cc9603b46..ecc96c5a642 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -674,7 +674,7 @@ freesms==0.2.0 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor -fritzconnection==1.6.0 +fritzconnection==1.7.0 # homeassistant.components.google_translate gTTS==2.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 00d7f17003d..a9b5b7c3513 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -391,7 +391,7 @@ freebox-api==0.0.10 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor -fritzconnection==1.6.0 +fritzconnection==1.7.0 # homeassistant.components.google_translate gTTS==2.2.3 From 538773a14a6eb48f0dc84832311bb3b29447375d Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Sat, 2 Oct 2021 09:29:56 +0200 Subject: [PATCH 0033/1038] Add SSDP discovery to Nanoleaf (#56907) --- .../components/nanoleaf/config_flow.py | 29 ++++++-- .../components/nanoleaf/manifest.json | 14 ++++ homeassistant/generated/ssdp.py | 14 ++++ tests/components/nanoleaf/test_config_flow.py | 73 ++++++++++++------- 4 files changed, 97 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/nanoleaf/config_flow.py b/homeassistant/components/nanoleaf/config_flow.py index d5fc023d3a1..0f4f8ff75bd 100644 --- a/homeassistant/components/nanoleaf/config_flow.py +++ b/homeassistant/components/nanoleaf/config_flow.py @@ -92,25 +92,42 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle Nanoleaf Zeroconf discovery.""" _LOGGER.debug("Zeroconf discovered: %s", discovery_info) - return await self._async_discovery_handler(discovery_info) + return await self._async_homekit_zeroconf_discovery_handler(discovery_info) async def async_step_homekit(self, discovery_info: DiscoveryInfoType) -> FlowResult: """Handle Nanoleaf Homekit discovery.""" _LOGGER.debug("Homekit discovered: %s", discovery_info) - return await self._async_discovery_handler(discovery_info) + return await self._async_homekit_zeroconf_discovery_handler(discovery_info) - async def _async_discovery_handler( + async def _async_homekit_zeroconf_discovery_handler( self, discovery_info: DiscoveryInfoType + ) -> FlowResult: + """Handle Nanoleaf Homekit and Zeroconf discovery.""" + return await self._async_discovery_handler( + discovery_info["host"], + discovery_info["name"].replace(f".{discovery_info['type']}", ""), + discovery_info["properties"]["id"], + ) + + async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: + """Handle Nanoleaf SSDP discovery.""" + _LOGGER.debug("SSDP discovered: %s", discovery_info) + return await self._async_discovery_handler( + discovery_info["_host"], + discovery_info["nl-devicename"], + discovery_info["nl-deviceid"], + ) + + async def _async_discovery_handler( + self, host: str, name: str, device_id: str ) -> FlowResult: """Handle Nanoleaf discovery.""" - host = discovery_info["host"] # The name is unique and printed on the device and cannot be changed. - name = discovery_info["name"].replace(f".{discovery_info['type']}", "") await self.async_set_unique_id(name) self._abort_if_unique_id_configured({CONF_HOST: host}) # Import from discovery integration - self.device_id = discovery_info["properties"]["id"] + self.device_id = device_id self.discovery_conf = cast( dict, await self.hass.async_add_executor_job( diff --git a/homeassistant/components/nanoleaf/manifest.json b/homeassistant/components/nanoleaf/manifest.json index 133257dc7fe..c527127f6e8 100644 --- a/homeassistant/components/nanoleaf/manifest.json +++ b/homeassistant/components/nanoleaf/manifest.json @@ -10,6 +10,20 @@ "NL*" ] }, + "ssdp": [ + { + "st": "Nanoleaf_aurora:light" + }, + { + "st": "nanoleaf:nl29" + }, + { + "st": "nanoleaf:nl42" + }, + { + "st": "nanoleaf:nl52" + } + ], "codeowners": ["@milanmeu"], "iot_class": "local_polling" } \ No newline at end of file diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index b058f972229..6d15477bf02 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -171,6 +171,20 @@ SSDP = { "manufacturer": "konnected.io" } ], + "nanoleaf": [ + { + "st": "Nanoleaf_aurora:light" + }, + { + "st": "nanoleaf:nl29" + }, + { + "st": "nanoleaf:nl42" + }, + { + "st": "nanoleaf:nl52" + } + ], "netgear": [ { "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", diff --git a/tests/components/nanoleaf/test_config_flow.py b/tests/components/nanoleaf/test_config_flow.py index 8f62830b219..ba0eff4abe3 100644 --- a/tests/components/nanoleaf/test_config_flow.py +++ b/tests/components/nanoleaf/test_config_flow.py @@ -74,10 +74,7 @@ async def test_user_unavailable_user_step_link_step(hass: HomeAssistant) -> None "homeassistant.components.nanoleaf.config_flow.Nanoleaf.authorize", side_effect=Unavailable, ): - result3 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {}, - ) + result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result3["type"] == "abort" assert result3["reason"] == "cannot_connect" @@ -115,10 +112,7 @@ async def test_user_error_setup_finish( "homeassistant.components.nanoleaf.config_flow.Nanoleaf.get_info", side_effect=error, ): - result3 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {}, - ) + result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result3["type"] == "abort" assert result3["reason"] == reason @@ -151,9 +145,7 @@ async def test_user_not_authorizing_new_tokens_user_step_link_step( assert result2["errors"] is None assert result2["step_id"] == "link" - result3 = await hass.config_entries.flow.async_configure( - result["flow_id"], - ) + result3 = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result3["type"] == "form" assert result3["errors"] is None assert result3["step_id"] == "link" @@ -165,10 +157,7 @@ async def test_user_not_authorizing_new_tokens_user_step_link_step( mock_nanoleaf.return_value.authorize.side_effect = None - result5 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {}, - ) + result5 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result5["type"] == "create_entry" assert result5["title"] == TEST_NAME assert result5["data"] == { @@ -213,20 +202,14 @@ async def test_user_exception_user_step(hass: HomeAssistant) -> None: mock_nanoleaf.return_value.authorize.side_effect = Exception() - result4 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {}, - ) + result4 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result4["type"] == "form" assert result4["step_id"] == "link" assert result4["errors"] == {"base": "unknown"} mock_nanoleaf.return_value.authorize.side_effect = None mock_nanoleaf.return_value.get_info.side_effect = Exception() - result5 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {}, - ) + result5 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result5["type"] == "abort" assert result5["reason"] == "unknown" @@ -307,10 +290,7 @@ async def test_reauth(hass: HomeAssistant) -> None: assert result["type"] == "form" assert result["step_id"] == "link" - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {}, - ) + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result2["type"] == "abort" assert result2["reason"] == "reauth_successful" @@ -460,3 +440,42 @@ async def test_import_discovery_integration( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_ssdp_discovery(hass: HomeAssistant) -> None: + """Test SSDP discovery.""" + with patch( + "homeassistant.components.nanoleaf.config_flow.load_json", + return_value={}, + ), patch( + "homeassistant.components.nanoleaf.config_flow.Nanoleaf", + return_value=_mock_nanoleaf(TEST_HOST, TEST_TOKEN), + ), patch( + "homeassistant.components.nanoleaf.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data={ + "_host": TEST_HOST, + "nl-devicename": TEST_NAME, + "nl-deviceid": TEST_DEVICE_ID, + }, + ) + + assert result["type"] == "form" + assert result["errors"] is None + assert result["step_id"] == "link" + + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result2["type"] == "create_entry" + assert result2["title"] == TEST_NAME + assert result2["data"] == { + CONF_HOST: TEST_HOST, + CONF_TOKEN: TEST_TOKEN, + } + + await hass.async_block_till_done() + assert len(mock_setup_entry.mock_calls) == 1 From d41832de5926f6d0e4f9e27bd6f5b1d5d98076cc Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Sat, 2 Oct 2021 09:30:20 +0200 Subject: [PATCH 0034/1038] Get min and max color temperature for Nanoleaf light from library (#56863) --- homeassistant/components/nanoleaf/light.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py index 1bc1e4a69b9..414b2079485 100644 --- a/homeassistant/components/nanoleaf/light.py +++ b/homeassistant/components/nanoleaf/light.py @@ -1,6 +1,7 @@ """Support for Nanoleaf Lights.""" from __future__ import annotations +import math from typing import Any from aionanoleaf import Nanoleaf, Unavailable @@ -27,8 +28,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import color as color_util from homeassistant.util.color import ( + color_temperature_kelvin_to_mired as kelvin_to_mired, color_temperature_mired_to_kelvin as mired_to_kelvin, ) @@ -85,8 +86,10 @@ class NanoleafLight(LightEntity): model=self._nanoleaf.model, sw_version=self._nanoleaf.firmware_version, ) - self._attr_min_mireds = 154 - self._attr_max_mireds = 833 + self._attr_min_mireds = math.ceil( + 1000000 / self._nanoleaf.color_temperature_max + ) + self._attr_max_mireds = kelvin_to_mired(self._nanoleaf.color_temperature_min) @property def brightness(self) -> int: @@ -96,9 +99,7 @@ class NanoleafLight(LightEntity): @property def color_temp(self) -> int: """Return the current color temperature.""" - return color_util.color_temperature_kelvin_to_mired( - self._nanoleaf.color_temperature - ) + return kelvin_to_mired(self._nanoleaf.color_temperature) @property def effect(self) -> str | None: From da3cc25234800e17ff5fa2f685120b9c9dcca0f1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 2 Oct 2021 03:19:11 -0500 Subject: [PATCH 0035/1038] Add DHCP support for TPLink KL430, KP115 (#56932) --- homeassistant/components/tplink/manifest.json | 8 ++++++++ homeassistant/generated/dhcp.py | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index cfc9fce5213..6712da00d0e 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -13,6 +13,14 @@ "hostname": "ep*", "macaddress": "E848B8*" }, + { + "hostname": "k[lp]*", + "macaddress": "E848B8*" + }, + { + "hostname": "k[lp]*", + "macaddress": "909A4A*" + }, { "hostname": "hs*", "macaddress": "1C3BF3*" diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 370a87e2575..34b0a468fc1 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -269,6 +269,16 @@ DHCP = [ "hostname": "ep*", "macaddress": "E848B8*" }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "E848B8*" + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "909A4A*" + }, { "domain": "tplink", "hostname": "hs*", From 39d73ecc19179523760738d3943e69198b21167c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 2 Oct 2021 10:19:54 +0200 Subject: [PATCH 0036/1038] Upgrade watchdog to 2.1.6 (#56933) --- homeassistant/components/folder_watcher/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/folder_watcher/manifest.json b/homeassistant/components/folder_watcher/manifest.json index a8b084eb801..c243c0d45c8 100644 --- a/homeassistant/components/folder_watcher/manifest.json +++ b/homeassistant/components/folder_watcher/manifest.json @@ -2,7 +2,7 @@ "domain": "folder_watcher", "name": "Folder Watcher", "documentation": "https://www.home-assistant.io/integrations/folder_watcher", - "requirements": ["watchdog==2.1.5"], + "requirements": ["watchdog==2.1.6"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index ecc96c5a642..e8098973fc2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2403,7 +2403,7 @@ wallbox==0.4.4 waqiasync==1.0.0 # homeassistant.components.folder_watcher -watchdog==2.1.5 +watchdog==2.1.6 # homeassistant.components.waterfurnace waterfurnace==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a9b5b7c3513..e068ae0e0ac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1365,7 +1365,7 @@ wakeonlan==2.0.1 wallbox==0.4.4 # homeassistant.components.folder_watcher -watchdog==2.1.5 +watchdog==2.1.6 # homeassistant.components.whirlpool whirlpool-sixth-sense==0.15.1 From 7c805f048c32d1b92b53927be68f68b7009c8bdf Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 2 Oct 2021 04:20:17 -0600 Subject: [PATCH 0037/1038] Address beta review comments for WattTime (#56919) --- homeassistant/components/watttime/__init__.py | 11 +++--- .../components/watttime/config_flow.py | 16 ++++----- .../components/watttime/manifest.json | 4 --- homeassistant/components/watttime/sensor.py | 35 +++++-------------- 4 files changed, 19 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/watttime/__init__.py b/homeassistant/components/watttime/__init__.py index d376dd40db6..6d23182c011 100644 --- a/homeassistant/components/watttime/__init__.py +++ b/homeassistant/components/watttime/__init__.py @@ -27,16 +27,13 @@ PLATFORMS: list[str] = ["sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up WattTime from a config entry.""" - hass.data.setdefault(DOMAIN, {DATA_COORDINATOR: {}}) + hass.data.setdefault(DOMAIN, {entry.entry_id: {DATA_COORDINATOR: {}}}) session = aiohttp_client.async_get_clientsession(hass) try: client = await Client.async_login( - entry.data[CONF_USERNAME], - entry.data[CONF_PASSWORD], - session=session, - logger=LOGGER, + entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], session=session ) except WattTimeError as err: LOGGER.error("Error while authenticating with WattTime: %s", err) @@ -62,7 +59,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = coordinator + hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] = coordinator hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -73,6 +70,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][DATA_COORDINATOR].pop(entry.entry_id) + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/watttime/config_flow.py b/homeassistant/components/watttime/config_flow.py index a6c5dd422c2..6c523f64331 100644 --- a/homeassistant/components/watttime/config_flow.py +++ b/homeassistant/components/watttime/config_flow.py @@ -118,16 +118,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="location", data_schema=STEP_LOCATION_DATA_SCHEMA ) - if user_input[CONF_LOCATION_TYPE] == LOCATION_TYPE_COORDINATES: - return self.async_show_form( - step_id="coordinates", data_schema=STEP_COORDINATES_DATA_SCHEMA + if user_input[CONF_LOCATION_TYPE] == LOCATION_TYPE_HOME: + return await self.async_step_coordinates( + { + CONF_LATITUDE: self.hass.config.latitude, + CONF_LONGITUDE: self.hass.config.longitude, + } ) - return await self.async_step_coordinates( - { - CONF_LATITUDE: self.hass.config.latitude, - CONF_LONGITUDE: self.hass.config.longitude, - } - ) + return await self.async_step_coordinates() async def async_step_user( self, user_input: dict[str, Any] | None = None diff --git a/homeassistant/components/watttime/manifest.json b/homeassistant/components/watttime/manifest.json index d4000b6f6b1..85a32bce331 100644 --- a/homeassistant/components/watttime/manifest.json +++ b/homeassistant/components/watttime/manifest.json @@ -6,10 +6,6 @@ "requirements": [ "aiowatttime==0.1.1" ], - "ssdp": [], - "zeroconf": [], - "homekit": {}, - "dependencies": [], "codeowners": [ "@bachya" ], diff --git a/homeassistant/components/watttime/sensor.py b/homeassistant/components/watttime/sensor.py index 4453044e0d2..6a6d05701c4 100644 --- a/homeassistant/components/watttime/sensor.py +++ b/homeassistant/components/watttime/sensor.py @@ -1,7 +1,6 @@ """Support for WattTime sensors.""" from __future__ import annotations -from dataclasses import dataclass from typing import TYPE_CHECKING from homeassistant.components.sensor import ( @@ -36,40 +35,24 @@ ATTR_BALANCING_AUTHORITY = "balancing_authority" DEFAULT_ATTRIBUTION = "Pickup data provided by WattTime" -SENSOR_TYPE_REALTIME_EMISSIONS_MOER = "realtime_emissions_moer" -SENSOR_TYPE_REALTIME_EMISSIONS_PERCENT = "realtime_emissions_percent" - - -@dataclass -class RealtimeEmissionsSensorDescriptionMixin: - """Define an entity description mixin for realtime emissions sensors.""" - - data_key: str - - -@dataclass -class RealtimeEmissionsSensorEntityDescription( - SensorEntityDescription, RealtimeEmissionsSensorDescriptionMixin -): - """Describe a realtime emissions sensor.""" +SENSOR_TYPE_REALTIME_EMISSIONS_MOER = "moer" +SENSOR_TYPE_REALTIME_EMISSIONS_PERCENT = "percent" REALTIME_EMISSIONS_SENSOR_DESCRIPTIONS = ( - RealtimeEmissionsSensorEntityDescription( + SensorEntityDescription( key=SENSOR_TYPE_REALTIME_EMISSIONS_MOER, name="Marginal Operating Emissions Rate", icon="mdi:blur", native_unit_of_measurement=f"{MASS_POUNDS} CO2/MWh", state_class=STATE_CLASS_MEASUREMENT, - data_key="moer", ), - RealtimeEmissionsSensorEntityDescription( + SensorEntityDescription( key=SENSOR_TYPE_REALTIME_EMISSIONS_PERCENT, name="Relative Marginal Emissions Intensity", icon="mdi:blur", native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, - data_key="percent", ), ) @@ -78,12 +61,12 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up WattTime sensors based on a config entry.""" - coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] + coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] async_add_entities( [ RealtimeEmissionsSensor(coordinator, description) for description in REALTIME_EMISSIONS_SENSOR_DESCRIPTIONS - if description.data_key in coordinator.data + if description.key in coordinator.data ] ) @@ -91,12 +74,10 @@ async def async_setup_entry( class RealtimeEmissionsSensor(CoordinatorEntity, SensorEntity): """Define a realtime emissions sensor.""" - entity_description: RealtimeEmissionsSensorEntityDescription - def __init__( self, coordinator: DataUpdateCoordinator, - description: RealtimeEmissionsSensorEntityDescription, + description: SensorEntityDescription, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) @@ -119,4 +100,4 @@ class RealtimeEmissionsSensor(CoordinatorEntity, SensorEntity): @property def native_value(self) -> StateType: """Return the value reported by the sensor.""" - return self.coordinator.data[self.entity_description.data_key] + return self.coordinator.data[self.entity_description.key] From deea9ee22e4dba25a379e7481eea9cbeef71a4c1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 2 Oct 2021 05:47:28 -0500 Subject: [PATCH 0038/1038] Bump PyFlume to 0.6.5 to fix compat with new JWT (#56936) Changelog: https://github.com/ChrisMandich/PyFlume/compare/5476fd67cfc8be768c0ea810d248f39399e038d1...v0.6.5 --- homeassistant/components/flume/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flume/manifest.json b/homeassistant/components/flume/manifest.json index d689f5fb17f..cdad0dd3f0c 100644 --- a/homeassistant/components/flume/manifest.json +++ b/homeassistant/components/flume/manifest.json @@ -2,7 +2,7 @@ "domain": "flume", "name": "Flume", "documentation": "https://www.home-assistant.io/integrations/flume/", - "requirements": ["pyflume==0.5.5"], + "requirements": ["pyflume==0.6.5"], "codeowners": ["@ChrisMandich", "@bdraco"], "config_flow": true, "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index e8098973fc2..155c5410376 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1480,7 +1480,7 @@ pyfireservicerota==0.0.43 pyflic==2.0.3 # homeassistant.components.flume -pyflume==0.5.5 +pyflume==0.6.5 # homeassistant.components.flunearyou pyflunearyou==2.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e068ae0e0ac..1a3c20ffb65 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -856,7 +856,7 @@ pyfido==2.1.1 pyfireservicerota==0.0.43 # homeassistant.components.flume -pyflume==0.5.5 +pyflume==0.6.5 # homeassistant.components.flunearyou pyflunearyou==2.0.2 From a99d92cdb46def4469b290ab3c7a35333a39fffd Mon Sep 17 00:00:00 2001 From: Oliver <10700296+ol-iver@users.noreply.github.com> Date: Sat, 2 Oct 2021 13:10:54 +0200 Subject: [PATCH 0039/1038] Update denonavr codeowner (#56940) --- CODEOWNERS | 2 +- homeassistant/components/denonavr/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 515aec64774..70fb56f73f4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -114,7 +114,7 @@ homeassistant/components/debugpy/* @frenck homeassistant/components/deconz/* @Kane610 homeassistant/components/delijn/* @bollewolle @Emilv2 homeassistant/components/demo/* @home-assistant/core -homeassistant/components/denonavr/* @scarface-4711 @starkillerOG +homeassistant/components/denonavr/* @ol-iver @starkillerOG homeassistant/components/derivative/* @afaucogney homeassistant/components/device_automation/* @home-assistant/core homeassistant/components/devolo_home_control/* @2Fake @Shutgun diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index ce79d937264..1eb4cef9d85 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/denonavr", "requirements": ["denonavr==0.10.9"], - "codeowners": ["@scarface-4711", "@starkillerOG"], + "codeowners": ["@ol-iver", "@starkillerOG"], "ssdp": [ { "manufacturer": "Denon", From f42c2f5170e8cec1714d22fe1cb80b5e312085a0 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 2 Oct 2021 12:59:05 +0000 Subject: [PATCH 0040/1038] [ci skip] Translation update --- .../accuweather/translations/hu.json | 2 +- .../components/adax/translations/es.json | 9 +- .../components/adax/translations/hu.json | 2 +- .../components/adax/translations/id.json | 20 ++++ .../components/adguard/translations/he.json | 3 + .../components/adguard/translations/hu.json | 4 +- .../components/adguard/translations/id.json | 2 +- .../components/agent_dvr/translations/hu.json | 4 +- .../components/airthings/translations/ca.json | 21 ++++ .../components/airthings/translations/de.json | 21 ++++ .../components/airthings/translations/en.json | 21 ++++ .../components/airthings/translations/et.json | 21 ++++ .../components/airthings/translations/he.json | 20 ++++ .../components/airthings/translations/hu.json | 21 ++++ .../components/airthings/translations/it.json | 21 ++++ .../components/airthings/translations/nl.json | 21 ++++ .../components/airthings/translations/no.json | 21 ++++ .../components/airthings/translations/ru.json | 21 ++++ .../airthings/translations/zh-Hant.json | 21 ++++ .../components/airtouch4/translations/es.json | 6 +- .../components/airtouch4/translations/hu.json | 2 +- .../components/airtouch4/translations/id.json | 17 ++++ .../components/airvisual/translations/hu.json | 2 +- .../alarmdecoder/translations/hu.json | 2 +- .../components/almond/translations/hu.json | 6 +- .../components/almond/translations/id.json | 2 +- .../components/ambee/translations/es.json | 14 +++ .../components/ambee/translations/hu.json | 2 +- .../components/ambee/translations/id.json | 6 +- .../ambee/translations/sensor.id.json | 9 ++ .../amberelectric/translations/ca.json | 22 +++++ .../amberelectric/translations/de.json | 22 +++++ .../amberelectric/translations/et.json | 22 +++++ .../amberelectric/translations/hu.json | 22 +++++ .../amberelectric/translations/it.json | 22 +++++ .../amberelectric/translations/nl.json | 22 +++++ .../amberelectric/translations/no.json | 22 +++++ .../amberelectric/translations/ru.json | 22 +++++ .../amberelectric/translations/zh-Hant.json | 22 +++++ .../ambiclimate/translations/ca.json | 2 +- .../ambiclimate/translations/hu.json | 6 +- .../components/apple_tv/translations/hu.json | 8 +- .../components/apple_tv/translations/id.json | 2 +- .../components/arcam_fmj/translations/hu.json | 6 +- .../components/arcam_fmj/translations/id.json | 2 +- .../components/asuswrt/translations/hu.json | 2 +- .../components/asuswrt/translations/ru.json | 2 +- .../components/atag/translations/hu.json | 2 +- .../components/august/translations/ca.json | 2 +- .../components/august/translations/hu.json | 2 +- .../components/auth/translations/fi.json | 5 + .../components/auth/translations/hu.json | 8 +- .../automation/translations/hu.json | 2 +- .../components/awair/translations/ca.json | 2 +- .../components/awair/translations/hu.json | 2 +- .../components/axis/translations/hu.json | 6 +- .../azure_devops/translations/ca.json | 2 +- .../azure_devops/translations/id.json | 2 +- .../binary_sensor/translations/id.json | 3 + .../binary_sensor/translations/is.json | 2 +- .../components/blebox/translations/hu.json | 2 +- .../components/blebox/translations/id.json | 2 +- .../components/blink/translations/hu.json | 2 +- .../bmw_connected_drive/translations/ca.json | 2 +- .../components/bond/translations/es.json | 4 +- .../components/bond/translations/hu.json | 4 +- .../components/bond/translations/id.json | 2 +- .../components/bosch_shc/translations/es.json | 9 +- .../components/bosch_shc/translations/hu.json | 6 +- .../components/braviatv/translations/hu.json | 4 +- .../components/broadlink/translations/es.json | 2 +- .../components/broadlink/translations/hu.json | 10 +- .../components/brother/translations/hu.json | 6 +- .../components/brother/translations/id.json | 2 +- .../components/bsblan/translations/hu.json | 2 +- .../components/bsblan/translations/id.json | 2 +- .../buienradar/translations/id.json | 7 ++ .../components/canary/translations/id.json | 2 +- .../components/cast/translations/hu.json | 6 +- .../components/cast/translations/id.json | 6 +- .../components/cast/translations/nl.json | 2 +- .../cert_expiry/translations/hu.json | 6 +- .../components/climacell/translations/hu.json | 2 +- .../cloudflare/translations/he.json | 2 +- .../cloudflare/translations/id.json | 2 +- .../components/co2signal/translations/es.json | 4 + .../components/co2signal/translations/hu.json | 4 +- .../components/co2signal/translations/id.json | 30 ++++++ .../coolmaster/translations/hu.json | 2 +- .../coronavirus/translations/id.json | 3 +- .../crownstone/translations/ca.json | 96 +++++++++++++++++++ .../crownstone/translations/cs.json | 31 ++++++ .../crownstone/translations/de.json | 96 +++++++++++++++++++ .../crownstone/translations/en.json | 21 ++++ .../crownstone/translations/es.json | 69 +++++++++++++ .../crownstone/translations/et.json | 96 +++++++++++++++++++ .../crownstone/translations/he.json | 53 ++++++++++ .../crownstone/translations/hu.json | 96 +++++++++++++++++++ .../crownstone/translations/id.json | 38 ++++++++ .../crownstone/translations/it.json | 96 +++++++++++++++++++ .../crownstone/translations/ko.json | 16 ++++ .../crownstone/translations/nl.json | 96 +++++++++++++++++++ .../crownstone/translations/no.json | 96 +++++++++++++++++++ .../crownstone/translations/ru.json | 96 +++++++++++++++++++ .../crownstone/translations/zh-Hant.json | 96 +++++++++++++++++++ .../components/daikin/translations/hu.json | 4 +- .../components/deconz/translations/es.json | 2 +- .../components/deconz/translations/hu.json | 16 ++-- .../components/deconz/translations/id.json | 6 +- .../components/demo/translations/he.json | 3 + .../components/demo/translations/hu.json | 2 +- .../components/demo/translations/ro.json | 13 +++ .../components/denonavr/translations/hu.json | 2 +- .../components/denonavr/translations/id.json | 2 +- .../devolo_home_control/translations/ca.json | 2 +- .../devolo_home_control/translations/id.json | 10 +- .../devolo_home_control/translations/ko.json | 7 ++ .../components/dexcom/translations/ca.json | 2 +- .../dialogflow/translations/hu.json | 4 +- .../components/directv/translations/hu.json | 4 +- .../components/directv/translations/id.json | 2 +- .../components/dlna_dmr/translations/ca.json | 44 +++++++++ .../components/dlna_dmr/translations/de.json | 44 +++++++++ .../components/dlna_dmr/translations/et.json | 44 +++++++++ .../components/dlna_dmr/translations/hu.json | 44 +++++++++ .../components/dlna_dmr/translations/it.json | 44 +++++++++ .../components/dlna_dmr/translations/nl.json | 44 +++++++++ .../components/dlna_dmr/translations/no.json | 44 +++++++++ .../components/dlna_dmr/translations/ru.json | 44 +++++++++ .../dlna_dmr/translations/zh-Hant.json | 44 +++++++++ .../components/doorbird/translations/hu.json | 4 +- .../components/doorbird/translations/id.json | 2 +- .../components/dsmr/translations/ca.json | 2 +- .../components/dsmr/translations/hu.json | 2 +- .../components/dunehd/translations/hu.json | 2 +- .../components/elgato/translations/hu.json | 4 +- .../components/elgato/translations/id.json | 8 +- .../components/emonitor/translations/hu.json | 4 +- .../components/emonitor/translations/id.json | 2 +- .../emulated_roku/translations/hu.json | 4 +- .../components/energy/translations/el.json | 3 + .../enphase_envoy/translations/hu.json | 2 +- .../enphase_envoy/translations/id.json | 2 +- .../components/epson/translations/hu.json | 2 +- .../components/esphome/translations/ca.json | 16 +++- .../components/esphome/translations/cs.json | 3 +- .../components/esphome/translations/de.json | 16 +++- .../components/esphome/translations/en.json | 16 +++- .../components/esphome/translations/es.json | 16 +++- .../components/esphome/translations/et.json | 16 +++- .../components/esphome/translations/he.json | 8 +- .../components/esphome/translations/hu.json | 30 ++++-- .../components/esphome/translations/id.json | 5 +- .../components/esphome/translations/it.json | 16 +++- .../components/esphome/translations/nl.json | 16 +++- .../components/esphome/translations/no.json | 16 +++- .../components/esphome/translations/ru.json | 16 +++- .../esphome/translations/zh-Hant.json | 16 +++- .../components/ezviz/translations/ca.json | 2 +- .../components/ezviz/translations/hu.json | 2 +- .../fireservicerota/translations/ca.json | 2 +- .../fjaraskupan/translations/es.json | 8 ++ .../fjaraskupan/translations/id.json | 13 +++ .../flick_electric/translations/ca.json | 2 +- .../flick_electric/translations/he.json | 4 +- .../flick_electric/translations/id.json | 4 +- .../components/flipr/translations/es.json | 11 ++- .../components/flipr/translations/id.json | 20 ++++ .../components/flo/translations/hu.json | 2 +- .../components/flume/translations/ca.json | 2 +- .../forecast_solar/translations/es.json | 5 +- .../forecast_solar/translations/id.json | 4 +- .../forked_daapd/translations/hu.json | 6 +- .../forked_daapd/translations/id.json | 2 +- .../components/foscam/translations/hu.json | 2 +- .../components/freebox/translations/hu.json | 4 +- .../freedompro/translations/id.json | 7 ++ .../components/fritz/translations/es.json | 6 +- .../components/fritz/translations/hu.json | 4 +- .../components/fritz/translations/ko.json | 38 ++++++++ .../components/fritz/translations/ru.json | 2 +- .../components/fritzbox/translations/hu.json | 6 +- .../components/fritzbox/translations/id.json | 2 +- .../fritzbox_callmonitor/translations/hu.json | 2 +- .../fritzbox_callmonitor/translations/id.json | 2 +- .../garages_amsterdam/translations/es.json | 2 + .../components/geofency/translations/hu.json | 4 +- .../components/glances/translations/hu.json | 2 +- .../components/goalzero/translations/es.json | 1 + .../components/goalzero/translations/hu.json | 6 +- .../components/gogogate2/translations/id.json | 2 +- .../components/gpslogger/translations/hu.json | 4 +- .../components/gree/translations/hu.json | 2 +- .../components/gree/translations/nl.json | 2 +- .../growatt_server/translations/es.json | 1 + .../growatt_server/translations/id.json | 1 + .../components/guardian/translations/hu.json | 6 +- .../components/hangouts/translations/fi.json | 2 + .../components/hangouts/translations/hu.json | 2 +- .../components/harmony/translations/hu.json | 4 +- .../components/harmony/translations/id.json | 2 +- .../components/hassio/translations/he.json | 10 +- .../components/heos/translations/hu.json | 4 +- .../components/hive/translations/ca.json | 2 +- .../components/hive/translations/hu.json | 6 +- .../components/hlk_sw16/translations/hu.json | 2 +- .../home_connect/translations/hu.json | 2 +- .../home_plus_control/translations/ca.json | 2 +- .../home_plus_control/translations/hu.json | 4 +- .../components/homekit/translations/ca.json | 3 +- .../components/homekit/translations/de.json | 1 + .../components/homekit/translations/el.json | 11 +++ .../components/homekit/translations/es.json | 6 +- .../components/homekit/translations/et.json | 3 +- .../components/homekit/translations/he.json | 3 + .../components/homekit/translations/hu.json | 11 ++- .../components/homekit/translations/id.json | 8 +- .../components/homekit/translations/it.json | 3 +- .../components/homekit/translations/nl.json | 3 +- .../components/homekit/translations/no.json | 3 +- .../components/homekit/translations/ru.json | 5 +- .../homekit/translations/zh-Hant.json | 3 +- .../homekit_controller/translations/hu.json | 14 +-- .../homekit_controller/translations/id.json | 2 +- .../homematicip_cloud/translations/fi.json | 9 ++ .../homematicip_cloud/translations/hu.json | 2 +- .../components/honeywell/translations/es.json | 6 +- .../components/honeywell/translations/id.json | 15 +++ .../huawei_lte/translations/hu.json | 4 +- .../huawei_lte/translations/id.json | 4 +- .../components/hue/translations/hu.json | 12 +-- .../translations/hu.json | 2 +- .../hvv_departures/translations/hu.json | 2 +- .../components/hyperion/translations/hu.json | 8 +- .../components/ialarm/translations/hu.json | 2 +- .../components/iaqualink/translations/hu.json | 2 +- .../components/icloud/translations/ca.json | 2 +- .../components/icloud/translations/hu.json | 2 +- .../components/ifttt/translations/hu.json | 6 +- .../components/insteon/translations/ca.json | 2 +- .../components/insteon/translations/hu.json | 14 +-- .../components/ios/translations/hu.json | 2 +- .../components/ios/translations/nl.json | 2 +- .../components/iotawatt/translations/el.json | 3 +- .../components/iotawatt/translations/es.json | 3 +- .../components/iotawatt/translations/hu.json | 2 +- .../components/iotawatt/translations/id.json | 17 ++++ .../components/iotawatt/translations/nl.json | 9 ++ .../components/ipp/translations/hu.json | 8 +- .../components/ipp/translations/id.json | 2 +- .../components/isy994/translations/es.json | 2 +- .../components/isy994/translations/hu.json | 6 +- .../components/isy994/translations/id.json | 2 +- .../components/juicenet/translations/ca.json | 2 +- .../keenetic_ndms2/translations/ca.json | 2 +- .../keenetic_ndms2/translations/hu.json | 2 +- .../keenetic_ndms2/translations/id.json | 1 + .../keenetic_ndms2/translations/ru.json | 2 +- .../components/kmtronic/translations/hu.json | 2 +- .../components/kodi/translations/hu.json | 6 +- .../components/kodi/translations/id.json | 2 +- .../components/konnected/translations/hu.json | 6 +- .../components/konnected/translations/id.json | 2 +- .../kostal_plenticore/translations/hu.json | 2 +- .../components/kraken/translations/es.json | 6 +- .../components/kraken/translations/hu.json | 2 +- .../components/kraken/translations/id.json | 12 +++ .../components/kraken/translations/nl.json | 2 +- .../components/kulersky/translations/hu.json | 2 +- .../components/kulersky/translations/nl.json | 2 +- .../components/life360/translations/ca.json | 2 +- .../components/lifx/translations/hu.json | 2 +- .../litterrobot/translations/ca.json | 2 +- .../litterrobot/translations/hu.json | 2 +- .../components/local_ip/translations/hu.json | 2 +- .../components/local_ip/translations/nl.json | 2 +- .../components/locative/translations/hu.json | 4 +- .../components/locative/translations/nl.json | 2 +- .../logi_circle/translations/ca.json | 2 +- .../logi_circle/translations/hu.json | 6 +- .../lutron_caseta/translations/es.json | 4 +- .../lutron_caseta/translations/hu.json | 4 +- .../lutron_caseta/translations/id.json | 2 +- .../components/lyric/translations/hu.json | 2 +- .../components/lyric/translations/id.json | 6 +- .../components/mailgun/translations/hu.json | 6 +- .../components/mazda/translations/ca.json | 2 +- .../components/mazda/translations/hu.json | 2 +- .../meteo_france/translations/hu.json | 2 +- .../meteoclimatic/translations/es.json | 4 + .../meteoclimatic/translations/id.json | 11 +++ .../components/mikrotik/translations/hu.json | 2 +- .../components/mikrotik/translations/ru.json | 2 +- .../components/mill/translations/ca.json | 2 +- .../minecraft_server/translations/hu.json | 4 +- .../mobile_app/translations/hu.json | 4 +- .../modem_callerid/translations/ca.json | 26 +++++ .../modem_callerid/translations/cs.json | 19 ++++ .../modem_callerid/translations/de.json | 26 +++++ .../modem_callerid/translations/en.json | 12 +-- .../modem_callerid/translations/es.json | 16 ++++ .../modem_callerid/translations/et.json | 26 +++++ .../modem_callerid/translations/he.json | 19 ++++ .../modem_callerid/translations/hu.json | 26 +++++ .../modem_callerid/translations/id.json | 20 ++++ .../modem_callerid/translations/it.json | 26 +++++ .../modem_callerid/translations/nl.json | 26 +++++ .../modem_callerid/translations/no.json | 26 +++++ .../modem_callerid/translations/ru.json | 26 +++++ .../modem_callerid/translations/zh-Hant.json | 26 +++++ .../modern_forms/translations/es.json | 10 ++ .../modern_forms/translations/hu.json | 8 +- .../modern_forms/translations/id.json | 22 +++++ .../modern_forms/translations/nl.json | 2 +- .../motion_blinds/translations/hu.json | 2 +- .../components/motioneye/translations/hu.json | 4 +- .../components/motioneye/translations/ko.json | 25 +++++ .../components/mqtt/translations/es.json | 1 + .../components/mqtt/translations/fi.json | 3 + .../components/mqtt/translations/he.json | 34 +++++++ .../components/mqtt/translations/hu.json | 10 +- .../components/mqtt/translations/id.json | 5 +- .../components/mutesync/translations/hu.json | 2 +- .../components/mutesync/translations/id.json | 15 +++ .../components/myq/translations/id.json | 8 +- .../components/nam/translations/hu.json | 4 +- .../components/nanoleaf/translations/el.json | 1 + .../components/nanoleaf/translations/es.json | 8 ++ .../components/nanoleaf/translations/hu.json | 4 +- .../components/nanoleaf/translations/id.json | 22 +++++ .../components/neato/translations/es.json | 2 +- .../components/neato/translations/hu.json | 4 +- .../components/neato/translations/nl.json | 2 +- .../components/nest/translations/fi.json | 3 + .../components/nest/translations/he.json | 5 +- .../components/nest/translations/hu.json | 2 +- .../components/netatmo/translations/hu.json | 2 +- .../components/netgear/translations/ca.json | 34 +++++++ .../components/netgear/translations/cs.json | 15 +++ .../components/netgear/translations/de.json | 34 +++++++ .../components/netgear/translations/en.json | 18 ++-- .../components/netgear/translations/es.json | 18 ++++ .../components/netgear/translations/et.json | 34 +++++++ .../components/netgear/translations/he.json | 26 +++++ .../components/netgear/translations/hu.json | 34 +++++++ .../components/netgear/translations/id.json | 18 ++++ .../components/netgear/translations/it.json | 34 +++++++ .../components/netgear/translations/nl.json | 34 +++++++ .../components/netgear/translations/no.json | 34 +++++++ .../netgear/translations/pt-BR.json | 28 ++++++ .../components/netgear/translations/ru.json | 34 +++++++ .../netgear/translations/zh-Hant.json | 34 +++++++ .../nfandroidtv/translations/es.json | 7 +- .../nfandroidtv/translations/hu.json | 2 +- .../nfandroidtv/translations/id.json | 10 ++ .../nightscout/translations/hu.json | 2 +- .../nightscout/translations/id.json | 2 +- .../nightscout/translations/no.json | 2 +- .../nmap_tracker/translations/es.json | 1 + .../nmap_tracker/translations/hu.json | 8 +- .../nmap_tracker/translations/id.json | 1 + .../nmap_tracker/translations/nl.json | 1 + .../nmap_tracker/translations/ru.json | 2 +- .../components/notion/translations/ca.json | 13 ++- .../components/notion/translations/de.json | 13 ++- .../components/notion/translations/en.json | 1 + .../components/notion/translations/et.json | 13 ++- .../components/notion/translations/he.json | 12 ++- .../components/notion/translations/hu.json | 13 ++- .../components/notion/translations/it.json | 13 ++- .../components/notion/translations/nl.json | 13 ++- .../components/notion/translations/no.json | 13 ++- .../components/notion/translations/ru.json | 13 ++- .../notion/translations/zh-Hant.json | 13 ++- .../components/nuheat/translations/de.json | 2 +- .../components/nuki/translations/hu.json | 2 +- .../components/nut/translations/hu.json | 2 +- .../components/nws/translations/hu.json | 2 +- .../components/nzbget/translations/hu.json | 2 +- .../components/nzbget/translations/id.json | 2 +- .../ondilo_ico/translations/hu.json | 2 +- .../components/onewire/translations/hu.json | 2 +- .../components/onvif/translations/hu.json | 14 +-- .../components/onvif/translations/id.json | 9 ++ .../opengarage/translations/ca.json | 22 +++++ .../opengarage/translations/de.json | 22 +++++ .../opengarage/translations/en.json | 22 +++++ .../opengarage/translations/es.json | 11 +++ .../opengarage/translations/et.json | 22 +++++ .../opengarage/translations/hu.json | 22 +++++ .../opengarage/translations/it.json | 22 +++++ .../opengarage/translations/nl.json | 22 +++++ .../opengarage/translations/no.json | 22 +++++ .../opengarage/translations/ru.json | 22 +++++ .../opengarage/translations/zh-Hant.json | 22 +++++ .../components/openuv/translations/es.json | 4 + .../components/openuv/translations/nl.json | 4 + .../openweathermap/translations/hu.json | 2 +- .../ovo_energy/translations/ca.json | 2 +- .../ovo_energy/translations/id.json | 2 +- .../components/owntracks/translations/hu.json | 4 +- .../components/ozw/translations/ca.json | 2 +- .../components/ozw/translations/hu.json | 6 +- .../p1_monitor/translations/es.json | 3 +- .../p1_monitor/translations/hu.json | 2 +- .../p1_monitor/translations/id.json | 16 ++++ .../panasonic_viera/translations/hu.json | 4 +- .../philips_js/translations/hu.json | 2 +- .../components/pi_hole/translations/hu.json | 2 +- .../components/picnic/translations/id.json | 5 + .../components/picnic/translations/ko.json | 21 ++++ .../components/plaato/translations/ca.json | 2 +- .../components/plaato/translations/es.json | 2 +- .../components/plaato/translations/hu.json | 6 +- .../components/plaato/translations/nl.json | 2 +- .../components/plant/translations/hu.json | 2 +- .../components/plex/translations/hu.json | 10 +- .../components/plugwise/translations/id.json | 2 +- .../plum_lightpad/translations/ca.json | 2 +- .../components/point/translations/he.json | 3 +- .../components/point/translations/hu.json | 8 +- .../components/point/translations/nl.json | 2 +- .../components/poolsense/translations/hu.json | 2 +- .../components/poolsense/translations/nl.json | 2 +- .../components/powerwall/translations/id.json | 2 +- .../components/profiler/translations/hu.json | 2 +- .../components/profiler/translations/nl.json | 2 +- .../progettihwsw/translations/he.json | 28 +++--- .../progettihwsw/translations/hu.json | 2 +- .../components/prosegur/translations/el.json | 11 +++ .../components/prosegur/translations/es.json | 10 +- .../components/prosegur/translations/id.json | 27 ++++++ .../components/ps4/translations/he.json | 3 + .../components/ps4/translations/hu.json | 4 +- .../pvpc_hourly_pricing/translations/hu.json | 2 +- .../pvpc_hourly_pricing/translations/id.json | 6 +- .../rainforest_eagle/translations/es.json | 7 +- .../rainforest_eagle/translations/hu.json | 2 +- .../rainforest_eagle/translations/id.json | 19 ++++ .../rainmachine/translations/hu.json | 2 +- .../components/renault/translations/ca.json | 2 +- .../components/renault/translations/cs.json | 9 +- .../components/renault/translations/el.json | 9 ++ .../components/renault/translations/es.json | 16 +++- .../components/renault/translations/hu.json | 2 +- .../components/renault/translations/id.json | 26 +++++ .../components/renault/translations/nl.json | 9 +- .../components/rfxtrx/translations/ca.json | 12 ++- .../components/rfxtrx/translations/de.json | 10 ++ .../components/rfxtrx/translations/en.json | 25 +++-- .../components/rfxtrx/translations/es.json | 10 ++ .../components/rfxtrx/translations/et.json | 10 ++ .../components/rfxtrx/translations/hu.json | 12 ++- .../components/rfxtrx/translations/it.json | 10 ++ .../components/rfxtrx/translations/nl.json | 10 ++ .../components/rfxtrx/translations/no.json | 10 ++ .../components/rfxtrx/translations/ru.json | 10 ++ .../rfxtrx/translations/zh-Hant.json | 10 ++ .../components/risco/translations/hu.json | 4 +- .../components/roku/translations/hu.json | 10 +- .../components/roku/translations/id.json | 2 +- .../components/roomba/translations/es.json | 2 +- .../components/roomba/translations/hu.json | 10 +- .../components/roomba/translations/id.json | 10 +- .../components/roon/translations/hu.json | 8 +- .../components/rpi_power/translations/hu.json | 4 +- .../components/rpi_power/translations/nl.json | 2 +- .../ruckus_unleashed/translations/hu.json | 2 +- .../components/samsungtv/translations/bg.json | 7 ++ .../components/samsungtv/translations/ca.json | 1 + .../components/samsungtv/translations/de.json | 1 + .../components/samsungtv/translations/en.json | 4 +- .../components/samsungtv/translations/es.json | 12 ++- .../components/samsungtv/translations/et.json | 1 + .../components/samsungtv/translations/hu.json | 11 ++- .../components/samsungtv/translations/id.json | 13 ++- .../components/samsungtv/translations/it.json | 1 + .../components/samsungtv/translations/nl.json | 1 + .../components/samsungtv/translations/no.json | 1 + .../components/samsungtv/translations/ru.json | 1 + .../samsungtv/translations/zh-Hant.json | 1 + .../screenlogic/translations/hu.json | 2 +- .../screenlogic/translations/id.json | 2 +- .../components/sensor/translations/el.json | 6 +- .../components/sensor/translations/id.json | 20 ++++ .../components/sentry/translations/hu.json | 2 +- .../components/sharkiq/translations/ca.json | 2 +- .../components/shelly/translations/ca.json | 8 +- .../components/shelly/translations/cs.json | 3 +- .../components/shelly/translations/de.json | 8 +- .../components/shelly/translations/en.json | 10 +- .../components/shelly/translations/es.json | 8 +- .../components/shelly/translations/et.json | 8 +- .../components/shelly/translations/he.json | 28 +++++- .../components/shelly/translations/hu.json | 12 ++- .../components/shelly/translations/id.json | 2 + .../components/shelly/translations/it.json | 8 +- .../components/shelly/translations/nl.json | 8 +- .../components/shelly/translations/no.json | 8 +- .../components/shelly/translations/ru.json | 10 +- .../shelly/translations/zh-Hant.json | 8 +- .../shopping_list/translations/hu.json | 2 +- .../components/sia/translations/es.json | 13 ++- .../components/sia/translations/id.json | 50 ++++++++++ .../simplisafe/translations/hu.json | 2 +- .../simplisafe/translations/id.json | 2 +- .../components/sma/translations/hu.json | 2 +- .../components/smappee/translations/hu.json | 6 +- .../components/smappee/translations/id.json | 2 +- .../smartthings/translations/hu.json | 8 +- .../components/smarttub/translations/ca.json | 2 +- .../components/smarttub/translations/hu.json | 4 +- .../components/smarttub/translations/id.json | 1 + .../components/smarttub/translations/ko.json | 3 + .../components/solarlog/translations/hu.json | 2 +- .../components/soma/translations/hu.json | 2 +- .../components/somfy/translations/hu.json | 2 +- .../somfy_mylink/translations/hu.json | 2 +- .../somfy_mylink/translations/id.json | 2 +- .../components/sonarr/translations/hu.json | 2 +- .../components/sonarr/translations/id.json | 2 +- .../components/songpal/translations/hu.json | 2 +- .../components/songpal/translations/id.json | 2 +- .../components/sonos/translations/he.json | 1 + .../components/sonos/translations/hu.json | 2 +- .../components/sonos/translations/id.json | 1 + .../speedtestdotnet/translations/hu.json | 4 +- .../speedtestdotnet/translations/nl.json | 2 +- .../components/spotify/translations/hu.json | 2 +- .../squeezebox/translations/hu.json | 4 +- .../squeezebox/translations/id.json | 2 +- .../components/subaru/translations/ca.json | 2 +- .../surepetcare/translations/ca.json | 20 ++++ .../surepetcare/translations/cs.json | 20 ++++ .../surepetcare/translations/de.json | 20 ++++ .../surepetcare/translations/en.json | 20 ++++ .../surepetcare/translations/es.json | 20 ++++ .../surepetcare/translations/et.json | 20 ++++ .../surepetcare/translations/he.json | 20 ++++ .../surepetcare/translations/hu.json | 20 ++++ .../surepetcare/translations/id.json | 20 ++++ .../surepetcare/translations/it.json | 20 ++++ .../surepetcare/translations/nl.json | 20 ++++ .../surepetcare/translations/no.json | 20 ++++ .../surepetcare/translations/pt-BR.json | 20 ++++ .../surepetcare/translations/ru.json | 20 ++++ .../surepetcare/translations/zh-Hant.json | 20 ++++ .../components/switch/translations/he.json | 15 +++ .../components/switchbot/translations/ca.json | 37 +++++++ .../components/switchbot/translations/cs.json | 14 +++ .../components/switchbot/translations/de.json | 37 +++++++ .../components/switchbot/translations/en.json | 17 ++-- .../components/switchbot/translations/es.json | 35 +++++++ .../components/switchbot/translations/et.json | 37 +++++++ .../components/switchbot/translations/he.json | 33 +++++++ .../components/switchbot/translations/hu.json | 39 ++++++++ .../components/switchbot/translations/id.json | 35 +++++++ .../components/switchbot/translations/it.json | 37 +++++++ .../components/switchbot/translations/nl.json | 37 +++++++ .../components/switchbot/translations/no.json | 37 +++++++ .../components/switchbot/translations/ro.json | 7 ++ .../components/switchbot/translations/ru.json | 37 +++++++ .../switchbot/translations/zh-Hant.json | 37 +++++++ .../switcher_kis/translations/es.json | 13 +++ .../switcher_kis/translations/hu.json | 2 +- .../switcher_kis/translations/id.json | 13 +++ .../switcher_kis/translations/nl.json | 2 +- .../components/syncthru/translations/id.json | 2 +- .../synology_dsm/translations/es.json | 16 +++- .../synology_dsm/translations/hu.json | 11 ++- .../synology_dsm/translations/id.json | 17 +++- .../synology_dsm/translations/it.json | 7 ++ .../synology_dsm/translations/nl.json | 7 ++ .../system_bridge/translations/hu.json | 2 +- .../components/tasmota/translations/hu.json | 4 +- .../tellduslive/translations/he.json | 3 +- .../tellduslive/translations/hu.json | 6 +- .../components/tibber/translations/hu.json | 2 +- .../components/tile/translations/ca.json | 2 +- .../components/toon/translations/he.json | 3 +- .../components/toon/translations/hu.json | 4 +- .../totalconnect/translations/ca.json | 2 +- .../totalconnect/translations/es.json | 2 +- .../totalconnect/translations/hu.json | 2 +- .../totalconnect/translations/id.json | 2 +- .../components/tplink/translations/ca.json | 19 ++++ .../components/tplink/translations/de.json | 19 ++++ .../components/tplink/translations/en.json | 6 +- .../components/tplink/translations/et.json | 19 ++++ .../components/tplink/translations/hu.json | 21 +++- .../components/tplink/translations/it.json | 19 ++++ .../components/tplink/translations/nl.json | 19 ++++ .../components/tplink/translations/no.json | 19 ++++ .../components/tplink/translations/ru.json | 19 ++++ .../tplink/translations/zh-Hant.json | 19 ++++ .../components/traccar/translations/hu.json | 4 +- .../components/tractive/translations/es.json | 11 ++- .../components/tradfri/translations/fi.json | 3 + .../components/tradfri/translations/hu.json | 6 +- .../transmission/translations/hu.json | 4 +- .../components/tuya/translations/af.json | 8 ++ .../components/tuya/translations/ca.json | 79 +++++++++++++++ .../components/tuya/translations/cs.json | 60 ++++++++++++ .../components/tuya/translations/de.json | 79 +++++++++++++++ .../components/tuya/translations/en.json | 74 +++++++++++--- .../components/tuya/translations/es.json | 79 +++++++++++++++ .../components/tuya/translations/et.json | 79 +++++++++++++++ .../components/tuya/translations/fi.json | 17 ++++ .../components/tuya/translations/fr.json | 65 +++++++++++++ .../components/tuya/translations/he.json | 30 ++++++ .../components/tuya/translations/hu.json | 65 +++++++++++++ .../components/tuya/translations/id.json | 65 +++++++++++++ .../components/tuya/translations/it.json | 79 +++++++++++++++ .../components/tuya/translations/ka.json | 37 +++++++ .../components/tuya/translations/ko.json | 65 +++++++++++++ .../components/tuya/translations/lb.json | 59 ++++++++++++ .../components/tuya/translations/nl.json | 78 +++++++++++++++ .../components/tuya/translations/no.json | 65 +++++++++++++ .../components/tuya/translations/pl.json | 65 +++++++++++++ .../components/tuya/translations/pt-BR.json | 17 ++++ .../components/tuya/translations/pt.json | 25 +++++ .../components/tuya/translations/ru.json | 79 +++++++++++++++ .../components/tuya/translations/sl.json | 11 +++ .../components/tuya/translations/sv.json | 17 ++++ .../components/tuya/translations/tr.json | 60 ++++++++++++ .../components/tuya/translations/uk.json | 63 ++++++++++++ .../components/tuya/translations/zh-Hans.json | 71 ++++++++++---- .../components/tuya/translations/zh-Hant.json | 79 +++++++++++++++ .../components/twilio/translations/hu.json | 6 +- .../components/twilio/translations/nl.json | 2 +- .../components/twinkly/translations/hu.json | 2 +- .../components/unifi/translations/hu.json | 14 +-- .../components/unifi/translations/id.json | 2 +- .../components/unifi/translations/ru.json | 2 +- .../components/updater/translations/hu.json | 2 +- .../components/upnp/translations/fi.json | 3 + .../components/upnp/translations/hu.json | 2 +- .../components/upnp/translations/id.json | 2 +- .../uptimerobot/translations/ca.json | 2 +- .../uptimerobot/translations/es.json | 12 +-- .../uptimerobot/translations/id.json | 26 +++++ .../components/vera/translations/hu.json | 12 +-- .../components/verisure/translations/ca.json | 2 +- .../components/verisure/translations/hu.json | 2 +- .../components/vilfo/translations/hu.json | 2 +- .../components/vizio/translations/hu.json | 6 +- .../components/volumio/translations/hu.json | 4 +- .../components/wallbox/translations/es.json | 8 +- .../components/wallbox/translations/id.json | 6 +- .../components/watttime/translations/ca.json | 34 +++++++ .../components/watttime/translations/cs.json | 30 ++++++ .../components/watttime/translations/de.json | 34 +++++++ .../components/watttime/translations/es.json | 34 +++++++ .../components/watttime/translations/et.json | 34 +++++++ .../components/watttime/translations/he.json | 30 ++++++ .../components/watttime/translations/hu.json | 34 +++++++ .../components/watttime/translations/id.json | 26 +++++ .../components/watttime/translations/it.json | 34 +++++++ .../components/watttime/translations/nl.json | 34 +++++++ .../components/watttime/translations/no.json | 34 +++++++ .../components/watttime/translations/ru.json | 34 +++++++ .../watttime/translations/zh-Hant.json | 34 +++++++ .../waze_travel_time/translations/id.json | 1 + .../components/wemo/translations/hu.json | 2 +- .../components/wemo/translations/id.json | 5 + .../components/whirlpool/translations/ca.json | 17 ++++ .../components/whirlpool/translations/cs.json | 17 ++++ .../components/whirlpool/translations/de.json | 17 ++++ .../components/whirlpool/translations/en.json | 7 +- .../components/whirlpool/translations/es.json | 17 ++++ .../components/whirlpool/translations/et.json | 17 ++++ .../components/whirlpool/translations/he.json | 17 ++++ .../components/whirlpool/translations/hu.json | 17 ++++ .../components/whirlpool/translations/id.json | 17 ++++ .../components/whirlpool/translations/it.json | 17 ++++ .../components/whirlpool/translations/nl.json | 17 ++++ .../components/whirlpool/translations/no.json | 17 ++++ .../whirlpool/translations/pt-BR.json | 17 ++++ .../components/whirlpool/translations/ru.json | 17 ++++ .../whirlpool/translations/zh-Hant.json | 17 ++++ .../components/wilight/translations/hu.json | 2 +- .../components/wilight/translations/id.json | 2 +- .../components/withings/translations/ca.json | 2 +- .../components/withings/translations/hu.json | 6 +- .../components/withings/translations/id.json | 2 +- .../components/wled/translations/hu.json | 8 +- .../components/wled/translations/id.json | 2 +- .../components/xbox/translations/hu.json | 2 +- .../xiaomi_aqara/translations/hu.json | 2 +- .../xiaomi_aqara/translations/id.json | 2 +- .../xiaomi_miio/translations/es.json | 7 +- .../xiaomi_miio/translations/hu.json | 4 +- .../xiaomi_miio/translations/id.json | 5 +- .../xiaomi_miio/translations/select.id.json | 9 ++ .../yale_smart_alarm/translations/ca.json | 2 +- .../yale_smart_alarm/translations/el.json | 11 +++ .../yale_smart_alarm/translations/es.json | 12 +-- .../yale_smart_alarm/translations/id.json | 28 ++++++ .../yamaha_musiccast/translations/es.json | 4 + .../yamaha_musiccast/translations/hu.json | 4 +- .../yamaha_musiccast/translations/nl.json | 2 +- .../components/yeelight/translations/es.json | 2 +- .../components/yeelight/translations/hu.json | 8 +- .../components/yeelight/translations/id.json | 4 +- .../components/youless/translations/es.json | 2 +- .../components/youless/translations/hu.json | 2 +- .../components/youless/translations/id.json | 15 +++ .../components/zerproc/translations/hu.json | 2 +- .../components/zerproc/translations/nl.json | 2 +- .../components/zha/translations/es.json | 3 +- .../components/zha/translations/hu.json | 2 +- .../components/zha/translations/id.json | 10 +- .../zoneminder/translations/hu.json | 2 +- .../components/zwave/translations/ca.json | 2 +- .../components/zwave/translations/hu.json | 6 +- .../components/zwave_js/translations/ca.json | 21 +++- .../components/zwave_js/translations/de.json | 17 ++++ .../components/zwave_js/translations/el.json | 1 + .../components/zwave_js/translations/en.json | 5 +- .../components/zwave_js/translations/es.json | 6 ++ .../components/zwave_js/translations/et.json | 17 ++++ .../components/zwave_js/translations/hu.json | 17 +++- .../components/zwave_js/translations/id.json | 15 ++- .../components/zwave_js/translations/it.json | 17 ++++ .../components/zwave_js/translations/nl.json | 11 ++- .../components/zwave_js/translations/no.json | 9 ++ .../components/zwave_js/translations/ru.json | 17 ++++ .../zwave_js/translations/zh-Hant.json | 17 ++++ 728 files changed, 8458 insertions(+), 822 deletions(-) create mode 100644 homeassistant/components/adax/translations/id.json create mode 100644 homeassistant/components/airthings/translations/ca.json create mode 100644 homeassistant/components/airthings/translations/de.json create mode 100644 homeassistant/components/airthings/translations/en.json create mode 100644 homeassistant/components/airthings/translations/et.json create mode 100644 homeassistant/components/airthings/translations/he.json create mode 100644 homeassistant/components/airthings/translations/hu.json create mode 100644 homeassistant/components/airthings/translations/it.json create mode 100644 homeassistant/components/airthings/translations/nl.json create mode 100644 homeassistant/components/airthings/translations/no.json create mode 100644 homeassistant/components/airthings/translations/ru.json create mode 100644 homeassistant/components/airthings/translations/zh-Hant.json create mode 100644 homeassistant/components/airtouch4/translations/id.json create mode 100644 homeassistant/components/ambee/translations/sensor.id.json create mode 100644 homeassistant/components/amberelectric/translations/ca.json create mode 100644 homeassistant/components/amberelectric/translations/de.json create mode 100644 homeassistant/components/amberelectric/translations/et.json create mode 100644 homeassistant/components/amberelectric/translations/hu.json create mode 100644 homeassistant/components/amberelectric/translations/it.json create mode 100644 homeassistant/components/amberelectric/translations/nl.json create mode 100644 homeassistant/components/amberelectric/translations/no.json create mode 100644 homeassistant/components/amberelectric/translations/ru.json create mode 100644 homeassistant/components/amberelectric/translations/zh-Hant.json create mode 100644 homeassistant/components/co2signal/translations/id.json create mode 100644 homeassistant/components/crownstone/translations/ca.json create mode 100644 homeassistant/components/crownstone/translations/cs.json create mode 100644 homeassistant/components/crownstone/translations/de.json create mode 100644 homeassistant/components/crownstone/translations/es.json create mode 100644 homeassistant/components/crownstone/translations/et.json create mode 100644 homeassistant/components/crownstone/translations/he.json create mode 100644 homeassistant/components/crownstone/translations/hu.json create mode 100644 homeassistant/components/crownstone/translations/id.json create mode 100644 homeassistant/components/crownstone/translations/it.json create mode 100644 homeassistant/components/crownstone/translations/ko.json create mode 100644 homeassistant/components/crownstone/translations/nl.json create mode 100644 homeassistant/components/crownstone/translations/no.json create mode 100644 homeassistant/components/crownstone/translations/ru.json create mode 100644 homeassistant/components/crownstone/translations/zh-Hant.json create mode 100644 homeassistant/components/demo/translations/he.json create mode 100644 homeassistant/components/demo/translations/ro.json create mode 100644 homeassistant/components/dlna_dmr/translations/ca.json create mode 100644 homeassistant/components/dlna_dmr/translations/de.json create mode 100644 homeassistant/components/dlna_dmr/translations/et.json create mode 100644 homeassistant/components/dlna_dmr/translations/hu.json create mode 100644 homeassistant/components/dlna_dmr/translations/it.json create mode 100644 homeassistant/components/dlna_dmr/translations/nl.json create mode 100644 homeassistant/components/dlna_dmr/translations/no.json create mode 100644 homeassistant/components/dlna_dmr/translations/ru.json create mode 100644 homeassistant/components/dlna_dmr/translations/zh-Hant.json create mode 100644 homeassistant/components/energy/translations/el.json create mode 100644 homeassistant/components/fjaraskupan/translations/es.json create mode 100644 homeassistant/components/fjaraskupan/translations/id.json create mode 100644 homeassistant/components/flipr/translations/id.json create mode 100644 homeassistant/components/fritz/translations/ko.json create mode 100644 homeassistant/components/homekit/translations/el.json create mode 100644 homeassistant/components/honeywell/translations/id.json create mode 100644 homeassistant/components/iotawatt/translations/id.json create mode 100644 homeassistant/components/kraken/translations/id.json create mode 100644 homeassistant/components/meteoclimatic/translations/id.json create mode 100644 homeassistant/components/modem_callerid/translations/ca.json create mode 100644 homeassistant/components/modem_callerid/translations/cs.json create mode 100644 homeassistant/components/modem_callerid/translations/de.json create mode 100644 homeassistant/components/modem_callerid/translations/es.json create mode 100644 homeassistant/components/modem_callerid/translations/et.json create mode 100644 homeassistant/components/modem_callerid/translations/he.json create mode 100644 homeassistant/components/modem_callerid/translations/hu.json create mode 100644 homeassistant/components/modem_callerid/translations/id.json create mode 100644 homeassistant/components/modem_callerid/translations/it.json create mode 100644 homeassistant/components/modem_callerid/translations/nl.json create mode 100644 homeassistant/components/modem_callerid/translations/no.json create mode 100644 homeassistant/components/modem_callerid/translations/ru.json create mode 100644 homeassistant/components/modem_callerid/translations/zh-Hant.json create mode 100644 homeassistant/components/modern_forms/translations/id.json create mode 100644 homeassistant/components/motioneye/translations/ko.json create mode 100644 homeassistant/components/mutesync/translations/id.json create mode 100644 homeassistant/components/nanoleaf/translations/id.json create mode 100644 homeassistant/components/netgear/translations/ca.json create mode 100644 homeassistant/components/netgear/translations/cs.json create mode 100644 homeassistant/components/netgear/translations/de.json create mode 100644 homeassistant/components/netgear/translations/es.json create mode 100644 homeassistant/components/netgear/translations/et.json create mode 100644 homeassistant/components/netgear/translations/he.json create mode 100644 homeassistant/components/netgear/translations/hu.json create mode 100644 homeassistant/components/netgear/translations/id.json create mode 100644 homeassistant/components/netgear/translations/it.json create mode 100644 homeassistant/components/netgear/translations/nl.json create mode 100644 homeassistant/components/netgear/translations/no.json create mode 100644 homeassistant/components/netgear/translations/pt-BR.json create mode 100644 homeassistant/components/netgear/translations/ru.json create mode 100644 homeassistant/components/netgear/translations/zh-Hant.json create mode 100644 homeassistant/components/nfandroidtv/translations/id.json create mode 100644 homeassistant/components/opengarage/translations/ca.json create mode 100644 homeassistant/components/opengarage/translations/de.json create mode 100644 homeassistant/components/opengarage/translations/en.json create mode 100644 homeassistant/components/opengarage/translations/es.json create mode 100644 homeassistant/components/opengarage/translations/et.json create mode 100644 homeassistant/components/opengarage/translations/hu.json create mode 100644 homeassistant/components/opengarage/translations/it.json create mode 100644 homeassistant/components/opengarage/translations/nl.json create mode 100644 homeassistant/components/opengarage/translations/no.json create mode 100644 homeassistant/components/opengarage/translations/ru.json create mode 100644 homeassistant/components/opengarage/translations/zh-Hant.json create mode 100644 homeassistant/components/p1_monitor/translations/id.json create mode 100644 homeassistant/components/picnic/translations/ko.json create mode 100644 homeassistant/components/prosegur/translations/el.json create mode 100644 homeassistant/components/prosegur/translations/id.json create mode 100644 homeassistant/components/rainforest_eagle/translations/id.json create mode 100644 homeassistant/components/renault/translations/el.json create mode 100644 homeassistant/components/renault/translations/id.json create mode 100644 homeassistant/components/samsungtv/translations/bg.json create mode 100644 homeassistant/components/sia/translations/id.json create mode 100644 homeassistant/components/surepetcare/translations/ca.json create mode 100644 homeassistant/components/surepetcare/translations/cs.json create mode 100644 homeassistant/components/surepetcare/translations/de.json create mode 100644 homeassistant/components/surepetcare/translations/en.json create mode 100644 homeassistant/components/surepetcare/translations/es.json create mode 100644 homeassistant/components/surepetcare/translations/et.json create mode 100644 homeassistant/components/surepetcare/translations/he.json create mode 100644 homeassistant/components/surepetcare/translations/hu.json create mode 100644 homeassistant/components/surepetcare/translations/id.json create mode 100644 homeassistant/components/surepetcare/translations/it.json create mode 100644 homeassistant/components/surepetcare/translations/nl.json create mode 100644 homeassistant/components/surepetcare/translations/no.json create mode 100644 homeassistant/components/surepetcare/translations/pt-BR.json create mode 100644 homeassistant/components/surepetcare/translations/ru.json create mode 100644 homeassistant/components/surepetcare/translations/zh-Hant.json create mode 100644 homeassistant/components/switchbot/translations/ca.json create mode 100644 homeassistant/components/switchbot/translations/cs.json create mode 100644 homeassistant/components/switchbot/translations/de.json create mode 100644 homeassistant/components/switchbot/translations/es.json create mode 100644 homeassistant/components/switchbot/translations/et.json create mode 100644 homeassistant/components/switchbot/translations/he.json create mode 100644 homeassistant/components/switchbot/translations/hu.json create mode 100644 homeassistant/components/switchbot/translations/id.json create mode 100644 homeassistant/components/switchbot/translations/it.json create mode 100644 homeassistant/components/switchbot/translations/nl.json create mode 100644 homeassistant/components/switchbot/translations/no.json create mode 100644 homeassistant/components/switchbot/translations/ro.json create mode 100644 homeassistant/components/switchbot/translations/ru.json create mode 100644 homeassistant/components/switchbot/translations/zh-Hant.json create mode 100644 homeassistant/components/switcher_kis/translations/es.json create mode 100644 homeassistant/components/switcher_kis/translations/id.json create mode 100644 homeassistant/components/tuya/translations/af.json create mode 100644 homeassistant/components/tuya/translations/ca.json create mode 100644 homeassistant/components/tuya/translations/cs.json create mode 100644 homeassistant/components/tuya/translations/de.json create mode 100644 homeassistant/components/tuya/translations/es.json create mode 100644 homeassistant/components/tuya/translations/et.json create mode 100644 homeassistant/components/tuya/translations/fi.json create mode 100644 homeassistant/components/tuya/translations/fr.json create mode 100644 homeassistant/components/tuya/translations/he.json create mode 100644 homeassistant/components/tuya/translations/hu.json create mode 100644 homeassistant/components/tuya/translations/id.json create mode 100644 homeassistant/components/tuya/translations/it.json create mode 100644 homeassistant/components/tuya/translations/ka.json create mode 100644 homeassistant/components/tuya/translations/ko.json create mode 100644 homeassistant/components/tuya/translations/lb.json create mode 100644 homeassistant/components/tuya/translations/nl.json create mode 100644 homeassistant/components/tuya/translations/no.json create mode 100644 homeassistant/components/tuya/translations/pl.json create mode 100644 homeassistant/components/tuya/translations/pt-BR.json create mode 100644 homeassistant/components/tuya/translations/pt.json create mode 100644 homeassistant/components/tuya/translations/ru.json create mode 100644 homeassistant/components/tuya/translations/sl.json create mode 100644 homeassistant/components/tuya/translations/sv.json create mode 100644 homeassistant/components/tuya/translations/tr.json create mode 100644 homeassistant/components/tuya/translations/uk.json create mode 100644 homeassistant/components/tuya/translations/zh-Hant.json create mode 100644 homeassistant/components/uptimerobot/translations/id.json create mode 100644 homeassistant/components/watttime/translations/ca.json create mode 100644 homeassistant/components/watttime/translations/cs.json create mode 100644 homeassistant/components/watttime/translations/de.json create mode 100644 homeassistant/components/watttime/translations/es.json create mode 100644 homeassistant/components/watttime/translations/et.json create mode 100644 homeassistant/components/watttime/translations/he.json create mode 100644 homeassistant/components/watttime/translations/hu.json create mode 100644 homeassistant/components/watttime/translations/id.json create mode 100644 homeassistant/components/watttime/translations/it.json create mode 100644 homeassistant/components/watttime/translations/nl.json create mode 100644 homeassistant/components/watttime/translations/no.json create mode 100644 homeassistant/components/watttime/translations/ru.json create mode 100644 homeassistant/components/watttime/translations/zh-Hant.json create mode 100644 homeassistant/components/whirlpool/translations/ca.json create mode 100644 homeassistant/components/whirlpool/translations/cs.json create mode 100644 homeassistant/components/whirlpool/translations/de.json create mode 100644 homeassistant/components/whirlpool/translations/es.json create mode 100644 homeassistant/components/whirlpool/translations/et.json create mode 100644 homeassistant/components/whirlpool/translations/he.json create mode 100644 homeassistant/components/whirlpool/translations/hu.json create mode 100644 homeassistant/components/whirlpool/translations/id.json create mode 100644 homeassistant/components/whirlpool/translations/it.json create mode 100644 homeassistant/components/whirlpool/translations/nl.json create mode 100644 homeassistant/components/whirlpool/translations/no.json create mode 100644 homeassistant/components/whirlpool/translations/pt-BR.json create mode 100644 homeassistant/components/whirlpool/translations/ru.json create mode 100644 homeassistant/components/whirlpool/translations/zh-Hant.json create mode 100644 homeassistant/components/xiaomi_miio/translations/select.id.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/el.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/id.json create mode 100644 homeassistant/components/youless/translations/id.json diff --git a/homeassistant/components/accuweather/translations/hu.json b/homeassistant/components/accuweather/translations/hu.json index 7b4d270f78b..8b0409d1f22 100644 --- a/homeassistant/components/accuweather/translations/hu.json +++ b/homeassistant/components/accuweather/translations/hu.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs", - "requests_exceeded": "T\u00fall\u00e9pt\u00e9k az Accuweather API-hoz beny\u00fajtott k\u00e9relmek megengedett sz\u00e1m\u00e1t. Meg kell v\u00e1rnia vagy m\u00f3dos\u00edtania kell az API-kulcsot." + "requests_exceeded": "Accuweather API-hoz enged\u00e9lyezett lek\u00e9r\u00e9sek sz\u00e1ma t\u00fal lett l\u00e9pve. Meg kell v\u00e1rnia m\u00edg a tilt\u00e1s lej\u00e1r vagy m\u00f3dos\u00edtania kell az API-kulcsot." }, "step": { "user": { diff --git a/homeassistant/components/adax/translations/es.json b/homeassistant/components/adax/translations/es.json index 20ecaaa0dd2..985d0ab663f 100644 --- a/homeassistant/components/adax/translations/es.json +++ b/homeassistant/components/adax/translations/es.json @@ -1,10 +1,17 @@ { "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + }, "step": { "user": { "data": { "account_id": "ID de la cuenta", - "host": "Anfitri\u00f3n", + "host": "Host", "password": "Contrase\u00f1a" } } diff --git a/homeassistant/components/adax/translations/hu.json b/homeassistant/components/adax/translations/hu.json index 726381a4dd7..94397487c87 100644 --- a/homeassistant/components/adax/translations/hu.json +++ b/homeassistant/components/adax/translations/hu.json @@ -11,7 +11,7 @@ "user": { "data": { "account_id": "Fi\u00f3k ID", - "host": "Gazdag\u00e9p", + "host": "C\u00edm", "password": "Jelsz\u00f3" } } diff --git a/homeassistant/components/adax/translations/id.json b/homeassistant/components/adax/translations/id.json new file mode 100644 index 00000000000..e554913bdc8 --- /dev/null +++ b/homeassistant/components/adax/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid" + }, + "step": { + "user": { + "data": { + "account_id": "ID Akun", + "host": "Host", + "password": "Kata Sandi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/translations/he.json b/homeassistant/components/adguard/translations/he.json index e2114d19d97..9970667cf40 100644 --- a/homeassistant/components/adguard/translations/he.json +++ b/homeassistant/components/adguard/translations/he.json @@ -7,6 +7,9 @@ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, "step": { + "hassio_confirm": { + "title": "AdGuard Home \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05d4\u05e8\u05d7\u05d1\u05ea Assistant Assistant" + }, "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7", diff --git a/homeassistant/components/adguard/translations/hu.json b/homeassistant/components/adguard/translations/hu.json index 8a860caf79d..3939de8aea5 100644 --- a/homeassistant/components/adguard/translations/hu.json +++ b/homeassistant/components/adguard/translations/hu.json @@ -9,12 +9,12 @@ }, "step": { "hassio_confirm": { - "description": "Be szeretn\u00e9 \u00e1ll\u00edtani a Home Assistant-ot, hogy csatlakozzon az AdGuard Home-hoz, amelyet a kieg\u00e9sz\u00edt\u0151 biztos\u00edt: {addon} ?", + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani Home Assistant-ot AdGuard Home-hoz val\u00f3 csatlakoz\u00e1shoz, {addon} kieg\u00e9sz\u00edt\u0151 \u00e1ltal?", "title": "Az AdGuard Home a Home Assistant kieg\u00e9sz\u00edt\u0151 seg\u00edts\u00e9g\u00e9vel" }, "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "password": "Jelsz\u00f3", "port": "Port", "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", diff --git a/homeassistant/components/adguard/translations/id.json b/homeassistant/components/adguard/translations/id.json index d3334997f59..91d06526184 100644 --- a/homeassistant/components/adguard/translations/id.json +++ b/homeassistant/components/adguard/translations/id.json @@ -9,7 +9,7 @@ }, "step": { "hassio_confirm": { - "description": "Ingin mengonfigurasi Home Assistant untuk terhubung ke AdGuard Home yang disediakan oleh add-on Supervisor {addon}?", + "description": "Ingin mengonfigurasi Home Assistant untuk terhubung ke AdGuard Home yang disediakan oleh add-on: {addon}?", "title": "AdGuard Home melalui add-on Home Assistant" }, "user": { diff --git a/homeassistant/components/agent_dvr/translations/hu.json b/homeassistant/components/agent_dvr/translations/hu.json index fff86517073..b8fec1c281d 100644 --- a/homeassistant/components/agent_dvr/translations/hu.json +++ b/homeassistant/components/agent_dvr/translations/hu.json @@ -4,13 +4,13 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, "error": { - "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "port": "Port" }, "title": "\u00c1ll\u00edtsa be az Agent DVR-t" diff --git a/homeassistant/components/airthings/translations/ca.json b/homeassistant/components/airthings/translations/ca.json new file mode 100644 index 00000000000..c90f9cc6364 --- /dev/null +++ b/homeassistant/components/airthings/translations/ca.json @@ -0,0 +1,21 @@ +{ + "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": { + "description": "Inicia sessi\u00f3 a {url} per obtenir les credencials", + "id": "ID", + "secret": "Secret" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings/translations/de.json b/homeassistant/components/airthings/translations/de.json new file mode 100644 index 00000000000..7bd5e347776 --- /dev/null +++ b/homeassistant/components/airthings/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "description": "Melde dich unter {url} an, um deine Zugangsdaten zu finden", + "id": "ID", + "secret": "Geheimnis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings/translations/en.json b/homeassistant/components/airthings/translations/en.json new file mode 100644 index 00000000000..a7430dedd81 --- /dev/null +++ b/homeassistant/components/airthings/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Account is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "description": "Login at {url} to find your credentials", + "id": "ID", + "secret": "Secret" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings/translations/et.json b/homeassistant/components/airthings/translations/et.json new file mode 100644 index 00000000000..708416f16c1 --- /dev/null +++ b/homeassistant/components/airthings/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Konto on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamise t\u00f5rge", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "description": "Logi sisse aadressil {url}, et leida oma mandaadid", + "id": "Kasutajatunnus", + "secret": "Salas\u00f5na" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings/translations/he.json b/homeassistant/components/airthings/translations/he.json new file mode 100644 index 00000000000..c6c0d910ae4 --- /dev/null +++ b/homeassistant/components/airthings/translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\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", + "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": { + "id": "\u05de\u05d6\u05d4\u05d4", + "secret": "\u05e1\u05d5\u05d3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings/translations/hu.json b/homeassistant/components/airthings/translations/hu.json new file mode 100644 index 00000000000..136348d38b4 --- /dev/null +++ b/homeassistant/components/airthings/translations/hu.json @@ -0,0 +1,21 @@ +{ + "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": { + "description": "Jelentkezzen be a {url} c\u00edmen hogy megkapja hiteles\u00edt\u0151 adatait", + "id": "Azonos\u00edt\u00f3", + "secret": "Titok" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings/translations/it.json b/homeassistant/components/airthings/translations/it.json new file mode 100644 index 00000000000..68a0c152f56 --- /dev/null +++ b/homeassistant/components/airthings/translations/it.json @@ -0,0 +1,21 @@ +{ + "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": { + "description": "Accedi a {url} per trovare le tue credenziali", + "id": "ID", + "secret": "Segreto" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings/translations/nl.json b/homeassistant/components/airthings/translations/nl.json new file mode 100644 index 00000000000..3f0e753b375 --- /dev/null +++ b/homeassistant/components/airthings/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "description": "Log in op {url} om uw inloggegevens te vinden", + "id": "ID", + "secret": "Geheim" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings/translations/no.json b/homeassistant/components/airthings/translations/no.json new file mode 100644 index 00000000000..8609dff2e16 --- /dev/null +++ b/homeassistant/components/airthings/translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "description": "Logg p\u00e5 {url} \u00e5 finne legitimasjonen din", + "id": "ID", + "secret": "Hemmelig" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings/translations/ru.json b/homeassistant/components/airthings/translations/ru.json new file mode 100644 index 00000000000..6ec7077860e --- /dev/null +++ b/homeassistant/components/airthings/translations/ru.json @@ -0,0 +1,21 @@ +{ + "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": { + "description": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0435 \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0434\u0430\u043d\u043d\u044b\u0435 \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043d\u0430 \u044d\u0442\u043e\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435: {url}", + "id": "ID", + "secret": "\u0421\u0435\u043a\u0440\u0435\u0442\u043d\u044b\u0439 \u043a\u043e\u0434" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings/translations/zh-Hant.json b/homeassistant/components/airthings/translations/zh-Hant.json new file mode 100644 index 00000000000..0cafeb9886d --- /dev/null +++ b/homeassistant/components/airthings/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "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": { + "description": "\u767b\u5165 {url} \u4ee5\u53d6\u5f97\u6191\u8b49", + "id": "ID", + "secret": "\u5bc6\u78bc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airtouch4/translations/es.json b/homeassistant/components/airtouch4/translations/es.json index eeae1153555..65616d2a2e9 100644 --- a/homeassistant/components/airtouch4/translations/es.json +++ b/homeassistant/components/airtouch4/translations/es.json @@ -1,12 +1,16 @@ { "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, "error": { + "cannot_connect": "No se pudo conectar", "no_units": "No se pudo encontrar ning\u00fan grupo AirTouch 4." }, "step": { "user": { "data": { - "host": "Anfitri\u00f3n" + "host": "Host" }, "title": "Configura los detalles de conexi\u00f3n de tu AirTouch 4." } diff --git a/homeassistant/components/airtouch4/translations/hu.json b/homeassistant/components/airtouch4/translations/hu.json index c5d54de31de..861582fad3e 100644 --- a/homeassistant/components/airtouch4/translations/hu.json +++ b/homeassistant/components/airtouch4/translations/hu.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "Gazdag\u00e9p" + "host": "C\u00edm" }, "title": "\u00c1ll\u00edtsa be az AirTouch 4 csatlakoz\u00e1si adatait." } diff --git a/homeassistant/components/airtouch4/translations/id.json b/homeassistant/components/airtouch4/translations/id.json new file mode 100644 index 00000000000..c8236f5ec73 --- /dev/null +++ b/homeassistant/components/airtouch4/translations/id.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/hu.json b/homeassistant/components/airvisual/translations/hu.json index 043a2402283..48d4f5b98eb 100644 --- a/homeassistant/components/airvisual/translations/hu.json +++ b/homeassistant/components/airvisual/translations/hu.json @@ -32,7 +32,7 @@ }, "node_pro": { "data": { - "ip_address": "Hoszt", + "ip_address": "C\u00edm", "password": "Jelsz\u00f3" }, "description": "Szem\u00e9lyes AirVisual egys\u00e9g figyel\u00e9se. A jelsz\u00f3 lek\u00e9rhet\u0151 a k\u00e9sz\u00fcl\u00e9k felhaszn\u00e1l\u00f3i fel\u00fclet\u00e9r\u0151l.", diff --git a/homeassistant/components/alarmdecoder/translations/hu.json b/homeassistant/components/alarmdecoder/translations/hu.json index ace9c7059ca..3c9781672f4 100644 --- a/homeassistant/components/alarmdecoder/translations/hu.json +++ b/homeassistant/components/alarmdecoder/translations/hu.json @@ -14,7 +14,7 @@ "data": { "device_baudrate": "Eszk\u00f6z \u00e1tviteli sebess\u00e9ge", "device_path": "Eszk\u00f6z el\u00e9r\u00e9si \u00fatja", - "host": "Hoszt", + "host": "C\u00edm", "port": "Port" }, "title": "Konfigur\u00e1lja a csatlakoz\u00e1si be\u00e1ll\u00edt\u00e1sokat" diff --git a/homeassistant/components/almond/translations/hu.json b/homeassistant/components/almond/translations/hu.json index 568cd7270de..27932696561 100644 --- a/homeassistant/components/almond/translations/hu.json +++ b/homeassistant/components/almond/translations/hu.json @@ -2,14 +2,14 @@ "config": { "abort": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "step": { "hassio_confirm": { - "description": "Be szeretn\u00e9 \u00e1ll\u00edtani a Home Assistant alkalmaz\u00e1st az Almondhoz val\u00f3 csatlakoz\u00e1shoz, amelyet a Supervisor kieg\u00e9sz\u00edt\u0151 biztos\u00edt: {addon} ?", - "title": "Almond a Supervisor kieg\u00e9sz\u00edt\u0151n kereszt\u00fcl" + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani Home Assistant-ot az Almondhoz val\u00f3 csatlakoz\u00e1shoz, {addon} kieg\u00e9sz\u00edt\u0151 \u00e1ltal?", + "title": "Almond - Home Assistant kieg\u00e9sz\u00edt\u0151 \u00e1ltal" }, "pick_implementation": { "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" diff --git a/homeassistant/components/almond/translations/id.json b/homeassistant/components/almond/translations/id.json index 21a627132c4..8e4302220b5 100644 --- a/homeassistant/components/almond/translations/id.json +++ b/homeassistant/components/almond/translations/id.json @@ -8,7 +8,7 @@ }, "step": { "hassio_confirm": { - "description": "Ingin mengonfigurasi Home Assistant untuk terhubung ke Almond yang disediakan oleh add-on Supervisor {addon}?", + "description": "Ingin mengonfigurasi Home Assistant untuk terhubung ke Almond yang disediakan oleh add-on: {addon}?", "title": "Almond melalui add-on Home Assistant" }, "pick_implementation": { diff --git a/homeassistant/components/ambee/translations/es.json b/homeassistant/components/ambee/translations/es.json index de5ce971fa0..7f4f8b75de5 100644 --- a/homeassistant/components/ambee/translations/es.json +++ b/homeassistant/components/ambee/translations/es.json @@ -1,12 +1,26 @@ { "config": { + "abort": { + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_api_key": "Clave API no v\u00e1lida" + }, "step": { "reauth_confirm": { "data": { + "api_key": "Clave API", "description": "Vuelva a autenticarse con su cuenta de Ambee." } }, "user": { + "data": { + "api_key": "Clave API", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nombre" + }, "description": "Configure Ambee para que se integre con Home Assistant." } } diff --git a/homeassistant/components/ambee/translations/hu.json b/homeassistant/components/ambee/translations/hu.json index 4cf99c596f0..299d97914bc 100644 --- a/homeassistant/components/ambee/translations/hu.json +++ b/homeassistant/components/ambee/translations/hu.json @@ -21,7 +21,7 @@ "longitude": "Hossz\u00fas\u00e1g", "name": "N\u00e9v" }, - "description": "\u00c1ll\u00edtsa be az Ambee-t a Homeassistanttal val\u00f3 integr\u00e1ci\u00f3hoz." + "description": "\u00c1ll\u00edtsa be Ambee-t Home Assistant-tal val\u00f3 integr\u00e1ci\u00f3hoz." } } } diff --git a/homeassistant/components/ambee/translations/id.json b/homeassistant/components/ambee/translations/id.json index ecf627579fe..a5790d95ecd 100644 --- a/homeassistant/components/ambee/translations/id.json +++ b/homeassistant/components/ambee/translations/id.json @@ -10,7 +10,8 @@ "step": { "reauth_confirm": { "data": { - "api_key": "Kunci API" + "api_key": "Kunci API", + "description": "Autentikasi ulang dengan akun Ambee Anda." } }, "user": { @@ -19,7 +20,8 @@ "latitude": "Lintang", "longitude": "Bujur", "name": "Nama" - } + }, + "description": "Siapkan Ambee Anda untuk diintegrasikan dengan Home Assistant." } } } diff --git a/homeassistant/components/ambee/translations/sensor.id.json b/homeassistant/components/ambee/translations/sensor.id.json new file mode 100644 index 00000000000..61bdea468ee --- /dev/null +++ b/homeassistant/components/ambee/translations/sensor.id.json @@ -0,0 +1,9 @@ +{ + "state": { + "ambee__risk": { + "high": "Tinggi", + "low": "Rendah", + "moderate": "Sedang" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/amberelectric/translations/ca.json b/homeassistant/components/amberelectric/translations/ca.json new file mode 100644 index 00000000000..cf9bca64df6 --- /dev/null +++ b/homeassistant/components/amberelectric/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "site": { + "data": { + "site_name": "Nom del lloc", + "site_nmi": "NMI del lloc" + }, + "description": "Selecciona l'NMI del lloc que vulguis afegir", + "title": "Amber Electric" + }, + "user": { + "data": { + "api_token": "Token d'API", + "site_id": "ID del lloc" + }, + "description": "Ves a {api_url} per generar una clau API", + "title": "Amber Electric" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/amberelectric/translations/de.json b/homeassistant/components/amberelectric/translations/de.json new file mode 100644 index 00000000000..2143795f479 --- /dev/null +++ b/homeassistant/components/amberelectric/translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "site": { + "data": { + "site_name": "Name des Standorts", + "site_nmi": "Standort NMI" + }, + "description": "W\u00e4hle die NMI des Standorts, den du hinzuf\u00fcgen m\u00f6chtest", + "title": "Amber Electric" + }, + "user": { + "data": { + "api_token": "API-Token", + "site_id": "Site-ID" + }, + "description": "Gehe zu {api_url}, um einen API-Schl\u00fcssel zu generieren", + "title": "Amber Electric" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/amberelectric/translations/et.json b/homeassistant/components/amberelectric/translations/et.json new file mode 100644 index 00000000000..05a7e6c6dc2 --- /dev/null +++ b/homeassistant/components/amberelectric/translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "site": { + "data": { + "site_name": "Saidi nimi", + "site_nmi": "Saidi NMI" + }, + "description": "Vali lisatava saidi NMI", + "title": "Amber Electric" + }, + "user": { + "data": { + "api_token": "API v\u00f5ti", + "site_id": "Saidi ID" + }, + "description": "API-v\u00f5tme saamiseks ava {api_url}.", + "title": "Amber Electric" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/amberelectric/translations/hu.json b/homeassistant/components/amberelectric/translations/hu.json new file mode 100644 index 00000000000..9811f5a5f8f --- /dev/null +++ b/homeassistant/components/amberelectric/translations/hu.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "site": { + "data": { + "site_name": "Hely neve", + "site_nmi": "Hely NMI" + }, + "description": "V\u00e1lassza ki a hozz\u00e1adni k\u00edv\u00e1nt hely NMI-j\u00e9t.", + "title": "Amber Electric" + }, + "user": { + "data": { + "api_token": "API Token", + "site_id": "Hely ID" + }, + "description": "API-kulcs gener\u00e1l\u00e1s\u00e1hoz l\u00e1togasson el ide: {api_url}", + "title": "Amber Electric" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/amberelectric/translations/it.json b/homeassistant/components/amberelectric/translations/it.json new file mode 100644 index 00000000000..5b061561954 --- /dev/null +++ b/homeassistant/components/amberelectric/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "site": { + "data": { + "site_name": "Nome del sito", + "site_nmi": "Sito NMI" + }, + "description": "Seleziona l'NMI del sito che desideri aggiungere", + "title": "Amber Electric" + }, + "user": { + "data": { + "api_token": "Token API", + "site_id": "ID sito" + }, + "description": "Vai su {api_url} per generare una chiave API", + "title": "Amber Electric" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/amberelectric/translations/nl.json b/homeassistant/components/amberelectric/translations/nl.json new file mode 100644 index 00000000000..a874c12f283 --- /dev/null +++ b/homeassistant/components/amberelectric/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "site": { + "data": { + "site_name": "Sitenaam", + "site_nmi": "Site NMI" + }, + "description": "Selecteer de NMI van de site die u wilt toevoegen", + "title": "Amber Electric" + }, + "user": { + "data": { + "api_token": "API Token", + "site_id": "Site ID" + }, + "description": "Ga naar {api_url} om een API sleutel aan te maken", + "title": "Amber Electric" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/amberelectric/translations/no.json b/homeassistant/components/amberelectric/translations/no.json new file mode 100644 index 00000000000..90d4bd930b9 --- /dev/null +++ b/homeassistant/components/amberelectric/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "site": { + "data": { + "site_name": "Side navn", + "site_nmi": "Nettsted NMI" + }, + "description": "Velg NMI for nettstedet du vil legge til", + "title": "Amber Electric" + }, + "user": { + "data": { + "api_token": "API-token", + "site_id": "Nettsted -ID" + }, + "description": "G\u00e5 til {api_url} \u00e5 generere en API -n\u00f8kkel", + "title": "Amber Electric" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/amberelectric/translations/ru.json b/homeassistant/components/amberelectric/translations/ru.json new file mode 100644 index 00000000000..4b8caee72ee --- /dev/null +++ b/homeassistant/components/amberelectric/translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "site": { + "data": { + "site_name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0447\u0430\u0441\u0442\u043a\u0430", + "site_nmi": "NMI \u0443\u0447\u0430\u0441\u0442\u043a\u0430" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 NMI \u0443\u0447\u0430\u0441\u0442\u043a\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c", + "title": "Amber Electric" + }, + "user": { + "data": { + "api_token": "\u0422\u043e\u043a\u0435\u043d API", + "site_id": "ID \u0443\u0447\u0430\u0441\u0442\u043a\u0430" + }, + "description": "\u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443 {api_url} \u0447\u0442\u043e\u0431\u044b \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API.", + "title": "Amber Electric" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/amberelectric/translations/zh-Hant.json b/homeassistant/components/amberelectric/translations/zh-Hant.json new file mode 100644 index 00000000000..0af0e5e60bb --- /dev/null +++ b/homeassistant/components/amberelectric/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "site": { + "data": { + "site_name": "\u4f4d\u5740\u540d\u7a31", + "site_nmi": "\u4f4d\u5740 NMI" + }, + "description": "\u9078\u64c7\u6240\u8981\u65b0\u589e\u7684\u4f4d\u5740 NMI", + "title": "Amber Electric" + }, + "user": { + "data": { + "api_token": "API \u6b0a\u6756", + "site_id": "\u4f4d\u5740 ID" + }, + "description": "\u9023\u7dda\u81f3 {api_url} \u4ee5\u7522\u751f API \u5bc6\u9470", + "title": "Amber Electric" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/translations/ca.json b/homeassistant/components/ambiclimate/translations/ca.json index 8e54a222217..234cb1a413c 100644 --- a/homeassistant/components/ambiclimate/translations/ca.json +++ b/homeassistant/components/ambiclimate/translations/ca.json @@ -2,7 +2,7 @@ "config": { "abort": { "access_token": "S'ha produ\u00eft un error desconegut al generat un token d'acc\u00e9s.", - "already_configured": "El compte ja ha estat configurat", + "already_configured": "El compte ja est\u00e0 configurat", "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3." }, "create_entry": { diff --git a/homeassistant/components/ambiclimate/translations/hu.json b/homeassistant/components/ambiclimate/translations/hu.json index 3898535c427..597645658d8 100644 --- a/homeassistant/components/ambiclimate/translations/hu.json +++ b/homeassistant/components/ambiclimate/translations/hu.json @@ -3,18 +3,18 @@ "abort": { "access_token": "Ismeretlen hiba a hozz\u00e1f\u00e9r\u00e9si token gener\u00e1l\u00e1s\u00e1ban.", "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", - "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t." + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t." }, "create_entry": { "default": "Sikeres hiteles\u00edt\u00e9s" }, "error": { - "follow_link": "K\u00e9rlek, k\u00f6vesd a hivatkoz\u00e1st \u00e9s hiteles\u00edtsd magad miel\u0151tt megnyomod a K\u00fcld\u00e9s gombot", + "follow_link": "K\u00e9rem, k\u00f6vesse a hivatkoz\u00e1st \u00e9s hiteles\u00edtse mag\u00e1t miel\u0151tt megnyomn\u00e1 a K\u00fcld\u00e9s gombot", "no_token": "Nem hiteles\u00edtett Ambiclimate" }, "step": { "auth": { - "description": "K\u00e9rj\u00fck, k\u00f6vesse ezt a [link] ({authorization_url} Author_url}) \u00e9s ** Enged\u00e9lyezze ** a hozz\u00e1f\u00e9r\u00e9st Ambiclimate -fi\u00f3kj\u00e1hoz, majd t\u00e9rjen vissza, \u00e9s nyomja meg az al\u00e1bbi ** K\u00fcld\u00e9s ** gombot.\n (Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a megadott visszah\u00edv\u00e1si URL {cb_url})", + "description": "K\u00e9rj\u00fck, 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 ** K\u00fcld\u00e9s ** gombot.\n(Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a megadott visszah\u00edv\u00e1si URL {cb_url})", "title": "Ambiclimate hiteles\u00edt\u00e9se" } } diff --git a/homeassistant/components/apple_tv/translations/hu.json b/homeassistant/components/apple_tv/translations/hu.json index 2b6275fc9f5..3d254422baf 100644 --- a/homeassistant/components/apple_tv/translations/hu.json +++ b/homeassistant/components/apple_tv/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", "backoff": "Az eszk\u00f6z jelenleg nem fogadja el a p\u00e1ros\u00edt\u00e1si k\u00e9relmeket (lehet, hogy t\u00fal sokszor adott meg \u00e9rv\u00e9nytelen PIN-k\u00f3dot), pr\u00f3b\u00e1lkozzon \u00fajra k\u00e9s\u0151bb.", "device_did_not_pair": "A p\u00e1ros\u00edt\u00e1s folyamat\u00e1t az eszk\u00f6zr\u0151l nem pr\u00f3b\u00e1lt\u00e1k befejezni.", "invalid_config": "Az eszk\u00f6z konfigur\u00e1l\u00e1sa nem teljes. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra hozz\u00e1adni.", @@ -19,7 +19,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Arra k\u00e9sz\u00fcl, hogy felvegye a (z) {name} nev\u0171 Apple TV-t a Home Assistant programba. \n\n ** A folyamat befejez\u00e9s\u00e9hez t\u00f6bb PIN-k\u00f3dot kell megadnia. ** \n\n Felh\u00edvjuk figyelm\u00e9t, hogy ezzel az integr\u00e1ci\u00f3val * nem fogja tudni kikapcsolni az Apple TV-t. Csak a Home Assistant m\u00e9dialej\u00e1tsz\u00f3ja kapcsol ki!", + "description": "Arra k\u00e9sz\u00fcl, hogy felvegye {name} nev\u0171 Apple TV-t a Home Assistant p\u00e9ld\u00e1ny\u00e1ba. \n\n ** A folyamat befejez\u00e9s\u00e9hez t\u00f6bb PIN-k\u00f3dot kell megadnia. ** \n\nFelh\u00edvjuk figyelm\u00e9t, hogy ezzel az integr\u00e1ci\u00f3val *nem* fogja tudni kikapcsolni az Apple TV-t. Csak a Home Assistant saj\u00e1t m\u00e9dialej\u00e1tsz\u00f3ja kapcsol ki!", "title": "Apple TV sikeresen hozz\u00e1adva" }, "pair_no_pin": { @@ -30,7 +30,7 @@ "data": { "pin": "PIN-k\u00f3d" }, - "description": "P\u00e1ros\u00edt\u00e1sra van sz\u00fcks\u00e9g a(z) {protocol} protokollhoz. K\u00e9rj\u00fck, adja meg a k\u00e9perny\u0151n megjelen\u0151 PIN-k\u00f3dot. A vezet\u0151 null\u00e1kat el kell hagyni, azaz \u00edrja be a 123 \u00e9rt\u00e9ket, ha a megjelen\u00edtett k\u00f3d 0123.", + "description": "P\u00e1ros\u00edt\u00e1sra van sz\u00fcks\u00e9g a(z) {protocol} protokollhoz. K\u00e9rj\u00fck, adja meg a k\u00e9perny\u0151n megjelen\u0151 PIN-k\u00f3dot. A vezet\u0151 null\u00e1kat el kell hagyni, pl. \u00edrja be a 123 \u00e9rt\u00e9ket, ha a megjelen\u00edtett k\u00f3d 0123.", "title": "P\u00e1ros\u00edt\u00e1s" }, "reconfigure": { @@ -45,7 +45,7 @@ "data": { "device_input": "Eszk\u00f6z" }, - "description": "El\u0151sz\u00f6r \u00edrja be a hozz\u00e1adni k\u00edv\u00e1nt Apple TV eszk\u00f6znev\u00e9t (pl. Konyha vagy H\u00e1l\u00f3szoba) vagy IP-c\u00edm\u00e9t. Ha valamilyen eszk\u00f6zt automatikusan tal\u00e1ltak a h\u00e1l\u00f3zat\u00e1n, az al\u00e1bb l\u00e1that\u00f3. \n\n Ha nem l\u00e1tja eszk\u00f6z\u00e9t, vagy b\u00e1rmilyen probl\u00e9m\u00e1t tapasztal, pr\u00f3b\u00e1lja meg megadni az eszk\u00f6z IP-c\u00edm\u00e9t. \n\n {devices}", + "description": "El\u0151sz\u00f6r \u00edrja be a hozz\u00e1adni k\u00edv\u00e1nt Apple TV eszk\u00f6znev\u00e9t (pl. Konyha vagy H\u00e1l\u00f3szoba) vagy IP-c\u00edm\u00e9t. Ha valamilyen eszk\u00f6zt automatikusan tal\u00e1ltak a h\u00e1l\u00f3zat\u00e1n, az al\u00e1bb l\u00e1that\u00f3. \n\nHa nem l\u00e1tja eszk\u00f6z\u00e9t, vagy b\u00e1rmilyen probl\u00e9m\u00e1t tapasztal, pr\u00f3b\u00e1lja meg megadni az eszk\u00f6z IP-c\u00edm\u00e9t. \n\n {devices}", "title": "\u00daj Apple TV be\u00e1ll\u00edt\u00e1sa" } } diff --git a/homeassistant/components/apple_tv/translations/id.json b/homeassistant/components/apple_tv/translations/id.json index 5646b498242..209ecbf8a83 100644 --- a/homeassistant/components/apple_tv/translations/id.json +++ b/homeassistant/components/apple_tv/translations/id.json @@ -16,7 +16,7 @@ "no_usable_service": "Perangkat ditemukan tetapi kami tidak dapat mengidentifikasi berbagai cara untuk membuat koneksi ke perangkat tersebut. Jika Anda terus melihat pesan ini, coba tentukan alamat IP-nya atau mulai ulang Apple TV Anda.", "unknown": "Kesalahan yang tidak diharapkan" }, - "flow_title": "Apple TV: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Anda akan menambahkan Apple TV bernama `{name}` ke Home Assistant.\n\n** Untuk menyelesaikan proses, Anda mungkin harus memasukkan kode PIN beberapa kali.**\n\nPerhatikan bahwa Anda *tidak* akan dapat mematikan Apple TV dengan integrasi ini. Hanya pemutar media di Home Assistant yang akan dimatikan!", diff --git a/homeassistant/components/arcam_fmj/translations/hu.json b/homeassistant/components/arcam_fmj/translations/hu.json index 9539ad39bed..c7532f24b76 100644 --- a/homeassistant/components/arcam_fmj/translations/hu.json +++ b/homeassistant/components/arcam_fmj/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "error": { @@ -16,10 +16,10 @@ }, "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "port": "Port" }, - "description": "K\u00e9rj\u00fck, adja meg az eszk\u00f6z gazdag\u00e9pnev\u00e9t vagy IP-c\u00edm\u00e9t." + "description": "K\u00e9rj\u00fck, adja meg az eszk\u00f6z hosztnev\u00e9t vagy c\u00edm\u00e9t" } } }, diff --git a/homeassistant/components/arcam_fmj/translations/id.json b/homeassistant/components/arcam_fmj/translations/id.json index 96b10140948..cee43cbb4e9 100644 --- a/homeassistant/components/arcam_fmj/translations/id.json +++ b/homeassistant/components/arcam_fmj/translations/id.json @@ -5,7 +5,7 @@ "already_in_progress": "Alur konfigurasi sedang berlangsung", "cannot_connect": "Gagal terhubung" }, - "flow_title": "Arcam FMJ di {host}", + "flow_title": "{host}", "step": { "confirm": { "description": "Ingin menambahkan Arcam FMJ `{host}` ke Home Assistant?" diff --git a/homeassistant/components/asuswrt/translations/hu.json b/homeassistant/components/asuswrt/translations/hu.json index 891150c1038..ff64372f1b0 100644 --- a/homeassistant/components/asuswrt/translations/hu.json +++ b/homeassistant/components/asuswrt/translations/hu.json @@ -14,7 +14,7 @@ "step": { "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "mode": "M\u00f3d", "name": "N\u00e9v", "password": "Jelsz\u00f3", diff --git a/homeassistant/components/asuswrt/translations/ru.json b/homeassistant/components/asuswrt/translations/ru.json index a2090b1faf6..f77fcb4fb3a 100644 --- a/homeassistant/components/asuswrt/translations/ru.json +++ b/homeassistant/components/asuswrt/translations/ru.json @@ -32,7 +32,7 @@ "step": { "init": { "data": { - "consider_home": "\u0412\u0440\u0435\u043c\u044f \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445), \u043f\u043e \u0438\u0441\u0442\u0435\u0447\u0435\u043d\u0438\u044e \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u0442\u0430\u0442\u0443\u0441 \"\u041d\u0435 \u0434\u043e\u043c\u0430\"", + "consider_home": "\u0412\u0440\u0435\u043c\u044f \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445), \u043f\u043e \u0438\u0441\u0442\u0435\u0447\u0435\u043d\u0438\u044e \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0441\u0447\u0438\u0442\u0430\u0442\u044c \u0447\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0434\u043e\u043c\u0430", "dnsmasq": "\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0432 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0435 \u0444\u0430\u0439\u043b\u043e\u0432 dnsmasq.leases", "interface": "\u0418\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441, \u0441 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0443 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, eth0, eth1 \u0438 \u0442. \u0434.)", "require_ip": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u0441 IP-\u0430\u0434\u0440\u0435\u0441\u043e\u043c (\u0434\u043b\u044f \u0440\u0435\u0436\u0438\u043c\u0430 \u0442\u043e\u0447\u043a\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430)", diff --git a/homeassistant/components/atag/translations/hu.json b/homeassistant/components/atag/translations/hu.json index 8c3b4a055b0..aa605923dfd 100644 --- a/homeassistant/components/atag/translations/hu.json +++ b/homeassistant/components/atag/translations/hu.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "port": "Port" }, "title": "Csatlakoz\u00e1s az eszk\u00f6zh\u00f6z" diff --git a/homeassistant/components/august/translations/ca.json b/homeassistant/components/august/translations/ca.json index f0b1fa43c3d..ee9a860b1d1 100644 --- a/homeassistant/components/august/translations/ca.json +++ b/homeassistant/components/august/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat", + "already_configured": "El compte ja est\u00e0 configurat", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { diff --git a/homeassistant/components/august/translations/hu.json b/homeassistant/components/august/translations/hu.json index aeaef514e71..22e16dda305 100644 --- a/homeassistant/components/august/translations/hu.json +++ b/homeassistant/components/august/translations/hu.json @@ -14,7 +14,7 @@ "data": { "password": "Jelsz\u00f3" }, - "description": "Add meg a(z) {username} jelszav\u00e1t.", + "description": "Adja meg a(z) {username} jelszav\u00e1t.", "title": "August fi\u00f3k \u00fajrahiteles\u00edt\u00e9se" }, "user_validate": { diff --git a/homeassistant/components/auth/translations/fi.json b/homeassistant/components/auth/translations/fi.json index 92e4f03c0f9..ca174d81e6e 100644 --- a/homeassistant/components/auth/translations/fi.json +++ b/homeassistant/components/auth/translations/fi.json @@ -12,6 +12,11 @@ "title": "Ilmoita kertaluonteinen salasana" }, "totp": { + "step": { + "init": { + "title": "M\u00e4\u00e4rit\u00e4 kaksivaiheinen todennus TOTP:n avulla" + } + }, "title": "TOTP" } } diff --git a/homeassistant/components/auth/translations/hu.json b/homeassistant/components/auth/translations/hu.json index 5e7b1835093..47ecf846e0f 100644 --- a/homeassistant/components/auth/translations/hu.json +++ b/homeassistant/components/auth/translations/hu.json @@ -9,11 +9,11 @@ }, "step": { "init": { - "description": "K\u00e9rlek, v\u00e1lassz egyet az \u00e9rtes\u00edt\u00e9si szolg\u00e1ltat\u00e1sok k\u00f6z\u00fcl:", + "description": "K\u00e9rem, v\u00e1lasszon egyet az \u00e9rtes\u00edt\u00e9si szolg\u00e1ltat\u00e1sok k\u00f6z\u00fcl:", "title": "\u00c1ll\u00edtsa be az \u00e9rtes\u00edt\u00e9si \u00f6sszetev\u0151 \u00e1ltal megadott egyszeri jelsz\u00f3t" }, "setup": { - "description": "Az egyszeri jelsz\u00f3 el lett k\u00fcldve a(z) **notify.{notify_service}** szolg\u00e1ltat\u00e1ssal. K\u00e9rlek, add meg al\u00e1bb:", + "description": "Az egyszeri jelsz\u00f3 el lett k\u00fcldve a(z) **notify.{notify_service}** szolg\u00e1ltat\u00e1ssal. K\u00e9rem, adja meg al\u00e1bb:", "title": "Be\u00e1ll\u00edt\u00e1s ellen\u0151rz\u00e9se" } }, @@ -21,11 +21,11 @@ }, "totp": { "error": { - "invalid_code": "\u00c9rv\u00e9nytelen k\u00f3d, pr\u00f3b\u00e1ld \u00fajra. Ha ez a hiba folyamatosan el\u0151fordul, akkor gy\u0151z\u0151dj meg r\u00f3la, hogy a Home Assistant rendszered \u00f3r\u00e1ja pontosan j\u00e1r." + "invalid_code": "\u00c9rv\u00e9nytelen k\u00f3d, pr\u00f3b\u00e1lja \u00fajra. Ha ez a hiba folyamatosan el\u0151fordul, akkor gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy a Home Assistant rendszer\u00e9nek \u00f3r\u00e1ja pontosan j\u00e1r." }, "step": { "init": { - "description": "Ahhoz, hogy haszn\u00e1lhasd a k\u00e9tfaktoros hiteles\u00edt\u00e9st id\u0151alap\u00fa egyszeri jelszavakkal, szkenneld be a QR k\u00f3dot a hiteles\u00edt\u00e9si applik\u00e1ci\u00f3ddal. Ha m\u00e9g nincsen, akkor a [Google Hiteles\u00edt\u0151](https://support.google.com/accounts/answer/1066447)t vagy az [Authy](https://authy.com/)-t aj\u00e1nljuk.\n\n{qr_code}\n\nA k\u00f3d beolvas\u00e1sa ut\u00e1n add meg a hat sz\u00e1mjegy\u0171 k\u00f3dot az applik\u00e1ci\u00f3b\u00f3l a telep\u00edt\u00e9s ellen\u0151rz\u00e9s\u00e9hez. Ha probl\u00e9m\u00e1ba \u00fctk\u00f6z\u00f6l a QR k\u00f3d beolvas\u00e1s\u00e1n\u00e1l, akkor ind\u00edts egy k\u00e9zi be\u00e1ll\u00edt\u00e1st a **`{code}`** k\u00f3ddal.", + "description": "Ahhoz, hogy haszn\u00e1lhassa a k\u00e9tfaktoros hiteles\u00edt\u00e9st id\u0151alap\u00fa egyszeri jelszavakkal, szkennelje be a QR k\u00f3dot a hiteles\u00edt\u00e9si applik\u00e1ci\u00f3j\u00e1val. Ha m\u00e9g nincs ilyenje, akkor aj\u00e1nljuk figyelm\u00e9be a [Google Hiteles\u00edt\u0151](https://support.google.com/accounts/answer/1066447)t vagy az [Authy](https://authy.com/)-t.\n\n{qr_code}\n\nA k\u00f3d beolvas\u00e1sa ut\u00e1n adja meg a hat sz\u00e1mjegy\u0171 k\u00f3dot az applik\u00e1ci\u00f3b\u00f3l a telep\u00edt\u00e9s ellen\u0151rz\u00e9s\u00e9hez. Ha probl\u00e9m\u00e1ba \u00fctk\u00f6zne a QR k\u00f3d beolvas\u00e1s\u00e1n\u00e1l, akkor ind\u00edtson egy k\u00e9zi be\u00e1ll\u00edt\u00e1st a **`{code}`** k\u00f3ddal.", "title": "K\u00e9tfaktoros hiteles\u00edt\u00e9s be\u00e1ll\u00edt\u00e1sa TOTP haszn\u00e1lat\u00e1val" } }, diff --git a/homeassistant/components/automation/translations/hu.json b/homeassistant/components/automation/translations/hu.json index 85640af23ba..559523b1b12 100644 --- a/homeassistant/components/automation/translations/hu.json +++ b/homeassistant/components/automation/translations/hu.json @@ -5,5 +5,5 @@ "on": "Be" } }, - "title": "Automatiz\u00e1l\u00e1s" + "title": "Automatizmus" } \ No newline at end of file diff --git a/homeassistant/components/awair/translations/ca.json b/homeassistant/components/awair/translations/ca.json index 2e75af9e744..ac69e06df1e 100644 --- a/homeassistant/components/awair/translations/ca.json +++ b/homeassistant/components/awair/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat", + "already_configured": "El compte ja est\u00e0 configurat", "no_devices_found": "No s'han trobat dispositius a la xarxa", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, diff --git a/homeassistant/components/awair/translations/hu.json b/homeassistant/components/awair/translations/hu.json index f465186a95b..62187becd37 100644 --- a/homeassistant/components/awair/translations/hu.json +++ b/homeassistant/components/awair/translations/hu.json @@ -15,7 +15,7 @@ "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", "email": "E-mail" }, - "description": "Add meg \u00fajra az Awair fejleszt\u0151i hozz\u00e1f\u00e9r\u00e9si tokent." + "description": "Adja meg \u00fajra az Awair fejleszt\u0151i hozz\u00e1f\u00e9r\u00e9si tokent." }, "user": { "data": { diff --git a/homeassistant/components/axis/translations/hu.json b/homeassistant/components/axis/translations/hu.json index 709de5851ad..0cddf167437 100644 --- a/homeassistant/components/axis/translations/hu.json +++ b/homeassistant/components/axis/translations/hu.json @@ -7,15 +7,15 @@ }, "error": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, - "flow_title": "Axis eszk\u00f6z: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "password": "Jelsz\u00f3", "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" diff --git a/homeassistant/components/azure_devops/translations/ca.json b/homeassistant/components/azure_devops/translations/ca.json index b3eb6e4eb8e..e6811e54078 100644 --- a/homeassistant/components/azure_devops/translations/ca.json +++ b/homeassistant/components/azure_devops/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat", + "already_configured": "El compte ja est\u00e0 configurat", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { diff --git a/homeassistant/components/azure_devops/translations/id.json b/homeassistant/components/azure_devops/translations/id.json index 42292805b08..bad7c022b93 100644 --- a/homeassistant/components/azure_devops/translations/id.json +++ b/homeassistant/components/azure_devops/translations/id.json @@ -9,7 +9,7 @@ "invalid_auth": "Autentikasi tidak valid", "project_error": "Tidak bisa mendapatkan info proyek." }, - "flow_title": "Azure DevOps: {project_url}", + "flow_title": "{project_url}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/binary_sensor/translations/id.json b/homeassistant/components/binary_sensor/translations/id.json index ac880aa28fa..54dcb66dd7a 100644 --- a/homeassistant/components/binary_sensor/translations/id.json +++ b/homeassistant/components/binary_sensor/translations/id.json @@ -178,6 +178,9 @@ "off": "Tidak ada", "on": "Terdeteksi" }, + "update": { + "on": "Pembaruan tersedia" + }, "vibration": { "off": "Tidak ada", "on": "Terdeteksi" diff --git a/homeassistant/components/binary_sensor/translations/is.json b/homeassistant/components/binary_sensor/translations/is.json index f53316ebd73..bd1ed9c389a 100644 --- a/homeassistant/components/binary_sensor/translations/is.json +++ b/homeassistant/components/binary_sensor/translations/is.json @@ -45,7 +45,7 @@ "on": "Hreyfing" }, "occupancy": { - "off": "Hreinsa", + "off": "Engin vi\u00f0vera", "on": "Uppg\u00f6tva\u00f0" }, "presence": { diff --git a/homeassistant/components/blebox/translations/hu.json b/homeassistant/components/blebox/translations/hu.json index ce51a8a0967..056402ea13f 100644 --- a/homeassistant/components/blebox/translations/hu.json +++ b/homeassistant/components/blebox/translations/hu.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", - "unsupported_version": "A BleBox eszk\u00f6z elavult firmware-rel rendelkezik. El\u0151sz\u00f6r friss\u00edtsd." + "unsupported_version": "A BleBox eszk\u00f6z elavult firmware-rel rendelkezik. K\u00e9rem, friss\u00edtse el\u0151bb." }, "flow_title": "{name} ({host})", "step": { diff --git a/homeassistant/components/blebox/translations/id.json b/homeassistant/components/blebox/translations/id.json index 2ef604d1bff..f0bb4d34746 100644 --- a/homeassistant/components/blebox/translations/id.json +++ b/homeassistant/components/blebox/translations/id.json @@ -9,7 +9,7 @@ "unknown": "Kesalahan yang tidak diharapkan", "unsupported_version": "Firmware Perangkat BleBox sudah usang. Tingkatkan terlebih dulu." }, - "flow_title": "Perangkat BleBox: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/blink/translations/hu.json b/homeassistant/components/blink/translations/hu.json index 135a2f7ef2e..1822dfbcf50 100644 --- a/homeassistant/components/blink/translations/hu.json +++ b/homeassistant/components/blink/translations/hu.json @@ -14,7 +14,7 @@ "data": { "2fa": "K\u00e9tfaktoros k\u00f3d" }, - "description": "Add meg az e-mail c\u00edmedre k\u00fcld\u00f6tt pint", + "description": "Adja meg az e-mail c\u00edm\u00e9re k\u00fcld\u00f6tt PIN-t", "title": "K\u00e9tfaktoros hiteles\u00edt\u00e9s" }, "user": { diff --git a/homeassistant/components/bmw_connected_drive/translations/ca.json b/homeassistant/components/bmw_connected_drive/translations/ca.json index d6bd70064c3..eb12ac6fc3b 100644 --- a/homeassistant/components/bmw_connected_drive/translations/ca.json +++ b/homeassistant/components/bmw_connected_drive/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat" + "already_configured": "El compte ja est\u00e0 configurat" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", diff --git a/homeassistant/components/bond/translations/es.json b/homeassistant/components/bond/translations/es.json index d9918238515..33d3dbb4408 100644 --- a/homeassistant/components/bond/translations/es.json +++ b/homeassistant/components/bond/translations/es.json @@ -9,13 +9,13 @@ "old_firmware": "Firmware antiguo no compatible en el dispositivo Bond - actual\u00edzalo antes de continuar", "unknown": "Error inesperado" }, - "flow_title": "Bond: {bond_id} ({host})", + "flow_title": "{name} ({host})", "step": { "confirm": { "data": { "access_token": "Token de acceso" }, - "description": "\u00bfQuieres configurar {bond_id}?" + "description": "\u00bfQuieres configurar {name}?" }, "user": { "data": { diff --git a/homeassistant/components/bond/translations/hu.json b/homeassistant/components/bond/translations/hu.json index 535d3586b93..c1bac971f4b 100644 --- a/homeassistant/components/bond/translations/hu.json +++ b/homeassistant/components/bond/translations/hu.json @@ -15,12 +15,12 @@ "data": { "access_token": "Hozz\u00e1f\u00e9r\u00e9si token" }, - "description": "Szeretn\u00e9d be\u00e1ll\u00edtani a(z) {name}-t?" + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}-t?" }, "user": { "data": { "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", - "host": "Hoszt" + "host": "C\u00edm" } } } diff --git a/homeassistant/components/bond/translations/id.json b/homeassistant/components/bond/translations/id.json index 56c633cf31c..00a9dbac45d 100644 --- a/homeassistant/components/bond/translations/id.json +++ b/homeassistant/components/bond/translations/id.json @@ -9,7 +9,7 @@ "old_firmware": "Firmware lama yang tidak didukung pada perangkat Bond - tingkatkan versi sebelum melanjutkan", "unknown": "Kesalahan yang tidak diharapkan" }, - "flow_title": "Bond: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "confirm": { "data": { diff --git a/homeassistant/components/bosch_shc/translations/es.json b/homeassistant/components/bosch_shc/translations/es.json index 6de8f923f5a..df180029c55 100644 --- a/homeassistant/components/bosch_shc/translations/es.json +++ b/homeassistant/components/bosch_shc/translations/es.json @@ -1,6 +1,12 @@ { "config": { + "abort": { + "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", "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.", "unknown": "Error inesperado" @@ -16,7 +22,8 @@ } }, "reauth_confirm": { - "description": "La integraci\u00f3n bosch_shc necesita volver a autentificar su cuenta" + "description": "La integraci\u00f3n bosch_shc necesita volver a autentificar su cuenta", + "title": "Volver a autenticar la integraci\u00f3n" }, "user": { "data": { diff --git a/homeassistant/components/bosch_shc/translations/hu.json b/homeassistant/components/bosch_shc/translations/hu.json index 8b4ebc6be32..cf0090475b7 100644 --- a/homeassistant/components/bosch_shc/translations/hu.json +++ b/homeassistant/components/bosch_shc/translations/hu.json @@ -14,7 +14,7 @@ "flow_title": "Bosch SHC: {name}", "step": { "confirm_discovery": { - "description": "K\u00e9rj\u00fck, addig nyomja a Bosch Smart Home Controller el\u00fcls\u0151 gombj\u00e1t, am\u00edg a LED villogni nem kezd.\n K\u00e9szen \u00e1ll a (z) {model} @ {host} be\u00e1ll\u00edt\u00e1s\u00e1nak folytat\u00e1s\u00e1ra a Home Assistant seg\u00edts\u00e9g\u00e9vel?" + "description": "K\u00e9rj\u00fck, addig nyomja a Bosch Smart Home Controller el\u00fcls\u0151 gombj\u00e1t, am\u00edg a LED villogni nem kezd.\nK\u00e9szen \u00e1ll {model} @ {host} be\u00e1ll\u00edt\u00e1s\u00e1nak folytat\u00e1s\u00e1ra Home Assistant seg\u00edts\u00e9g\u00e9vel?" }, "credentials": { "data": { @@ -27,9 +27,9 @@ }, "user": { "data": { - "host": "Hoszt" + "host": "C\u00edm" }, - "description": "\u00c1ll\u00edtsa be a Bosch intelligens otthoni vez\u00e9rl\u0151t, hogy lehet\u0151v\u00e9 tegye a fel\u00fcgyeletet \u00e9s a vez\u00e9rl\u00e9st a Home Assistant seg\u00edts\u00e9g\u00e9vel.", + "description": "\u00c1ll\u00edtsa be a Bosch intelligens otthoni vez\u00e9rl\u0151t, hogy lehet\u0151v\u00e9 tegye a fel\u00fcgyeletet \u00e9s a vez\u00e9rl\u00e9st Home Assistant seg\u00edts\u00e9g\u00e9vel.", "title": "SHC hiteles\u00edt\u00e9si param\u00e9terek" } } diff --git a/homeassistant/components/braviatv/translations/hu.json b/homeassistant/components/braviatv/translations/hu.json index 5f96af8bad7..00e88955c81 100644 --- a/homeassistant/components/braviatv/translations/hu.json +++ b/homeassistant/components/braviatv/translations/hu.json @@ -14,12 +14,12 @@ "data": { "pin": "PIN-k\u00f3d" }, - "description": "\u00cdrja be a Sony Bravia TV -n l\u00e1that\u00f3 PIN -k\u00f3dot. \n\n Ha a PIN -k\u00f3d nem jelenik meg, t\u00f6r\u00f6lje a Home Assistant regisztr\u00e1ci\u00f3j\u00e1t a t\u00e9v\u00e9n, l\u00e9pjen a k\u00f6vetkez\u0151re: Be\u00e1ll\u00edt\u00e1sok - > H\u00e1l\u00f3zat - > T\u00e1voli eszk\u00f6z be\u00e1ll\u00edt\u00e1sai - > T\u00e1vol\u00edtsa el a t\u00e1voli eszk\u00f6z regisztr\u00e1ci\u00f3j\u00e1t.", + "description": "\u00cdrja be a Sony Bravia TV -n l\u00e1that\u00f3 PIN -k\u00f3dot. \n\nHa a PIN -k\u00f3d nem jelenik meg, t\u00f6r\u00f6lje a Home Assistant regisztr\u00e1ci\u00f3j\u00e1t a t\u00e9v\u00e9n, l\u00e9pjen a k\u00f6vetkez\u0151re: Be\u00e1ll\u00edt\u00e1sok - > H\u00e1l\u00f3zat - > T\u00e1voli eszk\u00f6z be\u00e1ll\u00edt\u00e1sai - > T\u00e1vol\u00edtsa el a t\u00e1voli eszk\u00f6z regisztr\u00e1ci\u00f3j\u00e1t.", "title": "Sony Bravia TV enged\u00e9lyez\u00e9se" }, "user": { "data": { - "host": "Hoszt" + "host": "C\u00edm" }, "description": "\u00c1ll\u00edtsa be a Sony Bravia TV integr\u00e1ci\u00f3t. Ha probl\u00e9m\u00e1i vannak a konfigur\u00e1ci\u00f3val, l\u00e1togasson el a k\u00f6vetkez\u0151 oldalra: https://www.home-assistant.io/integrations/braviatv \n\n Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a TV be van kapcsolva.", "title": "Sony Bravia TV" diff --git a/homeassistant/components/broadlink/translations/es.json b/homeassistant/components/broadlink/translations/es.json index e7a35c2876f..d0020c55bca 100644 --- a/homeassistant/components/broadlink/translations/es.json +++ b/homeassistant/components/broadlink/translations/es.json @@ -38,7 +38,7 @@ "user": { "data": { "host": "Host", - "timeout": "Se acab\u00f3 el tiempo" + "timeout": "L\u00edmite de tiempo" }, "title": "Conectarse al dispositivo" } diff --git a/homeassistant/components/broadlink/translations/hu.json b/homeassistant/components/broadlink/translations/hu.json index 8b8dce984e5..3d792f43597 100644 --- a/homeassistant/components/broadlink/translations/hu.json +++ b/homeassistant/components/broadlink/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm", "not_supported": "Az eszk\u00f6z nem t\u00e1mogatott", @@ -22,22 +22,22 @@ "data": { "name": "N\u00e9v" }, - "title": "V\u00e1lassz egy nevet az eszk\u00f6znek" + "title": "V\u00e1lasszonegy nevet az eszk\u00f6znek" }, "reset": { - "description": "{name} ({model} a {host} c\u00edmen) z\u00e1rolva van. A hiteles\u00edt\u00e9shez \u00e9s a konfigur\u00e1ci\u00f3 befejez\u00e9s\u00e9hez fel kell oldani az eszk\u00f6z z\u00e1rol\u00e1s\u00e1t. Utas\u00edt\u00e1sok:\n 1. Nyisd meg a Broadlink alkalmaz\u00e1st.\n 2. Kattints az eszk\u00f6zre.\n 3. Kattints a jobb fels\u0151 sarokban tal\u00e1lhat\u00f3 `...` gombra.\n 4. G\u00f6rgess az oldal alj\u00e1ra.\n 5. Kapcsold ki a z\u00e1rol\u00e1s\u00e1t.", + "description": "{name} ({model} a {host} c\u00edmen) z\u00e1rolva van. A hiteles\u00edt\u00e9shez \u00e9s a konfigur\u00e1ci\u00f3 befejez\u00e9s\u00e9hez fel kell oldani az eszk\u00f6z z\u00e1rol\u00e1s\u00e1t. Utas\u00edt\u00e1sok:\n 1. Nyissa meg a Broadlink alkalmaz\u00e1st.\n 2. Kattintson az eszk\u00f6zre.\n 3. Kattintson a jobb fels\u0151 sarokban tal\u00e1lhat\u00f3 `...` gombra.\n 4. G\u00f6rgessen az oldal alj\u00e1ra.\n 5. Kapcsolja ki a z\u00e1rol\u00e1s\u00e1t.", "title": "Az eszk\u00f6z felold\u00e1sa" }, "unlock": { "data": { "unlock": "Igen, csin\u00e1ld." }, - "description": "{name} ({model} a {host} c\u00edmen) z\u00e1rolva van. Ez hiteles\u00edt\u00e9si probl\u00e9m\u00e1khoz vezethet a Home Assistantban. Szeretn\u00e9d feloldani?", + "description": "{name} ({model} a {host} c\u00edmen) z\u00e1rolva van. Ez hiteles\u00edt\u00e9si probl\u00e9m\u00e1khoz vezethet Home Assistant-ban. Szeretn\u00e9 feloldani?", "title": "Az eszk\u00f6z felold\u00e1sa (opcion\u00e1lis)" }, "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s" }, "title": "Csatlakoz\u00e1s az eszk\u00f6zh\u00f6z" diff --git a/homeassistant/components/brother/translations/hu.json b/homeassistant/components/brother/translations/hu.json index ae950f58f72..9d733e4cda6 100644 --- a/homeassistant/components/brother/translations/hu.json +++ b/homeassistant/components/brother/translations/hu.json @@ -9,11 +9,11 @@ "snmp_error": "Az SNMP szerver ki van kapcsolva, vagy a nyomtat\u00f3 nem t\u00e1mogatott.", "wrong_host": "\u00c9rv\u00e9nytelen \u00e1llom\u00e1sn\u00e9v vagy IP-c\u00edm." }, - "flow_title": "Brother nyomtat\u00f3: {model} {serial_number}", + "flow_title": "{model} {serial_number}", "step": { "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "type": "A nyomtat\u00f3 t\u00edpusa" }, "description": "A Brother nyomtat\u00f3 integr\u00e1ci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa. Ha probl\u00e9m\u00e1id vannak a konfigur\u00e1ci\u00f3val, l\u00e1togass el a k\u00f6vetkez\u0151 oldalra: https://www.home-assistant.io/integrations/brother" @@ -22,7 +22,7 @@ "data": { "type": "A nyomtat\u00f3 t\u00edpusa" }, - "description": "Hozz\u00e1 akarja adni a {model} Brother nyomtat\u00f3t, amelynek sorsz\u00e1ma: {serial_number} `, a Home Assistant-hoz?", + "description": "Hozz\u00e1 szeretn\u00e9 adni a {model} Brother nyomtat\u00f3t, amelynek sorsz\u00e1ma: `{serial_number}`, Home Assistant-hoz?", "title": "Felfedezett Brother nyomtat\u00f3" } } diff --git a/homeassistant/components/brother/translations/id.json b/homeassistant/components/brother/translations/id.json index 5e0b562017c..ed02999710e 100644 --- a/homeassistant/components/brother/translations/id.json +++ b/homeassistant/components/brother/translations/id.json @@ -9,7 +9,7 @@ "snmp_error": "Server SNMP dimatikan atau printer tidak didukung.", "wrong_host": "Nama host atau alamat IP tidak valid." }, - "flow_title": "Printer Brother: {model} {serial_number}", + "flow_title": "{model} {serial_number}", "step": { "user": { "data": { diff --git a/homeassistant/components/bsblan/translations/hu.json b/homeassistant/components/bsblan/translations/hu.json index 51feb8b75d7..60a781cc758 100644 --- a/homeassistant/components/bsblan/translations/hu.json +++ b/homeassistant/components/bsblan/translations/hu.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "passkey": "Jelsz\u00f3 karakterl\u00e1nc", "password": "Jelsz\u00f3", "port": "Port", diff --git a/homeassistant/components/bsblan/translations/id.json b/homeassistant/components/bsblan/translations/id.json index 6e8ac0bd4cb..83fdb88aae4 100644 --- a/homeassistant/components/bsblan/translations/id.json +++ b/homeassistant/components/bsblan/translations/id.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Gagal terhubung" }, - "flow_title": "BSB-Lan: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/buienradar/translations/id.json b/homeassistant/components/buienradar/translations/id.json index 194ecb51c12..a4331fced9f 100644 --- a/homeassistant/components/buienradar/translations/id.json +++ b/homeassistant/components/buienradar/translations/id.json @@ -1,8 +1,15 @@ { "config": { + "abort": { + "already_configured": "Lokasi sudah dikonfigurasi" + }, + "error": { + "already_configured": "Lokasi sudah dikonfigurasi" + }, "step": { "user": { "data": { + "latitude": "Lintang", "longitude": "Bujur" } } diff --git a/homeassistant/components/canary/translations/id.json b/homeassistant/components/canary/translations/id.json index 5f092847b4d..6fdc76feb72 100644 --- a/homeassistant/components/canary/translations/id.json +++ b/homeassistant/components/canary/translations/id.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Gagal terhubung" }, - "flow_title": "Canary: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/cast/translations/hu.json b/homeassistant/components/cast/translations/hu.json index 0f64f8de6fe..2d74d3183c8 100644 --- a/homeassistant/components/cast/translations/hu.json +++ b/homeassistant/components/cast/translations/hu.json @@ -11,11 +11,11 @@ "data": { "known_hosts": "Ismert hosztok" }, - "description": "K\u00e9rj\u00fck, add meg a Google Cast konfigur\u00e1ci\u00f3t.", + "description": "Ismert c\u00edmek - A cast eszk\u00f6z\u00f6k hostneveinek vagy IP-c\u00edmeinek vessz\u0151vel elv\u00e1lasztott list\u00e1ja, akkor haszn\u00e1lja, ha az mDNS felder\u00edt\u00e9s nem m\u0171k\u00f6dik.", "title": "Google Cast konfigur\u00e1ci\u00f3" }, "confirm": { - "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?" + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" } } }, @@ -29,7 +29,7 @@ "ignore_cec": "A CEC figyelmen k\u00edv\u00fcl hagy\u00e1sa", "uuid": "Enged\u00e9lyezett UUID-k" }, - "description": "Enged\u00e9lyezett UUID - vessz\u0151vel elv\u00e1lasztott lista a Cast-eszk\u00f6z\u00f6k UUID-j\u00e9b\u0151l, amelyeket hozz\u00e1 lehet adni a Home Assistanthoz. Csak akkor haszn\u00e1lja, ha nem akarja hozz\u00e1adni az \u00f6sszes rendelkez\u00e9sre \u00e1ll\u00f3 cast eszk\u00f6zt.\n CEC figyelmen k\u00edv\u00fcl hagy\u00e1sa - vessz\u0151vel elv\u00e1lasztott Chromecast-lista, amelynek figyelmen k\u00edv\u00fcl kell hagynia a CEC-adatokat az akt\u00edv bemenet meghat\u00e1roz\u00e1s\u00e1hoz. Ezt tov\u00e1bb\u00edtjuk a pychromecast.IGNORE_CEC c\u00edmre.", + "description": "Enged\u00e9lyezett UUID - vessz\u0151vel elv\u00e1lasztott lista a Cast-eszk\u00f6z\u00f6k UUID-j\u00e9b\u0151l, amelyeket hozz\u00e1 lehet adni Home Assistant-hoz. Csak akkor haszn\u00e1lja, ha nem akarja hozz\u00e1adni az \u00f6sszes rendelkez\u00e9sre \u00e1ll\u00f3 cast eszk\u00f6zt.\nCEC figyelmen k\u00edv\u00fcl hagy\u00e1sa - vessz\u0151vel elv\u00e1lasztott Chromecast-lista, amelynek figyelmen k\u00edv\u00fcl kell hagynia a CEC-adatokat az akt\u00edv bemenet meghat\u00e1roz\u00e1s\u00e1hoz. Ezt tov\u00e1bb\u00edtjuk a pychromecast.IGNORE_CEC c\u00edmre.", "title": "Speci\u00e1lis Google Cast-konfigur\u00e1ci\u00f3" }, "basic_options": { diff --git a/homeassistant/components/cast/translations/id.json b/homeassistant/components/cast/translations/id.json index b2c8d515548..b0a54f52897 100644 --- a/homeassistant/components/cast/translations/id.json +++ b/homeassistant/components/cast/translations/id.json @@ -9,10 +9,10 @@ "step": { "config": { "data": { - "known_hosts": "Daftar opsional host yang diketahui jika penemuan mDNS tidak berfungsi." + "known_hosts": "Host yang dikenal" }, - "description": "Masukkan konfigurasi Google Cast.", - "title": "Google Cast" + "description": "Host yang Dikenal - Daftar nama host atau alamat IP perangkat cast, dipisahkan dengan tanda koma, gunakan jika penemuan mDNS tidak berfungsi.", + "title": "Konfigurasi Google Cast" }, "confirm": { "description": "Ingin memulai penyiapan?" diff --git a/homeassistant/components/cast/translations/nl.json b/homeassistant/components/cast/translations/nl.json index fec645993b9..26dc954ef13 100644 --- a/homeassistant/components/cast/translations/nl.json +++ b/homeassistant/components/cast/translations/nl.json @@ -15,7 +15,7 @@ "title": "Google Cast configuratie" }, "confirm": { - "description": "Wil je beginnen met instellen?" + "description": "Wilt u beginnen met instellen?" } } }, diff --git a/homeassistant/components/cert_expiry/translations/hu.json b/homeassistant/components/cert_expiry/translations/hu.json index de459c324df..26f31465115 100644 --- a/homeassistant/components/cert_expiry/translations/hu.json +++ b/homeassistant/components/cert_expiry/translations/hu.json @@ -6,13 +6,13 @@ }, "error": { "connection_refused": "A kapcsolat megtagadva a gazdag\u00e9phez val\u00f3 csatlakoz\u00e1skor", - "connection_timeout": "T\u00fall\u00e9p\u00e9s, amikor ehhez a gazdag\u00e9phez kapcsol\u00f3dik", - "resolve_failed": "Ez a gazdag\u00e9p nem oldhat\u00f3 fel" + "connection_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s, ehhez a c\u00edmhez kapcsol\u00f3d\u00e1skor", + "resolve_failed": "Ez a c\u00edm nem oldhat\u00f3 fel" }, "step": { "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "name": "A tan\u00fas\u00edtv\u00e1ny neve", "port": "Port" }, diff --git a/homeassistant/components/climacell/translations/hu.json b/homeassistant/components/climacell/translations/hu.json index 909a5cdf1b5..3454a489455 100644 --- a/homeassistant/components/climacell/translations/hu.json +++ b/homeassistant/components/climacell/translations/hu.json @@ -15,7 +15,7 @@ "longitude": "Hossz\u00fas\u00e1g", "name": "N\u00e9v" }, - "description": "Ha a Sz\u00e9less\u00e9g \u00e9s Hossz\u00fas\u00e1g nincs megadva, akkor a Home Assistant konfigur\u00e1ci\u00f3j\u00e1ban l\u00e9v\u0151 alap\u00e9rtelmezett \u00e9rt\u00e9keket fogjuk haszn\u00e1lni. Minden el\u0151rejelz\u00e9si t\u00edpushoz l\u00e9trej\u00f6n egy entit\u00e1s, de alap\u00e9rtelmez\u00e9s szerint csak az \u00e1ltalad kiv\u00e1lasztottak lesznek enged\u00e9lyezve." + "description": "Ha a Sz\u00e9less\u00e9g \u00e9s Hossz\u00fas\u00e1g nincs megadva, akkor a Home Assistant konfigur\u00e1ci\u00f3j\u00e1ban l\u00e9v\u0151 alap\u00e9rtelmezett \u00e9rt\u00e9keket fogjuk haszn\u00e1lni. Minden el\u0151rejelz\u00e9si t\u00edpushoz l\u00e9trej\u00f6n egy entit\u00e1s, de alap\u00e9rtelmez\u00e9s szerint csak az \u00d6n \u00e1ltal kiv\u00e1lasztottak lesznek enged\u00e9lyezve." } } }, diff --git a/homeassistant/components/cloudflare/translations/he.json b/homeassistant/components/cloudflare/translations/he.json index fb0a20a223b..1f53e94240c 100644 --- a/homeassistant/components/cloudflare/translations/he.json +++ b/homeassistant/components/cloudflare/translations/he.json @@ -29,7 +29,7 @@ }, "zone": { "data": { - "zone": "\u05d0\u05b5\u05d6\u05d5\u05b9\u05e8" + "zone": "\u05d0\u05d6\u05d5\u05e8" } } } diff --git a/homeassistant/components/cloudflare/translations/id.json b/homeassistant/components/cloudflare/translations/id.json index c7878017de3..73f0455273c 100644 --- a/homeassistant/components/cloudflare/translations/id.json +++ b/homeassistant/components/cloudflare/translations/id.json @@ -10,7 +10,7 @@ "invalid_auth": "Autentikasi tidak valid", "invalid_zone": "Zona tidak valid" }, - "flow_title": "Cloudflare: {name}", + "flow_title": "{name}", "step": { "reauth_confirm": { "data": { diff --git a/homeassistant/components/co2signal/translations/es.json b/homeassistant/components/co2signal/translations/es.json index 61f21c21ca3..921dd22a76a 100644 --- a/homeassistant/components/co2signal/translations/es.json +++ b/homeassistant/components/co2signal/translations/es.json @@ -1,10 +1,13 @@ { "config": { "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", "api_ratelimit": "Se ha superado el l\u00edmite de velocidad de la API", "unknown": "Error inesperado" }, "error": { + "api_ratelimit": "Excedida tasa l\u00edmite del API", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { @@ -21,6 +24,7 @@ }, "user": { "data": { + "api_key": "Token de acceso", "location": "Obtener datos para" }, "description": "Visite https://co2signal.com/ para solicitar un token." diff --git a/homeassistant/components/co2signal/translations/hu.json b/homeassistant/components/co2signal/translations/hu.json index 00bc19e7b49..77dcbddb8f8 100644 --- a/homeassistant/components/co2signal/translations/hu.json +++ b/homeassistant/components/co2signal/translations/hu.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "api_ratelimit": "API D\u00edjkorl\u00e1t t\u00fall\u00e9pve", + "api_ratelimit": "API maxim\u00e1lis lek\u00e9r\u00e9ssz\u00e1m t\u00fall\u00e9pve", "unknown": "V\u00e1ratlan hiba" }, "error": { - "api_ratelimit": "API D\u00edjkorl\u00e1t t\u00fall\u00e9pve", + "api_ratelimit": "API maxim\u00e1lis lek\u00e9r\u00e9ssz\u00e1m t\u00fall\u00e9pve", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba" }, diff --git a/homeassistant/components/co2signal/translations/id.json b/homeassistant/components/co2signal/translations/id.json new file mode 100644 index 00000000000..76e72a93fd5 --- /dev/null +++ b/homeassistant/components/co2signal/translations/id.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "error": { + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Lintang", + "longitude": "Bujur" + } + }, + "country": { + "data": { + "country_code": "Kode Negara" + } + }, + "user": { + "data": { + "api_key": "Token Akses" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/translations/hu.json b/homeassistant/components/coolmaster/translations/hu.json index d52dba6b4b4..5f6f2eb2824 100644 --- a/homeassistant/components/coolmaster/translations/hu.json +++ b/homeassistant/components/coolmaster/translations/hu.json @@ -12,7 +12,7 @@ "fan_only": "T\u00e1mogaott csak ventil\u00e1tor m\u00f3d(ok)", "heat": "T\u00e1mogatott f\u0171t\u00e9si m\u00f3d(ok)", "heat_cool": "T\u00e1mogatott f\u0171t\u00e9si/h\u0171t\u00e9si m\u00f3d(ok)", - "host": "Hoszt", + "host": "C\u00edm", "off": "Ki lehet kapcsolni" }, "title": "\u00c1ll\u00edtsa be a CoolMasterNet kapcsolat r\u00e9szleteit." diff --git a/homeassistant/components/coronavirus/translations/id.json b/homeassistant/components/coronavirus/translations/id.json index e2626d16abb..f6bef10f8c0 100644 --- a/homeassistant/components/coronavirus/translations/id.json +++ b/homeassistant/components/coronavirus/translations/id.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Layanan sudah dikonfigurasi" + "already_configured": "Layanan sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung" }, "step": { "user": { diff --git a/homeassistant/components/crownstone/translations/ca.json b/homeassistant/components/crownstone/translations/ca.json new file mode 100644 index 00000000000..9de845d87c6 --- /dev/null +++ b/homeassistant/components/crownstone/translations/ca.json @@ -0,0 +1,96 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja est\u00e0 configurat", + "usb_setup_complete": "S'ha completat la configuraci\u00f3 USB de Crownstone.", + "usb_setup_unsuccessful": "La configuraci\u00f3 USB de Crownstone ha fallat." + }, + "error": { + "account_not_verified": "Compte no verificat. Activa el teu compte mitjan\u00e7ant el correu d'activaci\u00f3 de Crownstone.", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "usb_config": { + "data": { + "usb_path": "Ruta del dispositiu USB" + }, + "description": "Selecciona el port s\u00e8rie de l'adaptador USB Crownstone o selecciona 'No utilitzar USB' si no vols configurar l'adaptador USB.\n\nBusca un dispositiu amb VID 10C4 i PID EA60.", + "title": "Configuraci\u00f3 de l'adaptador USB Crownstone" + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "Ruta del dispositiu USB" + }, + "description": "Introdueix manualment la ruta de l'adaptador USB Crownstone.", + "title": "Ruta manual de l'adaptador USB Crownstone" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Esfera Crownstone" + }, + "description": "Selecciona la esfera Crownstone on es troba l'USB.", + "title": "Esfera Crownstone USB" + }, + "user": { + "data": { + "email": "Correu electr\u00f2nic", + "password": "Contrasenya" + }, + "title": "Compte de Crownstone" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "usb_sphere_option": "Esfera Crownstone on es troba l'USB.", + "use_usb_option": "Utilitza un adaptador USB Crownstone per a la transmissi\u00f3 de dades locals" + } + }, + "usb_config": { + "data": { + "usb_path": "Ruta del dispositiu USB" + }, + "description": "Selecciona el port s\u00e8rie de l'adaptador USB Crownstone.\n\nBusca un dispositiu amb VID 10C4 i PID EA60.", + "title": "Configuraci\u00f3 de l'adaptador USB Crownstone" + }, + "usb_config_option": { + "data": { + "usb_path": "Ruta del dispositiu USB" + }, + "description": "Selecciona el port s\u00e8rie de l'adaptador USB Crownstone.\n\nBusca un dispositiu amb VID 10C4 i PID EA60.", + "title": "Configuraci\u00f3 de l'adaptador USB Crownstone" + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "Ruta del dispositiu USB" + }, + "description": "Introdueix manualment la ruta de l'adaptador USB Crownstone.", + "title": "Ruta manual de l'adaptador USB Crownstone" + }, + "usb_manual_config_option": { + "data": { + "usb_manual_path": "Ruta del dispositiu USB" + }, + "description": "Introdueix manualment la ruta de l'adaptador USB Crownstone.", + "title": "Ruta manual de l'adaptador USB Crownstone" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Esfera Crownstone" + }, + "description": "Selecciona la esfera Crownstone on es troba l'USB.", + "title": "Esfera Crownstone USB" + }, + "usb_sphere_config_option": { + "data": { + "usb_sphere": "Esfera Crownstone" + }, + "description": "Selecciona la esfera Crownstone on es troba l'USB.", + "title": "Esfera Crownstone USB" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/cs.json b/homeassistant/components/crownstone/translations/cs.json new file mode 100644 index 00000000000..a7aaa1746f9 --- /dev/null +++ b/homeassistant/components/crownstone/translations/cs.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "usb_manual_config": { + "data": { + "usb_manual_path": "Cesta k USB za\u0159\u00edzen\u00ed" + } + }, + "user": { + "data": { + "email": "E-mail", + "password": "Heslo" + } + } + } + }, + "options": { + "step": { + "usb_config_option": { + "data": { + "usb_path": "Cesta k USB za\u0159\u00edzen\u00ed" + } + }, + "usb_manual_config_option": { + "data": { + "usb_manual_path": "Cesta k USB za\u0159\u00edzen\u00ed" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/de.json b/homeassistant/components/crownstone/translations/de.json new file mode 100644 index 00000000000..a969d9b2999 --- /dev/null +++ b/homeassistant/components/crownstone/translations/de.json @@ -0,0 +1,96 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert", + "usb_setup_complete": "Crownstone USB-Einrichtung abgeschlossen.", + "usb_setup_unsuccessful": "Crownstone USB-Einrichtung war nicht erfolgreich." + }, + "error": { + "account_not_verified": "Konto nicht verifiziert. Bitte aktiviere dein Konto \u00fcber die Aktivierungs-E-Mail von Crownstone.", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "usb_config": { + "data": { + "usb_path": "USB-Ger\u00e4te-Pfad" + }, + "description": "W\u00e4hle den seriellen Anschluss des Crownstone-USB-Dongles aus, oder w\u00e4hle \"Don't use USB\", wenn du keinen USB-Dongle einrichten m\u00f6chtest.\n\nSuche nach einem Ger\u00e4t mit VID 10C4 und PID EA60.", + "title": "Crownstone USB-Dongle-Konfiguration" + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "USB-Ger\u00e4te-Pfad" + }, + "description": "Gib den Pfad eines Crownstone USB-Dongles manuell ein.", + "title": "Crownstone USB-Dongle manueller Pfad" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "W\u00e4hle eine Crownstone Sphere aus, in der sich der USB-Stick befindet.", + "title": "Crownstone USB Sphere" + }, + "user": { + "data": { + "email": "E-Mail", + "password": "Passwort" + }, + "title": "Crownstone-Konto" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "usb_sphere_option": "Crownstone Sphere, wo sich der USB befindet", + "use_usb_option": "Verwende einen Crownstone USB-Dongle f\u00fcr die lokale Daten\u00fcbertragung" + } + }, + "usb_config": { + "data": { + "usb_path": "USB-Ger\u00e4te-Pfad" + }, + "description": "W\u00e4hle den seriellen Anschluss des Crownstone-USB-Dongles.\n\nSuche nach einem Ger\u00e4t mit VID 10C4 und PID EA60.", + "title": "Crownstone USB-Dongle-Konfiguration" + }, + "usb_config_option": { + "data": { + "usb_path": "USB-Ger\u00e4te-Pfad" + }, + "description": "W\u00e4hle den seriellen Anschluss des Crownstone-USB-Dongles.\n\nSuche nach einem Ger\u00e4t mit VID 10C4 und PID EA60.", + "title": "Crownstone USB-Dongle-Konfiguration" + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "USB-Ger\u00e4te-Pfad" + }, + "description": "Gib den Pfad eines Crownstone USB-Dongles manuell ein.", + "title": "Crownstone USB-Dongle manueller Pfad" + }, + "usb_manual_config_option": { + "data": { + "usb_manual_path": "USB-Ger\u00e4te-Pfad" + }, + "description": "Gib den Pfad eines Crownstone USB-Dongles manuell ein.", + "title": "Crownstone USB-Dongle manueller Pfad" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "W\u00e4hle eine Crownstone Sphere aus, in der sich der USB-Stick befindet.", + "title": "Crownstone USB Sphere" + }, + "usb_sphere_config_option": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "W\u00e4hle eine Crownstone Sphere aus, in der sich der USB-Stick befindet.", + "title": "Crownstone USB Sphere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/en.json b/homeassistant/components/crownstone/translations/en.json index 09a26b9739c..d6070c90a0f 100644 --- a/homeassistant/components/crownstone/translations/en.json +++ b/homeassistant/components/crownstone/translations/en.json @@ -56,6 +56,13 @@ "description": "Select the serial port of the Crownstone USB dongle.\n\nLook for a device with VID 10C4 and PID EA60.", "title": "Crownstone USB dongle configuration" }, + "usb_config_option": { + "data": { + "usb_path": "USB Device Path" + }, + "description": "Select the serial port of the Crownstone USB dongle.\n\nLook for a device with VID 10C4 and PID EA60.", + "title": "Crownstone USB dongle configuration" + }, "usb_manual_config": { "data": { "usb_manual_path": "USB Device Path" @@ -63,12 +70,26 @@ "description": "Manually enter the path of a Crownstone USB dongle.", "title": "Crownstone USB dongle manual path" }, + "usb_manual_config_option": { + "data": { + "usb_manual_path": "USB Device Path" + }, + "description": "Manually enter the path of a Crownstone USB dongle.", + "title": "Crownstone USB dongle manual path" + }, "usb_sphere_config": { "data": { "usb_sphere": "Crownstone Sphere" }, "description": "Select a Crownstone Sphere where the USB is located.", "title": "Crownstone USB Sphere" + }, + "usb_sphere_config_option": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "Select a Crownstone Sphere where the USB is located.", + "title": "Crownstone USB Sphere" } } } diff --git a/homeassistant/components/crownstone/translations/es.json b/homeassistant/components/crownstone/translations/es.json new file mode 100644 index 00000000000..f9038fb22b4 --- /dev/null +++ b/homeassistant/components/crownstone/translations/es.json @@ -0,0 +1,69 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya ha sido configurada" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "usb_config": { + "data": { + "usb_path": "Ruta del dispositivo USB" + } + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "Ruta del dispositivo USB" + } + }, + "user": { + "data": { + "email": "Correo electr\u00f3nico", + "password": "Contrase\u00f1a" + } + } + } + }, + "options": { + "step": { + "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" + }, + "usb_config_option": { + "data": { + "usb_path": "Ruta del dispositivo USB" + } + }, + "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" + }, + "usb_manual_config_option": { + "title": "Ruta manual del dispositivo USB Crownstone" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Esfera Crownstone" + }, + "description": "Selecciona una Esfera Crownstone donde se encuentra el USB.", + "title": "USB de Esfera Crownstone" + }, + "usb_sphere_config_option": { + "data": { + "usb_sphere": "Esfera Crownstone" + }, + "description": "Selecciona una Esfera Crownstone donde se encuentra el USB.", + "title": "USB de Esfera Crownstone" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/et.json b/homeassistant/components/crownstone/translations/et.json new file mode 100644 index 00000000000..3a651257e1a --- /dev/null +++ b/homeassistant/components/crownstone/translations/et.json @@ -0,0 +1,96 @@ +{ + "config": { + "abort": { + "already_configured": "Konto on juba h\u00e4\u00e4lestatud", + "usb_setup_complete": "Crownstone'i USB seadistamine on l\u00f5petatud.", + "usb_setup_unsuccessful": "Crownstone'i USB seadistamine nurjus." + }, + "error": { + "account_not_verified": "Konto pole kinnitatud. Aktiveeri oma konto Crownstone'i aktiveerimismeili kaudu.", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "usb_config": { + "data": { + "usb_path": "USB seadme rada" + }, + "description": "Vali Crownstone'i USB seadme jadaport v\u00f5i vali '\u00c4ra kasuta USB-d' kui ei soovi USB seadet h\u00e4\u00e4lestada. \n\n Otsi seadet mille VID on 10C4 ja PID on EA60.", + "title": "Crownstone'i USB seadme s\u00e4tted" + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "USB seadme rada" + }, + "description": "Sisesta k\u00e4sitsi Crownstone'i USBseadme rada.", + "title": "Crownstone'i USB seadme rada" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "Vali Crownstone Sphere kus USB asub.", + "title": "Crownstone USB Sphere" + }, + "user": { + "data": { + "email": "E-posti aadress", + "password": "Salas\u00f5na" + }, + "title": "Crownstone'i konto" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "usb_sphere_option": "Crownstone Sphere kus USB asub.", + "use_usb_option": "Kasuta Crownstone'i USB seadet kohalikuks andmeedastuseks" + } + }, + "usb_config": { + "data": { + "usb_path": "USB seadme rada" + }, + "description": "Vali Crownstone'i USB seadme jadaport. \n\n Otsi seadet mille VID on 10C4 ja PID on EA60.", + "title": "Crownstone'i USB seadme s\u00e4tted" + }, + "usb_config_option": { + "data": { + "usb_path": "USB seadme rada" + }, + "description": "Vali Crownstone'i USB seadme jadaport. \n\n Otsi seadet mille VID on 10C4 ja PID on EA60.", + "title": "Crownstone'i USB seadme s\u00e4tted" + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "USB seadme rada" + }, + "description": "Sisesta k\u00e4sitsi Crownstone'i USBseadme rada.", + "title": "Crownstone'i USB seadme rada" + }, + "usb_manual_config_option": { + "data": { + "usb_manual_path": "USB seadme rada" + }, + "description": "Sisesta k\u00e4sitsi Crownstone'i USBseadme rada.", + "title": "Crownstone'i USB seadme rada" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "Vali Crownstone Sphere kus USB asub.", + "title": "Crownstone USB Sphere" + }, + "usb_sphere_config_option": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "Vali Crownstone Sphere kus USB asub.", + "title": "Crownstone USB Sphere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/he.json b/homeassistant/components/crownstone/translations/he.json new file mode 100644 index 00000000000..af11b65839b --- /dev/null +++ b/homeassistant/components/crownstone/translations/he.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\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": { + "usb_config": { + "data": { + "usb_path": "\u05e0\u05ea\u05d9\u05d1 \u05d4\u05ea\u05e7\u05df USB" + } + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "\u05e0\u05ea\u05d9\u05d1 \u05d4\u05ea\u05e7\u05df USB" + } + }, + "user": { + "data": { + "email": "\u05d3\u05d5\u05d0\"\u05dc", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + } + } + }, + "options": { + "step": { + "usb_config": { + "data": { + "usb_path": "\u05e0\u05ea\u05d9\u05d1 \u05d4\u05ea\u05e7\u05df USB" + } + }, + "usb_config_option": { + "data": { + "usb_path": "\u05e0\u05ea\u05d9\u05d1 \u05d4\u05ea\u05e7\u05df USB" + } + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "\u05e0\u05ea\u05d9\u05d1 \u05d4\u05ea\u05e7\u05df USB" + } + }, + "usb_manual_config_option": { + "data": { + "usb_manual_path": "\u05e0\u05ea\u05d9\u05d1 \u05d4\u05ea\u05e7\u05df USB" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/hu.json b/homeassistant/components/crownstone/translations/hu.json new file mode 100644 index 00000000000..2c2a2e34fe1 --- /dev/null +++ b/homeassistant/components/crownstone/translations/hu.json @@ -0,0 +1,96 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "usb_setup_complete": "A Crownstone USB be\u00e1ll\u00edt\u00e1sa befejez\u0151d\u00f6tt.", + "usb_setup_unsuccessful": "A Crownstone USB be\u00e1ll\u00edt\u00e1sa sikertelen volt." + }, + "error": { + "account_not_verified": "Nem ellen\u0151rz\u00f6tt fi\u00f3k. K\u00e9rj\u00fck, aktiv\u00e1lja fi\u00f3kj\u00e1t a Crownstone-t\u00f3l kapott aktiv\u00e1l\u00f3 e-mailben.", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "usb_config": { + "data": { + "usb_path": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" + }, + "description": "V\u00e1lassza ki a Crownstone USB kulcs soros portj\u00e1t, vagy v\u00e1lassza 'Ne haszn\u00e1ljon USB-t' ha nem szerenke egy USB kulcsot be\u00e1ll\u00edtani most.\n\nKeressen egy VID 10C4 \u00e9s PID EA60 azonos\u00edt\u00f3val rendelkez\u0151 eszk\u00f6zt.", + "title": "Crownstone USB kulcs konfigur\u00e1ci\u00f3" + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" + }, + "description": "Adja meg manu\u00e1lisan a Crownstone USB kulcs \u00fatvonal\u00e1t.", + "title": "A Crownstone USB kulcs manu\u00e1lis el\u00e9r\u00e9si \u00fatja" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "V\u00e1lasszon egy Crownstone Sphere-t, ahol az USB tal\u00e1lhat\u00f3.", + "title": "Crownstone USB Sphere" + }, + "user": { + "data": { + "email": "E-mail", + "password": "Jelsz\u00f3" + }, + "title": "Crownstone fi\u00f3k" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "usb_sphere_option": "Crownstone Sphere, ahol az USB kulcs tal\u00e1lhat\u00f3", + "use_usb_option": "Crownstone USB-kulcs haszn\u00e1lata a helyi adat\u00e1tvitelhez" + } + }, + "usb_config": { + "data": { + "usb_path": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" + }, + "description": "V\u00e1lassza ki a Crownstone USB kulcs soros portj\u00e1t.\n\nKeressen egy VID 10C4 \u00e9s PID EA60 azonos\u00edt\u00f3val rendelkez\u0151 eszk\u00f6zt.", + "title": "Crownstone USB kulcs konfigur\u00e1ci\u00f3" + }, + "usb_config_option": { + "data": { + "usb_path": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" + }, + "description": "V\u00e1lassza ki a Crownstone USB kulcs soros portj\u00e1t.\n\nKeressen egy VID 10C4 \u00e9s PID EA60 azonos\u00edt\u00f3val rendelkez\u0151 eszk\u00f6zt.", + "title": "Crownstone USB kulcs konfigur\u00e1ci\u00f3" + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" + }, + "description": "Adja meg manu\u00e1lisan a Crownstone USB kulcs \u00fatvonal\u00e1t.", + "title": "A Crownstone USB kulcs manu\u00e1lis el\u00e9r\u00e9si \u00fatja" + }, + "usb_manual_config_option": { + "data": { + "usb_manual_path": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" + }, + "description": "Adja meg manu\u00e1lisan a Crownstone USB kulcs \u00fatvonal\u00e1t.", + "title": "A Crownstone USB kulcs manu\u00e1lis el\u00e9r\u00e9si \u00fatja" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "V\u00e1lasszon egy Crownstone Sphere-t, ahol az USB tal\u00e1lhat\u00f3.", + "title": "Crownstone USB Sphere" + }, + "usb_sphere_config_option": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "V\u00e1lasszon egy Crownstone Sphere-t, ahol az USB tal\u00e1lhat\u00f3.", + "title": "Crownstone USB Sphere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/id.json b/homeassistant/components/crownstone/translations/id.json new file mode 100644 index 00000000000..5bd28168d9a --- /dev/null +++ b/homeassistant/components/crownstone/translations/id.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi" + }, + "error": { + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "usb_config": { + "data": { + "usb_path": "Jalur Perangkat USB" + } + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "Jalur Perangkat USB" + } + }, + "user": { + "data": { + "email": "Email", + "password": "Kata Sandi" + } + } + } + }, + "options": { + "step": { + "usb_manual_config_option": { + "data": { + "usb_manual_path": "Jalur Perangkat USB" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/it.json b/homeassistant/components/crownstone/translations/it.json new file mode 100644 index 00000000000..1fb43e75684 --- /dev/null +++ b/homeassistant/components/crownstone/translations/it.json @@ -0,0 +1,96 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "usb_setup_complete": "Configurazione USB Crownstone completata.", + "usb_setup_unsuccessful": "La configurazione USB di Crownstone non ha avuto successo." + }, + "error": { + "account_not_verified": "Account non verificato. Attiva il tuo account tramite l'e-mail di attivazione di Crownstone.", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "usb_config": { + "data": { + "usb_path": "Percorso del dispositivo USB" + }, + "description": "Seleziona la porta seriale della chiavetta USB Crownstone. \n\nCerca un dispositivo con VID 10C4 e PID EA60.", + "title": "Configurazione della chiavetta USB Crownstone" + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "Percorso del dispositivo USB" + }, + "description": "Immettere manualmente il percorso di una chiavetta USB Crownstone.", + "title": "Percorso manuale della chiavetta USB Crownstone" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Sfera Crownstone" + }, + "description": "Seleziona una sfera di Crownstone dove si trova l'USB.", + "title": "Sfera USB Crownstone" + }, + "user": { + "data": { + "email": "E-mail", + "password": "Password" + }, + "title": "Account Crownstone" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "usb_sphere_option": "Sfera di Crownstone dove si trova l'USB", + "use_usb_option": "Utilizzare una chiavetta USB Crownstone per la trasmissione locale dei dati" + } + }, + "usb_config": { + "data": { + "usb_path": "Percorso del dispositivo USB" + }, + "description": "Seleziona la porta seriale della chiavetta USB Crownstone. \n\nCerca un dispositivo con VID 10C4 e PID EA60.", + "title": "Configurazione della chiavetta USB Crownstone" + }, + "usb_config_option": { + "data": { + "usb_path": "Percorso del dispositivo USB" + }, + "description": "Seleziona la porta seriale della chiavetta USB Crownstone. \n\nCerca un dispositivo con VID 10C4 e PID EA60.", + "title": "Configurazione della chiavetta USB Crownstone" + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "Percorso del dispositivo USB" + }, + "description": "Immettere manualmente il percorso di una chiavetta USB Crownstone.", + "title": "Percorso manuale della chiavetta USB Crownstone" + }, + "usb_manual_config_option": { + "data": { + "usb_manual_path": "Percorso del dispositivo USB" + }, + "description": "Immettere manualmente il percorso di una chiavetta USB Crownstone.", + "title": "Percorso manuale della chiavetta USB Crownstone" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Sfera Crownstone" + }, + "description": "Seleziona una sfera di Crownstone dove si trova l'USB.", + "title": "Sfera USB Crownstone" + }, + "usb_sphere_config_option": { + "data": { + "usb_sphere": "Sfera Crownstone" + }, + "description": "Seleziona una sfera Crownstone in cui si trova l'USB.", + "title": "Sfera USB Crownstone" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/ko.json b/homeassistant/components/crownstone/translations/ko.json new file mode 100644 index 00000000000..aadd2d3da42 --- /dev/null +++ b/homeassistant/components/crownstone/translations/ko.json @@ -0,0 +1,16 @@ +{ + "options": { + "step": { + "usb_config_option": { + "data": { + "usb_path": "USB \uc7a5\uce58 \uacbd\ub85c" + } + }, + "usb_manual_config_option": { + "data": { + "usb_manual_path": "USB \uc7a5\uce58 \uacbd\ub85c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/nl.json b/homeassistant/components/crownstone/translations/nl.json new file mode 100644 index 00000000000..1da12c8f841 --- /dev/null +++ b/homeassistant/components/crownstone/translations/nl.json @@ -0,0 +1,96 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd", + "usb_setup_complete": "Crownstone USB installatie voltooid.", + "usb_setup_unsuccessful": "Crownstone USB installatie is mislukt." + }, + "error": { + "account_not_verified": "Account niet geverifieerd. Gelieve uw account te activeren via de activeringsmail van Crownstone.", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "usb_config": { + "data": { + "usb_path": "USB-apparaatpad" + }, + "description": "Selecteer de seri\u00eble poort van de Crownstone USB dongle, of selecteer 'Don't use USB' als u geen USB dongle wilt instellen.\n\nZoek naar een apparaat met VID 10C4 en PID EA60.", + "title": "Crownstone USB dongle configuratie" + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "USB-apparaatpad" + }, + "description": "Voer handmatig het pad van een Crownstone USB dongle in.", + "title": "Crownstone USB-dongle handmatig pad" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "Selecteer een Crownstone Sphere waar de USB zich bevindt.", + "title": "Crownstone USB Sphere" + }, + "user": { + "data": { + "email": "E-mail", + "password": "Wachtwoord" + }, + "title": "Crownstone account" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "usb_sphere_option": "Crownstone Sphere waar de USB zich bevindt", + "use_usb_option": "Gebruik een Crownstone USB-dongle voor lokale gegevensoverdracht" + } + }, + "usb_config": { + "data": { + "usb_path": "USB-apparaatpad" + }, + "description": "Selecteer de seri\u00eble poort van de Crownstone USB dongle.\n\nZoek naar een apparaat met VID 10C4 en PID EA60.", + "title": "Crownstone USB dongle configuratie" + }, + "usb_config_option": { + "data": { + "usb_path": "USB-apparaatpad" + }, + "description": "Selecteer de seri\u00eble poort van de Crownstone USB dongle.\n\nZoek naar een apparaat met VID 10C4 en PID EA60.", + "title": "Crownstone USB dongle configuratie" + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "USB-apparaatpad" + }, + "description": "Voer handmatig het pad van een Crownstone USB dongle in.", + "title": "Crownstone USB-dongle handmatig pad" + }, + "usb_manual_config_option": { + "data": { + "usb_manual_path": "USB-apparaatpad" + }, + "description": "Voer handmatig het pad van een Crownstone USB dongle in.", + "title": "Crownstone USB-dongle handmatig pad" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "Selecteer een Crownstone Sphere waar de USB zich bevindt.", + "title": "Crownstone USB Sphere" + }, + "usb_sphere_config_option": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "Selecteer een Crownstone Sphere waar de USB zich bevindt.", + "title": "Crownstone USB Sphere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/no.json b/homeassistant/components/crownstone/translations/no.json new file mode 100644 index 00000000000..88f3578a9a4 --- /dev/null +++ b/homeassistant/components/crownstone/translations/no.json @@ -0,0 +1,96 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert", + "usb_setup_complete": "Crownstone USB -oppsett fullf\u00f8rt.", + "usb_setup_unsuccessful": "Crownstone USB -oppsett mislyktes." + }, + "error": { + "account_not_verified": "Kontoen er ikke bekreftet. Vennligst aktiver kontoen din via aktiverings -e -posten fra Crownstone.", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "usb_config": { + "data": { + "usb_path": "USB enhetsbane" + }, + "description": "Velg den serielle porten p\u00e5 Crownstone USB -dongelen, eller velg 'Ikke bruk USB' hvis du ikke vil konfigurere en USB -dongle. \n\n Se etter en enhet med VID 10C4 og PID EA60.", + "title": "Crownstone USB -dongle -konfigurasjon" + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "USB enhetsbane" + }, + "description": "Skriv inn banen til en Crownstone USB -dongle manuelt.", + "title": "Crownstone USB -dongle manuell bane" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Crownstone USB Sphere" + }, + "description": "Velg en Crownstone Sphere der USB -en er plassert.", + "title": "Crownstone USB Sphere" + }, + "user": { + "data": { + "email": "E-post", + "password": "Passord" + }, + "title": "Crownstone -konto" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "usb_sphere_option": "Velg en Crownstone Sphere der USB -en er plassert.", + "use_usb_option": "Bruk en Crownstone USB -dongle for lokal dataoverf\u00f8ring" + } + }, + "usb_config": { + "data": { + "usb_path": "USB enhetsbane" + }, + "description": "Velg serieporten til Crownstone USB -dongelen. \n\n Se etter en enhet med VID 10C4 og PID EA60.", + "title": "Crownstone USB -dongle -konfigurasjon" + }, + "usb_config_option": { + "data": { + "usb_path": "USB enhetsbane" + }, + "description": "Velg serieporten til Crownstone USB -dongelen. \n\n Se etter en enhet med VID 10C4 og PID EA60.", + "title": "Crownstone USB -dongle -konfigurasjon" + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "USB enhetsbane" + }, + "description": "Skriv inn banen til en Crownstone USB -dongle manuelt.", + "title": "Crownstone USB -dongle manuell bane" + }, + "usb_manual_config_option": { + "data": { + "usb_manual_path": "USB enhetsbane" + }, + "description": "Skriv inn banen til en Crownstone USB -dongle manuelt.", + "title": "Crownstone USB -dongle manuell bane" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "Velg en Crownstone Sphere der USB -en er plassert.", + "title": "Crownstone USB Sphere" + }, + "usb_sphere_config_option": { + "data": { + "usb_sphere": "Crownstone USB Sphere" + }, + "description": "Velg en Crownstone Sphere der USB -en er plassert.", + "title": "Crownstone USB Sphere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/ru.json b/homeassistant/components/crownstone/translations/ru.json new file mode 100644 index 00000000000..7dfd88bd63e --- /dev/null +++ b/homeassistant/components/crownstone/translations/ru.json @@ -0,0 +1,96 @@ +{ + "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.", + "usb_setup_complete": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Crownstone USB \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0430.", + "usb_setup_unsuccessful": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Crownstone USB \u043d\u0435 \u0443\u0434\u0430\u043b\u0430\u0441\u044c." + }, + "error": { + "account_not_verified": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u043d\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u043d\u0430. \u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0443\u0439\u0442\u0435 \u0435\u0451 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043f\u0438\u0441\u044c\u043c\u0430, \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u043e\u0433\u043e \u043f\u043e \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u0435.", + "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": { + "usb_config": { + "data": { + "usb_path": "\u041f\u0443\u0442\u044c \u043a USB-\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 USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Crownstone \u0438\u043b\u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 'Don't use USB', \u0435\u0441\u043b\u0438 \u0412\u044b \u043d\u0435 \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0442\u044c \u0435\u0433\u043e. \n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u044b\u0431\u0438\u0440\u0430\u0439\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441 VID 10C4 \u0438 PID EA60.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Crownstone" + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0432\u0440\u0443\u0447\u043d\u0443\u044e \u043f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Crownstone.", + "title": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Crownstone" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 Crownstone Sphere, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e.", + "title": "Crownstone USB Sphere" + }, + "user": { + "data": { + "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "title": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c Crownstone" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "usb_sphere_option": "Crownstone Sphere, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", + "use_usb_option": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Crownstone \u0434\u043b\u044f \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0434\u0430\u043d\u043d\u044b\u0445" + } + }, + "usb_config": { + "data": { + "usb_path": "\u041f\u0443\u0442\u044c \u043a USB-\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 USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Crownstone. \n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u044b\u0431\u0438\u0440\u0430\u0439\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441 VID 10C4 \u0438 PID EA60.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Crownstone" + }, + "usb_config_option": { + "data": { + "usb_path": "\u041f\u0443\u0442\u044c \u043a USB-\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 USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Crownstone. \n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u044b\u0431\u0438\u0440\u0430\u0439\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441 VID 10C4 \u0438 PID EA60.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Crownstone" + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0432\u0440\u0443\u0447\u043d\u0443\u044e \u043f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Crownstone.", + "title": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Crownstone" + }, + "usb_manual_config_option": { + "data": { + "usb_manual_path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0432\u0440\u0443\u0447\u043d\u0443\u044e \u043f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Crownstone.", + "title": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Crownstone" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 Crownstone Sphere, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e.", + "title": "Crownstone USB Sphere" + }, + "usb_sphere_config_option": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 Crownstone Sphere, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e.", + "title": "Crownstone USB Sphere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/zh-Hant.json b/homeassistant/components/crownstone/translations/zh-Hant.json new file mode 100644 index 00000000000..2c362ba0bcb --- /dev/null +++ b/homeassistant/components/crownstone/translations/zh-Hant.json @@ -0,0 +1,96 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "usb_setup_complete": "Crownstone USB \u8a2d\u5b9a\u5b8c\u6210\u3002", + "usb_setup_unsuccessful": "Crownstone USB \u8a2d\u5b9a\u6210\u529f\u3002" + }, + "error": { + "account_not_verified": "\u5e33\u865f\u5c1a\u672a\u9a57\u8b49\u3001\u8acb\u900f\u904e\u4f86\u81ea Crownstone \u7684\u9a57\u8b49\u90f5\u4ef6\u555f\u52d5\u60a8\u7684\u5e33\u865f\u3002", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "usb_config": { + "data": { + "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" + }, + "description": "\u9078\u64c7 Crownstone USB \u88dd\u7f6e\u5e8f\u5217\u57e0\uff0c\u6216\u5047\u5982\u60a8\u4e0d\u60f3\u8a2d\u5b9a USB \u88dd\u7f6e\u7684\u8a71\u3001\u8acb\u9078\u64c7 '\u4e0d\u4f7f\u7528 USB'\u3002\n\n\u8acb\u641c\u5c0b VID 10C4 \u53ca PID EA60 \u88dd\u7f6e\u3002", + "title": "Crownstone USB \u88dd\u7f6e\u8a2d\u5b9a" + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "USB \u88dd\u7f6e\u8def\u5f91" + }, + "description": "\u624b\u52d5\u8f38\u5165 Crownstone USB \u88dd\u7f6e\u8def\u5f91\u3002", + "title": "Crownstone USB \u88dd\u7f6e\u624b\u52d5\u8def\u5f91" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "\u9078\u64c7 Crownstone Sphere \u6240\u5728 USB \u8def\u5f91\u3002", + "title": "Crownstone USB Sphere" + }, + "user": { + "data": { + "email": "\u96fb\u5b50\u90f5\u4ef6", + "password": "\u5bc6\u78bc" + }, + "title": "Crownstone \u5e33\u865f" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "usb_sphere_option": "Crownstone Sphere \u6240\u5728 USB \u8def\u5f91", + "use_usb_option": "\u4f7f\u7528 Crownstone USB \u88dd\u7f6e\u9032\u884c\u672c\u5730\u7aef\u8cc7\u6599\u50b3\u8f38" + } + }, + "usb_config": { + "data": { + "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" + }, + "description": "\u9078\u64c7 Crownstone USB \u88dd\u7f6e\u5e8f\u5217\u57e0\u3002\n\n\u8acb\u641c\u5c0b VID 10C4 \u53ca PID EA60 \u88dd\u7f6e\u3002", + "title": "Crownstone USB \u88dd\u7f6e\u8a2d\u5b9a" + }, + "usb_config_option": { + "data": { + "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" + }, + "description": "\u9078\u64c7 Crownstone USB \u88dd\u7f6e\u5e8f\u5217\u57e0\u3002\n\n\u8acb\u641c\u5c0b VID 10C4 \u53ca PID EA60 \u88dd\u7f6e\u3002", + "title": "Crownstone USB \u88dd\u7f6e\u8a2d\u5b9a" + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "USB \u88dd\u7f6e\u8def\u5f91" + }, + "description": "\u624b\u52d5\u8f38\u5165 Crownstone USB \u88dd\u7f6e\u8def\u5f91\u3002", + "title": "Crownstone USB \u88dd\u7f6e\u624b\u52d5\u8def\u5f91" + }, + "usb_manual_config_option": { + "data": { + "usb_manual_path": "USB \u88dd\u7f6e\u8def\u5f91" + }, + "description": "\u624b\u52d5\u8f38\u5165 Crownstone USB \u88dd\u7f6e\u8def\u5f91\u3002", + "title": "Crownstone USB \u88dd\u7f6e\u624b\u52d5\u8def\u5f91" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "\u9078\u64c7 Crownstone Sphere \u6240\u5728 USB \u8def\u5f91\u3002", + "title": "Crownstone USB Sphere" + }, + "usb_sphere_config_option": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "\u9078\u64c7 Crownstone Sphere \u6240\u5728 USB \u8def\u5f91\u3002", + "title": "Crownstone USB Sphere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/translations/hu.json b/homeassistant/components/daikin/translations/hu.json index f1cb7eab8f6..6049890cb53 100644 --- a/homeassistant/components/daikin/translations/hu.json +++ b/homeassistant/components/daikin/translations/hu.json @@ -13,10 +13,10 @@ "user": { "data": { "api_key": "API kulcs", - "host": "Hoszt", + "host": "C\u00edm", "password": "Jelsz\u00f3" }, - "description": "Add meg a Daikin l\u00e9gkond\u00edcion\u00e1l\u00f3 IP-c\u00edm\u00e9t.", + "description": "Adja meg Daikin k\u00e9sz\u00fcl\u00e9k\u00e9nek az IP c\u00edm\u00e9t.\n\nNe feledje, hogy z API kulcs \u00e9s a Jelsz\u00f3 funkci\u00f3t csak a BRP072Cxx \u00e9s a SKYFi eszk\u00f6z\u00f6k haszn\u00e1lj\u00e1k.", "title": "A Daikin l\u00e9gkond\u00edcion\u00e1l\u00f3 konfigur\u00e1l\u00e1sa" } } diff --git a/homeassistant/components/deconz/translations/es.json b/homeassistant/components/deconz/translations/es.json index 3670caf18d0..5608b95288d 100644 --- a/homeassistant/components/deconz/translations/es.json +++ b/homeassistant/components/deconz/translations/es.json @@ -81,7 +81,7 @@ "remote_flip_180_degrees": "Dispositivo volteado 180 grados", "remote_flip_90_degrees": "Dispositivo volteado 90 grados", "remote_gyro_activated": "Dispositivo sacudido", - "remote_moved": "Dispositivo movido con \"{subtipo}\" hacia arriba", + "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} \"", diff --git a/homeassistant/components/deconz/translations/hu.json b/homeassistant/components/deconz/translations/hu.json index bc003a279e8..664f3768a22 100644 --- a/homeassistant/components/deconz/translations/hu.json +++ b/homeassistant/components/deconz/translations/hu.json @@ -2,8 +2,8 @@ "config": { "abort": { "already_configured": "A bridge m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", - "no_bridges": "Nem tal\u00e1ltam deCONZ bridget", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "no_bridges": "Nem tal\u00e1lhat\u00f3 deCONZ \u00e1tj\u00e1r\u00f3", "no_hardware_available": "Nincs deCONZ-hoz csatlakoztatott r\u00e1di\u00f3hardver", "not_deconz_bridge": "Nem egy deCONZ \u00e1tj\u00e1r\u00f3", "updated_instance": "A deCONZ-p\u00e9ld\u00e1ny \u00faj \u00e1llom\u00e1sc\u00edmmel friss\u00edtve" @@ -11,19 +11,19 @@ "error": { "no_key": "API kulcs lek\u00e9r\u00e9se nem siker\u00fclt" }, - "flow_title": "deCONZ Zigbee \u00e1tj\u00e1r\u00f3 ({host})", + "flow_title": "{host}", "step": { "hassio_confirm": { - "description": "Be szeretn\u00e9 \u00e1ll\u00edtani a Home Assistant-ot, hogy csatlakozzon a (z) {addon} \u00e1ltal biztos\u00edtott deCONZ \u00e1tj\u00e1r\u00f3hoz?", - "title": "deCONZ Zigbee \u00e1tj\u00e1r\u00f3 a Supervisor kieg\u00e9sz\u00edt\u0151 seg\u00edts\u00e9g\u00e9vel" + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani Home Assistant-ot, hogy csatlakozzon {addon} \u00e1ltal biztos\u00edtott deCONZ \u00e1tj\u00e1r\u00f3hoz?", + "title": "deCONZ Zigbee \u00e1tj\u00e1r\u00f3 Home Assistant kieg\u00e9sz\u00edt\u0151 \u00e1ltal" }, "link": { - "description": "Oldja fel a deCONZ \u00e1tj\u00e1r\u00f3t a Home Assistant-ban val\u00f3 regisztr\u00e1l\u00e1shoz.\n\n1. Menjen a deCONZ rendszer be\u00e1ll\u00edt\u00e1sokhoz\n2. Nyomja meg az \"\u00c1tj\u00e1r\u00f3 felold\u00e1sa\" gombot", + "description": "Enged\u00e9lyezze fel a deCONZ \u00e1tj\u00e1r\u00f3ban a Home Assistant-hoz val\u00f3 regisztr\u00e1l\u00e1st.\n\n1. V\u00e1lassza ki a deCONZ rendszer be\u00e1ll\u00edt\u00e1sait\n2. Nyomja meg az \"Authenticate app\" gombot", "title": "Kapcsol\u00f3d\u00e1s a deCONZ-hoz" }, "manual_input": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "port": "Port" } }, @@ -71,7 +71,7 @@ "remote_button_quintuple_press": "\"{subtype}\" gombra \u00f6tsz\u00f6r kattintottak", "remote_button_rotated": "A gomb elforgatva: \"{subtype}\"", "remote_button_rotated_fast": "A gomb gyorsan elfordult: \"{subtype}\"", - "remote_button_rotation_stopped": "A (z) \"{subtype}\" gomb forg\u00e1sa le\u00e1llt", + "remote_button_rotation_stopped": "A(z) \"{subtype}\" gomb forg\u00e1sa le\u00e1llt", "remote_button_short_press": "\"{subtype}\" gomb lenyomva", "remote_button_short_release": "\"{subtype}\" gomb elengedve", "remote_button_triple_press": "\"{subtype}\" gombra h\u00e1romszor kattintottak", diff --git a/homeassistant/components/deconz/translations/id.json b/homeassistant/components/deconz/translations/id.json index c6d54beaec2..f63261e6e87 100644 --- a/homeassistant/components/deconz/translations/id.json +++ b/homeassistant/components/deconz/translations/id.json @@ -11,11 +11,11 @@ "error": { "no_key": "Tidak bisa mendapatkan kunci API" }, - "flow_title": "Gateway Zigbee deCONZ ({host})", + "flow_title": "{host}", "step": { "hassio_confirm": { - "description": "Ingin mengonfigurasi Home Assistant untuk terhubung ke gateway deCONZ yang disediakan oleh add-on Supervisor {addon}?", - "title": "Gateway Zigbee deCONZ melalui add-on Supervisor" + "description": "Ingin mengonfigurasi Home Assistant untuk terhubung ke gateway deCONZ yang disediakan oleh add-on: {addon}?", + "title": "Gateway Zigbee deCONZ melalui add-on Home Assistant" }, "link": { "description": "Buka gateway deCONZ Anda untuk mendaftarkan ke Home Assistant. \n\n1. Buka pengaturan sistem deCONZ \n2. Tekan tombol \"Authenticate app\"", diff --git a/homeassistant/components/demo/translations/he.json b/homeassistant/components/demo/translations/he.json new file mode 100644 index 00000000000..c3162b87a5e --- /dev/null +++ b/homeassistant/components/demo/translations/he.json @@ -0,0 +1,3 @@ +{ + "title": "\u05d4\u05d3\u05d2\u05de\u05d4" +} \ No newline at end of file diff --git a/homeassistant/components/demo/translations/hu.json b/homeassistant/components/demo/translations/hu.json index 3bfe095189a..e77c21294b8 100644 --- a/homeassistant/components/demo/translations/hu.json +++ b/homeassistant/components/demo/translations/hu.json @@ -17,7 +17,7 @@ "options_2": { "data": { "multi": "T\u00f6bbsz\u00f6r\u00f6s kijel\u00f6l\u00e9s", - "select": "V\u00e1lassz egy lehet\u0151s\u00e9get", + "select": "V\u00e1lasszon egy lehet\u0151s\u00e9get", "string": "Karakterl\u00e1nc \u00e9rt\u00e9k" } } diff --git a/homeassistant/components/demo/translations/ro.json b/homeassistant/components/demo/translations/ro.json new file mode 100644 index 00000000000..96e182c6d54 --- /dev/null +++ b/homeassistant/components/demo/translations/ro.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "few": "Cateva", + "one": "Unu", + "other": "Altele" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/denonavr/translations/hu.json b/homeassistant/components/denonavr/translations/hu.json index 43ee362d65a..c22d392dc8a 100644 --- a/homeassistant/components/denonavr/translations/hu.json +++ b/homeassistant/components/denonavr/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", "cannot_connect": "Nem siker\u00fclt csatlakozni, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra. A h\u00e1l\u00f3zati \u00e9s Ethernet k\u00e1belek kih\u00faz\u00e1sa \u00e9s \u00fajracsatlakoztat\u00e1sa seg\u00edthet", "not_denonavr_manufacturer": "Nem egy Denon AVR h\u00e1l\u00f3zati vev\u0151, felfedezett gy\u00e1rt\u00f3 nem egyezik", "not_denonavr_missing": "Nem Denon AVR h\u00e1l\u00f3zati vev\u0151, a felfedez\u00e9si inform\u00e1ci\u00f3k nem teljesek" diff --git a/homeassistant/components/denonavr/translations/id.json b/homeassistant/components/denonavr/translations/id.json index d78f547ef35..0bafe289842 100644 --- a/homeassistant/components/denonavr/translations/id.json +++ b/homeassistant/components/denonavr/translations/id.json @@ -10,7 +10,7 @@ "error": { "discovery_error": "Gagal menemukan Network Receiver AVR Denon" }, - "flow_title": "Network Receiver Denon AVR: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Konfirmasikan penambahan Receiver", diff --git a/homeassistant/components/devolo_home_control/translations/ca.json b/homeassistant/components/devolo_home_control/translations/ca.json index 968624e15c8..73417c5c564 100644 --- a/homeassistant/components/devolo_home_control/translations/ca.json +++ b/homeassistant/components/devolo_home_control/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat", + "already_configured": "El compte ja est\u00e0 configurat", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { diff --git a/homeassistant/components/devolo_home_control/translations/id.json b/homeassistant/components/devolo_home_control/translations/id.json index 31f0f87dc00..41d2100b6ed 100644 --- a/homeassistant/components/devolo_home_control/translations/id.json +++ b/homeassistant/components/devolo_home_control/translations/id.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Akun sudah dikonfigurasi" + "already_configured": "Akun sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "invalid_auth": "Autentikasi tidak valid" @@ -13,6 +14,13 @@ "password": "Kata Sandi", "username": "Email/ID devolo" } + }, + "zeroconf_confirm": { + "data": { + "mydevolo_url": "URL mydevolo", + "password": "Kata Sandi", + "username": "Email/devolo ID" + } } } } diff --git a/homeassistant/components/devolo_home_control/translations/ko.json b/homeassistant/components/devolo_home_control/translations/ko.json index 9c9a21182cc..f3832dec7c1 100644 --- a/homeassistant/components/devolo_home_control/translations/ko.json +++ b/homeassistant/components/devolo_home_control/translations/ko.json @@ -13,6 +13,13 @@ "password": "\ube44\ubc00\ubc88\ud638", "username": "\uc774\uba54\uc77c / devolo ID" } + }, + "zeroconf_confirm": { + "data": { + "mydevolo_url": "mydevolo URL \uc8fc\uc18c", + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc774\uba54\uc77c / devolo ID" + } } } } diff --git a/homeassistant/components/dexcom/translations/ca.json b/homeassistant/components/dexcom/translations/ca.json index e188718a71d..7b97a209e49 100644 --- a/homeassistant/components/dexcom/translations/ca.json +++ b/homeassistant/components/dexcom/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat" + "already_configured": "El compte ja est\u00e0 configurat" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", diff --git a/homeassistant/components/dialogflow/translations/hu.json b/homeassistant/components/dialogflow/translations/hu.json index 17f38b0262f..23a6001d77c 100644 --- a/homeassistant/components/dialogflow/translations/hu.json +++ b/homeassistant/components/dialogflow/translations/hu.json @@ -2,14 +2,14 @@ "config": { "abort": { "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", - "webhook_not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." + "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, "create_entry": { "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a [Dialogflow webhook integr\u00e1ci\u00f3j\u00e1t]({dialogflow_url}). \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/json \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t]({docs_url})." }, "step": { "user": { - "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani az Dialogflowt?", + "description": "Biztos benne, hogy be szeretn\u00e9 \u00e1ll\u00edtani a Dialogflowt?", "title": "Dialogflow Webhook be\u00e1ll\u00edt\u00e1sa" } } diff --git a/homeassistant/components/directv/translations/hu.json b/homeassistant/components/directv/translations/hu.json index 3e0a7d5cb57..9e3aa3efb13 100644 --- a/homeassistant/components/directv/translations/hu.json +++ b/homeassistant/components/directv/translations/hu.json @@ -14,11 +14,11 @@ "one": "\u00dcres", "other": "\u00dcres" }, - "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}-t?" + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" }, "user": { "data": { - "host": "Hoszt" + "host": "C\u00edm" } } } diff --git a/homeassistant/components/directv/translations/id.json b/homeassistant/components/directv/translations/id.json index 74f778d6cee..fcf7318d906 100644 --- a/homeassistant/components/directv/translations/id.json +++ b/homeassistant/components/directv/translations/id.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Gagal terhubung" }, - "flow_title": "DirecTV: {name}", + "flow_title": "{name}", "step": { "ssdp_confirm": { "description": "Ingin menyiapkan {name}?" diff --git a/homeassistant/components/dlna_dmr/translations/ca.json b/homeassistant/components/dlna_dmr/translations/ca.json new file mode 100644 index 00000000000..cf3adc94405 --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/ca.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "could_not_connect": "No s'ha pogut connectar amb el dispositiu DLNA", + "discovery_error": "No s'ha pogut descobrir cap dispositiu DLNA coincident", + "incomplete_config": "Falta una variable obligat\u00f2ria a la configuraci\u00f3", + "non_unique_id": "S'han trobat diversos dispositius amb el mateix identificador \u00fanic", + "not_dmr": "El dispositiu no \u00e9s un renderitzador de mitjans digitals" + }, + "error": { + "could_not_connect": "No s'ha pogut connectar amb el dispositiu DLNA", + "not_dmr": "El dispositiu no \u00e9s un renderitzador de mitjans digitals" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Vols comen\u00e7ar la configuraci\u00f3?" + }, + "user": { + "data": { + "url": "URL" + }, + "description": "URL al fitxer XML de descripci\u00f3 de dispositiu", + "title": "Renderitzador de mitjans digitals DLNA" + } + } + }, + "options": { + "error": { + "invalid_url": "URL inv\u00e0lid" + }, + "step": { + "init": { + "data": { + "callback_url_override": "URL de crida de l'oient d'esdeveniments", + "listen_port": "Port de l'oient d'esdeveniments (aleatori si no es defineix)", + "poll_availability": "Sondeja per saber la disponibilitat del dispositiu" + }, + "title": "Configuraci\u00f3 del renderitzador de mitjans digitals DLNA" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/de.json b/homeassistant/components/dlna_dmr/translations/de.json new file mode 100644 index 00000000000..50f66761748 --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/de.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "could_not_connect": "Verbindung zum DLNA-Ger\u00e4t fehlgeschlagen", + "discovery_error": "Ein passendes DLNA-Ger\u00e4t konnte nicht gefunden werden", + "incomplete_config": "In der Konfiguration fehlt eine erforderliche Variable", + "non_unique_id": "Mehrere Ger\u00e4te mit derselben eindeutigen ID gefunden", + "not_dmr": "Ger\u00e4t ist kein Digital Media Renderer" + }, + "error": { + "could_not_connect": "Verbindung zum DLNA-Ger\u00e4t fehlgeschlagen", + "not_dmr": "Ger\u00e4t ist kein Digital Media Renderer" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" + }, + "user": { + "data": { + "url": "URL" + }, + "description": "URL zu einer XML-Datei mit Ger\u00e4tebeschreibung", + "title": "DLNA Digital Media Renderer" + } + } + }, + "options": { + "error": { + "invalid_url": "Ung\u00fcltige URL" + }, + "step": { + "init": { + "data": { + "callback_url_override": "R\u00fcckruf-URL des Ereignis-Listeners", + "listen_port": "Port des Ereignis-Listeners (zuf\u00e4llig, wenn nicht festgelegt)", + "poll_availability": "Abfrage der Ger\u00e4teverf\u00fcgbarkeit" + }, + "title": "DLNA Digital Media Renderer Konfiguration" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/et.json b/homeassistant/components/dlna_dmr/translations/et.json new file mode 100644 index 00000000000..e32101ab251 --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/et.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "could_not_connect": "DLNA seadmega \u00fchenduse loomine nurjus", + "discovery_error": "Sobiva DLNA -seadme leidmine nurjus", + "incomplete_config": "Seadetes puudub n\u00f5utav muutuja", + "non_unique_id": "Leiti mitu sama unikaalse ID-ga seadet", + "not_dmr": "Seade ei ole digitaalse meedia renderdaja" + }, + "error": { + "could_not_connect": "DLNA seadmega \u00fchenduse loomine nurjus", + "not_dmr": "Seade ei ole digitaalse meedia renderdaja" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Kas alustada seadistamist?" + }, + "user": { + "data": { + "url": "URL" + }, + "description": "URL aadress seadme kirjelduse XML-failile", + "title": "DLNA digitaalse meediumi renderdaja" + } + } + }, + "options": { + "error": { + "invalid_url": "Sobimatu URL" + }, + "step": { + "init": { + "data": { + "callback_url_override": "S\u00fcndmuse kuulaja URL", + "listen_port": "S\u00fcndmuste kuulaja port (juhuslik kui pole m\u00e4\u00e4ratud)", + "poll_availability": "K\u00fcsitle seadme saadavuse kohta" + }, + "title": "DLNA digitaalse meediumi renderdaja s\u00e4tted" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/hu.json b/homeassistant/components/dlna_dmr/translations/hu.json new file mode 100644 index 00000000000..faa7e73eb76 --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/hu.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r be van konfigur\u00e1lva", + "could_not_connect": "Nem siker\u00fclt csatlakozni a DLNA-eszk\u00f6zh\u00f6z", + "discovery_error": "Nem siker\u00fclt megfelel\u0151 DLNA-eszk\u00f6zt tal\u00e1lni", + "incomplete_config": "A konfigur\u00e1ci\u00f3b\u00f3l hi\u00e1nyzik egy sz\u00fcks\u00e9ges \u00e9rt\u00e9k", + "non_unique_id": "T\u00f6bb eszk\u00f6z tal\u00e1lhat\u00f3 ugyanazzal az egyedi azonos\u00edt\u00f3val", + "not_dmr": "Az eszk\u00f6z nem digit\u00e1lis m\u00e9dia renderel\u0151" + }, + "error": { + "could_not_connect": "Nem siker\u00fclt csatlakozni a DLNA-eszk\u00f6zh\u00f6z", + "not_dmr": "Az eszk\u00f6z nem digit\u00e1lis m\u00e9dia renderel\u0151" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Kezd\u0151dhet a be\u00e1ll\u00edt\u00e1s?" + }, + "user": { + "data": { + "url": "URL" + }, + "description": "Az eszk\u00f6z le\u00edr\u00e1s\u00e1nak XML-f\u00e1jl URL-c\u00edme", + "title": "DLNA digit\u00e1lis m\u00e9dia renderel\u0151" + } + } + }, + "options": { + "error": { + "invalid_url": "\u00c9rv\u00e9nytelen URL" + }, + "step": { + "init": { + "data": { + "callback_url_override": "Esem\u00e9nyfigyel\u0151 visszah\u00edv\u00e1si URL (callback)", + "listen_port": "Esem\u00e9nyfigyel\u0151 port (v\u00e9letlenszer\u0171, ha nincs be\u00e1ll\u00edtva)", + "poll_availability": "Eszk\u00f6z el\u00e9r\u00e9s\u00e9nek tesztel\u00e9se lek\u00e9rdez\u00e9ssel" + }, + "title": "DLNA konfigur\u00e1ci\u00f3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/it.json b/homeassistant/components/dlna_dmr/translations/it.json new file mode 100644 index 00000000000..5defd82a8be --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/it.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "could_not_connect": "Impossibile connettersi al dispositivo DLNA", + "discovery_error": "Impossibile individuare un dispositivo DLNA corrispondente", + "incomplete_config": "Nella configurazione manca una variabile richiesta", + "non_unique_id": "Pi\u00f9 dispositivi trovati con lo stesso ID univoco", + "not_dmr": "Il dispositivo non \u00e8 un Digital Media Renderer" + }, + "error": { + "could_not_connect": "Impossibile connettersi al dispositivo DLNA", + "not_dmr": "Il dispositivo non \u00e8 un Digital Media Renderer" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Vuoi iniziare la configurazione?" + }, + "user": { + "data": { + "url": "URL" + }, + "description": "URL di un file XML di descrizione del dispositivo", + "title": "DLNA Digital Media Renderer" + } + } + }, + "options": { + "error": { + "invalid_url": "URL non valido" + }, + "step": { + "init": { + "data": { + "callback_url_override": "URL di richiamata dell'ascoltatore di eventi", + "listen_port": "Porta dell'ascoltatore di eventi (casuale se non impostata)", + "poll_availability": "Interrogazione per la disponibilit\u00e0 del dispositivo" + }, + "title": "Configurazione DLNA Digital Media Renderer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/nl.json b/homeassistant/components/dlna_dmr/translations/nl.json new file mode 100644 index 00000000000..7387494b9b7 --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/nl.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "could_not_connect": "Mislukt om te verbinden met DNLA apparaat", + "discovery_error": "Kan geen overeenkomend DLNA-apparaat vinden", + "incomplete_config": "Configuratie mist een vereiste variabele", + "non_unique_id": "Meerdere apparaten gevonden met hetzelfde unieke ID", + "not_dmr": "Apparaat is geen Digital Media Renderer" + }, + "error": { + "could_not_connect": "Mislukt om te verbinden met DNLA apparaat", + "not_dmr": "Apparaat is geen Digital Media Renderer" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Wilt u beginnen met instellen?" + }, + "user": { + "data": { + "url": "URL" + }, + "description": "URL naar een XML-bestand met apparaatbeschrijvingen", + "title": "DLNA Digital Media Renderer" + } + } + }, + "options": { + "error": { + "invalid_url": "Ongeldige URL" + }, + "step": { + "init": { + "data": { + "callback_url_override": "Event listener callback URL", + "listen_port": "Poort om naar gebeurtenissen te luisteren (willekeurige poort indien niet ingesteld)", + "poll_availability": "Pollen voor apparaat beschikbaarheid" + }, + "title": "DLNA Digital Media Renderer instellingen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/no.json b/homeassistant/components/dlna_dmr/translations/no.json new file mode 100644 index 00000000000..1ddbfc32afe --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/no.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "could_not_connect": "Kunne ikke koble til DLNA -enhet", + "discovery_error": "Kunne ikke finne en matchende DLNA -enhet", + "incomplete_config": "Konfigurasjonen mangler en n\u00f8dvendig variabel", + "non_unique_id": "Flere enheter ble funnet med samme unike ID", + "not_dmr": "Enheten er ikke en Digital Media Renderer" + }, + "error": { + "could_not_connect": "Kunne ikke koble til DLNA -enhet", + "not_dmr": "Enheten er ikke en Digital Media Renderer" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Vil du starte oppsettet?" + }, + "user": { + "data": { + "url": "URL" + }, + "description": "URL til en enhetsbeskrivelse XML -fil", + "title": "DLNA Digital Media Renderer" + } + } + }, + "options": { + "error": { + "invalid_url": "ugyldig URL" + }, + "step": { + "init": { + "data": { + "callback_url_override": "URL for tilbakeringing av hendelseslytter", + "listen_port": "Hendelseslytterport (tilfeldig hvis den ikke er angitt)", + "poll_availability": "Avstemning for tilgjengelighet av enheter" + }, + "title": "DLNA Digital Media Renderer -konfigurasjon" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/ru.json b/homeassistant/components/dlna_dmr/translations/ru.json new file mode 100644 index 00000000000..bf1be8f6c3d --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/ru.json @@ -0,0 +1,44 @@ +{ + "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.", + "could_not_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443.", + "discovery_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c \u043f\u043e\u0434\u0445\u043e\u0434\u044f\u0449\u0435\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e DLNA.", + "incomplete_config": "\u0412 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u0430\u044f.", + "non_unique_id": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u0441 \u043e\u0434\u0438\u043d\u0430\u043a\u043e\u0432\u044b\u043c \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u043c \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u043e\u043c.", + "not_dmr": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043c\u0435\u0434\u0438\u0430\u0440\u0435\u043d\u0434\u0435\u0440\u0435\u0440\u043e\u043c (DMR)." + }, + "error": { + "could_not_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443.", + "not_dmr": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043c\u0435\u0434\u0438\u0430\u0440\u0435\u043d\u0434\u0435\u0440\u0435\u0440\u043e\u043c (DMR)." + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" + }, + "user": { + "data": { + "url": "URL-\u0430\u0434\u0440\u0435\u0441" + }, + "description": "URL-\u0430\u0434\u0440\u0435\u0441 XML-\u0444\u0430\u0439\u043b\u0430 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "title": "\u041c\u0435\u0434\u0438\u0430\u0440\u0435\u043d\u0434\u0435\u0440\u0435\u0440 DLNA" + } + } + }, + "options": { + "error": { + "invalid_url": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441." + }, + "step": { + "init": { + "data": { + "callback_url_override": "Callback URL \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u0439", + "listen_port": "\u041f\u043e\u0440\u0442 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 (\u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u0439, \u0435\u0441\u043b\u0438 \u043d\u0435 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d)", + "poll_availability": "\u041e\u043f\u0440\u043e\u0441 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e\u0441\u0442\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043c\u0435\u0434\u0438\u0430\u0440\u0435\u043d\u0434\u0435\u0440\u0435\u0440\u0430 DLNA" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/zh-Hant.json b/homeassistant/components/dlna_dmr/translations/zh-Hant.json new file mode 100644 index 00000000000..b7eab93d76d --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/zh-Hant.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "could_not_connect": "DLNA \u88dd\u7f6e\u9023\u7dda\u5931\u6557\u3002", + "discovery_error": "DLNA \u88dd\u7f6e\u63a2\u7d22\u5931\u6557", + "incomplete_config": "\u6240\u7f3a\u5c11\u7684\u8a2d\u5b9a\u70ba\u5fc5\u9808\u8b8a\u6578", + "non_unique_id": "\u627e\u5230\u591a\u7d44\u88dd\u7f6e\u4f7f\u7528\u4e86\u76f8\u540c\u552f\u4e00 ID", + "not_dmr": "\u88dd\u7f6e\u4e26\u975e Digital Media Renderer" + }, + "error": { + "could_not_connect": "DLNA \u88dd\u7f6e\u9023\u7dda\u5931\u6557\u3002", + "not_dmr": "\u88dd\u7f6e\u4e26\u975e Digital Media Renderer" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" + }, + "user": { + "data": { + "url": "\u7db2\u5740" + }, + "description": "\u88dd\u7f6e\u8aaa\u660e XML \u6a94\u6848\u4e4b URL", + "title": "DLNA Digital Media Renderer" + } + } + }, + "options": { + "error": { + "invalid_url": "URL \u7121\u6548" + }, + "step": { + "init": { + "data": { + "callback_url_override": "\u4e8b\u4ef6\u76e3\u807d\u56de\u547c URL", + "listen_port": "\u4e8b\u4ef6\u76e3\u807d\u901a\u8a0a\u57e0\uff08\u672a\u8a2d\u7f6e\u5247\u70ba\u96a8\u6a5f\uff09", + "poll_availability": "\u67e5\u8a62\u88dd\u7f6e\u53ef\u7528\u6027" + }, + "title": "DLNA Digital Media Renderer \u8a2d\u5b9a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/translations/hu.json b/homeassistant/components/doorbird/translations/hu.json index cb4c46e699a..48a124b4f17 100644 --- a/homeassistant/components/doorbird/translations/hu.json +++ b/homeassistant/components/doorbird/translations/hu.json @@ -10,11 +10,11 @@ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, - "flow_title": "DoorBird {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "name": "Eszk\u00f6z neve", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" diff --git a/homeassistant/components/doorbird/translations/id.json b/homeassistant/components/doorbird/translations/id.json index f708780ce31..60348ec26a1 100644 --- a/homeassistant/components/doorbird/translations/id.json +++ b/homeassistant/components/doorbird/translations/id.json @@ -10,7 +10,7 @@ "invalid_auth": "Autentikasi tidak valid", "unknown": "Kesalahan yang tidak diharapkan" }, - "flow_title": "DoorBird {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/dsmr/translations/ca.json b/homeassistant/components/dsmr/translations/ca.json index 263cb388980..1d61426ebea 100644 --- a/homeassistant/components/dsmr/translations/ca.json +++ b/homeassistant/components/dsmr/translations/ca.json @@ -28,7 +28,7 @@ }, "setup_serial_manual_path": { "data": { - "port": "Ruta del port USB del dispositiu" + "port": "Ruta del dispositiu USB" }, "title": "Ruta" }, diff --git a/homeassistant/components/dsmr/translations/hu.json b/homeassistant/components/dsmr/translations/hu.json index 86a15e99aab..1bca962e2f5 100644 --- a/homeassistant/components/dsmr/translations/hu.json +++ b/homeassistant/components/dsmr/translations/hu.json @@ -18,7 +18,7 @@ "setup_network": { "data": { "dsmr_version": "DSMR verzi\u00f3 kiv\u00e1laszt\u00e1sa", - "host": "Gazdag\u00e9p", + "host": "C\u00edm", "port": "Port" }, "title": "V\u00e1lassza ki a csatlakoz\u00e1si c\u00edmet" diff --git a/homeassistant/components/dunehd/translations/hu.json b/homeassistant/components/dunehd/translations/hu.json index 148a6fde0d0..15b2d297363 100644 --- a/homeassistant/components/dunehd/translations/hu.json +++ b/homeassistant/components/dunehd/translations/hu.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Hoszt" + "host": "C\u00edm" }, "description": "\u00c1ll\u00edtsa be a Dune HD integr\u00e1ci\u00f3t. Ha probl\u00e9m\u00e1i vannak a konfigur\u00e1ci\u00f3val, l\u00e1togasson el a k\u00f6vetkez\u0151 oldalra: https://www.home-assistant.io/integrations/dunehd \n\n Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a lej\u00e1tsz\u00f3 be van kapcsolva.", "title": "Dune HD" diff --git a/homeassistant/components/elgato/translations/hu.json b/homeassistant/components/elgato/translations/hu.json index 0cd9f2589b8..26740a33f21 100644 --- a/homeassistant/components/elgato/translations/hu.json +++ b/homeassistant/components/elgato/translations/hu.json @@ -7,11 +7,11 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, - "flow_title": "Elgato Key Light: {serial_number}", + "flow_title": "{serial_number}", "step": { "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "port": "Port" }, "description": "\u00c1ll\u00edtsa be az Elgato Light-ot, hogy integr\u00e1lhat\u00f3 legyen az HomeAssistantba." diff --git a/homeassistant/components/elgato/translations/id.json b/homeassistant/components/elgato/translations/id.json index b06691b9453..f9fa5690c1d 100644 --- a/homeassistant/components/elgato/translations/id.json +++ b/homeassistant/components/elgato/translations/id.json @@ -7,18 +7,18 @@ "error": { "cannot_connect": "Gagal terhubung" }, - "flow_title": "Elgato Key Light: {serial_number}", + "flow_title": "{serial_number}", "step": { "user": { "data": { "host": "Host", "port": "Port" }, - "description": "Siapkan Elgato Key Light Anda untuk diintegrasikan dengan Home Assistant." + "description": "Siapkan Elgato Light Anda untuk diintegrasikan dengan Home Assistant." }, "zeroconf_confirm": { - "description": "Ingin menambahkan Elgato Key Light dengan nomor seri `{serial_number}` ke Home Assistant?", - "title": "Perangkat Elgato Key Light yang ditemukan" + "description": "Ingin menambahkan Elgato Light dengan nomor seri `{serial_number}` ke Home Assistant?", + "title": "Perangkat Elgato Light yang ditemukan" } } } diff --git a/homeassistant/components/emonitor/translations/hu.json b/homeassistant/components/emonitor/translations/hu.json index 575e2a91d44..1a4fbb292e0 100644 --- a/homeassistant/components/emonitor/translations/hu.json +++ b/homeassistant/components/emonitor/translations/hu.json @@ -10,12 +10,12 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}({host})-t?", + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name} ({host})?", "title": "A SiteSage Emonitor be\u00e1ll\u00edt\u00e1sa" }, "user": { "data": { - "host": "Hoszt" + "host": "C\u00edm" } } } diff --git a/homeassistant/components/emonitor/translations/id.json b/homeassistant/components/emonitor/translations/id.json index 1365fed7d52..c967ad91d05 100644 --- a/homeassistant/components/emonitor/translations/id.json +++ b/homeassistant/components/emonitor/translations/id.json @@ -7,7 +7,7 @@ "cannot_connect": "Gagal terhubung", "unknown": "Kesalahan yang tidak diharapkan" }, - "flow_title": "SiteSage {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Ingin menyiapkan {name} ({host})?", diff --git a/homeassistant/components/emulated_roku/translations/hu.json b/homeassistant/components/emulated_roku/translations/hu.json index e733e9801df..53b66f6db19 100644 --- a/homeassistant/components/emulated_roku/translations/hu.json +++ b/homeassistant/components/emulated_roku/translations/hu.json @@ -8,8 +8,8 @@ "data": { "advertise_ip": "IP c\u00edm k\u00f6zl\u00e9se", "advertise_port": "Port k\u00f6zl\u00e9se", - "host_ip": "Hoszt IP c\u00edm", - "listen_port": "Port figyel\u00e9se", + "host_ip": "IP c\u00edm", + "listen_port": "Port", "name": "N\u00e9v", "upnp_bind_multicast": "K\u00f6t\u00f6tt multicast (igaz/hamis)" }, diff --git a/homeassistant/components/energy/translations/el.json b/homeassistant/components/energy/translations/el.json new file mode 100644 index 00000000000..cdc7b83c2ee --- /dev/null +++ b/homeassistant/components/energy/translations/el.json @@ -0,0 +1,3 @@ +{ + "title": "\u0395\u03bd\u03ad\u03c1\u03b3\u03b5\u03b9\u03b1" +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/hu.json b/homeassistant/components/enphase_envoy/translations/hu.json index ab92a4ad2bb..38177f8930c 100644 --- a/homeassistant/components/enphase_envoy/translations/hu.json +++ b/homeassistant/components/enphase_envoy/translations/hu.json @@ -13,7 +13,7 @@ "step": { "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } diff --git a/homeassistant/components/enphase_envoy/translations/id.json b/homeassistant/components/enphase_envoy/translations/id.json index ba3f8dd8cc6..31c8251820d 100644 --- a/homeassistant/components/enphase_envoy/translations/id.json +++ b/homeassistant/components/enphase_envoy/translations/id.json @@ -9,7 +9,7 @@ "invalid_auth": "Autentikasi tidak valid", "unknown": "Kesalahan yang tidak diharapkan" }, - "flow_title": "Envoy {serial} ({host})", + "flow_title": "{serial} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/epson/translations/hu.json b/homeassistant/components/epson/translations/hu.json index 8e0d7ec9a18..e3aa507b7c1 100644 --- a/homeassistant/components/epson/translations/hu.json +++ b/homeassistant/components/epson/translations/hu.json @@ -7,7 +7,7 @@ "step": { "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "name": "N\u00e9v" } } diff --git a/homeassistant/components/esphome/translations/ca.json b/homeassistant/components/esphome/translations/ca.json index d0c59194528..4c990994e47 100644 --- a/homeassistant/components/esphome/translations/ca.json +++ b/homeassistant/components/esphome/translations/ca.json @@ -2,11 +2,13 @@ "config": { "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", - "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs" + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "connection_error": "No s'ha pogut connectar amb ESP. Verifica que l'arxiu YAML cont\u00e9 la l\u00ednia 'api:'.", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "invalid_psk": "La clau de xifratge de transport \u00e9s inv\u00e0lida. Assegura't que coincideix amb la de la configuraci\u00f3", "resolve_error": "No s'ha pogut trobar l'adre\u00e7a de l'ESP. Si l'error persisteix, configura una adre\u00e7a IP est\u00e0tica: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, "flow_title": "{name}", @@ -21,6 +23,18 @@ "description": "Vols afegir el node `{name}` d'ESPHome a Home Assistant?", "title": "Node d'ESPHome descobert" }, + "encryption_key": { + "data": { + "noise_psk": "Clau de xifrat" + }, + "description": "Introdueix la clau de xifrat de {name} establerta a la configuraci\u00f3." + }, + "reauth_confirm": { + "data": { + "noise_psk": "Clau de xifrat" + }, + "description": "El dispositiu ESPHome {name} ha activat el xifratge de transport o ha canviat la clau de xifrat. Introdueix la clau actualitzada." + }, "user": { "data": { "host": "Amfitri\u00f3", diff --git a/homeassistant/components/esphome/translations/cs.json b/homeassistant/components/esphome/translations/cs.json index 9a451a8537f..fc4a7d5bf8c 100644 --- a/homeassistant/components/esphome/translations/cs.json +++ b/homeassistant/components/esphome/translations/cs.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", - "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1" + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { "connection_error": "Nelze se p\u0159ipojit k ESP. Zkontrolujte, zda va\u0161e YAML konfigurace obsahuje \u0159\u00e1dek 'api:'.", diff --git a/homeassistant/components/esphome/translations/de.json b/homeassistant/components/esphome/translations/de.json index 8084ef26f0e..6229c09a03e 100644 --- a/homeassistant/components/esphome/translations/de.json +++ b/homeassistant/components/esphome/translations/de.json @@ -2,11 +2,13 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt" + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "connection_error": "Keine Verbindung zum ESP m\u00f6glich. Achte darauf, dass deine YAML-Datei eine Zeile 'api:' enth\u00e4lt.", "invalid_auth": "Ung\u00fcltige Authentifizierung", + "invalid_psk": "Der Transportverschl\u00fcsselungsschl\u00fcssel ist ung\u00fcltig. Bitte stelle sicher, dass es mit deiner Konfiguration \u00fcbereinstimmt", "resolve_error": "Adresse des ESP kann nicht aufgel\u00f6st werden. Wenn dieser Fehler weiterhin besteht, lege eine statische IP-Adresse fest: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, "flow_title": "{name}", @@ -21,6 +23,18 @@ "description": "Willst du den ESPHome-Knoten `{name}` zu Home Assistant hinzuf\u00fcgen?", "title": "Gefundener ESPHome-Knoten" }, + "encryption_key": { + "data": { + "noise_psk": "Verschl\u00fcsselungsschl\u00fcssel" + }, + "description": "Bitte gib den Verschl\u00fcsselungsschl\u00fcssel ein, den du in deiner Konfiguration f\u00fcr {name} festgelegt hast." + }, + "reauth_confirm": { + "data": { + "noise_psk": "Verschl\u00fcsselungsschl\u00fcssel" + }, + "description": "Das ESPHome-Ger\u00e4t {name} hat die Transportverschl\u00fcsselung aktiviert oder den Verschl\u00fcsselungscode ge\u00e4ndert. Bitte gib den aktualisierten Schl\u00fcssel ein." + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/esphome/translations/en.json b/homeassistant/components/esphome/translations/en.json index c57c9d1acb0..5ca5c03f8e9 100644 --- a/homeassistant/components/esphome/translations/en.json +++ b/homeassistant/components/esphome/translations/en.json @@ -2,11 +2,13 @@ "config": { "abort": { "already_configured": "Device is already configured", - "already_in_progress": "Configuration flow is already in progress" + "already_in_progress": "Configuration flow is already in progress", + "reauth_successful": "Re-authentication was successful" }, "error": { "connection_error": "Can't connect to ESP. Please make sure your YAML file contains an 'api:' line.", "invalid_auth": "Invalid authentication", + "invalid_psk": "The transport encryption key is invalid. Please ensure it matches what you have in your configuration", "resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, "flow_title": "{name}", @@ -21,6 +23,18 @@ "description": "Do you want to add the ESPHome node `{name}` to Home Assistant?", "title": "Discovered ESPHome node" }, + "encryption_key": { + "data": { + "noise_psk": "Encryption key" + }, + "description": "Please enter the encryption key you set in your configuration for {name}." + }, + "reauth_confirm": { + "data": { + "noise_psk": "Encryption key" + }, + "description": "The ESPHome device {name} enabled transport encryption or changed the encryption key. Please enter the updated key." + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/esphome/translations/es.json b/homeassistant/components/esphome/translations/es.json index 9c4b3f52406..f7fd73cd227 100644 --- a/homeassistant/components/esphome/translations/es.json +++ b/homeassistant/components/esphome/translations/es.json @@ -2,11 +2,13 @@ "config": { "abort": { "already_configured": "ESP ya est\u00e1 configurado", - "already_in_progress": "La configuraci\u00f3n del ESP ya est\u00e1 en marcha" + "already_in_progress": "La configuraci\u00f3n del ESP ya est\u00e1 en marcha", + "reauth_successful": "La re-autenticaci\u00f3n ha funcionado" }, "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", "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" }, "flow_title": "ESPHome: {name}", @@ -21,6 +23,18 @@ "description": "\u00bfQuieres a\u00f1adir el nodo `{name}` de ESPHome 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}." + }, + "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." + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/esphome/translations/et.json b/homeassistant/components/esphome/translations/et.json index 4f018931141..ea5119b190d 100644 --- a/homeassistant/components/esphome/translations/et.json +++ b/homeassistant/components/esphome/translations/et.json @@ -2,11 +2,13 @@ "config": { "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", - "already_in_progress": "Seadistamine on juba k\u00e4imas" + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "connection_error": "ESP-ga ei saa \u00fchendust luua. Veendu, et YAML-fail sisaldab rida 'api:'.", "invalid_auth": "Tuvastamise viga", + "invalid_psk": "\u00dclekande kr\u00fcpteerimisv\u00f5ti on kehtetu. Veendu, et see vastab seadetes sisalduvale", "resolve_error": "ESP aadressi ei \u00f5nnestu lahendada. Kui see viga p\u00fcsib, m\u00e4\u00e4ra staatiline IP-aadress: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, "flow_title": "{name}", @@ -21,6 +23,18 @@ "description": "Kas soovid lisada ESPHome'i s\u00f5lme '{name}' Home Assistant-ile?", "title": "Avastastud ESPHome'i s\u00f5lm" }, + "encryption_key": { + "data": { + "noise_psk": "Kr\u00fcptimisv\u00f5ti" + }, + "description": "Sisesta kr\u00fcptimisv\u00f5ti mille m\u00e4\u00e4rasid oma {name} seadetes." + }, + "reauth_confirm": { + "data": { + "noise_psk": "Kr\u00fcptimisv\u00f5ti" + }, + "description": "ESPHome seade {name} lubas \u00fclekande kr\u00fcptimise v\u00f5i muutis kr\u00fcpteerimisv\u00f5tit. Palun sisesta uuendatud v\u00f5ti." + }, "user": { "data": { "host": "", diff --git a/homeassistant/components/esphome/translations/he.json b/homeassistant/components/esphome/translations/he.json index 5c0f832ba4c..11eaf41ff1a 100644 --- a/homeassistant/components/esphome/translations/he.json +++ b/homeassistant/components/esphome/translations/he.json @@ -2,7 +2,8 @@ "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" + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "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" @@ -15,6 +16,11 @@ }, "description": "\u05d0\u05e0\u05d0 \u05d4\u05d6\u05df \u05d0\u05ea \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d4\u05d2\u05d3\u05e8\u05ea \u05d1\u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc\u05da \u05e2\u05d1\u05d5\u05e8 {name}." }, + "encryption_key": { + "data": { + "noise_psk": "\u05de\u05e4\u05ea\u05d7 \u05d4\u05e6\u05e4\u05e0\u05d4" + } + }, "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7", diff --git a/homeassistant/components/esphome/translations/hu.json b/homeassistant/components/esphome/translations/hu.json index 6c4586fbd55..d7ac503d83c 100644 --- a/homeassistant/components/esphome/translations/hu.json +++ b/homeassistant/components/esphome/translations/hu.json @@ -2,31 +2,45 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van." + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" }, "error": { - "connection_error": "Nem lehet csatlakozni az ESP-hez. K\u00e9rlek gy\u0151z\u0151dj meg r\u00f3la, hogy a YAML f\u00e1jl tartalmaz egy \"api:\" sort.", + "connection_error": "Nem lehet csatlakozni az ESP-hez. K\u00e9rem, gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy a YAML f\u00e1jl tartalmaz egy \"api:\" sort.", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "resolve_error": "Az ESP c\u00edme nem oldhat\u00f3 fel. Ha a hiba tov\u00e1bbra is fenn\u00e1ll, k\u00e9rlek, \u00e1ll\u00edts be egy statikus IP-c\u00edmet: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "invalid_psk": "Az adat\u00e1tviteli titkos\u00edt\u00e1si kulcs \u00e9rv\u00e9nytelen. K\u00e9rj\u00fck, gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy megegyezik a konfigur\u00e1ci\u00f3ban l\u00e9v\u0151vel.", + "resolve_error": "Az ESP c\u00edme nem oldhat\u00f3 fel. Ha a hiba tov\u00e1bbra is fenn\u00e1ll, k\u00e9rem, \u00e1ll\u00edtson be egy statikus IP-c\u00edmet: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, - "flow_title": "ESPHome: {name}", + "flow_title": "{name}", "step": { "authenticate": { "data": { "password": "Jelsz\u00f3" }, - "description": "K\u00e9rlek, add meg a konfigur\u00e1ci\u00f3ban {name} n\u00e9vhez be\u00e1ll\u00edtott jelsz\u00f3t." + "description": "K\u00e9rem, adja meg a konfigur\u00e1ci\u00f3ban {name} n\u00e9vhez be\u00e1ll\u00edtott jelsz\u00f3t." }, "discovery_confirm": { - "description": "Szeretn\u00e9d hozz\u00e1adni a(z) `{name}` ESPHome csom\u00f3pontot a Home Assistant-hoz?", + "description": "Szeretn\u00e9 hozz\u00e1adni `{name}` ESPHome csom\u00f3pontot Home Assistant-hoz?", "title": "Felfedezett ESPHome csom\u00f3pont" }, + "encryption_key": { + "data": { + "noise_psk": "Titkos\u00edt\u00e1si kulcs" + }, + "description": "K\u00e9rj\u00fck, adja meg a {name} konfigur\u00e1ci\u00f3j\u00e1ban be\u00e1ll\u00edtott titkos\u00edt\u00e1si kulcsot." + }, + "reauth_confirm": { + "data": { + "noise_psk": "Titkos\u00edt\u00e1si kulcs" + }, + "description": "{name} ESPHome eszk\u00f6z enged\u00e9lyezte az adat\u00e1tviteli titkos\u00edt\u00e1st vagy megv\u00e1ltoztatta a titkos\u00edt\u00e1si kulcsot. K\u00e9rj\u00fck, adja meg az aktu\u00e1lis kulcsot." + }, "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "port": "Port" }, - "description": "K\u00e9rlek, add meg az [ESPHome](https://esphomelib.com/) csom\u00f3pontod kapcsol\u00f3d\u00e1si be\u00e1ll\u00edt\u00e1sait." + "description": "K\u00e9rem, adja meg az [ESPHome](https://esphomelib.com/) csom\u00f3pontj\u00e1nak kapcsol\u00f3d\u00e1si be\u00e1ll\u00edt\u00e1sait." } } } diff --git a/homeassistant/components/esphome/translations/id.json b/homeassistant/components/esphome/translations/id.json index a39a19e12db..530d86e2f56 100644 --- a/homeassistant/components/esphome/translations/id.json +++ b/homeassistant/components/esphome/translations/id.json @@ -2,14 +2,15 @@ "config": { "abort": { "already_configured": "Perangkat sudah dikonfigurasi", - "already_in_progress": "Alur konfigurasi sedang berlangsung" + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "connection_error": "Tidak dapat terhubung ke ESP. Pastikan file YAML Anda mengandung baris 'api:'.", "invalid_auth": "Autentikasi tidak valid", "resolve_error": "Tidak dapat menemukan alamat ESP. Jika kesalahan ini terus terjadi, atur alamat IP statis: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, - "flow_title": "ESPHome: {name}", + "flow_title": "{name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/esphome/translations/it.json b/homeassistant/components/esphome/translations/it.json index 34d1ec78f6e..390054bc345 100644 --- a/homeassistant/components/esphome/translations/it.json +++ b/homeassistant/components/esphome/translations/it.json @@ -2,11 +2,13 @@ "config": { "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso" + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "connection_error": "Impossibile connettersi ad ESP. Assicurati che il tuo file YAML contenga una riga \"api:\".", "invalid_auth": "Autenticazione non valida", + "invalid_psk": "La chiave di crittografia del trasporto non \u00e8 valida. Assicurati che corrisponda a ci\u00f2 che hai nella tua configurazione", "resolve_error": "Impossibile risolvere l'indirizzo dell'ESP. Se questo errore persiste, impostare un indirizzo IP statico: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, "flow_title": "{name}", @@ -21,6 +23,18 @@ "description": "Vuoi aggiungere il nodo ESPHome `{name}` a Home Assistant?", "title": "Trovato nodo ESPHome" }, + "encryption_key": { + "data": { + "noise_psk": "Chiave di crittografia" + }, + "description": "Inserisci la chiave di crittografia che hai impostato nella configurazione per {name}." + }, + "reauth_confirm": { + "data": { + "noise_psk": "Chiave di crittografia" + }, + "description": "Il dispositivo ESPHome {name} ha abilitato la crittografia del trasporto o ha modificato la chiave di crittografia. Inserisci la chiave aggiornata." + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/esphome/translations/nl.json b/homeassistant/components/esphome/translations/nl.json index 019c33004e7..7f6f821104c 100644 --- a/homeassistant/components/esphome/translations/nl.json +++ b/homeassistant/components/esphome/translations/nl.json @@ -2,11 +2,13 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al begonnen" + "already_in_progress": "De configuratiestroom is al begonnen", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { "connection_error": "Kan geen verbinding maken met ESP. Zorg ervoor dat uw YAML-bestand een regel 'api:' bevat.", "invalid_auth": "Ongeldige authenticatie", + "invalid_psk": "De transportcoderingssleutel is ongeldig. Zorg ervoor dat het overeenkomt met wat u in uw configuratie heeft", "resolve_error": "Kan het adres van de ESP niet vinden. Als deze fout aanhoudt, stel dan een statisch IP-adres in: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, "flow_title": "{name}", @@ -21,6 +23,18 @@ "description": "Wil je de ESPHome-node `{name}` toevoegen aan de Home Assistant?", "title": "ESPHome node ontdekt" }, + "encryption_key": { + "data": { + "noise_psk": "Coderingssleutel" + }, + "description": "Voer de coderingssleutel in die u in uw configuratie voor {name}." + }, + "reauth_confirm": { + "data": { + "noise_psk": "Coderingssleutel" + }, + "description": "Het ESPHome-apparaat {name} heeft transportcodering ingeschakeld of de coderingssleutel gewijzigd. Voer de bijgewerkte sleutel in." + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/esphome/translations/no.json b/homeassistant/components/esphome/translations/no.json index 14b92500f41..0d583893570 100644 --- a/homeassistant/components/esphome/translations/no.json +++ b/homeassistant/components/esphome/translations/no.json @@ -2,11 +2,13 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede" + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "connection_error": "Kan ikke koble til ESP. Kontroller at YAML filen din inneholder en \"api:\" linje.", "invalid_auth": "Ugyldig godkjenning", + "invalid_psk": "Transportkrypteringsn\u00f8kkelen er ugyldig. S\u00f8rg for at den samsvarer med det du har i konfigurasjonen", "resolve_error": "Kan ikke l\u00f8se adressen til ESP. Hvis denne feilen vedvarer, vennligst [sett en statisk IP-adresse](https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips)" }, "flow_title": "{name}", @@ -21,6 +23,18 @@ "description": "\u00d8nsker du \u00e5 legge ESPHome noden `{name}` til Home Assistant?", "title": "Oppdaget ESPHome node" }, + "encryption_key": { + "data": { + "noise_psk": "Krypteringsn\u00f8kkel" + }, + "description": "Skriv inn krypteringsn\u00f8kkelen du angav i konfigurasjonen for {name} ." + }, + "reauth_confirm": { + "data": { + "noise_psk": "Krypteringsn\u00f8kkel" + }, + "description": "ESPHome -enheten {name} aktiverte transportkryptering eller endret krypteringsn\u00f8kkelen. Skriv inn den oppdaterte n\u00f8kkelen." + }, "user": { "data": { "host": "Vert", diff --git a/homeassistant/components/esphome/translations/ru.json b/homeassistant/components/esphome/translations/ru.json index 5987a7db13b..8ba4a573cec 100644 --- a/homeassistant/components/esphome/translations/ru.json +++ b/homeassistant/components/esphome/translations/ru.json @@ -2,11 +2,13 @@ "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." + "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.", + "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": { "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a ESP. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u0430\u0448 YAML-\u0444\u0430\u0439\u043b \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u0442\u0440\u043e\u043a\u0443 'api:'.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "invalid_psk": "\u041a\u043b\u044e\u0447 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u043e\u0433\u043e \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u043e\u043d \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u043c\u0443 \u0432 \u0412\u0430\u0448\u0435\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438.", "resolve_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0430\u0434\u0440\u0435\u0441 ESP. \u0415\u0441\u043b\u0438 \u044d\u0442\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u044f\u0435\u0442\u0441\u044f, \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 IP-\u0430\u0434\u0440\u0435\u0441: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips." }, "flow_title": "{name}", @@ -21,6 +23,18 @@ "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c ESPHome `{name}`?", "title": "ESPHome" }, + "encryption_key": { + "data": { + "noise_psk": "\u041a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f, \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 {name}." + }, + "reauth_confirm": { + "data": { + "noise_psk": "\u041a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f" + }, + "description": "\u0414\u043b\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 {name} \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u043d\u043e \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u043e\u0433\u043e \u0443\u0440\u043e\u0432\u043d\u044f \u0438\u043b\u0438 \u0438\u0437\u043c\u0435\u043d\u0451\u043d \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044b\u0439 \u043a\u043b\u044e\u0447." + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/esphome/translations/zh-Hant.json b/homeassistant/components/esphome/translations/zh-Hant.json index 6ea440c02df..0b415a35c38 100644 --- a/homeassistant/components/esphome/translations/zh-Hant.json +++ b/homeassistant/components/esphome/translations/zh-Hant.json @@ -2,11 +2,13 @@ "config": { "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d" + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "connection_error": "\u7121\u6cd5\u9023\u7dda\u81f3 ESP\uff0c\u8acb\u78ba\u5b9a\u60a8\u7684 YAML \u6a94\u6848\u5305\u542b\u300capi:\u300d\u8a2d\u5b9a\u5217\u3002", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_psk": "\u50b3\u8f38\u5bc6\u9470\u7121\u6548\u3002\u8acb\u78ba\u5b9a\u8207\u8a2d\u5b9a\u5167\u6240\u8a2d\u5b9a\u4e4b\u5bc6\u9470\u76f8\u7b26\u5408", "resolve_error": "\u7121\u6cd5\u89e3\u6790 ESP \u4f4d\u5740\uff0c\u5047\u5982\u6b64\u932f\u8aa4\u6301\u7e8c\u767c\u751f\uff0c\u8acb\u53c3\u8003\u8aaa\u660e\u8a2d\u5b9a\u70ba\u975c\u614b\u56fa\u5b9a IP \uff1a https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, "flow_title": "{name}", @@ -21,6 +23,18 @@ "description": "\u662f\u5426\u8981\u5c07 ESPHome \u7bc0\u9ede `{name}` \u65b0\u589e\u81f3 Home Assistant\uff1f", "title": "\u81ea\u52d5\u63a2\u7d22\u5230 ESPHome \u7bc0\u9ede" }, + "encryption_key": { + "data": { + "noise_psk": "\u5bc6\u9470" + }, + "description": "\u8acb\u8f38\u5165 {name} \u8a2d\u5b9a\u4e2d\u6240\u8a2d\u5b9a\u4e4b\u5bc6\u9470\u3002" + }, + "reauth_confirm": { + "data": { + "noise_psk": "\u5bc6\u9470" + }, + "description": "ESPHome \u88dd\u7f6e {name} \u5df2\u958b\u555f\u50b3\u8f38\u52a0\u5bc6\u6216\u8b8a\u66f4\u5bc6\u9470\u3002\u8acb\u8f38\u5165\u66f4\u65b0\u5bc6\u9470\u3002" + }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", diff --git a/homeassistant/components/ezviz/translations/ca.json b/homeassistant/components/ezviz/translations/ca.json index c7c71e07122..7c71de300f6 100644 --- a/homeassistant/components/ezviz/translations/ca.json +++ b/homeassistant/components/ezviz/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured_account": "El compte ja ha estat configurat", + "already_configured_account": "El compte ja est\u00e0 configurat", "ezviz_cloud_account_missing": "Falta el compte d'Ezviz cloud. Torna'l a configurar", "unknown": "Error inesperat" }, diff --git a/homeassistant/components/ezviz/translations/hu.json b/homeassistant/components/ezviz/translations/hu.json index 3ece0a79dcf..5907f66ceb4 100644 --- a/homeassistant/components/ezviz/translations/hu.json +++ b/homeassistant/components/ezviz/translations/hu.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "Nem siker\u00fclt csatlakozni", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "invalid_host": "\u00c9rv\u00e9nytelen gazdag\u00e9pn\u00e9v vagy IP-c\u00edm" + "invalid_host": "\u00c9rv\u00e9nytelen C\u00edm" }, "flow_title": "{serial}", "step": { diff --git a/homeassistant/components/fireservicerota/translations/ca.json b/homeassistant/components/fireservicerota/translations/ca.json index 287bb81e51e..261350db3f8 100644 --- a/homeassistant/components/fireservicerota/translations/ca.json +++ b/homeassistant/components/fireservicerota/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat", + "already_configured": "El compte ja est\u00e0 configurat", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "create_entry": { diff --git a/homeassistant/components/fjaraskupan/translations/es.json b/homeassistant/components/fjaraskupan/translations/es.json new file mode 100644 index 00000000000..36ff1884048 --- /dev/null +++ b/homeassistant/components/fjaraskupan/translations/es.json @@ -0,0 +1,8 @@ +{ + "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." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fjaraskupan/translations/id.json b/homeassistant/components/fjaraskupan/translations/id.json new file mode 100644 index 00000000000..ed64894fff4 --- /dev/null +++ b/homeassistant/components/fjaraskupan/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 Fj\u00e4r\u00e5skupan?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/ca.json b/homeassistant/components/flick_electric/translations/ca.json index 74fd0e79708..b98cfc742db 100644 --- a/homeassistant/components/flick_electric/translations/ca.json +++ b/homeassistant/components/flick_electric/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat" + "already_configured": "El compte ja est\u00e0 configurat" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", diff --git a/homeassistant/components/flick_electric/translations/he.json b/homeassistant/components/flick_electric/translations/he.json index b1e5464047b..0cbd1ab331b 100644 --- a/homeassistant/components/flick_electric/translations/he.json +++ b/homeassistant/components/flick_electric/translations/he.json @@ -11,8 +11,8 @@ "step": { "user": { "data": { - "client_id": "Client ID (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)", - "client_secret": "Client Secret (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)", + "client_id": "\u05de\u05d6\u05d4\u05d4 \u05dc\u05e7\u05d5\u05d7 (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)", + "client_secret": "\u05e1\u05d5\u05d3 \u05dc\u05e7\u05d5\u05d7 (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" } diff --git a/homeassistant/components/flick_electric/translations/id.json b/homeassistant/components/flick_electric/translations/id.json index 8c283cfd56e..3085534a862 100644 --- a/homeassistant/components/flick_electric/translations/id.json +++ b/homeassistant/components/flick_electric/translations/id.json @@ -11,8 +11,8 @@ "step": { "user": { "data": { - "client_id": "ID Klien (Opsional)", - "client_secret": "Kode Rahasia Klien (Opsional)", + "client_id": "ID Klien (opsional)", + "client_secret": "Kode Rahasia Klien (opsional)", "password": "Kata Sandi", "username": "Nama Pengguna" }, diff --git a/homeassistant/components/flipr/translations/es.json b/homeassistant/components/flipr/translations/es.json index 69ff84c2e69..0a066451b84 100644 --- a/homeassistant/components/flipr/translations/es.json +++ b/homeassistant/components/flipr/translations/es.json @@ -1,8 +1,13 @@ { "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "no_flipr_id_found": "Por ahora no hay ning\u00fan ID de Flipr asociado a tu cuenta. Deber\u00edas verificar que est\u00e1 funcionando con la aplicaci\u00f3n m\u00f3vil de Flipr primero.", - "unknown": "Error desconocido" + "unknown": "Error inesperado" }, "step": { "flipr_id": { @@ -14,8 +19,8 @@ }, "user": { "data": { - "email": "Correo-e", - "password": "Clave" + "email": "Correo electr\u00f3nico", + "password": "Contrase\u00f1a" }, "description": "Con\u00e9ctese usando su cuenta Flipr.", "title": "Conectarse a Flipr" diff --git a/homeassistant/components/flipr/translations/id.json b/homeassistant/components/flipr/translations/id.json new file mode 100644 index 00000000000..63751867097 --- /dev/null +++ b/homeassistant/components/flipr/translations/id.json @@ -0,0 +1,20 @@ +{ + "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": { + "email": "Email", + "password": "Kata Sandi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flo/translations/hu.json b/homeassistant/components/flo/translations/hu.json index 0abcc301f0c..9590d3c12be 100644 --- a/homeassistant/components/flo/translations/hu.json +++ b/homeassistant/components/flo/translations/hu.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } diff --git a/homeassistant/components/flume/translations/ca.json b/homeassistant/components/flume/translations/ca.json index 04a7accf4a5..5cd81a00a67 100644 --- a/homeassistant/components/flume/translations/ca.json +++ b/homeassistant/components/flume/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat", + "already_configured": "El compte ja est\u00e0 configurat", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { diff --git a/homeassistant/components/forecast_solar/translations/es.json b/homeassistant/components/forecast_solar/translations/es.json index 8a1b51a5084..d688c577024 100644 --- a/homeassistant/components/forecast_solar/translations/es.json +++ b/homeassistant/components/forecast_solar/translations/es.json @@ -5,7 +5,10 @@ "data": { "azimuth": "Acimut (360 grados, 0 = Norte, 90 = Este, 180 = Sur, 270 = Oeste)", "declination": "Declinaci\u00f3n (0 = Horizontal, 90 = Vertical)", - "modules power": "Potencia total en vatios pico de sus m\u00f3dulos solares" + "latitude": "Latitud", + "longitude": "Longitud", + "modules power": "Potencia total en vatios pico de sus 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." } diff --git a/homeassistant/components/forecast_solar/translations/id.json b/homeassistant/components/forecast_solar/translations/id.json index b0a5ddcdc7e..130f66db7f5 100644 --- a/homeassistant/components/forecast_solar/translations/id.json +++ b/homeassistant/components/forecast_solar/translations/id.json @@ -3,7 +3,9 @@ "step": { "user": { "data": { - "latitude": "Lintang" + "latitude": "Lintang", + "longitude": "Bujur", + "name": "Nama" } } } diff --git a/homeassistant/components/forked_daapd/translations/hu.json b/homeassistant/components/forked_daapd/translations/hu.json index bbf8cb560ff..2058bbd1cbe 100644 --- a/homeassistant/components/forked_daapd/translations/hu.json +++ b/homeassistant/components/forked_daapd/translations/hu.json @@ -8,15 +8,15 @@ "forbidden": "Nem tud csatlakozni. K\u00e9rj\u00fck, ellen\u0151rizze a forked-daapd h\u00e1l\u00f3zati enged\u00e9lyeket.", "unknown_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", "websocket_not_enabled": "forked-daapd szerver websocket nincs enged\u00e9lyezve.", - "wrong_host_or_port": "Nem tud csatlakozni. K\u00e9rj\u00fck, ellen\u0151rizze a gazdag\u00e9pet \u00e9s a portot.", + "wrong_host_or_port": "Nem tud csatlakozni. K\u00e9rj\u00fck, ellen\u0151rizze a c\u00edmet \u00e9s a portot.", "wrong_password": "Helytelen jelsz\u00f3.", - "wrong_server_type": "A forked-daapd integr\u00e1ci\u00f3hoz forked-daapd szerver sz\u00fcks\u00e9ges, amelynek verzi\u00f3ja> = 27.0." + "wrong_server_type": "A forked-daapd integr\u00e1ci\u00f3hoz forked-daapd szerver sz\u00fcks\u00e9ges, amelynek verzi\u00f3ja legal\u00e1bb 27.0." }, "flow_title": "{name} ({host})", "step": { "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "name": "Megjelen\u00edt\u00e9si n\u00e9v", "password": "API jelsz\u00f3 (hagyja \u00fcresen, ha nincs jelsz\u00f3)", "port": "API port" diff --git a/homeassistant/components/forked_daapd/translations/id.json b/homeassistant/components/forked_daapd/translations/id.json index 76787e2a19b..f57a8fb8566 100644 --- a/homeassistant/components/forked_daapd/translations/id.json +++ b/homeassistant/components/forked_daapd/translations/id.json @@ -12,7 +12,7 @@ "wrong_password": "Kata sandi salah.", "wrong_server_type": "Integrasi forked-daapd membutuhkan server forked-daapd dengan versi >= 27.0." }, - "flow_title": "forked-daapd server: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/foscam/translations/hu.json b/homeassistant/components/foscam/translations/hu.json index 63ea95210ff..b303db792bb 100644 --- a/homeassistant/components/foscam/translations/hu.json +++ b/homeassistant/components/foscam/translations/hu.json @@ -12,7 +12,7 @@ "step": { "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "password": "Jelsz\u00f3", "port": "Port", "rtsp_port": "RTSP port", diff --git a/homeassistant/components/freebox/translations/hu.json b/homeassistant/components/freebox/translations/hu.json index c929d56f38e..873e1057c15 100644 --- a/homeassistant/components/freebox/translations/hu.json +++ b/homeassistant/components/freebox/translations/hu.json @@ -10,12 +10,12 @@ }, "step": { "link": { - "description": "Kattintson a \u201eK\u00fcld\u00e9s\u201d gombra, majd \u00e9rintse meg a jobbra mutat\u00f3 nyilat az \u00fatv\u00e1laszt\u00f3n a Freebox regisztr\u00e1l\u00e1s\u00e1hoz a HomeAssistant seg\u00edts\u00e9g\u00e9vel. \n\n ! [A gomb helye az \u00fatv\u00e1laszt\u00f3n] (/static/images/config_freebox.png)", + "description": "Kattintson a \u201eK\u00fcld\u00e9s\u201d gombra, majd \u00e9rintse meg a jobbra mutat\u00f3 nyilat az \u00fatv\u00e1laszt\u00f3n a Freebox regisztr\u00e1l\u00e1s\u00e1hoz Home Assistant seg\u00edts\u00e9g\u00e9vel. \n\n![A gomb helye a routeren] (/static/images/config_freebox.png)", "title": "Freebox \u00fatv\u00e1laszt\u00f3 linkel\u00e9se" }, "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "port": "Port" }, "title": "Freebox" diff --git a/homeassistant/components/freedompro/translations/id.json b/homeassistant/components/freedompro/translations/id.json index 82523dc65d1..9676af6d8f9 100644 --- a/homeassistant/components/freedompro/translations/id.json +++ b/homeassistant/components/freedompro/translations/id.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/fritz/translations/es.json b/homeassistant/components/fritz/translations/es.json index f9f586bfa71..45519eb7eb5 100644 --- a/homeassistant/components/fritz/translations/es.json +++ b/homeassistant/components/fritz/translations/es.json @@ -8,6 +8,7 @@ "error": { "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "cannot_connect": "No se pudo conectar", "connection_error": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, @@ -18,7 +19,7 @@ "password": "Contrase\u00f1a", "username": "Usuario" }, - "description": "Descubierto FRITZ!Box: {nombre}\n\nConfigurar FRITZ!Box Tools para controlar tu {nombre}", + "description": "Descubierto FRITZ!Box: {name}\n\nConfigurar FRITZ!Box Tools para controlar tu {name}", "title": "Configurar FRITZ!Box Tools" }, "reauth_confirm": { @@ -43,7 +44,8 @@ "data": { "host": "Anfitri\u00f3n", "password": "Contrase\u00f1a", - "port": "Puerto" + "port": "Puerto", + "username": "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/fritz/translations/hu.json b/homeassistant/components/fritz/translations/hu.json index 1433860bfa6..733a4fb1a8e 100644 --- a/homeassistant/components/fritz/translations/hu.json +++ b/homeassistant/components/fritz/translations/hu.json @@ -32,7 +32,7 @@ }, "start_config": { "data": { - "host": "Gazdag\u00e9p", + "host": "C\u00edm", "password": "Jelsz\u00f3", "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" @@ -42,7 +42,7 @@ }, "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "password": "Jelsz\u00f3", "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" diff --git a/homeassistant/components/fritz/translations/ko.json b/homeassistant/components/fritz/translations/ko.json new file mode 100644 index 00000000000..718b105df33 --- /dev/null +++ b/homeassistant/components/fritz/translations/ko.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", + "connection_error": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + }, + "reauth_confirm": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + }, + "start_config": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "password": "\ube44\ubc00\ubc88\ud638", + "port": "\ud3ec\ud2b8", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/ru.json b/homeassistant/components/fritz/translations/ru.json index 7c030ca4d88..54619e22a36 100644 --- a/homeassistant/components/fritz/translations/ru.json +++ b/homeassistant/components/fritz/translations/ru.json @@ -56,7 +56,7 @@ "step": { "init": { "data": { - "consider_home": "\u0412\u0440\u0435\u043c\u044f \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445), \u043f\u043e \u0438\u0441\u0442\u0435\u0447\u0435\u043d\u0438\u044e \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u0442\u0430\u0442\u0443\u0441 \"\u0414\u043e\u043c\u0430\"" + "consider_home": "\u0412\u0440\u0435\u043c\u044f, \u0432 \u0442\u0435\u0447\u0435\u043d\u0438\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0441\u0447\u0438\u0442\u0430\u0442\u044c \u0447\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043e\u043c\u0430 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" } } } diff --git a/homeassistant/components/fritzbox/translations/hu.json b/homeassistant/components/fritzbox/translations/hu.json index 50a81601310..c5d5e495131 100644 --- a/homeassistant/components/fritzbox/translations/hu.json +++ b/homeassistant/components/fritzbox/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", "not_supported": "Csatlakoztatva az AVM FRITZ! Boxhoz, de nem tudja vez\u00e9relni az intelligens otthoni eszk\u00f6z\u00f6ket.", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" @@ -17,7 +17,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}-t?" + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" }, "reauth_confirm": { "data": { @@ -28,7 +28,7 @@ }, "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, diff --git a/homeassistant/components/fritzbox/translations/id.json b/homeassistant/components/fritzbox/translations/id.json index 8dbd1f71534..f9c4f09b4ae 100644 --- a/homeassistant/components/fritzbox/translations/id.json +++ b/homeassistant/components/fritzbox/translations/id.json @@ -10,7 +10,7 @@ "error": { "invalid_auth": "Autentikasi tidak valid" }, - "flow_title": "AVM FRITZ!Box: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/fritzbox_callmonitor/translations/hu.json b/homeassistant/components/fritzbox_callmonitor/translations/hu.json index 5006dd77f14..86b4c637ca0 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/hu.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/hu.json @@ -17,7 +17,7 @@ }, "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "password": "Jelsz\u00f3", "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" diff --git a/homeassistant/components/fritzbox_callmonitor/translations/id.json b/homeassistant/components/fritzbox_callmonitor/translations/id.json index 43bb4a16b47..1325edd720c 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/id.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/id.json @@ -8,7 +8,7 @@ "error": { "invalid_auth": "Autentikasi tidak valid" }, - "flow_title": "Pantau panggilan AVM FRITZ!Box: {name}", + "flow_title": "{name}", "step": { "phonebook": { "data": { diff --git a/homeassistant/components/garages_amsterdam/translations/es.json b/homeassistant/components/garages_amsterdam/translations/es.json index bfea8be63c1..79433b6b854 100644 --- a/homeassistant/components/garages_amsterdam/translations/es.json +++ b/homeassistant/components/garages_amsterdam/translations/es.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/geofency/translations/hu.json b/homeassistant/components/geofency/translations/hu.json index 826b943e2f8..de8f368adb3 100644 --- a/homeassistant/components/geofency/translations/hu.json +++ b/homeassistant/components/geofency/translations/hu.json @@ -2,14 +2,14 @@ "config": { "abort": { "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", - "webhook_not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." + "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, "create_entry": { "default": "Az esem\u00e9ny Home Assistantnek val\u00f3 k\u00fcld\u00e9s\u00e9hez be kell \u00e1ll\u00edtanod a webhook funkci\u00f3t a Geofencyben. \n\n Az al\u00e1bbi inform\u00e1ci\u00f3kat haszn\u00e1ld: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Tov\u00e1bbi r\u00e9szletek a [dokument\u00e1ci\u00f3]({docs_url}) linken tal\u00e1lhat\u00f3k." }, "step": { "user": { - "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani a Geofency Webhookot?", + "description": "Biztos benne, hogy be szeretn\u00e9 \u00e1ll\u00edtani a Geofency Webhookot?", "title": "A Geofency Webhook be\u00e1ll\u00edt\u00e1sa" } } diff --git a/homeassistant/components/glances/translations/hu.json b/homeassistant/components/glances/translations/hu.json index d85baecb5ca..d93fa4bb66e 100644 --- a/homeassistant/components/glances/translations/hu.json +++ b/homeassistant/components/glances/translations/hu.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "name": "N\u00e9v", "password": "Jelsz\u00f3", "port": "Port", diff --git a/homeassistant/components/goalzero/translations/es.json b/homeassistant/components/goalzero/translations/es.json index 8c3aeb80965..fa54d6d6afc 100644 --- a/homeassistant/components/goalzero/translations/es.json +++ b/homeassistant/components/goalzero/translations/es.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya ha sido configurada", + "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos", "unknown": "Error inesperado" }, "error": { diff --git a/homeassistant/components/goalzero/translations/hu.json b/homeassistant/components/goalzero/translations/hu.json index f8c507a6625..62c0a1626f9 100644 --- a/homeassistant/components/goalzero/translations/hu.json +++ b/homeassistant/components/goalzero/translations/hu.json @@ -12,15 +12,15 @@ }, "step": { "confirm_discovery": { - "description": "DHCP foglal\u00e1s aj\u00e1nlott az \u00fatv\u00e1laszt\u00f3n. Ha nincs be\u00e1ll\u00edtva, akkor az eszk\u00f6z el\u00e9rhetetlenn\u00e9 v\u00e1lhat, am\u00edg a Home Assistant \u00e9szleli az \u00faj IP-c\u00edmet. Olvassa el az \u00fatv\u00e1laszt\u00f3 felhaszn\u00e1l\u00f3i k\u00e9zik\u00f6nyv\u00e9t.", + "description": "DHCP foglal\u00e1s aj\u00e1nlott az routeren. Ha nincs be\u00e1ll\u00edtva, akkor az eszk\u00f6z el\u00e9rhetetlenn\u00e9 v\u00e1lhat, am\u00edg Home Assistant \u00e9szleli az \u00faj IP-c\u00edmet. Olvassa el az router felhaszn\u00e1l\u00f3i k\u00e9zik\u00f6nyv\u00e9t.", "title": "Goal Zero Yeti" }, "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "name": "N\u00e9v" }, - "description": "El\u0151sz\u00f6r le kell t\u00f6ltenie a Goal Zero alkalmaz\u00e1st: https://www.goalzero.com/product-features/yeti-app/ \n\nK\u00f6vesse az utas\u00edt\u00e1sokat, hogy csatlakoztassa Yeti k\u00e9sz\u00fcl\u00e9k\u00e9t a Wi-Fi h\u00e1l\u00f3zathoz. DHCP foglal\u00e1s aj\u00e1nlott az \u00fatv\u00e1laszt\u00f3n. Ha nincs be\u00e1ll\u00edtva, akkor az eszk\u00f6z el\u00e9rhetetlenn\u00e9 v\u00e1lhat, am\u00edg a Home Assistant \u00e9szleli az \u00faj IP-c\u00edmet. Olvassa el az \u00fatv\u00e1laszt\u00f3 felhaszn\u00e1l\u00f3i k\u00e9zik\u00f6nyv\u00e9t.", + "description": "El\u0151sz\u00f6r le kell t\u00f6ltenie a Goal Zero alkalmaz\u00e1st: https://www.goalzero.com/product-features/yeti-app/ \n\nK\u00f6vesse az utas\u00edt\u00e1sokat, hogy csatlakoztassa Yeti k\u00e9sz\u00fcl\u00e9k\u00e9t a Wi-Fi h\u00e1l\u00f3zathoz. DHCP foglal\u00e1s aj\u00e1nlott az routeren. Ha nincs be\u00e1ll\u00edtva, akkor az eszk\u00f6z el\u00e9rhetetlenn\u00e9 v\u00e1lhat, am\u00edg a Home Assistant \u00e9szleli az \u00faj IP-c\u00edmet. Olvassa el az router felhaszn\u00e1l\u00f3i k\u00e9zik\u00f6nyv\u00e9t.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/gogogate2/translations/id.json b/homeassistant/components/gogogate2/translations/id.json index 89d25d74a48..04029205389 100644 --- a/homeassistant/components/gogogate2/translations/id.json +++ b/homeassistant/components/gogogate2/translations/id.json @@ -16,7 +16,7 @@ "username": "Nama Pengguna" }, "description": "Berikan informasi yang diperlukan di bawah ini.", - "title": "Siapkan GogoGate2 atau iSmartGate" + "title": "Siapkan GogoGate2 atau ismartgate" } } } diff --git a/homeassistant/components/gpslogger/translations/hu.json b/homeassistant/components/gpslogger/translations/hu.json index fe459ca3164..45832cf493f 100644 --- a/homeassistant/components/gpslogger/translations/hu.json +++ b/homeassistant/components/gpslogger/translations/hu.json @@ -2,14 +2,14 @@ "config": { "abort": { "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", - "webhook_not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." + "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, "create_entry": { "default": "Az esem\u00e9ny Home Assistantnek val\u00f3 k\u00fcld\u00e9s\u00e9hez be kell \u00e1ll\u00edtanod a webhook funkci\u00f3t a GPSLoggerben. \n\n Az al\u00e1bbi inform\u00e1ci\u00f3kat haszn\u00e1ld: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Tov\u00e1bbi r\u00e9szletek a [dokument\u00e1ci\u00f3]({docs_url}) linken tal\u00e1lhat\u00f3k." }, "step": { "user": { - "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani a GPSLogger Webhookot?", + "description": "Biztos benne, hogy be szeretn\u00e9 \u00e1ll\u00edtani a GPSLogger Webhookot?", "title": "GPSLogger Webhook be\u00e1ll\u00edt\u00e1sa" } } diff --git a/homeassistant/components/gree/translations/hu.json b/homeassistant/components/gree/translations/hu.json index 6c61530acbe..a56ebbfc906 100644 --- a/homeassistant/components/gree/translations/hu.json +++ b/homeassistant/components/gree/translations/hu.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?" + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" } } } diff --git a/homeassistant/components/gree/translations/nl.json b/homeassistant/components/gree/translations/nl.json index d11896014fd..0671f0b3674 100644 --- a/homeassistant/components/gree/translations/nl.json +++ b/homeassistant/components/gree/translations/nl.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Wil je beginnen met instellen?" + "description": "Wilt u beginnen met instellen?" } } } diff --git a/homeassistant/components/growatt_server/translations/es.json b/homeassistant/components/growatt_server/translations/es.json index 23860f225da..8fe4ae8b791 100644 --- a/homeassistant/components/growatt_server/translations/es.json +++ b/homeassistant/components/growatt_server/translations/es.json @@ -17,6 +17,7 @@ "data": { "name": "Nombre", "password": "Nombre", + "url": "URL", "username": "Usuario" }, "title": "Introduce tu informaci\u00f3n de Growatt." diff --git a/homeassistant/components/growatt_server/translations/id.json b/homeassistant/components/growatt_server/translations/id.json index 789d4e1732b..59975607fb7 100644 --- a/homeassistant/components/growatt_server/translations/id.json +++ b/homeassistant/components/growatt_server/translations/id.json @@ -8,6 +8,7 @@ "data": { "name": "Nama", "password": "Kata Sandi", + "url": "URL", "username": "Nama Pengguna" } } diff --git a/homeassistant/components/guardian/translations/hu.json b/homeassistant/components/guardian/translations/hu.json index 15469bead1e..ecd1b7de01b 100644 --- a/homeassistant/components/guardian/translations/hu.json +++ b/homeassistant/components/guardian/translations/hu.json @@ -2,12 +2,12 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { "discovery_confirm": { - "description": "Be akarja \u00e1ll\u00edtani ezt a Guardian eszk\u00f6zt?" + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani ezt a Guardian eszk\u00f6zt?" }, "user": { "data": { @@ -17,7 +17,7 @@ "description": "Konfigur\u00e1lja a helyi Elexa Guardian eszk\u00f6zt." }, "zeroconf_confirm": { - "description": "Be akarja \u00e1ll\u00edtani ezt a Guardian eszk\u00f6zt?" + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani ezt a Guardian eszk\u00f6zt?" } } } diff --git a/homeassistant/components/hangouts/translations/fi.json b/homeassistant/components/hangouts/translations/fi.json index 959a2c06a63..05b394c69f4 100644 --- a/homeassistant/components/hangouts/translations/fi.json +++ b/homeassistant/components/hangouts/translations/fi.json @@ -8,6 +8,7 @@ "data": { "2fa": "2FA-pin" }, + "description": "Tyhj\u00e4", "title": "Kaksivaiheinen tunnistus" }, "user": { @@ -15,6 +16,7 @@ "email": "S\u00e4hk\u00f6postiosoite", "password": "Salasana" }, + "description": "Tyhj\u00e4", "title": "Google Hangouts -kirjautuminen" } } diff --git a/homeassistant/components/hangouts/translations/hu.json b/homeassistant/components/hangouts/translations/hu.json index 2f02ba9f623..eda0144a818 100644 --- a/homeassistant/components/hangouts/translations/hu.json +++ b/homeassistant/components/hangouts/translations/hu.json @@ -12,7 +12,7 @@ "step": { "2fa": { "data": { - "2fa": "2FA Pin" + "2fa": "2FA PIN" }, "description": "\u00dcres", "title": "K\u00e9tfaktoros Hiteles\u00edt\u00e9s" diff --git a/homeassistant/components/harmony/translations/hu.json b/homeassistant/components/harmony/translations/hu.json index 4922bbd1ac6..900cd243247 100644 --- a/homeassistant/components/harmony/translations/hu.json +++ b/homeassistant/components/harmony/translations/hu.json @@ -10,12 +10,12 @@ "flow_title": "{name}", "step": { "link": { - "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}({host})-t?", + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name} ({host})?", "title": "A Logitech Harmony Hub be\u00e1ll\u00edt\u00e1sa" }, "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "name": "Hub neve" }, "title": "A Logitech Harmony Hub be\u00e1ll\u00edt\u00e1sa" diff --git a/homeassistant/components/harmony/translations/id.json b/homeassistant/components/harmony/translations/id.json index 0d2991b1feb..86ab0be3274 100644 --- a/homeassistant/components/harmony/translations/id.json +++ b/homeassistant/components/harmony/translations/id.json @@ -7,7 +7,7 @@ "cannot_connect": "Gagal terhubung", "unknown": "Kesalahan yang tidak diharapkan" }, - "flow_title": "Logitech Harmony Hub {name}", + "flow_title": "{name}", "step": { "link": { "description": "Ingin menyiapkan {name} ({host})?", diff --git a/homeassistant/components/hassio/translations/he.json b/homeassistant/components/hassio/translations/he.json index 17b7fcd0050..8926338221a 100644 --- a/homeassistant/components/hassio/translations/he.json +++ b/homeassistant/components/hassio/translations/he.json @@ -1,10 +1,18 @@ { "system_health": { "info": { + "board": "\u05dc\u05d5\u05d7", + "disk_total": "\u05e1\u05d4\"\u05db \u05d3\u05d9\u05e1\u05e7", + "disk_used": "\u05d3\u05d9\u05e1\u05e7 \u05d1\u05e9\u05d9\u05de\u05d5\u05e9", + "docker_version": "\u05d2\u05d9\u05e8\u05e1\u05ea Docker", + "healthy": "\u05d1\u05e8\u05d9\u05d0\u05d5\u05ea", "host_os": "\u05de\u05e2\u05e8\u05db\u05ea \u05d4\u05e4\u05e2\u05dc\u05d4 \u05de\u05d0\u05e8\u05d7\u05ea", + "installed_addons": "\u05d4\u05e8\u05d7\u05d1\u05d5\u05ea \u05de\u05d5\u05ea\u05e7\u05e0\u05d5\u05ea", "supervisor_api": "API \u05e9\u05dc \u05de\u05e4\u05e7\u05d7", "supervisor_version": "\u05d2\u05d9\u05e8\u05e1\u05ea \u05de\u05e4\u05e7\u05d7", - "update_channel": "\u05e2\u05e8\u05d5\u05e5 \u05e2\u05d3\u05db\u05d5\u05df" + "supported": "\u05e0\u05ea\u05de\u05da", + "update_channel": "\u05e2\u05e8\u05d5\u05e5 \u05e2\u05d3\u05db\u05d5\u05df", + "version_api": "\u05d2\u05e8\u05e1\u05ea API" } } } \ No newline at end of file diff --git a/homeassistant/components/heos/translations/hu.json b/homeassistant/components/heos/translations/hu.json index c487b49ee47..8996c2a4530 100644 --- a/homeassistant/components/heos/translations/hu.json +++ b/homeassistant/components/heos/translations/hu.json @@ -9,9 +9,9 @@ "step": { "user": { "data": { - "host": "Hoszt" + "host": "C\u00edm" }, - "description": "K\u00e9rj\u00fck, adja meg egy Heos-eszk\u00f6z gazdag\u00e9pnev\u00e9t vagy IP-c\u00edm\u00e9t (lehet\u0151leg egy vezet\u00e9kkel a h\u00e1l\u00f3zathoz csatlakoztatott eszk\u00f6zt).", + "description": "K\u00e9rj\u00fck, adja meg egy Heos-eszk\u00f6z hosztnev\u00e9t vagy c\u00edm\u00e9t (lehet\u0151leg egy vezet\u00e9kkel a h\u00e1l\u00f3zathoz csatlakoztatott eszk\u00f6zt).", "title": "Csatlakoz\u00e1s a Heos-hoz" } } diff --git a/homeassistant/components/hive/translations/ca.json b/homeassistant/components/hive/translations/ca.json index eacccda82e7..edebafba579 100644 --- a/homeassistant/components/hive/translations/ca.json +++ b/homeassistant/components/hive/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat", + "already_configured": "El compte ja est\u00e0 configurat", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", "unknown_entry": "No s'ha pogut trobar l'entrada existent." }, diff --git a/homeassistant/components/hive/translations/hu.json b/homeassistant/components/hive/translations/hu.json index ce07abcb338..469b99debe1 100644 --- a/homeassistant/components/hive/translations/hu.json +++ b/homeassistant/components/hive/translations/hu.json @@ -17,7 +17,7 @@ "data": { "2fa": "K\u00e9tfaktoros k\u00f3d" }, - "description": "Add meg a Hive hiteles\u00edt\u00e9si k\u00f3dj\u00e1t. \n \n\u00cdrd be a 0000 k\u00f3dot m\u00e1sik k\u00f3d k\u00e9r\u00e9s\u00e9hez.", + "description": "Adja meg a Hive hiteles\u00edt\u00e9si k\u00f3dj\u00e1t. \n \n\u00cdrd be a 0000 k\u00f3dot m\u00e1sik k\u00f3d k\u00e9r\u00e9s\u00e9hez.", "title": "Hive k\u00e9tfaktoros hiteles\u00edt\u00e9s." }, "reauth": { @@ -25,7 +25,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "Add meg \u00fajra a Hive bejelentkez\u00e9si adatait.", + "description": "Adja meg \u00fajra a Hive bejelentkez\u00e9si adatait.", "title": "Hive Bejelentkez\u00e9s" }, "user": { @@ -34,7 +34,7 @@ "scan_interval": "Szkennel\u00e9si intervallum (m\u00e1sodperc)", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "Add meg a Hive bejelentkez\u00e9si adatait \u00e9s konfigur\u00e1ci\u00f3j\u00e1t.", + "description": "Adja meg a Hive bejelentkez\u00e9si adatait \u00e9s konfigur\u00e1ci\u00f3j\u00e1t.", "title": "Hive Bejelentkez\u00e9s" } } diff --git a/homeassistant/components/hlk_sw16/translations/hu.json b/homeassistant/components/hlk_sw16/translations/hu.json index 0abcc301f0c..9590d3c12be 100644 --- a/homeassistant/components/hlk_sw16/translations/hu.json +++ b/homeassistant/components/hlk_sw16/translations/hu.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } diff --git a/homeassistant/components/home_connect/translations/hu.json b/homeassistant/components/home_connect/translations/hu.json index aa43f65b520..ca5f3e1e9ae 100644 --- a/homeassistant/components/home_connect/translations/hu.json +++ b/homeassistant/components/home_connect/translations/hu.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz." }, "create_entry": { diff --git a/homeassistant/components/home_plus_control/translations/ca.json b/homeassistant/components/home_plus_control/translations/ca.json index 90e23fcd7ab..6e6dc1e0577 100644 --- a/homeassistant/components/home_plus_control/translations/ca.json +++ b/homeassistant/components/home_plus_control/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat", + "already_configured": "El compte ja est\u00e0 configurat", "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", diff --git a/homeassistant/components/home_plus_control/translations/hu.json b/homeassistant/components/home_plus_control/translations/hu.json index 7bc04beb057..2dc22c7a729 100644 --- a/homeassistant/components/home_plus_control/translations/hu.json +++ b/homeassistant/components/home_plus_control/translations/hu.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", - "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, diff --git a/homeassistant/components/homekit/translations/ca.json b/homeassistant/components/homekit/translations/ca.json index a71287d3c0b..63f34999a4d 100644 --- a/homeassistant/components/homekit/translations/ca.json +++ b/homeassistant/components/homekit/translations/ca.json @@ -29,10 +29,11 @@ }, "cameras": { "data": { + "camera_audio": "C\u00e0meres que admeten \u00e0udio", "camera_copy": "C\u00e0meres que admeten fluxos H.264 natius" }, "description": "Comprova les c\u00e0meres que suporten fluxos nadius H.264. Si alguna c\u00e0mera not proporciona una sortida H.264, el sistema transcodificar\u00e0 el v\u00eddeo a H.264 per a HomeKit. La transcodificaci\u00f3 necessita una CPU potent i probablement no funcioni en ordinadors petits (SBC).", - "title": "Selecci\u00f3 del c\u00f2dec de v\u00eddeo de c\u00e0mera" + "title": "Configuraci\u00f3 de c\u00e0mera" }, "include_exclude": { "data": { diff --git a/homeassistant/components/homekit/translations/de.json b/homeassistant/components/homekit/translations/de.json index 06027c4c09e..a0c407c454e 100644 --- a/homeassistant/components/homekit/translations/de.json +++ b/homeassistant/components/homekit/translations/de.json @@ -29,6 +29,7 @@ }, "cameras": { "data": { + "camera_audio": "Kameras, die Audio unterst\u00fctzen", "camera_copy": "Kameras, die native H.264-Streams unterst\u00fctzen" }, "description": "Pr\u00fcfe alle Kameras, die native H.264-Streams unterst\u00fctzen. Wenn die Kamera keinen H.264-Stream ausgibt, transkodiert das System das Video in H.264 f\u00fcr HomeKit. Die Transkodierung erfordert eine leistungsstarke CPU und wird wahrscheinlich nicht auf Einplatinencomputern funktionieren.", diff --git a/homeassistant/components/homekit/translations/el.json b/homeassistant/components/homekit/translations/el.json new file mode 100644 index 00000000000..58d7a62bc59 --- /dev/null +++ b/homeassistant/components/homekit/translations/el.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "advanced": { + "data": { + "devices": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 (\u0395\u03bd\u03b1\u03cd\u03c3\u03bc\u03b1\u03c4\u03b1)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/es.json b/homeassistant/components/homekit/translations/es.json index e713391eb9e..6008d399d64 100644 --- a/homeassistant/components/homekit/translations/es.json +++ b/homeassistant/components/homekit/translations/es.json @@ -5,7 +5,7 @@ }, "step": { "pairing": { - "description": "Tan pronto como la pasarela {name} est\u00e9 lista, la vinculaci\u00f3n estar\u00e1 disponible en \"Notificaciones\" como \"configuraci\u00f3n de pasarela Homekit\"", + "description": "Para completar el emparejamiento, sigue las instrucciones en \"Notificaciones\" en \"Emparejamiento HomeKit\".", "title": "Vincular pasarela Homekit" }, "user": { @@ -21,13 +21,15 @@ "step": { "advanced": { "data": { - "auto_start": "Arranque autom\u00e1tico (desactivado si se utiliza Z-Wave u otro sistema de arranque retardado)" + "auto_start": "Arranque autom\u00e1tico (desactivado si se utiliza Z-Wave u otro sistema de arranque retardado)", + "devices": "Dispositivos (disparadores)" }, "description": "Esta configuraci\u00f3n solo necesita ser ajustada si el puente HomeKit no es funcional.", "title": "Configuraci\u00f3n avanzada" }, "cameras": { "data": { + "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.", diff --git a/homeassistant/components/homekit/translations/et.json b/homeassistant/components/homekit/translations/et.json index 4e454178048..cd02425a2b6 100644 --- a/homeassistant/components/homekit/translations/et.json +++ b/homeassistant/components/homekit/translations/et.json @@ -29,10 +29,11 @@ }, "cameras": { "data": { + "camera_audio": "Heliedastusega kaamerad", "camera_copy": "Kaamerad, mis toetavad riistvaralist H.264 voogu" }, "description": "Vali k\u00f5iki kaameraid, mis toetavad kohalikku H.264 voogu. Kui kaamera ei edasta H.264 voogu, kodeerib s\u00fcsteem video HomeKiti jaoks versioonile H.264. \u00dcmberkodeerimine n\u00f5uab j\u00f5udsat protsessorit ja t\u00f5en\u00e4oliselt ei t\u00f6\u00f6ta see \u00fcheplaadilistes arvutites.", - "title": "Vali kaamera videokoodek." + "title": "Kaamera s\u00e4tted" }, "include_exclude": { "data": { diff --git a/homeassistant/components/homekit/translations/he.json b/homeassistant/components/homekit/translations/he.json index ee476b92b8c..320bf203044 100644 --- a/homeassistant/components/homekit/translations/he.json +++ b/homeassistant/components/homekit/translations/he.json @@ -15,6 +15,9 @@ "devices": "\u05d4\u05ea\u05e7\u05e0\u05d9\u05dd (\u05d8\u05e8\u05d9\u05d2\u05e8\u05d9\u05dd)" } }, + "cameras": { + "title": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05de\u05e6\u05dc\u05de\u05d4" + }, "include_exclude": { "data": { "mode": "\u05de\u05e6\u05d1" diff --git a/homeassistant/components/homekit/translations/hu.json b/homeassistant/components/homekit/translations/hu.json index f60db036247..046cf57e9b9 100644 --- a/homeassistant/components/homekit/translations/hu.json +++ b/homeassistant/components/homekit/translations/hu.json @@ -24,15 +24,16 @@ "auto_start": "Automatikus ind\u00edt\u00e1s (tiltsa le, ha manu\u00e1lisan h\u00edvja a homekit.start szolg\u00e1ltat\u00e1st)", "devices": "Eszk\u00f6z\u00f6k (triggerek)" }, - "description": "Ezeket a be\u00e1ll\u00edt\u00e1sokat csak akkor kell m\u00f3dos\u00edtani, ha a HomeKit nem m\u0171k\u00f6dik.", + "description": "Programozhat\u00f3 kapcsol\u00f3k j\u00f6nnek l\u00e9tre minden kiv\u00e1lasztott eszk\u00f6zh\u00f6z. Amikor egy eszk\u00f6z esem\u00e9nyt ind\u00edt el, a HomeKit be\u00e1ll\u00edthat\u00f3 \u00fagy, hogy egy automatizmus vagy egy jelenet induljon el.", "title": "Halad\u00f3 be\u00e1ll\u00edt\u00e1sok" }, "cameras": { "data": { - "camera_copy": "A nat\u00edv H.264 streameket t\u00e1mogat\u00f3 kamer\u00e1k" + "camera_audio": "Hangot t\u00e1mogat\u00f3 kamer\u00e1k", + "camera_copy": "Nat\u00edv H.264 streameket t\u00e1mogat\u00f3 kamer\u00e1k" }, "description": "Ellen\u0151rizze az \u00f6sszes kamer\u00e1t, amely t\u00e1mogatja a nat\u00edv H.264 adatfolyamokat. Ha a f\u00e9nyk\u00e9pez\u0151g\u00e9p nem ad ki H.264 adatfolyamot, a rendszer \u00e1tk\u00f3dolja a vide\u00f3t H.264 form\u00e1tumba a HomeKit sz\u00e1m\u00e1ra. Az \u00e1tk\u00f3dol\u00e1shoz nagy teljes\u00edtm\u00e9ny\u0171 CPU sz\u00fcks\u00e9ges, \u00e9s val\u00f3sz\u00edn\u0171leg nem fog m\u0171k\u00f6dni egylapos sz\u00e1m\u00edt\u00f3g\u00e9peken.", - "title": "V\u00e1laszd ki a kamera vide\u00f3 kodekj\u00e9t." + "title": "V\u00e1lassza ki a kamera vide\u00f3 kodekj\u00e9t." }, "include_exclude": { "data": { @@ -40,7 +41,7 @@ "mode": "M\u00f3d" }, "description": "V\u00e1lassza ki a felvenni k\u00edv\u00e1nt entit\u00e1sokat. Kieg\u00e9sz\u00edt\u0151 m\u00f3dban csak egyetlen entit\u00e1s szerepel. H\u00eddbefogad\u00e1si m\u00f3dban a tartom\u00e1ny \u00f6sszes entit\u00e1sa szerepelni fog, hacsak nincsenek kijel\u00f6lve konkr\u00e9t entit\u00e1sok. H\u00eddkiz\u00e1r\u00e1si m\u00f3dban a domain \u00f6sszes entit\u00e1sa szerepelni fog, kiv\u00e9ve a kiz\u00e1rt entit\u00e1sokat. A legjobb teljes\u00edtm\u00e9ny \u00e9rdek\u00e9ben minden TV m\u00e9dialej\u00e1tsz\u00f3hoz, tev\u00e9kenys\u00e9galap\u00fa t\u00e1vir\u00e1ny\u00edt\u00f3hoz, z\u00e1rhoz \u00e9s f\u00e9nyk\u00e9pez\u0151g\u00e9phez k\u00fcl\u00f6n HomeKit tartoz\u00e9kot hoznak l\u00e9tre.", - "title": "V\u00e1laszd ki a felvenni k\u00edv\u00e1nt entit\u00e1sokat" + "title": "V\u00e1lassza ki a felvenni k\u00edv\u00e1nt entit\u00e1sokat" }, "init": { "data": { @@ -48,7 +49,7 @@ "mode": "M\u00f3d" }, "description": "A HomeKit konfigur\u00e1lhat\u00f3 \u00fagy, hogy egy h\u00edd vagy egyetlen tartoz\u00e9k l\u00e1that\u00f3 legyen. Kieg\u00e9sz\u00edt\u0151 m\u00f3dban csak egyetlen entit\u00e1s haszn\u00e1lhat\u00f3. A tartoz\u00e9k m\u00f3dra van sz\u00fcks\u00e9g ahhoz, hogy a TV -eszk\u00f6zoszt\u00e1ly\u00fa m\u00e9dialej\u00e1tsz\u00f3k megfelel\u0151en m\u0171k\u00f6djenek. A \u201eTartalmazand\u00f3 tartom\u00e1nyok\u201d entit\u00e1sai szerepelni fognak a HomeKitben. A k\u00f6vetkez\u0151 k\u00e9perny\u0151n kiv\u00e1laszthatja, hogy mely entit\u00e1sokat k\u00edv\u00e1nja felvenni vagy kiz\u00e1rni a list\u00e1b\u00f3l.", - "title": "V\u00e1laszd ki a felvenni k\u00edv\u00e1nt domaineket." + "title": "V\u00e1lassza ki a felvenni k\u00edv\u00e1nt domaineket." }, "yaml": { "description": "Ez a bejegyz\u00e9s YAML-en kereszt\u00fcl vez\u00e9relhet\u0151", diff --git a/homeassistant/components/homekit/translations/id.json b/homeassistant/components/homekit/translations/id.json index ecb35196228..64ce23a5224 100644 --- a/homeassistant/components/homekit/translations/id.json +++ b/homeassistant/components/homekit/translations/id.json @@ -12,7 +12,7 @@ "data": { "include_domains": "Domain yang disertakan" }, - "description": "Pilih domain yang akan disertakan. Semua entitas yang didukung di domain akan disertakan. Instans HomeKit terpisah dalam mode aksesori akan dibuat untuk setiap pemutar media TV dan kamera.", + "description": "Pilih domain yang akan disertakan. Semua entitas yang didukung di domain akan disertakan. Instans HomeKit terpisah dalam mode aksesori akan dibuat untuk setiap pemutar media TV, remote berbasis aktivitas, kunci, dan kamera.", "title": "Pilih domain yang akan disertakan" } } @@ -23,7 +23,7 @@ "data": { "auto_start": "Mulai otomatis (nonaktifkan jika Anda memanggil layanan homekit.start secara manual)" }, - "description": "Pengaturan ini hanya perlu disesuaikan jika HomeKit tidak berfungsi.", + "description": "Sakelar yang dapat diprogram dibuat untuk setiap perangkat yang dipilih. Saat pemicu perangkat aktif, HomeKit dapat dikonfigurasi untuk menjalankan otomatisasi atau scene.", "title": "Konfigurasi Tingkat Lanjut" }, "cameras": { @@ -31,14 +31,14 @@ "camera_copy": "Kamera yang mendukung aliran H.264 asli" }, "description": "Periksa semua kamera yang mendukung streaming H.264 asli. Jika kamera tidak mengeluarkan aliran H.264, sistem akan mentranskode video ke H.264 untuk HomeKit. Proses transcoding membutuhkan CPU kinerja tinggi dan tidak mungkin bekerja pada komputer papan tunggal.", - "title": "Pilih codec video kamera." + "title": "Konfigurasi Kamera" }, "include_exclude": { "data": { "entities": "Entitas", "mode": "Mode" }, - "description": "Pilih entitas yang akan disertakan. Dalam mode aksesori, hanya satu entitas yang disertakan. Dalam mode \"bridge include\", semua entitas di domain akan disertakan, kecuali entitas tertentu dipilih. Dalam mode \"bridge exclude\", semua entitas di domain akan disertakan, kecuali untuk entitas tertentu yang dipilih. Untuk kinerja terbaik, aksesori HomeKit terpisah diperlukan untuk masing-masing pemutar media, TV, dan kamera.", + "description": "Pilih entitas yang akan disertakan. Dalam mode aksesori, hanya satu entitas yang disertakan. Dalam mode \"bridge include\", semua entitas di domain akan disertakan, kecuali entitas tertentu dipilih. Dalam mode \"bridge exclude\", semua entitas di domain akan disertakan, kecuali untuk entitas tertentu yang dipilih. Untuk kinerja terbaik, aksesori HomeKit terpisah diperlukan untuk masing-masing pemutar media TV, remote berbasis aktivitas, kunci, dan kamera.", "title": "Pilih entitas untuk disertakan" }, "init": { diff --git a/homeassistant/components/homekit/translations/it.json b/homeassistant/components/homekit/translations/it.json index 74bc0032580..8e7ead91ac1 100644 --- a/homeassistant/components/homekit/translations/it.json +++ b/homeassistant/components/homekit/translations/it.json @@ -29,10 +29,11 @@ }, "cameras": { "data": { + "camera_audio": "Telecamere che supportano l'audio", "camera_copy": "Telecamere che supportano flussi H.264 nativi" }, "description": "Controllare tutte le telecamere che supportano i flussi H.264 nativi. Se la videocamera non emette uno stream H.264, il sistema provveder\u00e0 a transcodificare il video in H.264 per HomeKit. La transcodifica richiede una CPU performante ed \u00e8 improbabile che funzioni su computer a scheda singola.", - "title": "Seleziona il codec video della videocamera." + "title": "Configurazione della telecamera" }, "include_exclude": { "data": { diff --git a/homeassistant/components/homekit/translations/nl.json b/homeassistant/components/homekit/translations/nl.json index e08364e038f..2ab21f66db5 100644 --- a/homeassistant/components/homekit/translations/nl.json +++ b/homeassistant/components/homekit/translations/nl.json @@ -29,10 +29,11 @@ }, "cameras": { "data": { + "camera_audio": "Camera's die audio ondersteunen", "camera_copy": "Camera's die native H.264-streams ondersteunen" }, "description": "Controleer alle camera's die native H.264-streams ondersteunen. Als de camera geen H.264-stream uitvoert, transcodeert het systeem de video naar H.264 voor HomeKit. Transcodering vereist een performante CPU en het is onwaarschijnlijk dat dit werkt op computers met \u00e9\u00e9n bord.", - "title": "Selecteer de videocodec van de camera." + "title": "Cameraconfiguratie" }, "include_exclude": { "data": { diff --git a/homeassistant/components/homekit/translations/no.json b/homeassistant/components/homekit/translations/no.json index 08df5bd72fa..86e5c8d95cb 100644 --- a/homeassistant/components/homekit/translations/no.json +++ b/homeassistant/components/homekit/translations/no.json @@ -29,10 +29,11 @@ }, "cameras": { "data": { + "camera_audio": "Kameraer som st\u00f8tter lyd", "camera_copy": "Kameraer som st\u00f8tter opprinnelige H.264-str\u00f8mmer" }, "description": "Sjekk alle kameraer som st\u00f8tter opprinnelige H.264-str\u00f8mmer. Hvis kameraet ikke sender ut en H.264-str\u00f8m, vil systemet omkode videoen til H.264 for HomeKit. Transkoding krever en potent prosessor og er usannsynlig \u00e5 fungere p\u00e5 enkeltkortdatamaskiner som Raspberry Pi o.l.", - "title": "Velg videokodek for kamera." + "title": "Kamerakonfigurasjon" }, "include_exclude": { "data": { diff --git a/homeassistant/components/homekit/translations/ru.json b/homeassistant/components/homekit/translations/ru.json index 670c5e8002f..f871636df00 100644 --- a/homeassistant/components/homekit/translations/ru.json +++ b/homeassistant/components/homekit/translations/ru.json @@ -29,10 +29,11 @@ }, "cameras": { "data": { - "camera_copy": "\u041a\u0430\u043c\u0435\u0440\u044b, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442 \u043f\u043e\u0442\u043e\u043a\u0438 H.264" + "camera_audio": "\u041a\u0430\u043c\u0435\u0440\u044b \u0441 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u043e\u0439 \u0430\u0443\u0434\u0438\u043e", + "camera_copy": "\u041a\u0430\u043c\u0435\u0440\u044b \u0441 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u043e\u0439 H.264" }, "description": "\u0415\u0441\u043b\u0438 \u043a\u0430\u043c\u0435\u0440\u0430 \u043d\u0435 \u0432\u044b\u0432\u043e\u0434\u0438\u0442 \u043f\u043e\u0442\u043e\u043a H.264, \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u043f\u0435\u0440\u0435\u043a\u043e\u0434\u0438\u0440\u0443\u0435\u0442 \u0432\u0438\u0434\u0435\u043e \u0432 H.264 \u0434\u043b\u044f HomeKit. \u0422\u0440\u0430\u043d\u0441\u043a\u043e\u0434\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0432\u044b\u0441\u043e\u043a\u043e\u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u043e\u0440\u0430 \u0438 \u0432\u0440\u044f\u0434 \u043b\u0438 \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u043d\u0430 \u043e\u0434\u043d\u043e\u043f\u043b\u0430\u0442\u043d\u044b\u0445 \u043a\u043e\u043c\u043f\u044c\u044e\u0442\u0435\u0440\u0430\u0445.", - "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0432\u0438\u0434\u0435\u043e\u043a\u043e\u0434\u0435\u043a \u043a\u0430\u043c\u0435\u0440\u044b." + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043a\u0430\u043c\u0435\u0440\u044b" }, "include_exclude": { "data": { diff --git a/homeassistant/components/homekit/translations/zh-Hant.json b/homeassistant/components/homekit/translations/zh-Hant.json index a4a5ac06b96..ba1cd8adf88 100644 --- a/homeassistant/components/homekit/translations/zh-Hant.json +++ b/homeassistant/components/homekit/translations/zh-Hant.json @@ -29,10 +29,11 @@ }, "cameras": { "data": { + "camera_audio": "\u652f\u63f4\u97f3\u6548\u8f38\u51fa\u651d\u5f71\u6a5f", "camera_copy": "\u652f\u63f4\u539f\u751f H.264 \u4e32\u6d41\u651d\u5f71\u6a5f" }, "description": "\u6aa2\u67e5\u6240\u6709\u652f\u63f4\u539f\u751f H.264 \u4e32\u6d41\u4e4b\u651d\u5f71\u6a5f\u3002\u5047\u5982\u651d\u5f71\u6a5f\u4e0d\u652f\u63f4 H.264 \u4e32\u6d41\u3001\u7cfb\u7d71\u5c07\u6703\u91dd\u5c0d Homekit \u9032\u884c H.264 \u8f49\u78bc\u3002\u8f49\u78bc\u5c07\u9700\u8981\u4f7f\u7528 CPU \u9032\u884c\u904b\u7b97\u3001\u55ae\u6676\u7247\u96fb\u8166\u53ef\u80fd\u6703\u906d\u9047\u6548\u80fd\u554f\u984c\u3002", - "title": "\u9078\u64c7\u651d\u5f71\u6a5f\u7de8\u78bc\u3002" + "title": "\u651d\u5f71\u6a5f\u8a2d\u5b9a" }, "include_exclude": { "data": { diff --git a/homeassistant/components/homekit_controller/translations/hu.json b/homeassistant/components/homekit_controller/translations/hu.json index 1ad63bfb508..aef97c7b3ba 100644 --- a/homeassistant/components/homekit_controller/translations/hu.json +++ b/homeassistant/components/homekit_controller/translations/hu.json @@ -3,22 +3,22 @@ "abort": { "accessory_not_found_error": "Nem adhat\u00f3 hozz\u00e1 p\u00e1ros\u00edt\u00e1s, mert az eszk\u00f6z m\u00e1r nem tal\u00e1lhat\u00f3.", "already_configured": "A tartoz\u00e9k m\u00e1r konfigur\u00e1lva van ezzel a vez\u00e9rl\u0151vel.", - "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", "already_paired": "Ez a tartoz\u00e9k m\u00e1r p\u00e1ros\u00edtva van egy m\u00e1sik eszk\u00f6zzel. \u00c1ll\u00edtsa alaphelyzetbe a tartoz\u00e9kot, majd pr\u00f3b\u00e1lkozzon \u00fajra.", "ignored_model": "A HomeKit t\u00e1mogat\u00e1sa e modelln\u00e9l blokkolva van, mivel a szolg\u00e1ltat\u00e1shoz teljes nat\u00edv integr\u00e1ci\u00f3 \u00e9rhet\u0151 el.", - "invalid_config_entry": "Ez az eszk\u00f6z k\u00e9szen \u00e1ll a p\u00e1ros\u00edt\u00e1sra, de m\u00e1r van egy \u00fctk\u00f6z\u0151 konfigur\u00e1ci\u00f3s bejegyz\u00e9s a Home Assistant-ben, amelyet el\u0151sz\u00f6r el kell t\u00e1vol\u00edtani.", + "invalid_config_entry": "Ez az eszk\u00f6z k\u00e9szen \u00e1ll a p\u00e1ros\u00edt\u00e1sra, de m\u00e1r van egy \u00fctk\u00f6z\u0151 konfigur\u00e1ci\u00f3s bejegyz\u00e9s Home Assistant-ben, amelyet el\u0151sz\u00f6r el kell t\u00e1vol\u00edtani.", "invalid_properties": "Az eszk\u00f6z \u00e1ltal bejelentett \u00e9rv\u00e9nytelen tulajdons\u00e1gok.", "no_devices": "Nem tal\u00e1lhat\u00f3 nem p\u00e1ros\u00edtott eszk\u00f6z" }, "error": { "authentication_error": "Helytelen HomeKit k\u00f3d. K\u00e9rj\u00fck, ellen\u0151rizze, \u00e9s pr\u00f3b\u00e1lja \u00fajra.", "insecure_setup_code": "A k\u00e9rt telep\u00edt\u00e9si k\u00f3d trivi\u00e1lis jellege miatt nem biztons\u00e1gos. Ez a tartoz\u00e9k nem felel meg az alapvet\u0151 biztons\u00e1gi k\u00f6vetelm\u00e9nyeknek.", - "max_peers_error": "Az eszk\u00f6z megtagadta a p\u00e1ros\u00edt\u00e1s hozz\u00e1ad\u00e1s\u00e1t, mivel nincs ingyenes p\u00e1ros\u00edt\u00e1si t\u00e1rhely.", + "max_peers_error": "Az eszk\u00f6z megtagadta a p\u00e1ros\u00edt\u00e1s hozz\u00e1ad\u00e1s\u00e1t, mivel nincs szabad p\u00e1ros\u00edt\u00e1si t\u00e1rhelye.", "pairing_failed": "Nem kezelt hiba t\u00f6rt\u00e9nt az eszk\u00f6zzel val\u00f3 p\u00e1ros\u00edt\u00e1s sor\u00e1n. Lehet, hogy ez \u00e1tmeneti hiba, vagy az eszk\u00f6z jelenleg m\u00e9g nem t\u00e1mogatott.", "unable_to_pair": "Nem siker\u00fclt p\u00e1ros\u00edtani, pr\u00f3b\u00e1ld \u00fajra.", "unknown_error": "Az eszk\u00f6z ismeretlen hib\u00e1t jelentett. A p\u00e1ros\u00edt\u00e1s sikertelen." }, - "flow_title": "HomeKit tartoz\u00e9k: {name}", + "flow_title": "{name}", "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,8 +33,8 @@ "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": "\u00cdrja be a HomeKit p\u00e1ros\u00edt\u00e1si k\u00f3dj\u00e1t (XXX-XX-XXX form\u00e1tumban) a kieg\u00e9sz\u00edt\u0151 haszn\u00e1lat\u00e1hoz", - "title": "HomeKit tartoz\u00e9k p\u00e1ros\u00edt\u00e1sa" + "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.", + "title": "P\u00e1ros\u00edt\u00e1s egy eszk\u00f6zzel a HomeKit Accessory Protocol protokollon seg\u00edts\u00e9g\u00e9vel" }, "protocol_error": { "description": "El\u0151fordulhat, hogy a k\u00e9sz\u00fcl\u00e9k nincs p\u00e1ros\u00edt\u00e1si m\u00f3dban, \u00e9s sz\u00fcks\u00e9g lehet fizikai vagy virtu\u00e1lis gombnyom\u00e1sra. Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy az eszk\u00f6z p\u00e1ros\u00edt\u00e1si m\u00f3dban van, vagy pr\u00f3b\u00e1lja \u00fajraind\u00edtani az eszk\u00f6zt, majd folytassa a p\u00e1ros\u00edt\u00e1st.", @@ -44,7 +44,7 @@ "data": { "device": "Eszk\u00f6z" }, - "description": "V\u00e1lassza ki azt az eszk\u00f6zt, amelyet p\u00e1ros\u00edtani szeretne", + "description": "A HomeKit Controller biztons\u00e1gos titkos\u00edtott kapcsolaton kereszt\u00fcl kommunik\u00e1l a helyi h\u00e1l\u00f3zaton kereszt\u00fcl, k\u00fcl\u00f6n HomeKit vez\u00e9rl\u0151 vagy iCloud n\u00e9lk\u00fcl. V\u00e1lassza ki a p\u00e1ros\u00edtani k\u00edv\u00e1nt eszk\u00f6zt:", "title": "Eszk\u00f6z kiv\u00e1laszt\u00e1sa" } } diff --git a/homeassistant/components/homekit_controller/translations/id.json b/homeassistant/components/homekit_controller/translations/id.json index 49a37d3b3fb..839169fc6a9 100644 --- a/homeassistant/components/homekit_controller/translations/id.json +++ b/homeassistant/components/homekit_controller/translations/id.json @@ -17,7 +17,7 @@ "unable_to_pair": "Gagal memasangkan, coba lagi.", "unknown_error": "Perangkat melaporkan kesalahan yang tidak diketahui. Pemasangan gagal." }, - "flow_title": "{name} lewat HomeKit Accessory Protocol", + "flow_title": "{name}", "step": { "busy_error": { "description": "Batalkan pemasangan di semua pengontrol, atau coba mulai ulang perangkat, lalu lanjutkan untuk melanjutkan pemasangan.", diff --git a/homeassistant/components/homematicip_cloud/translations/fi.json b/homeassistant/components/homematicip_cloud/translations/fi.json index 9fcaacf4ba1..6a46955cddb 100644 --- a/homeassistant/components/homematicip_cloud/translations/fi.json +++ b/homeassistant/components/homematicip_cloud/translations/fi.json @@ -1,11 +1,20 @@ { "config": { "abort": { + "already_configured": "Laite on jo m\u00e4\u00e4ritetty", + "connection_aborted": "Yhdist\u00e4minen ep\u00e4onnistui", "unknown": "Tapahtui tuntematon virhe." }, "error": { "invalid_sgtin_or_pin": "Virheellinen PIN-koodi, yrit\u00e4 uudelleen.", "press_the_button": "Paina sinist\u00e4 painiketta." + }, + "step": { + "init": { + "data": { + "pin": "PIN-koodi" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/translations/hu.json b/homeassistant/components/homematicip_cloud/translations/hu.json index 90fee286a3a..2915d442a37 100644 --- a/homeassistant/components/homematicip_cloud/translations/hu.json +++ b/homeassistant/components/homematicip_cloud/translations/hu.json @@ -18,7 +18,7 @@ "name": "N\u00e9v (opcion\u00e1lis, minden eszk\u00f6z n\u00e9vel\u0151tagjak\u00e9nt haszn\u00e1latos)", "pin": "PIN-k\u00f3d" }, - "title": "V\u00e1lassz HomematicIP hozz\u00e1f\u00e9r\u00e9si pontot" + "title": "V\u00e1lasszon HomematicIP hozz\u00e1f\u00e9r\u00e9si pontot" }, "link": { "description": "A HomematicIP regisztr\u00e1l\u00e1s\u00e1hoz a Home Assistant alkalmaz\u00e1sban nyomja meg a hozz\u00e1f\u00e9r\u00e9si pont k\u00e9k gombj\u00e1t \u00e9s a bek\u00fcld\u00e9s gombot. \n\n ! [A gomb helye a h\u00eddon] (/ static / images / config_flows / config_homematicip_cloud.png)", diff --git a/homeassistant/components/honeywell/translations/es.json b/homeassistant/components/honeywell/translations/es.json index 70549039a04..9f6c562e888 100644 --- a/homeassistant/components/honeywell/translations/es.json +++ b/homeassistant/components/honeywell/translations/es.json @@ -1,9 +1,13 @@ { "config": { + "error": { + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + }, "step": { "user": { "data": { - "password": "Contrase\u00f1a" + "password": "Contrase\u00f1a", + "username": "Usuario" }, "description": "Por favor, introduzca las credenciales utilizadas para iniciar sesi\u00f3n en mytotalconnectcomfort.com.", "title": "Honeywell Total Connect Comfort (US)" diff --git a/homeassistant/components/honeywell/translations/id.json b/homeassistant/components/honeywell/translations/id.json new file mode 100644 index 00000000000..ee1540cc787 --- /dev/null +++ b/homeassistant/components/honeywell/translations/id.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "invalid_auth": "Autentikasi tidak valid" + }, + "step": { + "user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/translations/hu.json b/homeassistant/components/huawei_lte/translations/hu.json index 22bd37c37ba..91f70a17e46 100644 --- a/homeassistant/components/huawei_lte/translations/hu.json +++ b/homeassistant/components/huawei_lte/translations/hu.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", "not_huawei_lte": "Nem Huawei LTE eszk\u00f6z" }, "error": { - "connection_timeout": "Kapcsolat id\u0151t\u00fall\u00e9p\u00e9se", + "connection_timeout": "Kapcsolat id\u0151t\u00fall\u00e9p\u00e9s", "incorrect_password": "Hib\u00e1s jelsz\u00f3", "incorrect_username": "Helytelen felhaszn\u00e1l\u00f3n\u00e9v", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", diff --git a/homeassistant/components/huawei_lte/translations/id.json b/homeassistant/components/huawei_lte/translations/id.json index 2077b31ccd7..de784fd3e94 100644 --- a/homeassistant/components/huawei_lte/translations/id.json +++ b/homeassistant/components/huawei_lte/translations/id.json @@ -15,7 +15,7 @@ "response_error": "Kesalahan tidak dikenal dari perangkat", "unknown": "Kesalahan yang tidak diharapkan" }, - "flow_title": "Huawei LTE: {name}", + "flow_title": "{name}", "step": { "user": { "data": { @@ -23,7 +23,7 @@ "url": "URL", "username": "Nama Pengguna" }, - "description": "Masukkan detail akses perangkat. Menentukan nama pengguna dan kata sandi bersifat opsional, tetapi memungkinkan dukungan untuk fitur integrasi lainnya. Selain itu, penggunaan koneksi resmi dapat menyebabkan masalah mengakses antarmuka web perangkat dari luar Home Assistant saat integrasi aktif, dan sebaliknya.", + "description": "Masukkan detail akses perangkat.", "title": "Konfigurasikan Huawei LTE" } } diff --git a/homeassistant/components/hue/translations/hu.json b/homeassistant/components/hue/translations/hu.json index 30084ee9940..2f04c53163f 100644 --- a/homeassistant/components/hue/translations/hu.json +++ b/homeassistant/components/hue/translations/hu.json @@ -3,10 +3,10 @@ "abort": { "all_configured": "M\u00e1r minden Philips Hue bridge konfigur\u00e1lt", "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "discover_timeout": "Nem tal\u00e1ltam a Hue bridget", - "no_bridges": "Nem tal\u00e1ltam Philips Hue bridget", + "discover_timeout": "Nem tal\u00e1lhat\u00f3 a Hue bridge", + "no_bridges": "Nem tal\u00e1lhat\u00f3 Philips Hue bridget", "not_hue_bridge": "Nem egy Hue Bridge", "unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt" }, @@ -17,9 +17,9 @@ "step": { "init": { "data": { - "host": "Hoszt" + "host": "C\u00edm" }, - "title": "V\u00e1lassz Hue bridge-t" + "title": "V\u00e1lasszon Hue bridge-t" }, "link": { "description": "Nyomja meg a gombot a bridge-en a Philips Hue Home Assistant-ben val\u00f3 regisztr\u00e1l\u00e1s\u00e1hoz.\n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)", @@ -27,7 +27,7 @@ }, "manual": { "data": { - "host": "Hoszt" + "host": "C\u00edm" }, "title": "A Hue bridge manu\u00e1lis konfigur\u00e1l\u00e1sa" } diff --git a/homeassistant/components/hunterdouglas_powerview/translations/hu.json b/homeassistant/components/hunterdouglas_powerview/translations/hu.json index 1fedd8bc126..e6afd8a1dc4 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/hu.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/hu.json @@ -10,7 +10,7 @@ "flow_title": "{name} ({host})", "step": { "link": { - "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}({host})-t?", + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name} ({host})?", "title": "Csatlakozzon a PowerView Hubhoz" }, "user": { diff --git a/homeassistant/components/hvv_departures/translations/hu.json b/homeassistant/components/hvv_departures/translations/hu.json index dfbdd92f27a..41113527ecb 100644 --- a/homeassistant/components/hvv_departures/translations/hu.json +++ b/homeassistant/components/hvv_departures/translations/hu.json @@ -23,7 +23,7 @@ }, "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, diff --git a/homeassistant/components/hyperion/translations/hu.json b/homeassistant/components/hyperion/translations/hu.json index 852c108c0e9..3fa440c41d5 100644 --- a/homeassistant/components/hyperion/translations/hu.json +++ b/homeassistant/components/hyperion/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", "auth_new_token_not_granted_error": "Az \u00fajonnan l\u00e9trehozott tokent nem hagyt\u00e1k j\u00f3v\u00e1 a Hyperion felhaszn\u00e1l\u00f3i fel\u00fclet\u00e9n", "auth_new_token_not_work_error": "Nem siker\u00fclt hiteles\u00edteni az \u00fajonnan l\u00e9trehozott token haszn\u00e1lat\u00e1val", "auth_required_error": "Nem siker\u00fclt meghat\u00e1rozni, hogy sz\u00fcks\u00e9ges-e enged\u00e9ly", @@ -23,11 +23,11 @@ "description": "Konfigur\u00e1lja a jogosults\u00e1got a Hyperion Ambilight kiszolg\u00e1l\u00f3hoz" }, "confirm": { - "description": "Hozz\u00e1 szeretn\u00e9 adni ezt a Hyperion Ambilight-ot az Otthoni asszisztenshez? \n\n ** Host: ** {host}\n ** Port: ** {port}\n ** azonos\u00edt\u00f3 **: {id}", + "description": "Hozz\u00e1 szeretn\u00e9 adni ezt a Hyperion Ambilight-ot az Otthoni asszisztenshez? \n\n ** C\u00edm: ** {host}\n ** Port: ** {port}\n ** Azonos\u00edt\u00f3 **: {id}", "title": "Er\u0151s\u00edtse meg a Hyperion Ambilight szolg\u00e1ltat\u00e1s hozz\u00e1ad\u00e1s\u00e1t" }, "create_token": { - "description": "Az al\u00e1bbiakban v\u00e1lassza a ** K\u00fcld\u00e9s ** lehet\u0151s\u00e9get \u00faj hiteles\u00edt\u00e9si token k\u00e9r\u00e9s\u00e9hez. A k\u00e9relem j\u00f3v\u00e1hagy\u00e1s\u00e1hoz \u00e1tir\u00e1ny\u00edtunk a Hyperion felhaszn\u00e1l\u00f3i fel\u00fcletre. K\u00e9rj\u00fck, ellen\u0151rizze, hogy a megjelen\u00edtett azonos\u00edt\u00f3 \" {auth_id} \"", + "description": "Az al\u00e1bbiakban v\u00e1lassza a **K\u00fcld\u00e9s** lehet\u0151s\u00e9get \u00faj hiteles\u00edt\u00e9si token k\u00e9r\u00e9s\u00e9hez. A k\u00e9relem j\u00f3v\u00e1hagy\u00e1s\u00e1hoz \u00e1tir\u00e1ny\u00edtunk a Hyperion felhaszn\u00e1l\u00f3i fel\u00fcletre. K\u00e9rj\u00fck, ellen\u0151rizze, hogy a megjelen\u00edtett azonos\u00edt\u00f3 \"{auth_id}\"", "title": "\u00daj hiteles\u00edt\u00e9si token automatikus l\u00e9trehoz\u00e1sa" }, "create_token_external": { @@ -35,7 +35,7 @@ }, "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "port": "Port" } } diff --git a/homeassistant/components/ialarm/translations/hu.json b/homeassistant/components/ialarm/translations/hu.json index e69c6e7e7ea..a98836bb7b7 100644 --- a/homeassistant/components/ialarm/translations/hu.json +++ b/homeassistant/components/ialarm/translations/hu.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "Gazdag\u00e9p", + "host": "C\u00edm", "port": "Port" } } diff --git a/homeassistant/components/iaqualink/translations/hu.json b/homeassistant/components/iaqualink/translations/hu.json index 1ca85c41190..2b0b9ac3e67 100644 --- a/homeassistant/components/iaqualink/translations/hu.json +++ b/homeassistant/components/iaqualink/translations/hu.json @@ -12,7 +12,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "K\u00e9rj\u00fck, adja meg iAqualink-fi\u00f3kja felhaszn\u00e1l\u00f3nev\u00e9t \u00e9s jelszav\u00e1t.", + "description": "K\u00e9rj\u00fck, adja meg iAqualink-fi\u00f3kj\u00e1nak felhaszn\u00e1l\u00f3nev\u00e9t \u00e9s jelszav\u00e1t.", "title": "Csatlakoz\u00e1s az iAqualinkhez" } } diff --git a/homeassistant/components/icloud/translations/ca.json b/homeassistant/components/icloud/translations/ca.json index 6e92897161a..0ffdf5bc0c1 100644 --- a/homeassistant/components/icloud/translations/ca.json +++ b/homeassistant/components/icloud/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat", + "already_configured": "El compte ja est\u00e0 configurat", "no_device": "Cap dels teus dispositius t\u00e9 activada la opci\u00f3 \"Troba el meu iPhone\"", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, diff --git a/homeassistant/components/icloud/translations/hu.json b/homeassistant/components/icloud/translations/hu.json index 722b3711e67..e858eedb757 100644 --- a/homeassistant/components/icloud/translations/hu.json +++ b/homeassistant/components/icloud/translations/hu.json @@ -8,7 +8,7 @@ "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "send_verification_code": "Nem siker\u00fclt elk\u00fcldeni az ellen\u0151rz\u0151 k\u00f3dot", - "validate_verification_code": "Nem siker\u00fclt ellen\u0151rizni az ellen\u0151rz\u0151 k\u00f3dot, ki kell v\u00e1lasztania egy megb\u00edzhat\u00f3s\u00e1gi eszk\u00f6zt, \u00e9s \u00fajra kell ind\u00edtania az ellen\u0151rz\u00e9st" + "validate_verification_code": "Nem siker\u00fclt hiteles\u00edteni az ellen\u0151rz\u0151 k\u00f3dot, k\u00e9rem, pr\u00f3b\u00e1lja meg \u00fajra" }, "step": { "reauth": { diff --git a/homeassistant/components/ifttt/translations/hu.json b/homeassistant/components/ifttt/translations/hu.json index 9898beb3e92..2f64056e985 100644 --- a/homeassistant/components/ifttt/translations/hu.json +++ b/homeassistant/components/ifttt/translations/hu.json @@ -2,14 +2,14 @@ "config": { "abort": { "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", - "webhook_not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." + "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, "create_entry": { - "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, akkor az [IFTTT Webhook applet]({applet_url}) \"Make a web request\" m\u0171velet\u00e9t kell haszn\u00e1lnia. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/json \n\n L\u00e1sd [a dokument\u00e1ci\u00f3]({docs_url}), hogyan konfigur\u00e1lhatja az automatiz\u00e1l\u00e1sokat a bej\u00f6v\u0151 adatok kezel\u00e9s\u00e9re." + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, akkor az [IFTTT Webhook applet]({applet_url}) \"Make a web request\" m\u0171velet\u00e9t kell haszn\u00e1lnia. \n\nT\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/json \n\nL\u00e1sd [a dokument\u00e1ci\u00f3]({docs_url}), hogyan konfigur\u00e1lhatja az automatizmusokat a bej\u00f6v\u0151 adatok kezel\u00e9s\u00e9re." }, "step": { "user": { - "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani az IFTTT-t?", + "description": "Biztos benne, hogy be szeretn\u00e9 \u00e1ll\u00edtani az IFTTT-t?", "title": "IFTTT Webhook Applet be\u00e1ll\u00edt\u00e1sa" } } diff --git a/homeassistant/components/insteon/translations/ca.json b/homeassistant/components/insteon/translations/ca.json index 63601dd8071..59c711c3dae 100644 --- a/homeassistant/components/insteon/translations/ca.json +++ b/homeassistant/components/insteon/translations/ca.json @@ -29,7 +29,7 @@ }, "plm": { "data": { - "device": "Ruta del port USB del dispositiu" + "device": "Ruta del dispositiu USB" }, "description": "Configura el m\u00f2dem Insteon PowerLink (PLM).", "title": "Insteon PLM" diff --git a/homeassistant/components/insteon/translations/hu.json b/homeassistant/components/insteon/translations/hu.json index 8444aa97655..f34307a67a4 100644 --- a/homeassistant/components/insteon/translations/hu.json +++ b/homeassistant/components/insteon/translations/hu.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "select_single": "V\u00e1lassz egy lehet\u0151s\u00e9get" + "select_single": "V\u00e1lasszon egy lehet\u0151s\u00e9get" }, "step": { "hubv1": { @@ -25,7 +25,7 @@ "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, "description": "Konfigur\u00e1lja az Insteon Hub 2. verzi\u00f3j\u00e1t.", - "title": "Insteon Hub 2. verzi\u00f3" + "title": "Insteon Hub Version 2" }, "plm": { "data": { @@ -38,7 +38,7 @@ "data": { "modem_type": "Modem t\u00edpusa." }, - "description": "V\u00e1laszd ki az Insteon modem t\u00edpus\u00e1t.", + "description": "V\u00e1lassza ki az Insteon modem t\u00edpus\u00e1t.", "title": "Insteon" } } @@ -47,14 +47,14 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "input_error": "\u00c9rv\u00e9nytelen bejegyz\u00e9sek, ellen\u0151rizze \u00e9rt\u00e9keket.", - "select_single": "V\u00e1lassz egy lehet\u0151s\u00e9get" + "select_single": "V\u00e1lasszon egy lehet\u0151s\u00e9get" }, "step": { "add_override": { "data": { - "address": "Eszk\u00f6z c\u00edme (azaz 1a2b3c)", - "cat": "Eszk\u00f6zkateg\u00f3ria (azaz 0x10)", - "subcat": "Eszk\u00f6z alkateg\u00f3ria (azaz 0x0a)" + "address": "Eszk\u00f6z c\u00edme (pl. 1a2b3c)", + "cat": "Eszk\u00f6zkateg\u00f3ria (pl. 0x10)", + "subcat": "Eszk\u00f6z alkateg\u00f3ria (pl. 0x0a)" }, "description": "Eszk\u00f6z-fel\u00fclb\u00edr\u00e1l\u00e1s hozz\u00e1ad\u00e1sa.", "title": "Insteon" diff --git a/homeassistant/components/ios/translations/hu.json b/homeassistant/components/ios/translations/hu.json index dda7af8c541..06a80cc8c5e 100644 --- a/homeassistant/components/ios/translations/hu.json +++ b/homeassistant/components/ios/translations/hu.json @@ -5,7 +5,7 @@ }, "step": { "confirm": { - "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?" + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" } } } diff --git a/homeassistant/components/ios/translations/nl.json b/homeassistant/components/ios/translations/nl.json index 78757f9f715..1e660ec2f5d 100644 --- a/homeassistant/components/ios/translations/nl.json +++ b/homeassistant/components/ios/translations/nl.json @@ -5,7 +5,7 @@ }, "step": { "confirm": { - "description": "Wil je beginnen met instellen?" + "description": "Wilt u beginnen met instellen?" } } } diff --git a/homeassistant/components/iotawatt/translations/el.json b/homeassistant/components/iotawatt/translations/el.json index 0030674e3ca..44996764873 100644 --- a/homeassistant/components/iotawatt/translations/el.json +++ b/homeassistant/components/iotawatt/translations/el.json @@ -9,7 +9,8 @@ "data": { "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2", "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" - } + }, + "description": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae IoTawatt \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2. \u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03ba\u03b1\u03b9 \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03a5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae." } } } diff --git a/homeassistant/components/iotawatt/translations/es.json b/homeassistant/components/iotawatt/translations/es.json index 07540d160bb..00c04d7771f 100644 --- a/homeassistant/components/iotawatt/translations/es.json +++ b/homeassistant/components/iotawatt/translations/es.json @@ -10,7 +10,8 @@ "data": { "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." }, "user": { "data": { diff --git a/homeassistant/components/iotawatt/translations/hu.json b/homeassistant/components/iotawatt/translations/hu.json index 1c545b3d3ce..52d46f97a84 100644 --- a/homeassistant/components/iotawatt/translations/hu.json +++ b/homeassistant/components/iotawatt/translations/hu.json @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "Gazdag\u00e9p" + "host": "C\u00edm" } } } diff --git a/homeassistant/components/iotawatt/translations/id.json b/homeassistant/components/iotawatt/translations/id.json new file mode 100644 index 00000000000..a48af7cd34d --- /dev/null +++ b/homeassistant/components/iotawatt/translations/id.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "auth": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iotawatt/translations/nl.json b/homeassistant/components/iotawatt/translations/nl.json index 3d1e6d3ef17..617073e91c0 100644 --- a/homeassistant/components/iotawatt/translations/nl.json +++ b/homeassistant/components/iotawatt/translations/nl.json @@ -1,7 +1,16 @@ { "config": { + "error": { + "cannot_connect": "Kon niet verbinden", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, "step": { "auth": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, "description": "Het IoTawatt-apparaat vereist authenticatie. Voer de gebruikersnaam en het wachtwoord in en klik op de knop Verzenden." }, "user": { diff --git a/homeassistant/components/ipp/translations/hu.json b/homeassistant/components/ipp/translations/hu.json index a024cfb2e56..18381fde2cf 100644 --- a/homeassistant/components/ipp/translations/hu.json +++ b/homeassistant/components/ipp/translations/hu.json @@ -13,21 +13,21 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s", "connection_upgrade": "Nem siker\u00fclt csatlakozni a nyomtat\u00f3hoz. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra az SSL/TLS opci\u00f3 bejel\u00f6l\u00e9s\u00e9vel." }, - "flow_title": "Nyomtat\u00f3: {name}", + "flow_title": "{name}", "step": { "user": { "data": { "base_path": "Relat\u00edv \u00fatvonal a nyomtat\u00f3hoz", - "host": "Hoszt", + "host": "C\u00edm", "port": "Port", "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" }, - "description": "\u00c1ll\u00edtsa be a nyomtat\u00f3t az Internet Printing Protocol (IPP) protokollon kereszt\u00fcl, hogy integr\u00e1lhat\u00f3 legyen a Home Assistant seg\u00edts\u00e9g\u00e9vel.", + "description": "\u00c1ll\u00edtsa be a nyomtat\u00f3t az Internet Printing Protocol (IPP) protokollon kereszt\u00fcl, hogy integr\u00e1lhat\u00f3 legyen Home Assistant seg\u00edts\u00e9g\u00e9vel.", "title": "Kapcsolja \u00f6ssze a nyomtat\u00f3t" }, "zeroconf_confirm": { - "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}-t?", + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?", "title": "Felfedezett nyomtat\u00f3" } } diff --git a/homeassistant/components/ipp/translations/id.json b/homeassistant/components/ipp/translations/id.json index c2b95751d4b..f65b853d671 100644 --- a/homeassistant/components/ipp/translations/id.json +++ b/homeassistant/components/ipp/translations/id.json @@ -13,7 +13,7 @@ "cannot_connect": "Gagal terhubung", "connection_upgrade": "Gagal terhubung ke printer. Coba lagi dengan mencentang opsi SSL/TLS." }, - "flow_title": "Printer: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/isy994/translations/es.json b/homeassistant/components/isy994/translations/es.json index 46dc3260f83..324b94e9938 100644 --- a/homeassistant/components/isy994/translations/es.json +++ b/homeassistant/components/isy994/translations/es.json @@ -9,7 +9,7 @@ "invalid_host": "La entrada del host no estaba en formato URL completo, por ejemplo, http://192.168.10.100:80", "unknown": "Error inesperado" }, - "flow_title": "Dispositivos Universales ISY994 {nombre} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/isy994/translations/hu.json b/homeassistant/components/isy994/translations/hu.json index dab85300e6d..d9cce2fefcb 100644 --- a/homeassistant/components/isy994/translations/hu.json +++ b/homeassistant/components/isy994/translations/hu.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "invalid_host": "A gazdag\u00e9p bejegyz\u00e9se nem volt teljes URL-form\u00e1tumban, p\u00e9ld\u00e1ul: http://192.168.10.100:80", + "invalid_host": "A c\u00edm bejegyz\u00e9se nem volt teljes URL-form\u00e1tumban, p\u00e9ld\u00e1ul: http://192.168.10.100:80", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "flow_title": "{name} ({host})", @@ -18,7 +18,7 @@ "tls": "Az ISY vez\u00e9rl\u0151 TLS verzi\u00f3ja.", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "A gazdag\u00e9p bejegyz\u00e9s\u00e9nek teljes URL form\u00e1tumban kell lennie, pl. Http://192.168.10.100:80", + "description": "A c\u00edm bejegyz\u00e9s\u00e9nek teljes URL form\u00e1tumban kell lennie, pl. Http://192.168.10.100:80", "title": "Csatlakozzon az ISY994-hez" } } @@ -40,7 +40,7 @@ "system_health": { "info": { "device_connected": "ISY csatlakozik", - "host_reachable": "El\u00e9rhet\u0151 gazdag\u00e9p", + "host_reachable": "C\u00edm el\u00e9rhet\u0151", "last_heartbeat": "Utols\u00f3 sz\u00edvver\u00e9s ideje", "websocket_status": "Esem\u00e9nySocket \u00e1llapota" } diff --git a/homeassistant/components/isy994/translations/id.json b/homeassistant/components/isy994/translations/id.json index fec6d1090b0..099e3607d1e 100644 --- a/homeassistant/components/isy994/translations/id.json +++ b/homeassistant/components/isy994/translations/id.json @@ -9,7 +9,7 @@ "invalid_host": "Entri host tidak dalam format URL lengkap, misalnya, http://192.168.10.100:80", "unknown": "Kesalahan yang tidak diharapkan" }, - "flow_title": "Universal Devices ISY994 {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/juicenet/translations/ca.json b/homeassistant/components/juicenet/translations/ca.json index f5df6921062..01a3a0bcae4 100644 --- a/homeassistant/components/juicenet/translations/ca.json +++ b/homeassistant/components/juicenet/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat" + "already_configured": "El compte ja est\u00e0 configurat" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", diff --git a/homeassistant/components/keenetic_ndms2/translations/ca.json b/homeassistant/components/keenetic_ndms2/translations/ca.json index 0acb0ef0266..748a55885e4 100644 --- a/homeassistant/components/keenetic_ndms2/translations/ca.json +++ b/homeassistant/components/keenetic_ndms2/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat", + "already_configured": "El compte ja est\u00e0 configurat", "no_udn": "La informaci\u00f3 de descobriment SSDP no t\u00e9 UDN", "not_keenetic_ndms2": "El dispositiu descobert no \u00e9s un router Keenetic" }, diff --git a/homeassistant/components/keenetic_ndms2/translations/hu.json b/homeassistant/components/keenetic_ndms2/translations/hu.json index c2327130a11..2575d832863 100644 --- a/homeassistant/components/keenetic_ndms2/translations/hu.json +++ b/homeassistant/components/keenetic_ndms2/translations/hu.json @@ -12,7 +12,7 @@ "step": { "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "password": "Jelsz\u00f3", "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" diff --git a/homeassistant/components/keenetic_ndms2/translations/id.json b/homeassistant/components/keenetic_ndms2/translations/id.json index bb30e715579..900745bc29e 100644 --- a/homeassistant/components/keenetic_ndms2/translations/id.json +++ b/homeassistant/components/keenetic_ndms2/translations/id.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Gagal terhubung" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/keenetic_ndms2/translations/ru.json b/homeassistant/components/keenetic_ndms2/translations/ru.json index 3c7eed4be01..810c2bfff05 100644 --- a/homeassistant/components/keenetic_ndms2/translations/ru.json +++ b/homeassistant/components/keenetic_ndms2/translations/ru.json @@ -25,7 +25,7 @@ "step": { "user": { "data": { - "consider_home": "\u0412\u0440\u0435\u043c\u044f \u043e\u0442 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0433\u043e \u0441\u0435\u0430\u043d\u0441\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c (\u0441\u0435\u043a.), \u043f\u043e \u0438\u0441\u0442\u0435\u0447\u0435\u043d\u0438\u044e \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u0442\u0430\u0442\u0443\u0441 \"\u041d\u0435 \u0434\u043e\u043c\u0430\"", + "consider_home": "\u0412\u0440\u0435\u043c\u044f, \u0432 \u0442\u0435\u0447\u0435\u043d\u0438\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0441\u0447\u0438\u0442\u0430\u0442\u044c \u0447\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043e\u043c\u0430 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", "include_arp": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435 ARP (\u0438\u0433\u043d\u043e\u0440\u0438\u0440\u0443\u044e\u0442\u0441\u044f, \u0435\u0441\u043b\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442\u0441\u044f \u0434\u0430\u043d\u043d\u044b\u0435 \u0442\u043e\u0447\u043a\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430)", "include_associated": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435 \u0442\u043e\u0447\u0435\u043a \u0434\u043e\u0441\u0442\u0443\u043f\u0430 WiFi (\u0438\u0433\u043d\u043e\u0440\u0438\u0440\u0443\u044e\u0442\u0441\u044f, \u0435\u0441\u043b\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442\u0441\u044f \u0434\u0430\u043d\u043d\u044b\u0435 hotspot)", "interfaces": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u044b \u0434\u043b\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f", diff --git a/homeassistant/components/kmtronic/translations/hu.json b/homeassistant/components/kmtronic/translations/hu.json index 4fe9a3875e6..3ea79e3bd89 100644 --- a/homeassistant/components/kmtronic/translations/hu.json +++ b/homeassistant/components/kmtronic/translations/hu.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } diff --git a/homeassistant/components/kodi/translations/hu.json b/homeassistant/components/kodi/translations/hu.json index 9ae1e0741d5..017d33010ac 100644 --- a/homeassistant/components/kodi/translations/hu.json +++ b/homeassistant/components/kodi/translations/hu.json @@ -19,15 +19,15 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "Add meg a Kodi felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t. Ezek megtal\u00e1lhat\u00f3k a Rendszer/Be\u00e1ll\u00edt\u00e1sok/H\u00e1l\u00f3zat/Szolg\u00e1ltat\u00e1sok r\u00e9szben." + "description": "Adja meg a Kodi felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t. Ezek megtal\u00e1lhat\u00f3k a Rendszer/Be\u00e1ll\u00edt\u00e1sok/H\u00e1l\u00f3zat/Szolg\u00e1ltat\u00e1sok r\u00e9szben." }, "discovery_confirm": { - "description": "Szeretn\u00e9d hozz\u00e1adni a Kodi (`{name}`)-t a Home Assistant-hoz?", + "description": "Szeretn\u00e9 hozz\u00e1adni a Kodi (`{name}`)-t Home Assistant-hoz?", "title": "Felfedezett Kodi" }, "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "port": "Port", "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata" }, diff --git a/homeassistant/components/kodi/translations/id.json b/homeassistant/components/kodi/translations/id.json index 1a81ab72fab..16ce1e2c43b 100644 --- a/homeassistant/components/kodi/translations/id.json +++ b/homeassistant/components/kodi/translations/id.json @@ -12,7 +12,7 @@ "invalid_auth": "Autentikasi tidak valid", "unknown": "Kesalahan yang tidak diharapkan" }, - "flow_title": "Kodi: {name}", + "flow_title": "{name}", "step": { "credentials": { "data": { diff --git a/homeassistant/components/konnected/translations/hu.json b/homeassistant/components/konnected/translations/hu.json index 1ad58223b88..f5431480ebb 100644 --- a/homeassistant/components/konnected/translations/hu.json +++ b/homeassistant/components/konnected/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", "not_konn_panel": "Nem felismert Konnected.io eszk\u00f6z", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, @@ -11,7 +11,7 @@ }, "step": { "confirm": { - "description": "Modell: {model}\nAzonos\u00edt\u00f3: {id}\nGazdag\u00e9p: {host}\nPort: {port} \n\n Az IO \u00e9s a panel viselked\u00e9s\u00e9t a Konnected Alarm Panel be\u00e1ll\u00edt\u00e1saiban konfigur\u00e1lhatja.", + "description": "Modell: {model}\nAzonos\u00edt\u00f3: {id}\nC\u00edm: {host}\nPort: {port} \n\nAz IO \u00e9s a panel viselked\u00e9s\u00e9t a Konnected Alarm Panel be\u00e1ll\u00edt\u00e1saiban konfigur\u00e1lhatja.", "title": "Konnected eszk\u00f6z k\u00e9sz" }, "import_confirm": { @@ -23,7 +23,7 @@ "host": "IP c\u00edm", "port": "Port" }, - "description": "K\u00e9rj\u00fck, adja meg a Konnected Panel gazdag\u00e9p\u00e9nek adatait." + "description": "K\u00e9rj\u00fck, adja meg a Konnected Panel csatlakoz\u00e1si adatait." } } }, diff --git a/homeassistant/components/konnected/translations/id.json b/homeassistant/components/konnected/translations/id.json index 633e6bba2df..b80b86c25c9 100644 --- a/homeassistant/components/konnected/translations/id.json +++ b/homeassistant/components/konnected/translations/id.json @@ -78,7 +78,7 @@ "alarm2_out2": "OUT2/ALARM2", "out1": "OUT1" }, - "description": "Pilih konfigurasi I/O lainnya di bawah ini. Anda dapat mengonfigurasi detail opsi pada langkah berikutnya.", + "description": "Pilih konfigurasi I/O lainnya di bawah ini. Anda dapat mengonfigurasi detail opsi pada langkah berikutnya.", "title": "Konfigurasikan I/O yang Diperluas" }, "options_misc": { diff --git a/homeassistant/components/kostal_plenticore/translations/hu.json b/homeassistant/components/kostal_plenticore/translations/hu.json index b235578e9c3..3ffe413a82b 100644 --- a/homeassistant/components/kostal_plenticore/translations/hu.json +++ b/homeassistant/components/kostal_plenticore/translations/hu.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Gazdag\u00e9p", + "host": "C\u00edm", "password": "Jelsz\u00f3" } } diff --git a/homeassistant/components/kraken/translations/es.json b/homeassistant/components/kraken/translations/es.json index 1befa14a52b..86df8397c15 100644 --- a/homeassistant/components/kraken/translations/es.json +++ b/homeassistant/components/kraken/translations/es.json @@ -1,11 +1,15 @@ { "config": { + "abort": { + "already_configured": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." + }, "step": { "user": { "data": { "one": "", "other": "Otros" - } + }, + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" } } }, diff --git a/homeassistant/components/kraken/translations/hu.json b/homeassistant/components/kraken/translations/hu.json index 793a3433eb8..6ea1c832188 100644 --- a/homeassistant/components/kraken/translations/hu.json +++ b/homeassistant/components/kraken/translations/hu.json @@ -13,7 +13,7 @@ "one": "\u00dcres", "other": "\u00dcres" }, - "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?" + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" } } }, diff --git a/homeassistant/components/kraken/translations/id.json b/homeassistant/components/kraken/translations/id.json new file mode 100644 index 00000000000..a436ac4aee5 --- /dev/null +++ b/homeassistant/components/kraken/translations/id.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "step": { + "user": { + "description": "Ingin memulai penyiapan?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kraken/translations/nl.json b/homeassistant/components/kraken/translations/nl.json index 25fe63bebd5..09b93b205e3 100644 --- a/homeassistant/components/kraken/translations/nl.json +++ b/homeassistant/components/kraken/translations/nl.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Wil je beginnen met instellen?" + "description": "Wilt u beginnen met instellen?" } } }, diff --git a/homeassistant/components/kulersky/translations/hu.json b/homeassistant/components/kulersky/translations/hu.json index 6c61530acbe..a56ebbfc906 100644 --- a/homeassistant/components/kulersky/translations/hu.json +++ b/homeassistant/components/kulersky/translations/hu.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?" + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" } } } diff --git a/homeassistant/components/kulersky/translations/nl.json b/homeassistant/components/kulersky/translations/nl.json index d11896014fd..0671f0b3674 100644 --- a/homeassistant/components/kulersky/translations/nl.json +++ b/homeassistant/components/kulersky/translations/nl.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Wil je beginnen met instellen?" + "description": "Wilt u beginnen met instellen?" } } } diff --git a/homeassistant/components/life360/translations/ca.json b/homeassistant/components/life360/translations/ca.json index cf57e4e1d2f..875692a661a 100644 --- a/homeassistant/components/life360/translations/ca.json +++ b/homeassistant/components/life360/translations/ca.json @@ -8,7 +8,7 @@ "default": "Per configurar les opcions avan\u00e7ades mira la [documentaci\u00f3 de Life360]({docs_url})." }, "error": { - "already_configured": "El compte ja ha estat configurat", + "already_configured": "El compte ja est\u00e0 configurat", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "invalid_username": "Nom d'usuari incorrecte", "unknown": "Error inesperat" diff --git a/homeassistant/components/lifx/translations/hu.json b/homeassistant/components/lifx/translations/hu.json index f706dcefa96..3d728f21d07 100644 --- a/homeassistant/components/lifx/translations/hu.json +++ b/homeassistant/components/lifx/translations/hu.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a LIFX-t?" + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: LIFX?" } } } diff --git a/homeassistant/components/litterrobot/translations/ca.json b/homeassistant/components/litterrobot/translations/ca.json index b7ca6053fbc..5165473860a 100644 --- a/homeassistant/components/litterrobot/translations/ca.json +++ b/homeassistant/components/litterrobot/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat" + "already_configured": "El compte ja est\u00e0 configurat" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", diff --git a/homeassistant/components/litterrobot/translations/hu.json b/homeassistant/components/litterrobot/translations/hu.json index fd8db27da5e..cc0c820facf 100644 --- a/homeassistant/components/litterrobot/translations/hu.json +++ b/homeassistant/components/litterrobot/translations/hu.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", diff --git a/homeassistant/components/local_ip/translations/hu.json b/homeassistant/components/local_ip/translations/hu.json index e930d58784a..cfb92ddb7b6 100644 --- a/homeassistant/components/local_ip/translations/hu.json +++ b/homeassistant/components/local_ip/translations/hu.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?", + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?", "title": "Helyi IP c\u00edm" } } diff --git a/homeassistant/components/local_ip/translations/nl.json b/homeassistant/components/local_ip/translations/nl.json index 3ea8140a96e..4b2672d2a3b 100644 --- a/homeassistant/components/local_ip/translations/nl.json +++ b/homeassistant/components/local_ip/translations/nl.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "Wil je beginnen met instellen?", + "description": "Wilt u beginnen met instellen?", "title": "Lokaal IP-adres" } } diff --git a/homeassistant/components/locative/translations/hu.json b/homeassistant/components/locative/translations/hu.json index 8dc03e9c37a..893e22f1471 100644 --- a/homeassistant/components/locative/translations/hu.json +++ b/homeassistant/components/locative/translations/hu.json @@ -2,14 +2,14 @@ "config": { "abort": { "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", - "webhook_not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." + "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, "create_entry": { "default": "Ha helyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a webhook funkci\u00f3t a Locative alkalmaz\u00e1sban. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - M\u00f3dszer: POST \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t]({docs_url})." }, "step": { "user": { - "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?", + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?", "title": "Locative Webhook be\u00e1ll\u00edt\u00e1sa" } } diff --git a/homeassistant/components/locative/translations/nl.json b/homeassistant/components/locative/translations/nl.json index d66a1262b5d..ed39d00430b 100644 --- a/homeassistant/components/locative/translations/nl.json +++ b/homeassistant/components/locative/translations/nl.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Wil je beginnen met instellen?", + "description": "Wilt u beginnen met instellen?", "title": "Stel de Locative Webhook in" } } diff --git a/homeassistant/components/logi_circle/translations/ca.json b/homeassistant/components/logi_circle/translations/ca.json index 9f46b3f621a..da66dbf55dd 100644 --- a/homeassistant/components/logi_circle/translations/ca.json +++ b/homeassistant/components/logi_circle/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat", + "already_configured": "El compte ja est\u00e0 configurat", "external_error": "S'ha produ\u00eft una excepci\u00f3 d'un altre flux de dades.", "external_setup": "Logi Circle s'ha configurat correctament des d'un altre flux de dades.", "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3." diff --git a/homeassistant/components/logi_circle/translations/hu.json b/homeassistant/components/logi_circle/translations/hu.json index 73522a59519..f79ab3944dc 100644 --- a/homeassistant/components/logi_circle/translations/hu.json +++ b/homeassistant/components/logi_circle/translations/hu.json @@ -4,16 +4,16 @@ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", "external_error": "Kiv\u00e9tel t\u00f6rt\u00e9nt egy m\u00e1sik folyamatb\u00f3l.", "external_setup": "LogiCircle sikeresen konfigur\u00e1lva egy m\u00e1sik folyamatb\u00f3l.", - "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t." + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t." }, "error": { "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", - "follow_link": "K\u00e9rlek, k\u00f6vesd a hivatkoz\u00e1st \u00e9s hiteles\u00edtsd magad miel\u0151tt megnyomod a K\u00fcld\u00e9s gombot", + "follow_link": "K\u00e9rem, k\u00f6vesse a hivatkoz\u00e1st \u00e9s hiteles\u00edtse mag\u00e1t miel\u0151tt megnyomn\u00e1 a K\u00fcld\u00e9s gombot", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "step": { "auth": { - "description": "K\u00e9rj\u00fck, k\u00f6vesse az al\u00e1bbi linket, \u00e9s ** Fogadja el ** a LogiCircle -fi\u00f3kj\u00e1hoz val\u00f3 hozz\u00e1f\u00e9r\u00e9st, majd t\u00e9rjen vissza, \u00e9s nyomja meg az al\u00e1bbi ** K\u00fcld\u00e9s ** gombot. \n\n [Link] ({authorization_url})", + "description": "K\u00e9rj\u00fck, k\u00f6vesse az al\u00e1bbi linket, \u00e9s ** Fogadja el ** a LogiCircle -fi\u00f3kj\u00e1hoz val\u00f3 hozz\u00e1f\u00e9r\u00e9st, majd t\u00e9rjen vissza, \u00e9s nyomja meg az al\u00e1bbi ** K\u00fcld\u00e9s ** gombot. \n\n [Link]({authorization_url})", "title": "Hiteles\u00edt\u00e9s a LogiCircle seg\u00edts\u00e9g\u00e9vel" }, "user": { diff --git a/homeassistant/components/lutron_caseta/translations/es.json b/homeassistant/components/lutron_caseta/translations/es.json index 9dbedba1457..098a90377d8 100644 --- a/homeassistant/components/lutron_caseta/translations/es.json +++ b/homeassistant/components/lutron_caseta/translations/es.json @@ -69,8 +69,8 @@ "stop_all": "Detener todo" }, "trigger_type": { - "press": "\"{subtipo}\" presionado", - "release": "\"{subtipo}\" liberado" + "press": "\"{subtype}\" presionado", + "release": "\"{subtype}\" liberado" } } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/hu.json b/homeassistant/components/lutron_caseta/translations/hu.json index 0e8960530e3..f3fca2ff705 100644 --- a/homeassistant/components/lutron_caseta/translations/hu.json +++ b/homeassistant/components/lutron_caseta/translations/hu.json @@ -15,12 +15,12 @@ "title": "Nem siker\u00fclt import\u00e1lni a Cas\u00e9ta h\u00edd konfigur\u00e1ci\u00f3j\u00e1t." }, "link": { - "description": "A(z) {name} {host} p\u00e1ros\u00edt\u00e1s\u00e1hoz az \u0171rlap elk\u00fcld\u00e9se ut\u00e1n nyomja meg a h\u00edd h\u00e1tulj\u00e1n tal\u00e1lhat\u00f3 fekete gombot.", + "description": "A(z) {name} ({host}) p\u00e1ros\u00edt\u00e1s\u00e1hoz az \u0171rlap elk\u00fcld\u00e9se ut\u00e1n nyomja meg a h\u00edd h\u00e1tulj\u00e1n tal\u00e1lhat\u00f3 fekete gombot.", "title": "P\u00e1ros\u00edtsd a h\u00edddal" }, "user": { "data": { - "host": "Hoszt" + "host": "C\u00edm" }, "description": "Add meg az eszk\u00f6z IP-c\u00edm\u00e9t.", "title": "Automatikus csatlakoz\u00e1s a h\u00eddhoz" diff --git a/homeassistant/components/lutron_caseta/translations/id.json b/homeassistant/components/lutron_caseta/translations/id.json index b14e9ad1c23..409cea59060 100644 --- a/homeassistant/components/lutron_caseta/translations/id.json +++ b/homeassistant/components/lutron_caseta/translations/id.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "Gagal terhubung" }, - "flow_title": "Lutron Cas\u00e9ta {name} ({host})", + "flow_title": "{name} ({host})", "step": { "import_failed": { "description": "Tidak dapat menyiapkan bridge (host: {host} ) yang diimpor dari configuration.yaml.", diff --git a/homeassistant/components/lyric/translations/hu.json b/homeassistant/components/lyric/translations/hu.json index c6174673a90..7586310c8a7 100644 --- a/homeassistant/components/lyric/translations/hu.json +++ b/homeassistant/components/lyric/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", - "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "reauth_successful": "Az \u00fajhiteles\u00edt\u00e9s sikeres volt" }, "create_entry": { diff --git a/homeassistant/components/lyric/translations/id.json b/homeassistant/components/lyric/translations/id.json index 876fe2f8c39..f1057fc7cb2 100644 --- a/homeassistant/components/lyric/translations/id.json +++ b/homeassistant/components/lyric/translations/id.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", - "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi." + "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", + "reauth_successful": "Autentikasi ulang berhasil" }, "create_entry": { "default": "Berhasil diautentikasi" @@ -10,6 +11,9 @@ "step": { "pick_implementation": { "title": "Pilih Metode Autentikasi" + }, + "reauth_confirm": { + "title": "Autentikasi Ulang Integrasi" } } } diff --git a/homeassistant/components/mailgun/translations/hu.json b/homeassistant/components/mailgun/translations/hu.json index 14c2293734c..b40c4316bba 100644 --- a/homeassistant/components/mailgun/translations/hu.json +++ b/homeassistant/components/mailgun/translations/hu.json @@ -2,14 +2,14 @@ "config": { "abort": { "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", - "webhook_not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." + "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, "create_entry": { - "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant programnak, be kell \u00e1ll\u00edtania a [Webhooks with Mailgun]({mailgun_url}) alkalmaz\u00e1st. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/json \n\n L\u00e1sd [a dokument\u00e1ci\u00f3]({docs_url}), hogyan konfigur\u00e1lhatja az automatiz\u00e1l\u00e1sokat a bej\u00f6v\u0151 adatok kezel\u00e9s\u00e9re." + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant programnak, be kell \u00e1ll\u00edtania a [Webhooks with Mailgun]({mailgun_url}) alkalmaz\u00e1st. \n\nT\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/json \n\nL\u00e1sd [a dokument\u00e1ci\u00f3]({docs_url}), hogyan konfigur\u00e1lhatja az automatizmusokat a bej\u00f6v\u0151 adatok kezel\u00e9s\u00e9re." }, "step": { "user": { - "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani a Mailgunt?", + "description": "Biztos benne, hogy be szeretn\u00e9 \u00e1ll\u00edtani a Mailgunt?", "title": "Mailgun Webhook be\u00e1ll\u00edt\u00e1sa" } } diff --git a/homeassistant/components/mazda/translations/ca.json b/homeassistant/components/mazda/translations/ca.json index ef00713216a..17ef370b007 100644 --- a/homeassistant/components/mazda/translations/ca.json +++ b/homeassistant/components/mazda/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat", + "already_configured": "El compte ja est\u00e0 configurat", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { diff --git a/homeassistant/components/mazda/translations/hu.json b/homeassistant/components/mazda/translations/hu.json index e6b80240184..c3f00040ea3 100644 --- a/homeassistant/components/mazda/translations/hu.json +++ b/homeassistant/components/mazda/translations/hu.json @@ -5,7 +5,7 @@ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" }, "error": { - "account_locked": "Fi\u00f3k z\u00e1rolva. K\u00e9rlek, pr\u00f3b\u00e1ld \u00fajra k\u00e9s\u0151bb.", + "account_locked": "Fi\u00f3k z\u00e1rolva. K\u00e9rem, pr\u00f3b\u00e1lja \u00fajra k\u00e9s\u0151bb.", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" diff --git a/homeassistant/components/meteo_france/translations/hu.json b/homeassistant/components/meteo_france/translations/hu.json index 112f70b6ea6..8034f6d0586 100644 --- a/homeassistant/components/meteo_france/translations/hu.json +++ b/homeassistant/components/meteo_france/translations/hu.json @@ -12,7 +12,7 @@ "data": { "city": "V\u00e1ros" }, - "description": "V\u00e1laszd ki a v\u00e1rost a list\u00e1b\u00f3l", + "description": "V\u00e1lassza ki a v\u00e1rost a list\u00e1b\u00f3l", "title": "M\u00e9t\u00e9o-France" }, "user": { diff --git a/homeassistant/components/meteoclimatic/translations/es.json b/homeassistant/components/meteoclimatic/translations/es.json index 2cb627d4ae0..ab84e6604e3 100644 --- a/homeassistant/components/meteoclimatic/translations/es.json +++ b/homeassistant/components/meteoclimatic/translations/es.json @@ -1,8 +1,12 @@ { "config": { "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", "unknown": "Error inesperado" }, + "error": { + "not_found": "No se encontraron dispositivos en la red" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/meteoclimatic/translations/id.json b/homeassistant/components/meteoclimatic/translations/id.json new file mode 100644 index 00000000000..81dddee653f --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/id.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "error": { + "not_found": "Tidak ada perangkat yang ditemukan di jaringan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/hu.json b/homeassistant/components/mikrotik/translations/hu.json index 248884f9687..3e5281fc06a 100644 --- a/homeassistant/components/mikrotik/translations/hu.json +++ b/homeassistant/components/mikrotik/translations/hu.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "name": "N\u00e9v", "password": "Jelsz\u00f3", "port": "Port", diff --git a/homeassistant/components/mikrotik/translations/ru.json b/homeassistant/components/mikrotik/translations/ru.json index 06e9d647545..015d2061c76 100644 --- a/homeassistant/components/mikrotik/translations/ru.json +++ b/homeassistant/components/mikrotik/translations/ru.json @@ -27,7 +27,7 @@ "device_tracker": { "data": { "arp_ping": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c ARP-\u043f\u0438\u043d\u0433", - "detection_time": "\u0412\u0440\u0435\u043c\u044f \u043e\u0442 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0433\u043e \u0441\u0435\u0430\u043d\u0441\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c (\u0441\u0435\u043a.), \u043f\u043e \u0438\u0441\u0442\u0435\u0447\u0435\u043d\u0438\u044e \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u0442\u0430\u0442\u0443\u0441 \"\u041d\u0435 \u0434\u043e\u043c\u0430\".", + "detection_time": "\u0412\u0440\u0435\u043c\u044f, \u0432 \u0442\u0435\u0447\u0435\u043d\u0438\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0441\u0447\u0438\u0442\u0430\u0442\u044c \u0447\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043e\u043c\u0430 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", "force_dhcp": "\u041f\u0440\u0438\u043d\u0443\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c DHCP" } } diff --git a/homeassistant/components/mill/translations/ca.json b/homeassistant/components/mill/translations/ca.json index 13ce41cec91..309e5ccc41c 100644 --- a/homeassistant/components/mill/translations/ca.json +++ b/homeassistant/components/mill/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat" + "already_configured": "El compte ja est\u00e0 configurat" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3" diff --git a/homeassistant/components/minecraft_server/translations/hu.json b/homeassistant/components/minecraft_server/translations/hu.json index ef3c228d2d5..02c2a06d8ab 100644 --- a/homeassistant/components/minecraft_server/translations/hu.json +++ b/homeassistant/components/minecraft_server/translations/hu.json @@ -4,14 +4,14 @@ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" }, "error": { - "cannot_connect": "Nem siker\u00fclt csatlakozni a szerverhez. K\u00e9rj\u00fck, ellen\u0151rizze a gazdag\u00e9pet \u00e9s a portot, majd pr\u00f3b\u00e1lkozzon \u00fajra. Gondoskodjon arr\u00f3l, hogy a szerveren legal\u00e1bb a Minecraft 1.7-es verzi\u00f3j\u00e1t futtassa.", + "cannot_connect": "Nem siker\u00fclt csatlakozni a szerverhez. K\u00e9rj\u00fck, ellen\u0151rizze a c\u00edmet \u00e9s a portot, majd pr\u00f3b\u00e1lkozzon \u00fajra. Gondoskodjon arr\u00f3l, hogy a szerveren legal\u00e1bb a Minecraft 1.7-es verzi\u00f3j\u00e1t futtassa.", "invalid_ip": "Az IP -c\u00edm \u00e9rv\u00e9nytelen (a MAC -c\u00edmet nem siker\u00fclt meghat\u00e1rozni). K\u00e9rj\u00fck, jav\u00edtsa ki, \u00e9s pr\u00f3b\u00e1lja \u00fajra.", "invalid_port": "A portnak 1024 \u00e9s 65535 k\u00f6z\u00f6tt kell lennie. K\u00e9rj\u00fck, jav\u00edtsa ki, \u00e9s pr\u00f3b\u00e1lja \u00fajra." }, "step": { "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "name": "N\u00e9v" }, "description": "\u00c1ll\u00edtsa be a Minecraft Server p\u00e9ld\u00e1nyt, hogy lehet\u0151v\u00e9 tegye a megfigyel\u00e9st.", diff --git a/homeassistant/components/mobile_app/translations/hu.json b/homeassistant/components/mobile_app/translations/hu.json index 90690e2545b..1dda8ce7223 100644 --- a/homeassistant/components/mobile_app/translations/hu.json +++ b/homeassistant/components/mobile_app/translations/hu.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "install_app": "Nyisd meg a mobil alkalmaz\u00e1st a Home Assistant-tal val\u00f3 integr\u00e1ci\u00f3hoz. A kompatibilis alkalmaz\u00e1sok list\u00e1j\u00e1nak megtekint\u00e9s\u00e9hez ellen\u0151rizd [a le\u00edr\u00e1st]({apps_url})." + "install_app": "Nyissa meg a mobil alkalmaz\u00e1st Home Assistant-tal val\u00f3 integr\u00e1ci\u00f3hoz. A kompatibilis alkalmaz\u00e1sok list\u00e1j\u00e1nak megtekint\u00e9s\u00e9hez ellen\u0151rizze [a le\u00edr\u00e1st]({apps_url})." }, "step": { "confirm": { - "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a mobil alkalmaz\u00e1s komponenst?" + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani a mobil alkalmaz\u00e1s komponenst?" } } }, diff --git a/homeassistant/components/modem_callerid/translations/ca.json b/homeassistant/components/modem_callerid/translations/ca.json new file mode 100644 index 00000000000..d94d4cf392d --- /dev/null +++ b/homeassistant/components/modem_callerid/translations/ca.json @@ -0,0 +1,26 @@ +{ + "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'ha trobat cap dispositiu restant" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "usb_confirm": { + "description": "Integraci\u00f3 per a trucades fixes amb el m\u00f2dem de veu CX93001. Pot obtenir l'identificador del que truca i pot rebutjar trucades entrants.", + "title": "M\u00f2dem telef\u00f2nic" + }, + "user": { + "data": { + "name": "Nom", + "port": "Port" + }, + "description": "Integraci\u00f3 per a trucades fixes amb el m\u00f2dem de veu CX93001. Pot obtenir l'identificador del que truca i pot rebutjar trucades entrants.", + "title": "M\u00f2dem telef\u00f2nic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/cs.json b/homeassistant/components/modem_callerid/translations/cs.json new file mode 100644 index 00000000000..05861d2c427 --- /dev/null +++ b/homeassistant/components/modem_callerid/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "name": "Jm\u00e9no", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/de.json b/homeassistant/components/modem_callerid/translations/de.json new file mode 100644 index 00000000000..0bc505be5c8 --- /dev/null +++ b/homeassistant/components/modem_callerid/translations/de.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "no_devices_found": "Keine weiteren Ger\u00e4te gefunden" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "usb_confirm": { + "description": "Dies ist eine Integration f\u00fcr Festnetzanrufe mit einem CX93001 Sprachmodem. Damit k\u00f6nnen Anrufer-ID-Informationen mit einer Option zum Abweisen eines eingehenden Anrufs abgerufen werden.", + "title": "Telefonmodem" + }, + "user": { + "data": { + "name": "Name", + "port": "Port" + }, + "description": "Dies ist eine Integration f\u00fcr Festnetzanrufe mit einem CX93001 Sprachmodem. Damit k\u00f6nnen Anrufer-ID-Informationen mit einer Option zum Abweisen eines eingehenden Anrufs abgerufen werden.", + "title": "Telefonmodem" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/en.json b/homeassistant/components/modem_callerid/translations/en.json index 207f9ab7a17..5450a930ff3 100644 --- a/homeassistant/components/modem_callerid/translations/en.json +++ b/homeassistant/components/modem_callerid/translations/en.json @@ -9,17 +9,17 @@ "cannot_connect": "Failed to connect" }, "step": { + "usb_confirm": { + "description": "This is an integration for landline calls using a CX93001 voice modem. This can retrieve caller ID information with an option to reject an incoming call.", + "title": "Phone Modem" + }, "user": { "data": { "name": "Name", "port": "Port" }, - "title": "Phone Modem", - "description": "This is an integration for landline calls using a CX93001 voice modem. This can retrieve caller id information with an option to reject an incoming call." - }, - "usb_confirm": { - "title": "Phone Modem", - "description": "This is an integration for landline calls using a CX93001 voice modem. This can retrieve caller ID information with an option to reject an incoming call." + "description": "This is an integration for landline calls using a CX93001 voice modem. This can retrieve caller ID information with an option to reject an incoming call.", + "title": "Phone Modem" } } } diff --git a/homeassistant/components/modem_callerid/translations/es.json b/homeassistant/components/modem_callerid/translations/es.json new file mode 100644 index 00000000000..eaf0a9afea1 --- /dev/null +++ b/homeassistant/components/modem_callerid/translations/es.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso" + }, + "step": { + "user": { + "data": { + "name": "Nombre", + "port": "Puerto" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/et.json b/homeassistant/components/modem_callerid/translations/et.json new file mode 100644 index 00000000000..463d24e8f9f --- /dev/null +++ b/homeassistant/components/modem_callerid/translations/et.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine juba k\u00e4ib", + "no_devices_found": "Lisatavaid seadmeid ei leitud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "usb_confirm": { + "description": "See on sidumine fiksv\u00f5rgu telefonile kasutades CX93001 modemit. See v\u00f5ib hankida helistaja ID teabe koos sissetulevast k\u00f5nestloobumise v\u00f5imalusega.", + "title": "Telefoniliini modem" + }, + "user": { + "data": { + "name": "Nimi", + "port": "Port" + }, + "description": "See on sidumine fiksv\u00f5rgu telefonile kasutades CX93001 modemit. See v\u00f5ib hankida helistaja ID teabe koos sissetulevast k\u00f5nestloobumise v\u00f5imalusega.", + "title": "Telefoniliini modem" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/he.json b/homeassistant/components/modem_callerid/translations/he.json new file mode 100644 index 00000000000..e156f21f826 --- /dev/null +++ b/homeassistant/components/modem_callerid/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", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "user": { + "data": { + "name": "\u05e9\u05dd", + "port": "\u05e4\u05ea\u05d7\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/hu.json b/homeassistant/components/modem_callerid/translations/hu.json new file mode 100644 index 00000000000..cb8433e0028 --- /dev/null +++ b/homeassistant/components/modem_callerid/translations/hu.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 egy\u00e9b eszk\u00f6z" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "usb_confirm": { + "description": "Ez egy integr\u00e1ci\u00f3 a CX93001 hangmodemmel t\u00f6rt\u00e9n\u0151 vezet\u00e9kes h\u00edv\u00e1sokhoz. Ez k\u00e9pes lek\u00e9rdezni a h\u00edv\u00f3azonos\u00edt\u00f3 inform\u00e1ci\u00f3t a bej\u00f6v\u0151 h\u00edv\u00e1s visszautas\u00edt\u00e1s\u00e1nak lehet\u0151s\u00e9g\u00e9vel.", + "title": "Telefon modem" + }, + "user": { + "data": { + "name": "N\u00e9v", + "port": "Port" + }, + "description": "Ez egy integr\u00e1ci\u00f3 a CX93001 hangmodemmel t\u00f6rt\u00e9n\u0151 vezet\u00e9kes h\u00edv\u00e1sokhoz. Ez k\u00e9pes lek\u00e9rdezni a h\u00edv\u00f3azonos\u00edt\u00f3 inform\u00e1ci\u00f3t a bej\u00f6v\u0151 h\u00edv\u00e1s visszautas\u00edt\u00e1s\u00e1nak lehet\u0151s\u00e9g\u00e9vel.", + "title": "Telefon modem" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/id.json b/homeassistant/components/modem_callerid/translations/id.json new file mode 100644 index 00000000000..9e8fc6738b9 --- /dev/null +++ b/homeassistant/components/modem_callerid/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "name": "Nama", + "port": "Port" + }, + "title": "Modem Telepon" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/it.json b/homeassistant/components/modem_callerid/translations/it.json new file mode 100644 index 00000000000..65d1c74f956 --- /dev/null +++ b/homeassistant/components/modem_callerid/translations/it.json @@ -0,0 +1,26 @@ +{ + "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 rimanente trovato" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "usb_confirm": { + "description": "Questa \u00e8 un'integrazione per le chiamate su linea fissa che utilizza un modem vocale CX93001. Questo pu\u00f2 recuperare le informazioni sull'ID del chiamante con un'opzione per rifiutare una chiamata in arrivo.", + "title": "Modem del telefono" + }, + "user": { + "data": { + "name": "Nome", + "port": "Porta" + }, + "description": "Questa \u00e8 un'integrazione per le chiamate su linea fissa che utilizza un modem vocale CX93001. Questo pu\u00f2 recuperare le informazioni sull'ID del chiamante con un'opzione per rifiutare una chiamata in arrivo.", + "title": "Modem del telefono" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/nl.json b/homeassistant/components/modem_callerid/translations/nl.json new file mode 100644 index 00000000000..4077a03105b --- /dev/null +++ b/homeassistant/components/modem_callerid/translations/nl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang", + "no_devices_found": "Geen resterende apparaten gevonden" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "step": { + "usb_confirm": { + "description": "Dit is een integratie voor vaste telefoongesprekken met een CX93001 spraakmodem. Hiermee kan beller-ID informatie worden opgehaald met een optie om een inkomende oproep te weigeren.", + "title": "Telefoonmodem" + }, + "user": { + "data": { + "name": "Naam", + "port": "Poort" + }, + "description": "Dit is een integratie voor vaste telefoongesprekken met een CX93001 spraakmodem. Hiermee kan beller-ID informatie worden opgehaald met een optie om een inkomende oproep te weigeren.", + "title": "Telefoonmodem" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/no.json b/homeassistant/components/modem_callerid/translations/no.json new file mode 100644 index 00000000000..2e1103b5092 --- /dev/null +++ b/homeassistant/components/modem_callerid/translations/no.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "no_devices_found": "Ingen gjenv\u00e6rende enheter funnet" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "usb_confirm": { + "description": "Dette er en integrasjon for fasttelefonsamtaler ved hjelp av et talemodem CX93001. Dette kan hente oppringer -ID -informasjon med et alternativ for \u00e5 avvise et innkommende anrop.", + "title": "Telefonmodem" + }, + "user": { + "data": { + "name": "Navn", + "port": "Port" + }, + "description": "Dette er en integrasjon for fasttelefonsamtaler ved hjelp av et talemodem CX93001. Dette kan hente oppringer -ID -informasjon med et alternativ for \u00e5 avvise et innkommende anrop.", + "title": "Telefonmodem" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/ru.json b/homeassistant/components/modem_callerid/translations/ru.json new file mode 100644 index 00000000000..f5fa5061a4a --- /dev/null +++ b/homeassistant/components/modem_callerid/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.", + "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": "\u041f\u043e\u0434\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u043d\u0435 \u043d\u0430\u0439\u0434\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." + }, + "step": { + "usb_confirm": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0433\u043e\u043b\u043e\u0441\u043e\u0432\u043e\u0433\u043e \u043c\u043e\u0434\u0435\u043c\u0430 CX93001 \u0434\u043b\u044f \u0437\u0432\u043e\u043d\u043a\u043e\u0432 \u043f\u043e \u0441\u0442\u0430\u0446\u0438\u043e\u043d\u0430\u0440\u043d\u043e\u0439 \u043b\u0438\u043d\u0438\u0438. \u041f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e\u0431 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0435 \u0432\u044b\u0437\u044b\u0432\u0430\u044e\u0449\u0435\u0433\u043e \u0430\u0431\u043e\u043d\u0435\u043d\u0442\u0430 \u0441 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c\u044e \u043e\u0442\u043a\u043b\u043e\u043d\u0435\u043d\u0438\u044f \u0432\u0445\u043e\u0434\u044f\u0449\u0435\u0433\u043e \u0432\u044b\u0437\u043e\u0432\u0430.", + "title": "\u0422\u0435\u043b\u0435\u0444\u043e\u043d\u043d\u044b\u0439 \u043c\u043e\u0434\u0435\u043c" + }, + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0433\u043e\u043b\u043e\u0441\u043e\u0432\u043e\u0433\u043e \u043c\u043e\u0434\u0435\u043c\u0430 CX93001 \u0434\u043b\u044f \u0437\u0432\u043e\u043d\u043a\u043e\u0432 \u043f\u043e \u0441\u0442\u0430\u0446\u0438\u043e\u043d\u0430\u0440\u043d\u043e\u0439 \u043b\u0438\u043d\u0438\u0438. \u041f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e\u0431 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0435 \u0432\u044b\u0437\u044b\u0432\u0430\u044e\u0449\u0435\u0433\u043e \u0430\u0431\u043e\u043d\u0435\u043d\u0442\u0430 \u0441 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c\u044e \u043e\u0442\u043a\u043b\u043e\u043d\u0435\u043d\u0438\u044f \u0432\u0445\u043e\u0434\u044f\u0449\u0435\u0433\u043e \u0432\u044b\u0437\u043e\u0432\u0430.", + "title": "\u0422\u0435\u043b\u0435\u0444\u043e\u043d\u043d\u044b\u0439 \u043c\u043e\u0434\u0435\u043c" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/zh-Hant.json b/homeassistant/components/modem_callerid/translations/zh-Hant.json new file mode 100644 index 00000000000..542a12e8c5d --- /dev/null +++ b/homeassistant/components/modem_callerid/translations/zh-Hant.json @@ -0,0 +1,26 @@ +{ + "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": "\u627e\u4e0d\u5230\u5269\u9918\u88dd\u7f6e" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "usb_confirm": { + "description": "\u6b64\u6574\u5408\u4f7f\u7528 CX93001 \u8a9e\u97f3\u6578\u64da\u6a5f\u9032\u884c\u5e02\u8a71\u901a\u8a71\u3002\u53ef\u7528\u4ee5\u6aa2\u67e5\u4f86\u96fb ID \u8cc7\u8a0a\u3001\u4e26\u9032\u884c\u62d2\u63a5\u4f86\u96fb\u7684\u529f\u80fd\u3002", + "title": "\u624b\u6a5f\u6578\u64da\u6a5f" + }, + "user": { + "data": { + "name": "\u540d\u7a31", + "port": "\u901a\u8a0a\u57e0" + }, + "description": "\u6b64\u6574\u5408\u4f7f\u7528 CX93001 \u8a9e\u97f3\u6578\u64da\u6a5f\u9032\u884c\u5e02\u8a71\u901a\u8a71\u3002\u53ef\u7528\u4ee5\u6aa2\u67e5\u4f86\u96fb ID \u8cc7\u8a0a\u3001\u4e26\u9032\u884c\u62d2\u63a5\u4f86\u96fb\u7684\u529f\u80fd\u3002", + "title": "\u624b\u6a5f\u6578\u64da\u6a5f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/es.json b/homeassistant/components/modern_forms/translations/es.json index 25a432214fc..f651dca40a5 100644 --- a/homeassistant/components/modern_forms/translations/es.json +++ b/homeassistant/components/modern_forms/translations/es.json @@ -1,7 +1,17 @@ { "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar" + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, "flow_title": "{name}", "step": { + "confirm": { + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" + }, "user": { "data": { "host": "Anfitri\u00f3n" diff --git a/homeassistant/components/modern_forms/translations/hu.json b/homeassistant/components/modern_forms/translations/hu.json index fee0216224c..5bea7c3054e 100644 --- a/homeassistant/components/modern_forms/translations/hu.json +++ b/homeassistant/components/modern_forms/translations/hu.json @@ -10,16 +10,16 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?" + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" }, "user": { "data": { - "host": "Hoszt" + "host": "C\u00edm" }, - "description": "\u00c1ll\u00edtsa be a Modern Forms-t, hogy integr\u00e1l\u00f3djon a Home Assistant programba." + "description": "\u00c1ll\u00edtsa be Modern Forms-t, hogy integr\u00e1l\u00f3djon Home Assistant-ba." }, "zeroconf_confirm": { - "description": "Hozz\u00e1 szeretn\u00e9 adni a(z) {name} `nev\u0171 Modern Forms rajong\u00f3t a Home Assistanthoz?", + "description": "Hozz\u00e1 szeretn\u00e9 adni `{name}`nev\u0171 Modern Forms rajong\u00f3t Home Assistanthoz?", "title": "Felfedezte a Modern Forms rajong\u00f3i eszk\u00f6zt" } } diff --git a/homeassistant/components/modern_forms/translations/id.json b/homeassistant/components/modern_forms/translations/id.json new file mode 100644 index 00000000000..8b2f9fcfa1d --- /dev/null +++ b/homeassistant/components/modern_forms/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Ingin memulai penyiapan?" + }, + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/nl.json b/homeassistant/components/modern_forms/translations/nl.json index 5a3d63e15a7..ccbdf7d5b44 100644 --- a/homeassistant/components/modern_forms/translations/nl.json +++ b/homeassistant/components/modern_forms/translations/nl.json @@ -10,7 +10,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Wil je beginnen met instellen?" + "description": "Wilt u beginnen met instellen?" }, "user": { "data": { diff --git a/homeassistant/components/motion_blinds/translations/hu.json b/homeassistant/components/motion_blinds/translations/hu.json index a2560e5fa79..32ff2dcc58e 100644 --- a/homeassistant/components/motion_blinds/translations/hu.json +++ b/homeassistant/components/motion_blinds/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", "connection_error": "Sikertelen csatlakoz\u00e1s" }, "error": { diff --git a/homeassistant/components/motioneye/translations/hu.json b/homeassistant/components/motioneye/translations/hu.json index 5b23c74dc76..c381d3954d4 100644 --- a/homeassistant/components/motioneye/translations/hu.json +++ b/homeassistant/components/motioneye/translations/hu.json @@ -12,7 +12,7 @@ }, "step": { "hassio_confirm": { - "description": "Be szeretn\u00e9 \u00e1ll\u00edtani a Home Assistant, hogy csatlakozzon a(z) {addon} \u00e1ltal biztos\u00edtott motionEye szolg\u00e1ltat\u00e1shoz?", + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani Home Assistant-ot, hogy csatlakozzon {addon} kieg\u00e9sz\u00edt\u0151 \u00e1ltal biztos\u00edtott motionEye szolg\u00e1ltat\u00e1shoz?", "title": "motionEye a Home Assistant kieg\u00e9sz\u00edt\u0151 seg\u00edts\u00e9g\u00e9vel" }, "user": { @@ -30,7 +30,7 @@ "step": { "init": { "data": { - "webhook_set": "\u00c1ll\u00edtsa be a motionEye webhookokat az esem\u00e9nyek jelent\u00e9s\u00e9nek a Home Assistant sz\u00e1m\u00e1ra", + "webhook_set": "\u00c1ll\u00edtsa be a motionEye webhookokat az esem\u00e9nyek jelent\u00e9s\u00e9nek Home Assistant sz\u00e1m\u00e1ra", "webhook_set_overwrite": "Fel\u00fcl\u00edrja a fel nem ismert webhookokat" } } diff --git a/homeassistant/components/motioneye/translations/ko.json b/homeassistant/components/motioneye/translations/ko.json new file mode 100644 index 00000000000..ff2a843677d --- /dev/null +++ b/homeassistant/components/motioneye/translations/ko.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_url": "\uc798\ubabb\ub41c URL", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "admin_password": "Admin \ube44\ubc00\ubc88\ud638", + "admin_username": "Admin \uc0ac\uc6a9\uc790 \uc774\ub984", + "surveillance_password": "Surveillance \ube44\ubc00\ubc88\ud638", + "surveillance_username": "Surveillance \uc0ac\uc6a9\uc790 \uc774\ub984", + "url": "URL \uc8fc\uc18c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/es.json b/homeassistant/components/mqtt/translations/es.json index 2cabe392308..50cac3172ab 100644 --- a/homeassistant/components/mqtt/translations/es.json +++ b/homeassistant/components/mqtt/translations/es.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "El servicio ya est\u00e1 configurado", "single_instance_allowed": "S\u00f3lo se permite una \u00fanica configuraci\u00f3n de MQTT." }, "error": { diff --git a/homeassistant/components/mqtt/translations/fi.json b/homeassistant/components/mqtt/translations/fi.json index 27a956beb33..bc974dfd7d9 100644 --- a/homeassistant/components/mqtt/translations/fi.json +++ b/homeassistant/components/mqtt/translations/fi.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Yhdist\u00e4minen ep\u00e4onnistui" + }, "step": { "broker": { "data": { diff --git a/homeassistant/components/mqtt/translations/he.json b/homeassistant/components/mqtt/translations/he.json index df987bd35a2..36521ce6839 100644 --- a/homeassistant/components/mqtt/translations/he.json +++ b/homeassistant/components/mqtt/translations/he.json @@ -19,18 +19,45 @@ "description": "\u05e0\u05d0 \u05dc\u05d4\u05d6\u05d9\u05df \u05d0\u05ea \u05e4\u05e8\u05d8\u05d9 \u05d4\u05d7\u05d9\u05d1\u05d5\u05e8 \u05e9\u05dc \u05d4\u05d1\u05e8\u05d5\u05e7\u05e8 MQTT \u05e9\u05dc\u05da." }, "hassio_confirm": { + "data": { + "discovery": "\u05d0\u05d9\u05e4\u05e9\u05d5\u05e8 \u05d2\u05d9\u05dc\u05d5\u05d9" + }, "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea \u05ea\u05e6\u05d5\u05e8\u05ea \u05d4-Home Assistant \u05db\u05da \u05e9\u05ea\u05ea\u05d7\u05d1\u05e8 \u05dc\u05de\u05ea\u05d5\u05d5\u05da MQTT \u05d4\u05de\u05e1\u05d5\u05e4\u05e7 \u05e2\u05dc \u05d9\u05d3\u05d9 \u05d4\u05d4\u05e8\u05d7\u05d1\u05d4 {addon}?", "title": "MQTT \u05d1\u05e8\u05d5\u05e7\u05e8 \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05d4\u05e8\u05d7\u05d1\u05ea Home Assistant" } } }, + "device_automation": { + "trigger_subtype": { + "button_1": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05e8\u05d0\u05e9\u05d5\u05df", + "button_2": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05e9\u05e0\u05d9", + "button_3": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05e9\u05dc\u05d9\u05e9\u05d9", + "button_4": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05e8\u05d1\u05d9\u05e2\u05d9", + "button_5": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05d7\u05de\u05d9\u05e9\u05d9", + "button_6": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05e9\u05d9\u05e9\u05d9", + "turn_off": "\u05db\u05d1\u05d4", + "turn_on": "\u05d4\u05e4\u05e2\u05dc" + }, + "trigger_type": { + "button_double_press": "\"{subtype}\" \u05d4\u05e7\u05e9\u05d4 \u05db\u05e4\u05d5\u05dc\u05d4", + "button_long_press": "\"{subtype}\" \u05dc\u05d7\u05d9\u05e6\u05d4 \u05de\u05de\u05d5\u05e9\u05db\u05ea", + "button_long_release": "\"{subtype}\" \u05e9\u05d5\u05d7\u05e8\u05e8 \u05dc\u05d0\u05d7\u05e8 \u05dc\u05d7\u05d9\u05e6\u05d4 \u05de\u05de\u05d5\u05e9\u05db\u05ea", + "button_quadruple_press": "\"{subtype}\" \u05dc\u05d7\u05d9\u05e6\u05d4 \u05de\u05e8\u05d5\u05d1\u05e2\u05ea", + "button_quintuple_press": "\"{subtype}\" \u05dc\u05d7\u05d9\u05e6\u05d4 \u05de\u05d7\u05d5\u05de\u05e9\u05ea", + "button_short_press": "\"{subtype}\" \u05e0\u05dc\u05d7\u05e5", + "button_short_release": "\"{subtype}\" \u05e9\u05d5\u05d7\u05e8\u05e8", + "button_triple_press": "\"{subtype}\" \u05dc\u05d7\u05d9\u05e6\u05d4 \u05de\u05e9\u05d5\u05dc\u05e9\u05ea" + } + }, "options": { "error": { + "bad_birth": "\u05e0\u05d5\u05e9\u05d0 \u05dc\u05d9\u05d3\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9.", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, "step": { "broker": { "data": { + "broker": "\u05d1\u05e8\u05d5\u05e7\u05e8", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "port": "\u05e4\u05ea\u05d7\u05d4", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" @@ -39,6 +66,13 @@ "title": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05de\u05ea\u05d5\u05d5\u05da" }, "options": { + "data": { + "birth_enable": "\u05d0\u05e4\u05e9\u05e8 \u05d4\u05d5\u05d3\u05e2\u05ea \u05dc\u05d9\u05d3\u05d4", + "birth_payload": "\u05de\u05d8\u05e2\u05df \u05d4\u05d5\u05d3\u05e2\u05ea \u05dc\u05d9\u05d3\u05d4", + "birth_retain": "\u05d4\u05d5\u05d3\u05e2\u05ea \u05dc\u05d9\u05d3\u05d4 \u05e0\u05e9\u05de\u05e8\u05ea", + "birth_topic": "\u05e0\u05d5\u05e9\u05d0 \u05d4\u05d5\u05d3\u05e2\u05ea \u05dc\u05d9\u05d3\u05d4", + "discovery": "\u05d0\u05d9\u05e4\u05e9\u05d5\u05e8 \u05d2\u05d9\u05dc\u05d5\u05d9" + }, "description": "\u05d2\u05d9\u05dc\u05d5\u05d9 - \u05d0\u05dd \u05d2\u05d9\u05dc\u05d5\u05d9 \u05de\u05d5\u05e4\u05e2\u05dc (\u05de\u05d5\u05de\u05dc\u05e5), Home Assistant \u05d9\u05d2\u05dc\u05d4 \u05d1\u05d0\u05d5\u05e4\u05df \u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d5\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05d4\u05de\u05e4\u05e8\u05e1\u05de\u05d9\u05dd \u05d0\u05ea \u05ea\u05e6\u05d5\u05e8\u05ea\u05dd \u05d1\u05de\u05ea\u05d5\u05d5\u05da MQTT. \u05d0\u05dd \u05d4\u05d2\u05d9\u05dc\u05d5\u05d9 \u05d0\u05d9\u05e0\u05d5 \u05de\u05d5\u05e4\u05e2\u05dc, \u05db\u05dc \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05d7\u05d9\u05d9\u05d1\u05ea \u05dc\u05d4\u05d9\u05e2\u05e9\u05d5\u05ea \u05d1\u05d0\u05d5\u05e4\u05df \u05d9\u05d3\u05e0\u05d9.\n\u05d4\u05d5\u05d3\u05e2\u05ea \u05dc\u05d9\u05d3\u05d4 - \u05d4\u05d5\u05d3\u05e2\u05ea \u05d4\u05dc\u05d9\u05d3\u05d4 \u05ea\u05d9\u05e9\u05dc\u05d7 \u05d1\u05db\u05dc \u05e4\u05e2\u05dd \u05e9-Home Assistant \u05de\u05ea\u05d7\u05d1\u05e8 (\u05de\u05d7\u05d3\u05e9) \u05dc\u05de\u05ea\u05d5\u05d5\u05da MQTT.\n\u05d4\u05d5\u05d3\u05e2\u05ea \u05e8\u05e6\u05d5\u05df - \u05d4\u05d5\u05d3\u05e2\u05ea \u05d4\u05e8\u05e6\u05d5\u05df \u05ea\u05d9\u05e9\u05dc\u05d7 \u05d1\u05db\u05dc \u05e4\u05e2\u05dd \u05e9-Home Assistant \u05d9\u05d0\u05d1\u05d3 \u05d0\u05ea \u05d4\u05e7\u05e9\u05e8 \u05e9\u05dc\u05d5 \u05dc\u05de\u05ea\u05d5\u05d5\u05da, \u05d2\u05dd \u05d1\u05de\u05e7\u05e8\u05d4 \u05e9\u05dc \u05e0\u05d9\u05ea\u05d5\u05e7 \u05e0\u05e7\u05d9 (\u05dc\u05de\u05e9\u05dc \u05db\u05d9\u05d1\u05d5\u05d9 \u05e9\u05dc Home Assistant) \u05d5\u05d2\u05dd \u05d1\u05de\u05e7\u05e8\u05d4 \u05e9\u05dc \u05e0\u05d9\u05ea\u05d5\u05e7 \u05dc\u05d0 \u05e0\u05e7\u05d9 (\u05dc\u05de\u05e9\u05dc Home Assistant \u05de\u05ea\u05e8\u05e1\u05e7 \u05d0\u05d5 \u05de\u05d0\u05d1\u05d3 \u05d0\u05ea \u05d7\u05d9\u05d1\u05d5\u05e8 \u05d4\u05e8\u05e9\u05ea \u05e9\u05dc\u05d5).", "title": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea MQTT" } diff --git a/homeassistant/components/mqtt/translations/hu.json b/homeassistant/components/mqtt/translations/hu.json index ad371afabc8..471982756eb 100644 --- a/homeassistant/components/mqtt/translations/hu.json +++ b/homeassistant/components/mqtt/translations/hu.json @@ -16,14 +16,14 @@ "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "K\u00e9rlek, add meg az MQTT br\u00f3ker kapcsol\u00f3d\u00e1si adatait." + "description": "K\u00e9rem, adja meg az MQTT br\u00f3ker kapcsol\u00f3d\u00e1si adatait." }, "hassio_confirm": { "data": { "discovery": "Felfedez\u00e9s enged\u00e9lyez\u00e9se" }, - "description": "Be szeretn\u00e9d konfigru\u00e1lni, hogy a Home Assistant a(z) {addon} Supervisor add-on \u00e1ltal biztos\u00edtott MQTT br\u00f3kerhez csatlakozzon?", - "title": "MQTT Br\u00f3ker a Supervisor b\u0151v\u00edtm\u00e9nnyel" + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani Home Assistant-ot MQTT br\u00f3kerhez val\u00f3 csatlakoz\u00e1shoz, {addon} kieg\u00e9sz\u00edt\u0151 \u00e1ltal?", + "title": "MQTT Br\u00f3ker - Home Assistant kieg\u00e9sz\u00edt\u0151 \u00e1ltal" } } }, @@ -63,7 +63,7 @@ "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "K\u00e9rlek, add meg az MQTT br\u00f3ker kapcsol\u00f3d\u00e1si adatait.", + "description": "K\u00e9rem, adja meg az MQTT br\u00f3ker kapcsol\u00f3d\u00e1si adatait.", "title": "Br\u00f3ker opci\u00f3k" }, "options": { @@ -80,7 +80,7 @@ "will_retain": "\u00dczenet megtart\u00e1sa", "will_topic": "\u00dczenet t\u00e9m\u00e1ja" }, - "description": "Felfedez\u00e9s - Ha a felfedez\u00e9s enged\u00e9lyezve van (aj\u00e1nlott), a Home Assistant automatikusan felfedezi azokat az eszk\u00f6z\u00f6ket \u00e9s entit\u00e1sokat, amelyek k\u00f6zz\u00e9teszik konfigur\u00e1ci\u00f3jukat az MQTT br\u00f3keren. Ha a felfedez\u00e9s le van tiltva, minden konfigur\u00e1ci\u00f3t manu\u00e1lisan kell elv\u00e9gezni.\nSz\u00fclet\u00e9si \u00fczenet - A sz\u00fclet\u00e9si \u00fczenetet minden alkalommal elk\u00fcldi, amikor a Home Assistant (\u00fajra) csatlakozik az MQTT br\u00f3kerhez.\nAkarat \u00fczenet - Az akarat\u00fczenet minden alkalommal el lesz k\u00fcldve, amikor a Home Assistant elvesz\u00edti a kapcsolatot a k\u00f6zvet\u00edt\u0151vel, mind takar\u00edt\u00e1s eset\u00e9n (pl. A Home Assistant le\u00e1ll\u00edt\u00e1sa), mind tiszt\u00e1talans\u00e1g eset\u00e9n (pl. Home Assistant \u00f6sszeomlik vagy megszakad a h\u00e1l\u00f3zati kapcsolata) bontani.", + "description": "Discovery - Ha a felfedez\u00e9s enged\u00e9lyezve van (aj\u00e1nlott), akkor Home Assistant automatikusan felfedezi azokat az eszk\u00f6z\u00f6ket \u00e9s entit\u00e1sokat, amelyek k\u00f6zz\u00e9teszik konfigur\u00e1ci\u00f3jukat az MQTT br\u00f3keren. Ha a felfedez\u00e9s le van tiltva, minden konfigur\u00e1ci\u00f3t manu\u00e1lisan kell elv\u00e9gezni.\nBirth \u00fczenet - A sz\u00fclet\u00e9si \u00fczenet minden alkalommal el lesz k\u00fcldve, amikor Home Assistant (\u00fajra) csatlakozik az MQTT br\u00f3kerhez.\nWill \u00fczenet - Az akarat\u00fczenet minden alkalommal el lesz k\u00fcldve, amikor Home Assistant elvesz\u00edti a kapcsolatot a k\u00f6zvet\u00edt\u0151vel, mind takar\u00edt\u00e1s eset\u00e9n (pl. Home Assistant le\u00e1ll\u00edt\u00e1sa), mind rendelenes helyzetben (pl. Home Assistant \u00f6sszeomlik vagy megszakad a h\u00e1l\u00f3zati kapcsolata).", "title": "MQTT opci\u00f3k" } } diff --git a/homeassistant/components/mqtt/translations/id.json b/homeassistant/components/mqtt/translations/id.json index 2a3171456c8..14e047c1694 100644 --- a/homeassistant/components/mqtt/translations/id.json +++ b/homeassistant/components/mqtt/translations/id.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Layanan sudah dikonfigurasi", "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." }, "error": { @@ -21,8 +22,8 @@ "data": { "discovery": "Aktifkan penemuan" }, - "description": "Ingin mengonfigurasi Home Assistant untuk terhubung ke broker MQTT yang disediakan oleh add-on Supervisor {addon}?", - "title": "MQTT Broker via add-on Supervisor" + "description": "Ingin mengonfigurasi Home Assistant untuk terhubung ke broker MQTT yang disediakan oleh add-on {addon}?", + "title": "MQTT Broker via add-on Home Assistant" } } }, diff --git a/homeassistant/components/mutesync/translations/hu.json b/homeassistant/components/mutesync/translations/hu.json index 68cb5c18d27..0fd40705765 100644 --- a/homeassistant/components/mutesync/translations/hu.json +++ b/homeassistant/components/mutesync/translations/hu.json @@ -8,7 +8,7 @@ "step": { "user": { "data": { - "host": "Gazdag\u00e9p" + "host": "C\u00edm" } } } diff --git a/homeassistant/components/mutesync/translations/id.json b/homeassistant/components/mutesync/translations/id.json new file mode 100644 index 00000000000..66c930e348b --- /dev/null +++ b/homeassistant/components/mutesync/translations/id.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/translations/id.json b/homeassistant/components/myq/translations/id.json index 2cc790d15e0..4972803f37d 100644 --- a/homeassistant/components/myq/translations/id.json +++ b/homeassistant/components/myq/translations/id.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Layanan sudah dikonfigurasi" + "already_configured": "Layanan sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "cannot_connect": "Gagal terhubung", @@ -9,6 +10,11 @@ "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + } + }, "user": { "data": { "password": "Kata Sandi", diff --git a/homeassistant/components/nam/translations/hu.json b/homeassistant/components/nam/translations/hu.json index 8776ae92e20..0698b4d3e26 100644 --- a/homeassistant/components/nam/translations/hu.json +++ b/homeassistant/components/nam/translations/hu.json @@ -11,11 +11,11 @@ "flow_title": "{name}", "step": { "confirm_discovery": { - "description": "Be szeretn\u00e9 \u00e1ll\u00edtani a Nettigo Air Monitor-ot a {host} c\u00edmen?" + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani Nettigo Air Monitor-ot a {host} c\u00edmen?" }, "user": { "data": { - "host": "Gazdag\u00e9p" + "host": "C\u00edm" }, "description": "\u00c1ll\u00edtsa be a Nettigo Air Monitor integr\u00e1ci\u00f3j\u00e1t." } diff --git a/homeassistant/components/nanoleaf/translations/el.json b/homeassistant/components/nanoleaf/translations/el.json index be6719d8c2a..5112f61ef9f 100644 --- a/homeassistant/components/nanoleaf/translations/el.json +++ b/homeassistant/components/nanoleaf/translations/el.json @@ -11,6 +11,7 @@ "not_allowing_new_tokens": "\u03a4\u03bf Nanoleaf \u03b4\u03b5\u03bd \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03ad\u03b1 tokens, \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03b1\u03c1\u03b1\u03c0\u03ac\u03bd\u03c9 \u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2.", "unknown": "\u0391\u03bd\u03b5\u03c0\u03ac\u03bd\u03c4\u03b5\u03c7\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, + "flow_title": "{name}", "step": { "link": { "description": "\u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c0\u03b1\u03c1\u03b1\u03c4\u03b5\u03c4\u03b1\u03bc\u03ad\u03bd\u03b1 \u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03c3\u03c4\u03bf Nanoleaf \u03b3\u03b9\u03b1 5 \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1 \u03bc\u03ad\u03c7\u03c1\u03b9 \u03bd\u03b1 \u03b1\u03c1\u03c7\u03af\u03c3\u03bf\u03c5\u03bd \u03bd\u03b1 \u03b1\u03bd\u03b1\u03b2\u03bf\u03c3\u03b2\u03ae\u03bd\u03bf\u03c5\u03bd \u03bf\u03b9 \u03bb\u03c5\u03c7\u03bd\u03af\u03b5\u03c2 LED \u03c4\u03bf\u03c5 \u03ba\u03bf\u03c5\u03bc\u03c0\u03b9\u03bf\u03cd \u03ba\u03b1\u03b9, \u03c3\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 **SUBMIT** \u03bc\u03ad\u03c3\u03b1 \u03c3\u03b5 30 \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1.", diff --git a/homeassistant/components/nanoleaf/translations/es.json b/homeassistant/components/nanoleaf/translations/es.json index 16b28203215..2efbfb875f4 100644 --- a/homeassistant/components/nanoleaf/translations/es.json +++ b/homeassistant/components/nanoleaf/translations/es.json @@ -1,13 +1,21 @@ { "config": { "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar", + "invalid_token": "Token de acceso no v\u00e1lido", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "unknown": "Error inesperado" }, "error": { + "cannot_connect": "No se pudo conectar", "unknown": "Error inesperado" }, "flow_title": "{name}", "step": { + "link": { + "title": "Link Nanoleaf" + }, "user": { "data": { "host": "Anfitri\u00f3n" diff --git a/homeassistant/components/nanoleaf/translations/hu.json b/homeassistant/components/nanoleaf/translations/hu.json index 7c5854055a4..176d47cc38f 100644 --- a/homeassistant/components/nanoleaf/translations/hu.json +++ b/homeassistant/components/nanoleaf/translations/hu.json @@ -15,12 +15,12 @@ "flow_title": "{name}", "step": { "link": { - "description": "Nyomja meg \u00e9s tartsa lenyomva a Nanoleaf bekapcsol\u00f3gombj\u00e1t 5 m\u00e1sodpercig, am\u00edg a gomb LED-je villogni nem kezd, majd kattintson a **SUBMIT** gombra 30 m\u00e1sodpercen bel\u00fcl.", + "description": "Nyomja meg \u00e9s tartsa lenyomva a Nanoleaf bekapcsol\u00f3gombj\u00e1t 5 m\u00e1sodpercig, am\u00edg a gomb LED-je villogni nem kezd, majd kattintson a **K\u00fcld\u00e9s** gombra 30 m\u00e1sodpercen bel\u00fcl.", "title": "Nanoleaf link" }, "user": { "data": { - "host": "Gazdag\u00e9p" + "host": "C\u00edm" } } } diff --git a/homeassistant/components/nanoleaf/translations/id.json b/homeassistant/components/nanoleaf/translations/id.json new file mode 100644 index 00000000000..b0e3328df0b --- /dev/null +++ b/homeassistant/components/nanoleaf/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", + "invalid_token": "Token akses tidak valid", + "reauth_successful": "Autentikasi ulang berhasil", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/es.json b/homeassistant/components/neato/translations/es.json index f9b6fe54e22..64c58279614 100644 --- a/homeassistant/components/neato/translations/es.json +++ b/homeassistant/components/neato/translations/es.json @@ -8,7 +8,7 @@ "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "create_entry": { - "default": "Ver [documentaci\u00f3n Neato]({docs_url})." + "default": "Autenticado correctamente" }, "step": { "pick_implementation": { diff --git a/homeassistant/components/neato/translations/hu.json b/homeassistant/components/neato/translations/hu.json index 90fb417e6a6..20bc76ca6c0 100644 --- a/homeassistant/components/neato/translations/hu.json +++ b/homeassistant/components/neato/translations/hu.json @@ -3,7 +3,7 @@ "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.", - "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" }, @@ -15,7 +15,7 @@ "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" }, "reauth_confirm": { - "title": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?" + "title": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" } } }, diff --git a/homeassistant/components/neato/translations/nl.json b/homeassistant/components/neato/translations/nl.json index 3d7bbab2e75..2e9ab212fa9 100644 --- a/homeassistant/components/neato/translations/nl.json +++ b/homeassistant/components/neato/translations/nl.json @@ -15,7 +15,7 @@ "title": "Kies een authenticatie methode" }, "reauth_confirm": { - "title": "Wil je beginnen met instellen?" + "title": "Wilt u beginnen met instellen?" } } }, diff --git a/homeassistant/components/nest/translations/fi.json b/homeassistant/components/nest/translations/fi.json index 5365f73b721..e4235ee096e 100644 --- a/homeassistant/components/nest/translations/fi.json +++ b/homeassistant/components/nest/translations/fi.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "unknown": "Odottamaton virhe" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/nest/translations/he.json b/homeassistant/components/nest/translations/he.json index a3b5411b536..6efee1d74bd 100644 --- a/homeassistant/components/nest/translations/he.json +++ b/homeassistant/components/nest/translations/he.json @@ -5,7 +5,8 @@ "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.", "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})", "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", - "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." + "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.", + "unknown_authorize_url_generation": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4 \u05d1\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05e9\u05dc \u05d4\u05e8\u05e9\u05d0\u05d4." }, "create_entry": { "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4" @@ -28,7 +29,7 @@ "data": { "code": "\u05e7\u05d5\u05d3 PIN" }, - "description": "\u05db\u05d3\u05d9 \u05dc\u05e7\u05e9\u05e8 \u05d0\u05ea \u05d7\u05e9\u05d1\u05d5\u05df Nest \u05e9\u05dc\u05da, [\u05d0\u05de\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05e9\u05dc\u05da] ({url}). \n\n \u05dc\u05d0\u05d7\u05e8 \u05d4\u05d0\u05d9\u05e9\u05d5\u05e8, \u05d4\u05e2\u05ea\u05e7 \u05d0\u05ea \u05e7\u05d5\u05d3 \u05d4PIN \u05e9\u05e1\u05d5\u05e4\u05e7 \u05d5\u05d4\u05d3\u05d1\u05e7 \u05d0\u05d5\u05ea\u05d5 \u05dc\u05de\u05d8\u05d4.", + "description": "\u05db\u05d3\u05d9 \u05dc\u05e7\u05e9\u05e8 \u05d0\u05ea \u05d7\u05e9\u05d1\u05d5\u05df Nest \u05e9\u05dc\u05da, [\u05d4\u05e8\u05e9\u05d0\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05e9\u05dc\u05da]({url}). \n\n \u05dc\u05d0\u05d7\u05e8 \u05d4\u05d0\u05d9\u05e9\u05d5\u05e8, \u05d9\u05e9 \u05dc\u05d4\u05e2\u05ea\u05d9\u05e7 \u05d0\u05ea \u05e7\u05d5\u05d3 \u05d4PIN \u05e9\u05e1\u05d5\u05e4\u05e7 \u05d5\u05dc\u05d4\u05d3\u05d1\u05d9\u05e7 \u05d0\u05d5\u05ea\u05d5 \u05dc\u05de\u05d8\u05d4.", "title": "\u05e7\u05d9\u05e9\u05d5\u05e8 \u05d7\u05e9\u05d1\u05d5\u05df Nest" }, "pick_implementation": { diff --git a/homeassistant/components/nest/translations/hu.json b/homeassistant/components/nest/translations/hu.json index 5690724c4a0..58f8ea30caf 100644 --- a/homeassistant/components/nest/translations/hu.json +++ b/homeassistant/components/nest/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.", - "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", diff --git a/homeassistant/components/netatmo/translations/hu.json b/homeassistant/components/netatmo/translations/hu.json index 48f084f84c2..cb634547efc 100644 --- a/homeassistant/components/netatmo/translations/hu.json +++ b/homeassistant/components/netatmo/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", - "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, diff --git a/homeassistant/components/netgear/translations/ca.json b/homeassistant/components/netgear/translations/ca.json new file mode 100644 index 00000000000..48de8c99684 --- /dev/null +++ b/homeassistant/components/netgear/translations/ca.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "config": "Error de connexi\u00f3 o d'inici de sessi\u00f3: comprova la configuraci\u00f3" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3 (opcional)", + "password": "Contrasenya", + "port": "Port (opcional)", + "ssl": "Utilitza un certificat SSL", + "username": "Nom d'usuari (opcional)" + }, + "description": "Amfitri\u00f3 predeterminat: {host}\nPort predeterminat: {port}\nNom d'usuari predeterminat: {username}", + "title": "Netgear" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Temps per considerar 'a casa' (segons)" + }, + "description": "Especifica les configuracions opcional", + "title": "Netgear" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netgear/translations/cs.json b/homeassistant/components/netgear/translations/cs.json new file mode 100644 index 00000000000..786cd2229ab --- /dev/null +++ b/homeassistant/components/netgear/translations/cs.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel (nepovinn\u00fd)", + "password": "Heslo", + "port": "Port (nepovinn\u00fd)", + "ssl": "Pou\u017e\u00edv\u00e1 SSL certifik\u00e1t", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no (nepovinn\u00e9)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netgear/translations/de.json b/homeassistant/components/netgear/translations/de.json new file mode 100644 index 00000000000..d1ee1310cad --- /dev/null +++ b/homeassistant/components/netgear/translations/de.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "config": "Verbindungs- oder Anmeldefehler: Bitte \u00fcberpr\u00fcfe deine Konfiguration" + }, + "step": { + "user": { + "data": { + "host": "Host (Optional)", + "password": "Passwort", + "port": "Port (Optional)", + "ssl": "Verwendet ein SSL-Zertifikat", + "username": "Benutzername (Optional)" + }, + "description": "Standardhost: {host}\nStandardport: {port}\nStandardbenutzername: {username}", + "title": "Netgear" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Zu Hause Zeit (Sekunden)" + }, + "description": "Optionale Einstellungen angeben", + "title": "Netgear" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netgear/translations/en.json b/homeassistant/components/netgear/translations/en.json index 64dbeda0d7f..f9c2dbf2c91 100644 --- a/homeassistant/components/netgear/translations/en.json +++ b/homeassistant/components/netgear/translations/en.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Host already configured" + "already_configured": "Device is already configured" }, "error": { "config": "Connection or login error: please check your configuration" @@ -12,21 +12,23 @@ "host": "Host (Optional)", "password": "Password", "port": "Port (Optional)", - "ssl": "Use SSL (Optional)", + "ssl": "Uses an SSL certificate", "username": "Username (Optional)" }, - "description": "Default host: {host}\nDefault port: {port}\nDefault username: {username}" + "description": "Default host: {host}\nDefault port: {port}\nDefault username: {username}", + "title": "Netgear" } } }, "options": { "step": { - "init": { - "description": "Specify optional settings", - "data": { - "consider_home": "Consider home time (seconds)" + "init": { + "data": { + "consider_home": "Consider home time (seconds)" + }, + "description": "Specify optional settings", + "title": "Netgear" } - } } } } \ No newline at end of file diff --git a/homeassistant/components/netgear/translations/es.json b/homeassistant/components/netgear/translations/es.json new file mode 100644 index 00000000000..57054de1c37 --- /dev/null +++ b/homeassistant/components/netgear/translations/es.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "host": "Host (Opcional)", + "password": "Contrase\u00f1a", + "port": "Puerto (Opcional)", + "ssl": "Utiliza un certificado SSL", + "username": "Usuario (Opcional)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netgear/translations/et.json b/homeassistant/components/netgear/translations/et.json new file mode 100644 index 00000000000..ad100c4b83e --- /dev/null +++ b/homeassistant/components/netgear/translations/et.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "config": "\u00dchenduse v\u00f5i sisselogimise viga: kontrolli oma s\u00e4tteid" + }, + "step": { + "user": { + "data": { + "host": "Host (valikuline)", + "password": "Salas\u00f5na", + "port": "Port (valikuline)", + "ssl": "Kasutusel on SSL sert", + "username": "Kasutajanimi (valikuline)" + }, + "description": "Vaikimisi host: {host}\nVaikeport: {port}\nVaikimisi kasutajanimi: {username}", + "title": "Netgear" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Kohaloleku m\u00e4\u00e4ramise aeg (sekundites)" + }, + "description": "Valikuliste s\u00e4tete m\u00e4\u00e4ramine", + "title": "Netgear" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netgear/translations/he.json b/homeassistant/components/netgear/translations/he.json new file mode 100644 index 00000000000..f1f42b6c771 --- /dev/null +++ b/homeassistant/components/netgear/translations/he.json @@ -0,0 +1,26 @@ +{ + "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": { + "host": "\u05de\u05d0\u05e8\u05d7 (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "port": "\u05e4\u05ea\u05d7\u05d4 (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)", + "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9 (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)" + }, + "title": "Netgear" + } + } + }, + "options": { + "step": { + "init": { + "title": "Netgear" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netgear/translations/hu.json b/homeassistant/components/netgear/translations/hu.json new file mode 100644 index 00000000000..64452c9ef58 --- /dev/null +++ b/homeassistant/components/netgear/translations/hu.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "config": "Csatlakoz\u00e1si vagy bejelentkez\u00e9si hiba: k\u00e9rj\u00fck, ellen\u0151rizze a konfigur\u00e1ci\u00f3t" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm (nem k\u00f6telez\u0151)", + "password": "Jelsz\u00f3", + "port": "Port (nem k\u00f6telez\u0151)", + "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v (nem k\u00f6telez\u0151)" + }, + "description": "Alap\u00e9rtelmezett c\u00edm: {host}\nAlap\u00e9rtelmezett port: {port}\nAlap\u00e9rtelmezett felhaszn\u00e1l\u00f3n\u00e9v: {username}", + "title": "Netgear" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Otthoni \u00e1llapotnak tekint\u00e9s (m\u00e1sodperc)" + }, + "description": "Opcion\u00e1lis be\u00e1ll\u00edt\u00e1sok megad\u00e1sa", + "title": "Netgear" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netgear/translations/id.json b/homeassistant/components/netgear/translations/id.json new file mode 100644 index 00000000000..a6a41a5023f --- /dev/null +++ b/homeassistant/components/netgear/translations/id.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "step": { + "user": { + "data": { + "host": "Host (Opsional)", + "password": "Kata Sandi", + "port": "Port (Opsional)", + "ssl": "Menggunakan sertifikat SSL", + "username": "Nama Pengguna (Opsional)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netgear/translations/it.json b/homeassistant/components/netgear/translations/it.json new file mode 100644 index 00000000000..72feece850a --- /dev/null +++ b/homeassistant/components/netgear/translations/it.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "config": "Errore di connessione o di login: controlla la tua configurazione" + }, + "step": { + "user": { + "data": { + "host": "Host (Facoltativo)", + "password": "Password", + "port": "Porta (Facoltativo)", + "ssl": "Utilizza un certificato SSL", + "username": "Nome utente (Facoltativo)" + }, + "description": "Host predefinito: {host}\nPorta predefinita: {port}\nNome utente predefinito: {username}", + "title": "Netgear" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Considera il tempo in casa (secondi)" + }, + "description": "Specificare le impostazioni opzionali", + "title": "Netgear" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netgear/translations/nl.json b/homeassistant/components/netgear/translations/nl.json new file mode 100644 index 00000000000..22ac348af4e --- /dev/null +++ b/homeassistant/components/netgear/translations/nl.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al ingesteld" + }, + "error": { + "config": "Verbindings- of inlogfout; controleer uw configuratie" + }, + "step": { + "user": { + "data": { + "host": "Host (optioneel)", + "password": "Wachtwoord", + "port": "Poort (optioneel)", + "ssl": "Gebruikt een SSL certificaat", + "username": "Gebruikersnaam (optioneel)" + }, + "description": "Standaard host: {host}\nStandaard poort: {port}\nStandaard gebruikersnaam: {username}", + "title": "Netgear" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Overweeg thuis tijd (seconden)" + }, + "description": "Optionele instellingen opgeven", + "title": "Netgear" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netgear/translations/no.json b/homeassistant/components/netgear/translations/no.json new file mode 100644 index 00000000000..52020ae3824 --- /dev/null +++ b/homeassistant/components/netgear/translations/no.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "config": "Tilkoblings- eller p\u00e5loggingsfeil: Kontroller konfigurasjonen" + }, + "step": { + "user": { + "data": { + "host": "Vert (valgfritt)", + "password": "Passord", + "port": "Port (valgfritt)", + "ssl": "Bruker et SSL-sertifikat", + "username": "Brukernavn (Valgfritt)" + }, + "description": "Standard vert: {host}\nStandardport: {port}\nStandard brukernavn: {username}", + "title": "Netgear" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Vurder hjemmetid (sekunder)" + }, + "description": "Spesifiser valgfrie innstillinger", + "title": "Netgear" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netgear/translations/pt-BR.json b/homeassistant/components/netgear/translations/pt-BR.json new file mode 100644 index 00000000000..ec18c9a65df --- /dev/null +++ b/homeassistant/components/netgear/translations/pt-BR.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "host": "Host (Opcional)", + "password": "Senha", + "port": "Porta (Opcional)", + "ssl": "Utilize um certificado SSL", + "username": "Usu\u00e1rio (Opcional)" + }, + "description": "Host padr\u00e3o: {host}\n Porta padr\u00e3o: {port}\n Usu\u00e1rio padr\u00e3o: {username}", + "title": "Netgear" + } + } + }, + "options": { + "step": { + "init": { + "description": "Especifique configura\u00e7\u00f5es opcionais", + "title": "Netgear" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netgear/translations/ru.json b/homeassistant/components/netgear/translations/ru.json new file mode 100644 index 00000000000..035492a01fe --- /dev/null +++ b/homeassistant/components/netgear/translations/ru.json @@ -0,0 +1,34 @@ +{ + "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": { + "config": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0438\u043b\u0438 \u0432\u0445\u043e\u0434\u0430 \u0432 \u0441\u0438\u0441\u0442\u0435\u043c\u0443. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)" + }, + "description": "\u0425\u043e\u0441\u0442 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: {host}\n\u041f\u043e\u0440\u0442 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: {port}\n\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: {username}", + "title": "Netgear" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "\u0412\u0440\u0435\u043c\u044f, \u0432 \u0442\u0435\u0447\u0435\u043d\u0438\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0441\u0447\u0438\u0442\u0430\u0442\u044c \u0447\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043e\u043c\u0430 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + }, + "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438", + "title": "Netgear" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netgear/translations/zh-Hant.json b/homeassistant/components/netgear/translations/zh-Hant.json new file mode 100644 index 00000000000..a4978fbb6bc --- /dev/null +++ b/homeassistant/components/netgear/translations/zh-Hant.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "config": "\u9023\u7dda\u6216\u767b\u5165\u932f\u8aa4\uff1a\u8acb\u6aa2\u67e5\u60a8\u7684\u8a2d\u5b9a" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef\uff08\u9078\u9805\uff09", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0\uff08\u9078\u9805\uff09", + "ssl": "\u4f7f\u7528 SSL \u8a8d\u8b49", + "username": "\u4f7f\u7528\u8005\u540d\u7a31\uff08\u9078\u9805\uff09" + }, + "description": "\u9810\u8a2d\u4e3b\u6a5f\u7aef\uff1a{host}\n\u9810\u8a2d\u901a\u8a0a\u57e0\uff1a{port}\n\u9810\u8a2d\u4f7f\u7528\u8005\u540d\u7a31\uff1a{username}", + "title": "Netgear" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "\u5224\u5b9a\u5728\u5bb6\u6642\u9593\uff08\u79d2\uff09" + }, + "description": "\u6307\u5b9a\u9078\u9805\u8a2d\u5b9a", + "title": "Netgear" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/translations/es.json b/homeassistant/components/nfandroidtv/translations/es.json index efb7c6a5c8a..880835cfb1e 100644 --- a/homeassistant/components/nfandroidtv/translations/es.json +++ b/homeassistant/components/nfandroidtv/translations/es.json @@ -1,12 +1,17 @@ { "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, "error": { + "cannot_connect": "No se pudo conectar", "unknown": "Error inesperado" }, "step": { "user": { "data": { - "host": "Anfitri\u00f3n" + "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.", "title": "Notificaciones para Android TV / Fire TV" diff --git a/homeassistant/components/nfandroidtv/translations/hu.json b/homeassistant/components/nfandroidtv/translations/hu.json index e7dea95e4d0..c0dc8d679d6 100644 --- a/homeassistant/components/nfandroidtv/translations/hu.json +++ b/homeassistant/components/nfandroidtv/translations/hu.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "H\u00e1zigazda", + "host": "C\u00edm", "name": "N\u00e9v" }, "description": "Ehhez az integr\u00e1ci\u00f3hoz az \u00c9rtes\u00edt\u00e9sek az Android TV alkalmaz\u00e1shoz sz\u00fcks\u00e9ges. \n\nAndroid TV eset\u00e9n: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nA Fire TV eset\u00e9ben: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\nBe kell \u00e1ll\u00edtania a DHCP -foglal\u00e1st az \u00fatv\u00e1laszt\u00f3n (l\u00e1sd az \u00fatv\u00e1laszt\u00f3 felhaszn\u00e1l\u00f3i k\u00e9zik\u00f6nyv\u00e9t), vagy egy statikus IP -c\u00edmet az eszk\u00f6z\u00f6n. Ha nem, az eszk\u00f6z v\u00e9g\u00fcl el\u00e9rhetetlenn\u00e9 v\u00e1lik.", diff --git a/homeassistant/components/nfandroidtv/translations/id.json b/homeassistant/components/nfandroidtv/translations/id.json new file mode 100644 index 00000000000..087e25a22ae --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/id.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/hu.json b/homeassistant/components/nightscout/translations/hu.json index b3e5a36e172..569a4f3aca9 100644 --- a/homeassistant/components/nightscout/translations/hu.json +++ b/homeassistant/components/nightscout/translations/hu.json @@ -15,7 +15,7 @@ "api_key": "API kulcs", "url": "URL" }, - "description": "- URL: a nightcout p\u00e9ld\u00e1ny c\u00edme. Vagyis: https://myhomeassistant.duckdns.org:5423\n - API kulcs (opcion\u00e1lis): Csak akkor haszn\u00e1lja, ha a p\u00e9ld\u00e1nya v\u00e9dett (auth_default_roles! = Olvashat\u00f3).", + "description": "- URL: a nightscout p\u00e9ld\u00e1ny c\u00edme. Pl: https://myhomeassistant.duckdns.org:5423\n - API kulcs (opcion\u00e1lis): Csak akkor haszn\u00e1lja, ha a p\u00e9ld\u00e1nya v\u00e9dett (auth_default_roles != readable).", "title": "Adja meg a Nightscout szerver adatait." } } diff --git a/homeassistant/components/nightscout/translations/id.json b/homeassistant/components/nightscout/translations/id.json index 75496084bc4..147c3131213 100644 --- a/homeassistant/components/nightscout/translations/id.json +++ b/homeassistant/components/nightscout/translations/id.json @@ -15,7 +15,7 @@ "api_key": "Kunci API", "url": "URL" }, - "description": "- URL: alamat instans nightscout Anda, misalnya https://myhomeassistant.duckdns.org:5423\n- Kunci API (Opsional): Hanya gunakan jika instans Anda dilindungi (auth_default_roles != dapat dibaca).", + "description": "- URL: alamat instans nightscout Anda, misalnya https://myhomeassistant.duckdns.org:5423\n- Kunci API (opsional): Hanya gunakan jika instans Anda dilindungi (auth_default_roles != dapat dibaca).", "title": "Masukkan informasi server Nightscout Anda." } } diff --git a/homeassistant/components/nightscout/translations/no.json b/homeassistant/components/nightscout/translations/no.json index d68fe45c684..6a21b13aca7 100644 --- a/homeassistant/components/nightscout/translations/no.json +++ b/homeassistant/components/nightscout/translations/no.json @@ -15,7 +15,7 @@ "api_key": "API-n\u00f8kkel", "url": "URL" }, - "description": "- URL: Adressen til din nattscout-forekomst. F. Eks: https://myhomeassistant.duckdns.org:5423 \n- API-n\u00f8kkel (valgfritt): Bruk bare hvis forekomsten din er beskyttet (auth_default_roles! = readable).", + "description": "- URL: adressen til nightscout -forekomsten din. Dvs: https://myhomeassistant.duckdns.org:5423\n - API -n\u00f8kkel (valgfritt): Bruk bare hvis forekomsten din er beskyttet (auth_default_roles! = Lesbar).", "title": "Skriv inn informasjon om Nightscout-serveren." } } diff --git a/homeassistant/components/nmap_tracker/translations/es.json b/homeassistant/components/nmap_tracker/translations/es.json index d5c3d71321f..212b56a9606 100644 --- a/homeassistant/components/nmap_tracker/translations/es.json +++ b/homeassistant/components/nmap_tracker/translations/es.json @@ -25,6 +25,7 @@ "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.", "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)", "hosts": "Direcciones de red (separadas por comas) para escanear", diff --git a/homeassistant/components/nmap_tracker/translations/hu.json b/homeassistant/components/nmap_tracker/translations/hu.json index e7443f41a0e..7385f12b3df 100644 --- a/homeassistant/components/nmap_tracker/translations/hu.json +++ b/homeassistant/components/nmap_tracker/translations/hu.json @@ -4,7 +4,7 @@ "already_configured": "A hely m\u00e1r konfigur\u00e1lva van" }, "error": { - "invalid_hosts": "\u00c9rv\u00e9nytelen gazdag\u00e9pek" + "invalid_hosts": "\u00c9rv\u00e9nytelen c\u00edmek" }, "step": { "user": { @@ -14,13 +14,13 @@ "hosts": "H\u00e1l\u00f3zati c\u00edmek (vessz\u0151vel elv\u00e1lasztva) a beolvas\u00e1shoz", "scan_options": "Nyersen konfigur\u00e1lhat\u00f3 szkennel\u00e9si lehet\u0151s\u00e9gek az Nmap sz\u00e1m\u00e1ra" }, - "description": "\u00c1ll\u00edtsa be a gazdag\u00e9peket, hogy az Nmap ellen\u0151rizhesse \u0151ket. A h\u00e1l\u00f3zati c\u00edm IP-c\u00edm (192.168.1.1), IP-h\u00e1l\u00f3zat (192.168.0.0/24) vagy IP-tartom\u00e1ny (192.168.1.0-32) lehet." + "description": "\u00c1ll\u00edtsa be a hogy milyen c\u00edmeket szkenneljen az Nmap. A h\u00e1l\u00f3zati c\u00edm lehet IP-c\u00edm (pl. 192.168.1.1), IP-h\u00e1l\u00f3zat (pl. 192.168.0.0/24) vagy IP-tartom\u00e1ny (pl. 192.168.1.0-32)." } } }, "options": { "error": { - "invalid_hosts": "\u00c9rv\u00e9nytelen gazdag\u00e9p" + "invalid_hosts": "\u00c9rv\u00e9nytelen c\u00edmek" }, "step": { "init": { @@ -33,7 +33,7 @@ "scan_options": "Nyersen konfigur\u00e1lhat\u00f3 beolvas\u00e1si lehet\u0151s\u00e9gek az Nmap sz\u00e1m\u00e1ra", "track_new_devices": "\u00daj eszk\u00f6z\u00f6k nyomon k\u00f6vet\u00e9se" }, - "description": "\u00c1ll\u00edtsa be a gazdag\u00e9peket, amelyeket a Nmap ellen\u0151riz. A h\u00e1l\u00f3zati c\u00edm IP-c\u00edm (192.168.1.1), IP-h\u00e1l\u00f3zat (192.168.0.0/24) vagy IP-tartom\u00e1ny (192.168.1.0-32) lehet." + "description": "\u00c1ll\u00edtsa be a hogy milyen c\u00edmeket szkenneljen az Nmap. A h\u00e1l\u00f3zati c\u00edm lehet IP-c\u00edm (pl. 192.168.1.1), IP-h\u00e1l\u00f3zat (pl. 192.168.0.0/24) vagy IP-tartom\u00e1ny (pl. 192.168.1.0-32)." } } }, diff --git a/homeassistant/components/nmap_tracker/translations/id.json b/homeassistant/components/nmap_tracker/translations/id.json index d36ba84e8ac..6c06e815565 100644 --- a/homeassistant/components/nmap_tracker/translations/id.json +++ b/homeassistant/components/nmap_tracker/translations/id.json @@ -8,6 +8,7 @@ "step": { "init": { "data": { + "interval_seconds": "Interval pindai", "track_new_devices": "Lacak perangkat baru" } } diff --git a/homeassistant/components/nmap_tracker/translations/nl.json b/homeassistant/components/nmap_tracker/translations/nl.json index e9675dfc328..8385aca1ffe 100644 --- a/homeassistant/components/nmap_tracker/translations/nl.json +++ b/homeassistant/components/nmap_tracker/translations/nl.json @@ -25,6 +25,7 @@ "step": { "init": { "data": { + "consider_home": "Aantal seconden wachten tot het markeren van een apparaattracker als niet thuis nadat hij niet is gezien.", "exclude": "Netwerkadressen (door komma's gescheiden) om uit te sluiten van scannen", "home_interval": "Minimum aantal minuten tussen scans van actieve apparaten (batterij sparen)", "hosts": "Netwerkadressen (gescheiden door komma's) om te scannen", diff --git a/homeassistant/components/nmap_tracker/translations/ru.json b/homeassistant/components/nmap_tracker/translations/ru.json index b899d63ce83..ba143e20d01 100644 --- a/homeassistant/components/nmap_tracker/translations/ru.json +++ b/homeassistant/components/nmap_tracker/translations/ru.json @@ -25,7 +25,7 @@ "step": { "init": { "data": { - "consider_home": "\u0412\u0440\u0435\u043c\u044f \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445), \u043f\u043e \u0438\u0441\u0442\u0435\u0447\u0435\u043d\u0438\u044e \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u0442\u0430\u0442\u0443\u0441 \"\u041d\u0435 \u0434\u043e\u043c\u0430\"", + "consider_home": "\u0412\u0440\u0435\u043c\u044f \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445), \u043f\u043e \u0438\u0441\u0442\u0435\u0447\u0435\u043d\u0438\u044e \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0441\u0447\u0438\u0442\u0430\u0442\u044c \u0447\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0434\u043e\u043c\u0430", "exclude": "\u0421\u0435\u0442\u0435\u0432\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u0434\u043b\u044f \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0438\u0437 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f (\u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043f\u044f\u0442\u0443\u044e)", "home_interval": "\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043c\u0438\u043d\u0443\u0442 \u043c\u0435\u0436\u0434\u0443 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f\u043c\u0438 \u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 (\u044d\u043a\u043e\u043d\u043e\u043c\u0438\u044f \u0437\u0430\u0440\u044f\u0434\u0430 \u0431\u0430\u0442\u0430\u0440\u0435\u0438)", "hosts": "\u0421\u0435\u0442\u0435\u0432\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u0434\u043b\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f (\u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043f\u044f\u0442\u0443\u044e)", diff --git a/homeassistant/components/notion/translations/ca.json b/homeassistant/components/notion/translations/ca.json index 5d89413d36f..51ca461f854 100644 --- a/homeassistant/components/notion/translations/ca.json +++ b/homeassistant/components/notion/translations/ca.json @@ -1,13 +1,22 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat" + "already_configured": "El compte ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", - "no_devices": "No s'han trobat dispositius al compte" + "no_devices": "No s'han trobat dispositius al compte", + "unknown": "Error inesperat" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrasenya" + }, + "description": "Torna a introduir la contrasenya de {username}.", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, "user": { "data": { "password": "Contrasenya", diff --git a/homeassistant/components/notion/translations/de.json b/homeassistant/components/notion/translations/de.json index 0b421911aa7..59ab1fdc1be 100644 --- a/homeassistant/components/notion/translations/de.json +++ b/homeassistant/components/notion/translations/de.json @@ -1,13 +1,22 @@ { "config": { "abort": { - "already_configured": "Konto wurde bereits konfiguriert" + "already_configured": "Konto wurde bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "invalid_auth": "Ung\u00fcltige Authentifizierung", - "no_devices": "Keine Ger\u00e4te im Konto gefunden" + "no_devices": "Keine Ger\u00e4te im Konto gefunden", + "unknown": "Unerwarteter Fehler" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passwort" + }, + "description": "Bitte gib das Passwort f\u00fcr {username} erneut ein.", + "title": "Integration erneut authentifizieren" + }, "user": { "data": { "password": "Passwort", diff --git a/homeassistant/components/notion/translations/en.json b/homeassistant/components/notion/translations/en.json index 0eb4689bce6..afd58a4d404 100644 --- a/homeassistant/components/notion/translations/en.json +++ b/homeassistant/components/notion/translations/en.json @@ -6,6 +6,7 @@ }, "error": { "invalid_auth": "Invalid authentication", + "no_devices": "No devices found in account", "unknown": "Unexpected error" }, "step": { diff --git a/homeassistant/components/notion/translations/et.json b/homeassistant/components/notion/translations/et.json index a377f1e69ab..7639901201d 100644 --- a/homeassistant/components/notion/translations/et.json +++ b/homeassistant/components/notion/translations/et.json @@ -1,13 +1,22 @@ { "config": { "abort": { - "already_configured": "Konto on juba seadistatud" + "already_configured": "Konto on juba seadistatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "invalid_auth": "Tuvastamise viga", - "no_devices": "Kontolt ei leitud \u00fchtegi seadet" + "no_devices": "Kontolt ei leitud \u00fchtegi seadet", + "unknown": "Ootamatu t\u00f5rge" }, "step": { + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "Sisesta uuesti {username} salas\u00f5na.", + "title": "Taastuvasta sidumine" + }, "user": { "data": { "password": "Salas\u00f5na", diff --git a/homeassistant/components/notion/translations/he.json b/homeassistant/components/notion/translations/he.json index 1a397f894cf..159db09e3b3 100644 --- a/homeassistant/components/notion/translations/he.json +++ b/homeassistant/components/notion/translations/he.json @@ -1,12 +1,20 @@ { "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": { - "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + "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": { + "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/notion/translations/hu.json b/homeassistant/components/notion/translations/hu.json index b4d57f83bb3..43f4f1f914c 100644 --- a/homeassistant/components/notion/translations/hu.json +++ b/homeassistant/components/notion/translations/hu.json @@ -1,13 +1,22 @@ { "config": { "abort": { - "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" }, "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "no_devices": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a fi\u00f3kban" + "no_devices": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a fi\u00f3kban", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "K\u00e9rem, adja meg ism\u00e9t {username} jelszav\u00e1t", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { "password": "Jelsz\u00f3", diff --git a/homeassistant/components/notion/translations/it.json b/homeassistant/components/notion/translations/it.json index 3304d2b395a..69d2294394b 100644 --- a/homeassistant/components/notion/translations/it.json +++ b/homeassistant/components/notion/translations/it.json @@ -1,13 +1,22 @@ { "config": { "abort": { - "already_configured": "L'account \u00e8 gi\u00e0 configurato" + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "invalid_auth": "Autenticazione non valida", - "no_devices": "Nessun dispositivo trovato nell'account" + "no_devices": "Nessun dispositivo trovato nell'account", + "unknown": "Errore imprevisto" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "Inserisci nuovamente la password per {username}.", + "title": "Autenticare nuovamente l'integrazione" + }, "user": { "data": { "password": "Password", diff --git a/homeassistant/components/notion/translations/nl.json b/homeassistant/components/notion/translations/nl.json index acb42046c90..81da85f6240 100644 --- a/homeassistant/components/notion/translations/nl.json +++ b/homeassistant/components/notion/translations/nl.json @@ -1,13 +1,22 @@ { "config": { "abort": { - "already_configured": "Account is al geconfigureerd" + "already_configured": "Account is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { "invalid_auth": "Ongeldige authenticatie", - "no_devices": "Geen apparaten gevonden in account" + "no_devices": "Geen apparaten gevonden in account", + "unknown": "Onverwachte fout" }, "step": { + "reauth_confirm": { + "data": { + "password": "Wachtwoord" + }, + "description": "Voer het wachtwoord voor {username} opnieuw in.", + "title": "Verifieer de integratie opnieuw" + }, "user": { "data": { "password": "Wachtwoord", diff --git a/homeassistant/components/notion/translations/no.json b/homeassistant/components/notion/translations/no.json index c1d8a1d17b5..0bbbeb9c0dd 100644 --- a/homeassistant/components/notion/translations/no.json +++ b/homeassistant/components/notion/translations/no.json @@ -1,13 +1,22 @@ { "config": { "abort": { - "already_configured": "Kontoen er allerede konfigurert" + "already_configured": "Kontoen er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "invalid_auth": "Ugyldig godkjenning", - "no_devices": "Ingen enheter funnet i kontoen" + "no_devices": "Ingen enheter funnet i kontoen", + "unknown": "Uventet feil" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passord" + }, + "description": "Skriv inn passordet for {username} p\u00e5 nytt.", + "title": "Godkjenne integrering p\u00e5 nytt" + }, "user": { "data": { "password": "Passord", diff --git a/homeassistant/components/notion/translations/ru.json b/homeassistant/components/notion/translations/ru.json index 4b9a45bbf3f..bebd8a66e0e 100644 --- a/homeassistant/components/notion/translations/ru.json +++ b/homeassistant/components/notion/translations/ru.json @@ -1,13 +1,22 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", - "no_devices": "\u041d\u0435\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e." + "no_devices": "\u041d\u0435\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e.", + "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": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f {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/notion/translations/zh-Hant.json b/homeassistant/components/notion/translations/zh-Hant.json index 865bd1dbd08..951ec07c8ad 100644 --- a/homeassistant/components/notion/translations/zh-Hant.json +++ b/homeassistant/components/notion/translations/zh-Hant.json @@ -1,13 +1,22 @@ { "config": { "abort": { - "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "no_devices": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u88dd\u7f6e" + "no_devices": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u88dd\u7f6e", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "\u8acb\u8f38\u5165 {username} \u5bc6\u78bc\u3002", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, "user": { "data": { "password": "\u5bc6\u78bc", diff --git a/homeassistant/components/nuheat/translations/de.json b/homeassistant/components/nuheat/translations/de.json index 0ab69dd4557..682867d5404 100644 --- a/homeassistant/components/nuheat/translations/de.json +++ b/homeassistant/components/nuheat/translations/de.json @@ -16,7 +16,7 @@ "serial_number": "Seriennummer des Thermostats.", "username": "Benutzername" }, - "description": "Du musst die numerische Seriennummer oder ID deines Thermostats erhalten, indem du dich bei https://MyNuHeat.com anmeldest und deine Thermostate ausw\u00e4hlst.", + "description": "Du musst die numerische Seriennummer oder ID deines Thermostats erhalten, indem du dich bei https://MyNuHeat.com anmeldest und dein(e) Thermostat(e) ausw\u00e4hlst.", "title": "Stelle eine Verbindung zu NuHeat her" } } diff --git a/homeassistant/components/nuki/translations/hu.json b/homeassistant/components/nuki/translations/hu.json index 7a0b6b6159e..a5da7700b6f 100644 --- a/homeassistant/components/nuki/translations/hu.json +++ b/homeassistant/components/nuki/translations/hu.json @@ -18,7 +18,7 @@ }, "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "port": "Port", "token": "Hozz\u00e1f\u00e9r\u00e9si token" } diff --git a/homeassistant/components/nut/translations/hu.json b/homeassistant/components/nut/translations/hu.json index bfc8e01c11a..aa8f7c37105 100644 --- a/homeassistant/components/nut/translations/hu.json +++ b/homeassistant/components/nut/translations/hu.json @@ -23,7 +23,7 @@ }, "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "password": "Jelsz\u00f3", "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" diff --git a/homeassistant/components/nws/translations/hu.json b/homeassistant/components/nws/translations/hu.json index ec9bf3f4988..4533733e866 100644 --- a/homeassistant/components/nws/translations/hu.json +++ b/homeassistant/components/nws/translations/hu.json @@ -16,7 +16,7 @@ "station": "METAR \u00e1llom\u00e1s k\u00f3dja" }, "description": "Ha a METAR \u00e1llom\u00e1s k\u00f3dja nincs megadva, a sz\u00e9less\u00e9gi \u00e9s hossz\u00fas\u00e1gi fokokat haszn\u00e1lja a legk\u00f6zelebbi \u00e1llom\u00e1s megkeres\u00e9s\u00e9hez. Egyel\u0151re az API-kulcs b\u00e1rmi lehet. Javasoljuk, hogy \u00e9rv\u00e9nyes e -mail c\u00edmet haszn\u00e1ljon.", - "title": "Csatlakozzon az National Weather Service-hez" + "title": "Csatlakoz\u00e1s az National Weather Service-hez" } } } diff --git a/homeassistant/components/nzbget/translations/hu.json b/homeassistant/components/nzbget/translations/hu.json index 829fa03fe8e..6db44f83c28 100644 --- a/homeassistant/components/nzbget/translations/hu.json +++ b/homeassistant/components/nzbget/translations/hu.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "name": "N\u00e9v", "password": "Jelsz\u00f3", "port": "Port", diff --git a/homeassistant/components/nzbget/translations/id.json b/homeassistant/components/nzbget/translations/id.json index af096f4ef5f..585d50dc2f0 100644 --- a/homeassistant/components/nzbget/translations/id.json +++ b/homeassistant/components/nzbget/translations/id.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Gagal terhubung" }, - "flow_title": "NZBGet: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/ondilo_ico/translations/hu.json b/homeassistant/components/ondilo_ico/translations/hu.json index cae1f6d20c0..a6979721779 100644 --- a/homeassistant/components/ondilo_ico/translations/hu.json +++ b/homeassistant/components/ondilo_ico/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", - "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t." + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t." }, "create_entry": { "default": "Sikeres hiteles\u00edt\u00e9s" diff --git a/homeassistant/components/onewire/translations/hu.json b/homeassistant/components/onewire/translations/hu.json index e2c7ffa8c03..4d53659788d 100644 --- a/homeassistant/components/onewire/translations/hu.json +++ b/homeassistant/components/onewire/translations/hu.json @@ -10,7 +10,7 @@ "step": { "owserver": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "port": "Port" }, "title": "Owserver adatok be\u00e1ll\u00edt\u00e1sa" diff --git a/homeassistant/components/onvif/translations/hu.json b/homeassistant/components/onvif/translations/hu.json index c43df53ae9f..f0df008f145 100644 --- a/homeassistant/components/onvif/translations/hu.json +++ b/homeassistant/components/onvif/translations/hu.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", - "no_h264": "Nem voltak el\u00e9rhet\u0151 H264 streamek. Ellen\u0151rizd a profil konfigur\u00e1ci\u00f3j\u00e1t a k\u00e9sz\u00fcl\u00e9ken.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "no_h264": "Nem voltak el\u00e9rhet\u0151 H264 streamek. Ellen\u0151rizze a profil konfigur\u00e1ci\u00f3j\u00e1t a k\u00e9sz\u00fcl\u00e9ken.", "no_mac": "Nem siker\u00fclt konfigur\u00e1lni az egyedi azonos\u00edt\u00f3t az ONVIF eszk\u00f6zh\u00f6z.", - "onvif_error": "Hiba t\u00f6rt\u00e9nt az ONVIF eszk\u00f6z be\u00e1ll\u00edt\u00e1sakor. Tov\u00e1bbi inform\u00e1ci\u00f3k\u00e9rt ellen\u0151rizd a napl\u00f3kat." + "onvif_error": "Hiba t\u00f6rt\u00e9nt az ONVIF eszk\u00f6z be\u00e1ll\u00edt\u00e1sakor. Tov\u00e1bbi inform\u00e1ci\u00f3k\u00e9rt ellen\u0151rizze a napl\u00f3kat." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" @@ -20,7 +20,7 @@ }, "configure": { "data": { - "host": "Gazdag\u00e9p", + "host": "C\u00edm", "name": "N\u00e9v", "password": "Jelsz\u00f3", "port": "Port", @@ -37,13 +37,13 @@ }, "device": { "data": { - "host": "V\u00e1laszd ki a felfedezett ONVIF eszk\u00f6zt" + "host": "V\u00e1lassza ki a felfedezett ONVIF eszk\u00f6zt" }, "title": "ONVIF eszk\u00f6z kiv\u00e1laszt\u00e1sa" }, "manual_input": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "name": "N\u00e9v", "port": "Port" }, @@ -53,7 +53,7 @@ "data": { "auto": "Automatikus keres\u00e9s" }, - "description": "A k\u00fcld\u00e9s gombra kattintva olyan ONVIF-eszk\u00f6z\u00f6ket keres\u00fcnk a h\u00e1l\u00f3zat\u00e1ban, amelyek t\u00e1mogatj\u00e1k az S profilt.\n\nEgyes gy\u00e1rt\u00f3k alap\u00e9rtelmez\u00e9s szerint elkezdt\u00e9k letiltani az ONVIF-et. Ellen\u0151rizze, hogy az ONVIF enged\u00e9lyezve van-e a kamera konfigur\u00e1ci\u00f3j\u00e1ban.", + "description": "A K\u00fcld\u00e9s gombra kattintva olyan ONVIF-eszk\u00f6z\u00f6ket keres\u00fcnk a h\u00e1l\u00f3zat\u00e1ban, amelyek t\u00e1mogatj\u00e1k az S profilt.\n\nEgyes gy\u00e1rt\u00f3k alap\u00e9rtelmez\u00e9s szerint elkezdt\u00e9k letiltani az ONVIF-et. Ellen\u0151rizze, hogy az ONVIF enged\u00e9lyezve van-e a kamera konfigur\u00e1ci\u00f3j\u00e1ban.", "title": "ONVIF eszk\u00f6z be\u00e1ll\u00edt\u00e1sa" } } diff --git a/homeassistant/components/onvif/translations/id.json b/homeassistant/components/onvif/translations/id.json index 3ed50ae63c4..6fcb49dcd99 100644 --- a/homeassistant/components/onvif/translations/id.json +++ b/homeassistant/components/onvif/translations/id.json @@ -18,6 +18,15 @@ }, "title": "Konfigurasikan autentikasi" }, + "configure": { + "data": { + "host": "Host", + "name": "Nama", + "password": "Kata Sandi", + "port": "Port", + "username": "Nama Pengguna" + } + }, "configure_profile": { "data": { "include": "Buat entitas kamera" diff --git a/homeassistant/components/opengarage/translations/ca.json b/homeassistant/components/opengarage/translations/ca.json new file mode 100644 index 00000000000..6a8e611b188 --- /dev/null +++ b/homeassistant/components/opengarage/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "device_key": "Clau del dispositiu", + "host": "Amfitri\u00f3", + "port": "Port", + "verify_ssl": "Verifica el certificat SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opengarage/translations/de.json b/homeassistant/components/opengarage/translations/de.json new file mode 100644 index 00000000000..4e39620a9a9 --- /dev/null +++ b/homeassistant/components/opengarage/translations/de.json @@ -0,0 +1,22 @@ +{ + "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": { + "device_key": "Ger\u00e4teschl\u00fcssel", + "host": "Host", + "port": "Port", + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opengarage/translations/en.json b/homeassistant/components/opengarage/translations/en.json new file mode 100644 index 00000000000..9a103e2a1c0 --- /dev/null +++ b/homeassistant/components/opengarage/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "device_key": "Device key", + "host": "Host", + "port": "Port", + "verify_ssl": "Verify SSL certificate" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opengarage/translations/es.json b/homeassistant/components/opengarage/translations/es.json new file mode 100644 index 00000000000..77ca2a2d001 --- /dev/null +++ b/homeassistant/components/opengarage/translations/es.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "device_key": "Clave del dispositivo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opengarage/translations/et.json b/homeassistant/components/opengarage/translations/et.json new file mode 100644 index 00000000000..eb25c27492b --- /dev/null +++ b/homeassistant/components/opengarage/translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamnie nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "device_key": "Seadme v\u00f5ti", + "host": "Host", + "port": "Port", + "verify_ssl": "Kontrolli SSL serti" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opengarage/translations/hu.json b/homeassistant/components/opengarage/translations/hu.json new file mode 100644 index 00000000000..2c7687261c8 --- /dev/null +++ b/homeassistant/components/opengarage/translations/hu.json @@ -0,0 +1,22 @@ +{ + "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": { + "device_key": "Eszk\u00f6zkulcs", + "host": "C\u00edm", + "port": "Port", + "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opengarage/translations/it.json b/homeassistant/components/opengarage/translations/it.json new file mode 100644 index 00000000000..0bd8adf23e0 --- /dev/null +++ b/homeassistant/components/opengarage/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "device_key": "Chiave del dispositivo", + "host": "Host", + "port": "Porta", + "verify_ssl": "Verificare il certificato SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opengarage/translations/nl.json b/homeassistant/components/opengarage/translations/nl.json new file mode 100644 index 00000000000..96190a06817 --- /dev/null +++ b/homeassistant/components/opengarage/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "device_key": "Apparaatsleutel", + "host": "Host", + "port": "Poort", + "verify_ssl": "SSL-certificaat verifi\u00ebren" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opengarage/translations/no.json b/homeassistant/components/opengarage/translations/no.json new file mode 100644 index 00000000000..5c5189a9de9 --- /dev/null +++ b/homeassistant/components/opengarage/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "device_key": "Enhetsn\u00f8kkel", + "host": "Vert", + "port": "Port", + "verify_ssl": "Verifisere SSL-sertifikat" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opengarage/translations/ru.json b/homeassistant/components/opengarage/translations/ru.json new file mode 100644 index 00000000000..85f528778bf --- /dev/null +++ b/homeassistant/components/opengarage/translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "device_key": "\u041a\u043b\u044e\u0447 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opengarage/translations/zh-Hant.json b/homeassistant/components/opengarage/translations/zh-Hant.json new file mode 100644 index 00000000000..fffbd19b551 --- /dev/null +++ b/homeassistant/components/opengarage/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "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": { + "device_key": "\u88dd\u7f6e\u5bc6\u9470", + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0", + "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/translations/es.json b/homeassistant/components/openuv/translations/es.json index c912cef6c54..014eba04f52 100644 --- a/homeassistant/components/openuv/translations/es.json +++ b/homeassistant/components/openuv/translations/es.json @@ -21,6 +21,10 @@ "options": { "step": { "init": { + "data": { + "from_window": "\u00cdndice UV inicial para la ventana de protecci\u00f3n", + "to_window": "\u00cdndice UV final para la ventana de protecci\u00f3n" + }, "title": "Configurar OpenUV" } } diff --git a/homeassistant/components/openuv/translations/nl.json b/homeassistant/components/openuv/translations/nl.json index 0129e24e304..bd56a1fa4e0 100644 --- a/homeassistant/components/openuv/translations/nl.json +++ b/homeassistant/components/openuv/translations/nl.json @@ -21,6 +21,10 @@ "options": { "step": { "init": { + "data": { + "from_window": "Uv-index starten voor het beschermingsvenster", + "to_window": "Uv-index voor het beveiligingsvenster be\u00ebindigen" + }, "title": "Configureer OpenUV" } } diff --git a/homeassistant/components/openweathermap/translations/hu.json b/homeassistant/components/openweathermap/translations/hu.json index 2fd2f0acc7a..99932ff5c68 100644 --- a/homeassistant/components/openweathermap/translations/hu.json +++ b/homeassistant/components/openweathermap/translations/hu.json @@ -17,7 +17,7 @@ "mode": "M\u00f3d", "name": "Az integr\u00e1ci\u00f3 neve" }, - "description": "Az OpenWeatherMap integr\u00e1ci\u00f3 be\u00e1ll\u00edt\u00e1sa. Az API kulcs l\u00e9trehoz\u00e1s\u00e1hoz menj az https://openweathermap.org/appid oldalra", + "description": "Az OpenWeatherMap integr\u00e1ci\u00f3 be\u00e1ll\u00edt\u00e1sa. Az API kulcs l\u00e9trehoz\u00e1s\u00e1hoz l\u00e1togasson el a https://openweathermap.org/appid oldalra", "title": "OpenWeatherMap" } } diff --git a/homeassistant/components/ovo_energy/translations/ca.json b/homeassistant/components/ovo_energy/translations/ca.json index f8552caa86b..0d0677ec522 100644 --- a/homeassistant/components/ovo_energy/translations/ca.json +++ b/homeassistant/components/ovo_energy/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "error": { - "already_configured": "El compte ja ha estat configurat", + "already_configured": "El compte ja est\u00e0 configurat", "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, diff --git a/homeassistant/components/ovo_energy/translations/id.json b/homeassistant/components/ovo_energy/translations/id.json index 05c38f244e7..fa072b59236 100644 --- a/homeassistant/components/ovo_energy/translations/id.json +++ b/homeassistant/components/ovo_energy/translations/id.json @@ -5,7 +5,7 @@ "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid" }, - "flow_title": "OVO Energy: {username}", + "flow_title": "{username}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/owntracks/translations/hu.json b/homeassistant/components/owntracks/translations/hu.json index f103fc9bbe1..84a40a1a593 100644 --- a/homeassistant/components/owntracks/translations/hu.json +++ b/homeassistant/components/owntracks/translations/hu.json @@ -4,11 +4,11 @@ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "create_entry": { - "default": "\n\nAndroidon, nyisd meg [az OwnTracks appot]({android_url}), menj a preferences -> connectionre. V\u00e1ltoztasd meg a al\u00e1bbi be\u00e1ll\u00edt\u00e1sokat:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\niOS-en, nyisd meg [az OwnTracks appot]({ios_url}), kattints az (i) ikonra bal oldalon fel\u00fcl -> settings. V\u00e1ltoztasd meg az al\u00e1bbi be\u00e1ll\u00edt\u00e1sokat:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\nN\u00e9zd meg [a dokument\u00e1ci\u00f3t]({docs_url}) tov\u00e1bbi inform\u00e1ci\u00f3k\u00e9rt." + "default": "\n\nAndroidon, nyissa meg [az OwnTracks appot]({android_url}), majd v\u00e1lassza ki a Preferences -> Connection men\u00fct. V\u00e1ltoztassa meg az al\u00e1bbi be\u00e1ll\u00edt\u00e1sokat:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\niOS-en, nyissa meg [az OwnTracks appot]({ios_url}), kattintson az (i) ikonra bal oldalon fel\u00fcl -> settings. V\u00e1ltoztassa meg az al\u00e1bbi be\u00e1ll\u00edt\u00e1sokat:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\nN\u00e9zze meg [a dokument\u00e1ci\u00f3t]({docs_url}) tov\u00e1bbi inform\u00e1ci\u00f3k\u00e9rt." }, "step": { "user": { - "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani az Owntracks-t?", + "description": "Biztos benne, hogy be szeretn\u00e9 \u00e1ll\u00edtani az Owntracks-t?", "title": "Owntracks be\u00e1ll\u00edt\u00e1sa" } } diff --git a/homeassistant/components/ozw/translations/ca.json b/homeassistant/components/ozw/translations/ca.json index 835c16eb449..9c9fc17e58e 100644 --- a/homeassistant/components/ozw/translations/ca.json +++ b/homeassistant/components/ozw/translations/ca.json @@ -32,7 +32,7 @@ "start_addon": { "data": { "network_key": "Clau de xarxa", - "usb_path": "Ruta del port USB del dispositiu" + "usb_path": "Ruta del dispositiu USB" }, "title": "Introdueix la configuraci\u00f3 del complement OpenZWave" } diff --git a/homeassistant/components/ozw/translations/hu.json b/homeassistant/components/ozw/translations/hu.json index a43f234c909..06d921c86d3 100644 --- a/homeassistant/components/ozw/translations/hu.json +++ b/homeassistant/components/ozw/translations/hu.json @@ -5,7 +5,7 @@ "addon_install_failed": "Nem siker\u00fclt telep\u00edteni az OpenZWave b\u0151v\u00edtm\u00e9nyt.", "addon_set_config_failed": "Nem siker\u00fclt be\u00e1ll\u00edtani az OpenZWave konfigur\u00e1ci\u00f3t.", "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", "mqtt_required": "Az MQTT integr\u00e1ci\u00f3 nincs be\u00e1ll\u00edtva", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, @@ -26,8 +26,8 @@ "data": { "use_addon": "Haszn\u00e1ld az OpenZWave Supervisor b\u0151v\u00edtm\u00e9nyt" }, - "description": "Szeretn\u00e9d haszn\u00e1lni az OpenZWave Supervisor b\u0151v\u00edtm\u00e9nyt?", - "title": "V\u00e1laszd ki a csatlakoz\u00e1si m\u00f3dot" + "description": "Szeretn\u00e9 haszn\u00e1lni az OpenZWave Supervisor b\u0151v\u00edtm\u00e9nyt?", + "title": "V\u00e1lassza ki a csatlakoz\u00e1si m\u00f3dot" }, "start_addon": { "data": { diff --git a/homeassistant/components/p1_monitor/translations/es.json b/homeassistant/components/p1_monitor/translations/es.json index 023a2a9f17c..5c8552d224b 100644 --- a/homeassistant/components/p1_monitor/translations/es.json +++ b/homeassistant/components/p1_monitor/translations/es.json @@ -1,12 +1,13 @@ { "config": { "error": { + "cannot_connect": "No se pudo conectar", "unknown": "Error inesperado" }, "step": { "user": { "data": { - "host": "Anfitri\u00f3n", + "host": "Host", "name": "Nombre" } } diff --git a/homeassistant/components/p1_monitor/translations/hu.json b/homeassistant/components/p1_monitor/translations/hu.json index 80d00e51571..f9025022c6d 100644 --- a/homeassistant/components/p1_monitor/translations/hu.json +++ b/homeassistant/components/p1_monitor/translations/hu.json @@ -7,7 +7,7 @@ "step": { "user": { "data": { - "host": "Gazdag\u00e9p", + "host": "C\u00edm", "name": "N\u00e9v" }, "description": "\u00c1ll\u00edtsa be a P1 monitort az Otthoni asszisztenssel val\u00f3 integr\u00e1ci\u00f3hoz." diff --git a/homeassistant/components/p1_monitor/translations/id.json b/homeassistant/components/p1_monitor/translations/id.json new file mode 100644 index 00000000000..8c96f3ee6cb --- /dev/null +++ b/homeassistant/components/p1_monitor/translations/id.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nama" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/hu.json b/homeassistant/components/panasonic_viera/translations/hu.json index df520bb1ca5..e373a352a45 100644 --- a/homeassistant/components/panasonic_viera/translations/hu.json +++ b/homeassistant/components/panasonic_viera/translations/hu.json @@ -14,7 +14,7 @@ "data": { "pin": "PIN-k\u00f3d" }, - "description": "Add meg a TV-k\u00e9sz\u00fcl\u00e9ken megjelen\u0151 PIN-k\u00f3dot", + "description": "Adja meg a TV-k\u00e9sz\u00fcl\u00e9ken megjelen\u0151 PIN-k\u00f3dot", "title": "P\u00e1ros\u00edt\u00e1s" }, "user": { @@ -22,7 +22,7 @@ "host": "IP c\u00edm", "name": "N\u00e9v" }, - "description": "Add meg a Panasonic Viera TV-hez tartoz\u00f3 IP c\u00edmet", + "description": "Adja meg a Panasonic Viera TV-hez tartoz\u00f3 IP c\u00edmet", "title": "A TV be\u00e1ll\u00edt\u00e1sa" } } diff --git a/homeassistant/components/philips_js/translations/hu.json b/homeassistant/components/philips_js/translations/hu.json index 1fe4811d21e..544b44ee8ee 100644 --- a/homeassistant/components/philips_js/translations/hu.json +++ b/homeassistant/components/philips_js/translations/hu.json @@ -20,7 +20,7 @@ "user": { "data": { "api_version": "API Verzi\u00f3", - "host": "Hoszt" + "host": "C\u00edm" } } } diff --git a/homeassistant/components/pi_hole/translations/hu.json b/homeassistant/components/pi_hole/translations/hu.json index a8f8563da41..71321c4cf85 100644 --- a/homeassistant/components/pi_hole/translations/hu.json +++ b/homeassistant/components/pi_hole/translations/hu.json @@ -15,7 +15,7 @@ "user": { "data": { "api_key": "API kulcs", - "host": "Hoszt", + "host": "C\u00edm", "location": "Elhelyezked\u00e9s", "name": "N\u00e9v", "port": "Port", diff --git a/homeassistant/components/picnic/translations/id.json b/homeassistant/components/picnic/translations/id.json index 0455a5b3b5e..819125c6909 100644 --- a/homeassistant/components/picnic/translations/id.json +++ b/homeassistant/components/picnic/translations/id.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { diff --git a/homeassistant/components/picnic/translations/ko.json b/homeassistant/components/picnic/translations/ko.json new file mode 100644 index 00000000000..fe58774c459 --- /dev/null +++ b/homeassistant/components/picnic/translations/ko.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "country_code": "\uad6d\uac00 \ucf54\ub4dc", + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/translations/ca.json b/homeassistant/components/plaato/translations/ca.json index c4669b219ab..06aa27e5b37 100644 --- a/homeassistant/components/plaato/translations/ca.json +++ b/homeassistant/components/plaato/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat", + "already_configured": "El compte ja est\u00e0 configurat", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.", "webhook_not_internet_accessible": "La teva inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per poder rebre missatges webhook." }, diff --git a/homeassistant/components/plaato/translations/es.json b/homeassistant/components/plaato/translations/es.json index e0b6c767043..d38bf2a8265 100644 --- a/homeassistant/components/plaato/translations/es.json +++ b/homeassistant/components/plaato/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 Plaato Airlock.\n\nCompleta la siguiente informaci\u00f3n:\n\n- URL: `{webhook_url}`\n- M\u00e9todo: POST\n\nEcha un vistazo a [la documentaci\u00f3n]({docs_url}) para m\u00e1s detalles." + "default": "\u00a1Tu Plaato {device_type} 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 Airlock", diff --git a/homeassistant/components/plaato/translations/hu.json b/homeassistant/components/plaato/translations/hu.json index 4778b41e8be..a25c0c35672 100644 --- a/homeassistant/components/plaato/translations/hu.json +++ b/homeassistant/components/plaato/translations/hu.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", - "webhook_not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." + "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, "create_entry": { "default": "A Plaato {device_type} **{device_name}** n\u00e9vvel sikeresen telep\u00edtve lett!" @@ -27,11 +27,11 @@ "device_name": "Eszk\u00f6z neve", "device_type": "A Plaato eszk\u00f6z t\u00edpusa" }, - "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?", + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?", "title": "A Plaato eszk\u00f6z\u00f6k be\u00e1ll\u00edt\u00e1sa" }, "webhook": { - "description": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a webhook funkci\u00f3t a Plaato Airlock-ban. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t] ( {docs_url} ).", + "description": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a webhook funkci\u00f3t a Plaato Airlock-ban. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1ssa a [dokument\u00e1ci\u00f3t]({docs_url}).", "title": "Haszn\u00e1land\u00f3 webhook" } } diff --git a/homeassistant/components/plaato/translations/nl.json b/homeassistant/components/plaato/translations/nl.json index 23fae52b020..7dc3eaf6fb7 100644 --- a/homeassistant/components/plaato/translations/nl.json +++ b/homeassistant/components/plaato/translations/nl.json @@ -27,7 +27,7 @@ "device_name": "Geef uw apparaat een naam", "device_type": "Type Plaato-apparaat" }, - "description": "Wil je beginnen met instellen?", + "description": "Wilt u beginnen met instellen?", "title": "Stel de Plaato-apparaten in" }, "webhook": { diff --git a/homeassistant/components/plant/translations/hu.json b/homeassistant/components/plant/translations/hu.json index 3206ef7064d..ad2061411f5 100644 --- a/homeassistant/components/plant/translations/hu.json +++ b/homeassistant/components/plant/translations/hu.json @@ -5,5 +5,5 @@ "problem": "Probl\u00e9ma" } }, - "title": "N\u00f6v\u00e9ny" + "title": "N\u00f6v\u00e9nyfigyel\u0151" } \ No newline at end of file diff --git a/homeassistant/components/plex/translations/hu.json b/homeassistant/components/plex/translations/hu.json index c0ecbe3e02c..cde11b9c7cc 100644 --- a/homeassistant/components/plex/translations/hu.json +++ b/homeassistant/components/plex/translations/hu.json @@ -3,15 +3,15 @@ "abort": { "all_configured": "Az \u00f6sszes \u00f6sszekapcsolt szerver m\u00e1r konfigur\u00e1lva van", "already_configured": "Ez a Plex szerver m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt", "token_request_timeout": "Token k\u00e9r\u00e9sre sz\u00e1nt id\u0151 lej\u00e1rt", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { - "faulty_credentials": "A hiteles\u00edt\u00e9s sikertelen", - "host_or_token": "Legal\u00e1bb egyet kell megadnia a Gazdag\u00e9p vagy a Token k\u00f6z\u00fcl", - "no_servers": "Nincs szerver csatlakoztatva a fi\u00f3khoz", + "faulty_credentials": "A hiteles\u00edt\u00e9s sikertelen, k\u00e9rem, ellen\u0151rizze a token-t", + "host_or_token": "Legal\u00e1bb egyet kell megadnia a C\u00edm vagy a Token k\u00f6z\u00fcl", + "no_servers": "Nincsenek Plex-fi\u00f3khoz kapcsol\u00f3d\u00f3 kiszolg\u00e1l\u00f3k", "not_found": "A Plex szerver nem tal\u00e1lhat\u00f3", "ssl_error": "SSL tan\u00fas\u00edtv\u00e1ny probl\u00e9ma" }, @@ -19,7 +19,7 @@ "step": { "manual_setup": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "port": "Port", "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", "token": "Token (opcion\u00e1lis)", diff --git a/homeassistant/components/plugwise/translations/id.json b/homeassistant/components/plugwise/translations/id.json index 9047bf477bd..f3eeb0926ba 100644 --- a/homeassistant/components/plugwise/translations/id.json +++ b/homeassistant/components/plugwise/translations/id.json @@ -8,7 +8,7 @@ "invalid_auth": "Autentikasi tidak valid", "unknown": "Kesalahan yang tidak diharapkan" }, - "flow_title": "Smile: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/plum_lightpad/translations/ca.json b/homeassistant/components/plum_lightpad/translations/ca.json index 86f649d57d7..c1854b868e6 100644 --- a/homeassistant/components/plum_lightpad/translations/ca.json +++ b/homeassistant/components/plum_lightpad/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat" + "already_configured": "El compte ja est\u00e0 configurat" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3" diff --git a/homeassistant/components/point/translations/he.json b/homeassistant/components/point/translations/he.json index 24decb09dd8..a226a9e4c6d 100644 --- a/homeassistant/components/point/translations/he.json +++ b/homeassistant/components/point/translations/he.json @@ -3,7 +3,8 @@ "abort": { "already_setup": "\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.", "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.", - "no_flows": "\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." + "no_flows": "\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.", + "unknown_authorize_url_generation": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4 \u05d1\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05e9\u05dc \u05d4\u05e8\u05e9\u05d0\u05d4." }, "create_entry": { "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4" diff --git a/homeassistant/components/point/translations/hu.json b/homeassistant/components/point/translations/hu.json index 17dc73a189b..c582bbfc7cd 100644 --- a/homeassistant/components/point/translations/hu.json +++ b/homeassistant/components/point/translations/hu.json @@ -4,26 +4,26 @@ "already_setup": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", "external_setup": "A pont sikeresen konfigur\u00e1lva van egy m\u00e1sik folyamatb\u00f3l.", - "no_flows": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", + "no_flows": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "unknown_authorize_url_generation": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n." }, "create_entry": { "default": "Sikeres hiteles\u00edt\u00e9s" }, "error": { - "follow_link": "K\u00e9rlek, k\u00f6vesd a hivatkoz\u00e1st \u00e9s hiteles\u00edtsd magad miel\u0151tt megnyomod a K\u00fcld\u00e9s gombot", + "follow_link": "K\u00e9rem, k\u00f6vesse a hivatkoz\u00e1st \u00e9s hiteles\u00edtse mag\u00e1t miel\u0151tt megnyomn\u00e1 a K\u00fcld\u00e9s gombot", "no_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token" }, "step": { "auth": { - "description": "K\u00e9rlek k\u00f6vesd az al\u00e1bbi linket \u00e9s a **Fogadd el** a hozz\u00e1f\u00e9r\u00e9st a Minut fi\u00f3kj\u00e1hoz, majd t\u00e9rj vissza \u00e9s nyomd meg a **K\u00fcld\u00e9s ** gombot. \n\n [Link]({authorization_url})", + "description": "K\u00e9rem k\u00f6vesse az al\u00e1bbi linket \u00e9s a **Fogadja el** a hozz\u00e1f\u00e9r\u00e9st a Minut fi\u00f3kj\u00e1hoz, majd t\u00e9rjen vissza \u00e9s nyomja meg a **K\u00fcld\u00e9s ** gombot. \n\n [Link]({authorization_url})", "title": "Point hiteles\u00edt\u00e9se" }, "user": { "data": { "flow_impl": "Szolg\u00e1ltat\u00f3" }, - "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?", + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?", "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" } } diff --git a/homeassistant/components/point/translations/nl.json b/homeassistant/components/point/translations/nl.json index 37dae8481eb..f0ab4d8696e 100644 --- a/homeassistant/components/point/translations/nl.json +++ b/homeassistant/components/point/translations/nl.json @@ -23,7 +23,7 @@ "data": { "flow_impl": "Leverancier" }, - "description": "Wil je beginnen met instellen?", + "description": "Wilt u beginnen met instellen?", "title": "Kies een authenticatie methode" } } diff --git a/homeassistant/components/poolsense/translations/hu.json b/homeassistant/components/poolsense/translations/hu.json index 80562b34e28..39274e14c21 100644 --- a/homeassistant/components/poolsense/translations/hu.json +++ b/homeassistant/components/poolsense/translations/hu.json @@ -12,7 +12,7 @@ "email": "E-mail", "password": "Jelsz\u00f3" }, - "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?", + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?", "title": "PoolSense" } } diff --git a/homeassistant/components/poolsense/translations/nl.json b/homeassistant/components/poolsense/translations/nl.json index f88d14e297a..1fd59ebf2ea 100644 --- a/homeassistant/components/poolsense/translations/nl.json +++ b/homeassistant/components/poolsense/translations/nl.json @@ -12,7 +12,7 @@ "email": "E-mail", "password": "Wachtwoord" }, - "description": "Wil je beginnen met instellen?", + "description": "Wilt u beginnen met instellen?", "title": "PoolSense" } } diff --git a/homeassistant/components/powerwall/translations/id.json b/homeassistant/components/powerwall/translations/id.json index a5ae5f5e979..95f8d600901 100644 --- a/homeassistant/components/powerwall/translations/id.json +++ b/homeassistant/components/powerwall/translations/id.json @@ -10,7 +10,7 @@ "unknown": "Kesalahan yang tidak diharapkan", "wrong_version": "Powerwall Anda menggunakan versi perangkat lunak yang tidak didukung. Pertimbangkan untuk memutakhirkan atau melaporkan masalah ini agar dapat diatasi." }, - "flow_title": "Tesla Powerwall ({ip_address})", + "flow_title": "{ip_address}", "step": { "user": { "data": { diff --git a/homeassistant/components/profiler/translations/hu.json b/homeassistant/components/profiler/translations/hu.json index c5d28903888..215cd02307b 100644 --- a/homeassistant/components/profiler/translations/hu.json +++ b/homeassistant/components/profiler/translations/hu.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?" + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" } } } diff --git a/homeassistant/components/profiler/translations/nl.json b/homeassistant/components/profiler/translations/nl.json index 8690611b1c9..8b99a128bd3 100644 --- a/homeassistant/components/profiler/translations/nl.json +++ b/homeassistant/components/profiler/translations/nl.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "Wil je beginnen met instellen?" + "description": "Wilt u beginnen met instellen?" } } } diff --git a/homeassistant/components/progettihwsw/translations/he.json b/homeassistant/components/progettihwsw/translations/he.json index 67c80866be0..4095264c76c 100644 --- a/homeassistant/components/progettihwsw/translations/he.json +++ b/homeassistant/components/progettihwsw/translations/he.json @@ -10,20 +10,20 @@ "step": { "relay_modes": { "data": { - "relay_1": "Relay 1", - "relay_10": "Relay 10", - "relay_11": "Relay 11", - "relay_12": "Relay 12", - "relay_13": "Relay 13", - "relay_15": "Relay 15", - "relay_2": "Relay 2", - "relay_3": "Relay 3", - "relay_4": "Relay 4", - "relay_5": "Relay 5", - "relay_6": "Relay 6", - "relay_7": "Relay 7", - "relay_8": "Relay 8", - "relay_9": "Relay 9" + "relay_1": "\u05de\u05de\u05e1\u05e8 1", + "relay_10": "\u05de\u05de\u05e1\u05e8 10", + "relay_11": "\u05de\u05de\u05e1\u05e8 11", + "relay_12": "\u05de\u05de\u05e1\u05e8 12", + "relay_13": "\u05de\u05de\u05e1\u05e8 13", + "relay_15": "\u05de\u05de\u05e1\u05e8 15", + "relay_2": "\u05de\u05de\u05e1\u05e8 2", + "relay_3": "\u05de\u05de\u05e1\u05e8 3", + "relay_4": "\u05de\u05de\u05e1\u05e8 4", + "relay_5": "\u05de\u05de\u05e1\u05e8 5", + "relay_6": "\u05de\u05de\u05e1\u05e8 6", + "relay_7": "\u05de\u05de\u05e1\u05e8 7", + "relay_8": "\u05de\u05de\u05e1\u05e8 8", + "relay_9": "\u05de\u05de\u05e1\u05e8 9" }, "title": "\u05d4\u05d2\u05d3\u05e8\u05ea \u05de\u05de\u05e1\u05e8\u05d9\u05dd" }, diff --git a/homeassistant/components/progettihwsw/translations/hu.json b/homeassistant/components/progettihwsw/translations/hu.json index fea70ec88ac..84258a6a01b 100644 --- a/homeassistant/components/progettihwsw/translations/hu.json +++ b/homeassistant/components/progettihwsw/translations/hu.json @@ -31,7 +31,7 @@ }, "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "port": "Port" }, "title": "\u00c1ll\u00edtsa be" diff --git a/homeassistant/components/prosegur/translations/el.json b/homeassistant/components/prosegur/translations/el.json new file mode 100644 index 00000000000..c5dee661aa2 --- /dev/null +++ b/homeassistant/components/prosegur/translations/el.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "description": "\u0395\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03bc\u03b5 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc Prosegur." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/es.json b/homeassistant/components/prosegur/translations/es.json index 272b501da30..af4f61d6fdc 100644 --- a/homeassistant/components/prosegur/translations/es.json +++ b/homeassistant/components/prosegur/translations/es.json @@ -1,27 +1,27 @@ { "config": { "abort": { - "already_configured": "El sistema 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 err\u00f3nea", - "unknown": "Error desconocido" + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" }, "step": { "reauth_confirm": { "data": { "description": "Vuelva a autenticarse con su cuenta Prosegur.", "password": "Contrase\u00f1a", - "username": "Nombre de Usuario" + "username": "Usuario" } }, "user": { "data": { "country": "Pa\u00eds", "password": "Contrase\u00f1a", - "username": "Nombre de Usuario" + "username": "Usuario" } } } diff --git a/homeassistant/components/prosegur/translations/id.json b/homeassistant/components/prosegur/translations/id.json new file mode 100644 index 00000000000..9616471c03a --- /dev/null +++ b/homeassistant/components/prosegur/translations/id.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + }, + "user": { + "data": { + "country": "Negara", + "password": "Kata Sandi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/translations/he.json b/homeassistant/components/ps4/translations/he.json index e9543da8206..421f869d8c5 100644 --- a/homeassistant/components/ps4/translations/he.json +++ b/homeassistant/components/ps4/translations/he.json @@ -19,6 +19,9 @@ "region": "\u05d0\u05d9\u05d6\u05d5\u05e8" }, "title": "\u05e4\u05dc\u05d9\u05d9\u05e1\u05d8\u05d9\u05d9\u05e9\u05df 4" + }, + "mode": { + "title": "\u05e4\u05dc\u05d9\u05d9\u05e1\u05d8\u05d9\u05d9\u05e9\u05df 4" } } } diff --git a/homeassistant/components/ps4/translations/hu.json b/homeassistant/components/ps4/translations/hu.json index 97614bcac57..753ea60b282 100644 --- a/homeassistant/components/ps4/translations/hu.json +++ b/homeassistant/components/ps4/translations/hu.json @@ -9,7 +9,7 @@ }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "credential_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s. Az \u00fajraind\u00edt\u00e1shoz nyomja meg a bek\u00fcld\u00e9s gombot.", + "credential_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s. Az \u00fajraind\u00edt\u00e1shoz nyomja meg a K\u00fcld\u00e9s gombot.", "login_failed": "Nem siker\u00fclt p\u00e1ros\u00edtani a PlayStation 4-gyel. Ellen\u0151rizze, hogy a PIN-k\u00f3d helyes-e.", "no_ipaddress": "\u00cdrja be a konfigur\u00e1lni k\u00edv\u00e1nt PlayStation 4 IP c\u00edm\u00e9t" }, @@ -30,7 +30,7 @@ }, "mode": { "data": { - "ip_address": "IP c\u00edm (Hagyd \u00fcresen az Automatikus Felder\u00edt\u00e9s haszn\u00e1lat\u00e1hoz).", + "ip_address": "IP c\u00edm (Hagyja \u00fcresen az Automatikus Felder\u00edt\u00e9s haszn\u00e1lat\u00e1hoz).", "mode": "Konfigur\u00e1ci\u00f3s m\u00f3d" }, "description": "V\u00e1lassza ki a m\u00f3dot a konfigur\u00e1l\u00e1shoz. Az IP c\u00edm mez\u0151 \u00fcresen maradhat, ha az Automatikus felder\u00edt\u00e9s lehet\u0151s\u00e9get v\u00e1lasztja, mivel az eszk\u00f6z\u00f6k automatikusan felfedez\u0151dnek.", diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/hu.json b/homeassistant/components/pvpc_hourly_pricing/translations/hu.json index 1f706862ee1..c654c92969a 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/hu.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/hu.json @@ -24,7 +24,7 @@ "power_p3": "Szerz\u0151d\u00f6tt teljes\u00edtm\u00e9ny P3 v\u00f6lgyid\u0151szakra (kW)", "tariff": "Alkalmazand\u00f3 tarifa f\u00f6ldrajzi z\u00f3n\u00e1k szerint" }, - "description": "Ez az \u00e9rz\u00e9kel\u0151 a hivatalos API-t haszn\u00e1lja a [villamos energia \u00f3r\u00e1nk\u00e9nti \u00e1raz\u00e1s\u00e1nak (PVPC)] (https://www.esios.ree.es/es/pvpc) megszerz\u00e9s\u00e9hez Spanyolorsz\u00e1gban.\n Pontosabb magyar\u00e1zat\u00e9rt keresse fel az [integr\u00e1ci\u00f3s dokumentumok] oldalt (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", + "description": "Ez az \u00e9rz\u00e9kel\u0151 a hivatalos API-t haszn\u00e1lja a [villamos energia \u00f3r\u00e1nk\u00e9nti \u00e1raz\u00e1s\u00e1nak (PVPC)] (https://www.esios.ree.es/es/pvpc) megszerz\u00e9s\u00e9hez Spanyolorsz\u00e1gban.\nPontosabb magyar\u00e1zat\u00e9rt keresse fel az [integr\u00e1ci\u00f3s dokumentumok] oldalt (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", "title": "\u00c9rz\u00e9kel\u0151 be\u00e1ll\u00edt\u00e1sa" } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/id.json b/homeassistant/components/pvpc_hourly_pricing/translations/id.json index 8601c31fda0..9a8a18a7543 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/id.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/id.json @@ -7,10 +7,10 @@ "user": { "data": { "name": "Nama Sensor", - "tariff": "Tarif kontrak (1, 2, atau 3 periode)" + "tariff": "Tarif yang berlaku menurut zona geografis" }, - "description": "Sensor ini menggunakan API resmi untuk mendapatkan [harga listrik per jam (PVPC)](https://www.esios.ree.es/es/pvpc) di Spanyol.\nUntuk penjelasan yang lebih tepat, kunjungi [dokumen integrasi](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\nPilih tarif kontrak berdasarkan jumlah periode penagihan per hari:\n- 1 periode: normal\n- 2 periode: diskriminasi (tarif per malam)\n- 3 periode: mobil listrik (tarif per malam 3 periode)", - "title": "Pemilihan tarif" + "description": "Sensor ini menggunakan API resmi untuk mendapatkan [harga listrik per jam (PVPC)](https://www.esios.ree.es/es/pvpc) di Spanyol.\nUntuk penjelasan yang lebih tepat, kunjungi [dokumen integrasi](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", + "title": "Penyiapan sensor" } } } diff --git a/homeassistant/components/rainforest_eagle/translations/es.json b/homeassistant/components/rainforest_eagle/translations/es.json index 53d9cb6f7c8..08649fda7ec 100644 --- a/homeassistant/components/rainforest_eagle/translations/es.json +++ b/homeassistant/components/rainforest_eagle/translations/es.json @@ -1,12 +1,17 @@ { "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { "user": { "data": { - "host": "Anfitri\u00f3n" + "host": "Host" } } } diff --git a/homeassistant/components/rainforest_eagle/translations/hu.json b/homeassistant/components/rainforest_eagle/translations/hu.json index 10f5a16cd23..c3d489c8eec 100644 --- a/homeassistant/components/rainforest_eagle/translations/hu.json +++ b/homeassistant/components/rainforest_eagle/translations/hu.json @@ -12,7 +12,7 @@ "user": { "data": { "cloud_id": "Cloud ID", - "host": "Gazdag\u00e9p", + "host": "C\u00edm", "install_code": "Telep\u00edt\u00e9si k\u00f3d" } } diff --git a/homeassistant/components/rainforest_eagle/translations/id.json b/homeassistant/components/rainforest_eagle/translations/id.json new file mode 100644 index 00000000000..80db8f3182d --- /dev/null +++ b/homeassistant/components/rainforest_eagle/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": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/hu.json b/homeassistant/components/rainmachine/translations/hu.json index 1ff7dc34b9c..c6120797a72 100644 --- a/homeassistant/components/rainmachine/translations/hu.json +++ b/homeassistant/components/rainmachine/translations/hu.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "ip_address": "Hosztn\u00e9v vagy IP c\u00edm", + "ip_address": "C\u00edm", "password": "Jelsz\u00f3", "port": "Port" }, diff --git a/homeassistant/components/renault/translations/ca.json b/homeassistant/components/renault/translations/ca.json index 4aacab5cfc8..e16cb333acf 100644 --- a/homeassistant/components/renault/translations/ca.json +++ b/homeassistant/components/renault/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat", + "already_configured": "El compte ja est\u00e0 configurat", "kamereon_no_account": "No s'ha pogut trobar cap compte Kamereon", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, diff --git a/homeassistant/components/renault/translations/cs.json b/homeassistant/components/renault/translations/cs.json index d731b4c2ec0..94f2bbd5773 100644 --- a/homeassistant/components/renault/translations/cs.json +++ b/homeassistant/components/renault/translations/cs.json @@ -1,12 +1,19 @@ { "config": { "abort": { - "already_configured": "\u00da\u010det je ji\u017e nastaven" + "already_configured": "\u00da\u010det je ji\u017e nastaven", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { "invalid_credentials": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" }, "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "title": "Znovu ov\u011b\u0159it integraci" + }, "user": { "data": { "password": "Heslo", diff --git a/homeassistant/components/renault/translations/el.json b/homeassistant/components/renault/translations/el.json new file mode 100644 index 00000000000..4f29e856865 --- /dev/null +++ b/homeassistant/components/renault/translations/el.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "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 {username}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/es.json b/homeassistant/components/renault/translations/es.json index 987377770dd..cf0f88983e0 100644 --- a/homeassistant/components/renault/translations/es.json +++ b/homeassistant/components/renault/translations/es.json @@ -1,11 +1,12 @@ { "config": { "abort": { - "already_configured": "La cuenta ya est\u00e1 configurada", - "kamereon_no_account": "No se pudo encontrar la cuenta de Kamereon." + "already_configured": "La cuenta ya ha sido configurada", + "kamereon_no_account": "No se pudo encontrar la cuenta de Kamereon.", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "invalid_credentials": "Autenticaci\u00f3n err\u00f3nea" + "invalid_credentials": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { "kamereon": { @@ -14,11 +15,18 @@ }, "title": "Selecciona el id de la cuenta de Kamereon" }, + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a" + }, + "description": "Por favor, actualiza tu contrase\u00f1a para {username}", + "title": "Volver a autenticar la integraci\u00f3n" + }, "user": { "data": { "locale": "Configuraci\u00f3n regional", "password": "Contrase\u00f1a", - "username": "Correo-e" + "username": "Correo electr\u00f3nico" }, "title": "Establecer las credenciales de Renault" } diff --git a/homeassistant/components/renault/translations/hu.json b/homeassistant/components/renault/translations/hu.json index 9e63117a7c4..d74d8cdf9e4 100644 --- a/homeassistant/components/renault/translations/hu.json +++ b/homeassistant/components/renault/translations/hu.json @@ -19,7 +19,7 @@ "data": { "password": "Jelsz\u00f3" }, - "description": "K\u00e9rj\u00fck, friss\u00edtse a (z) {username} jelszav\u00e1t", + "description": "K\u00e9rj\u00fck, friss\u00edtse {username} jelszav\u00e1t", "title": "Integr\u00e1ci\u00f3 \u00fajb\u00f3li hiteles\u00edt\u00e9se" }, "user": { diff --git a/homeassistant/components/renault/translations/id.json b/homeassistant/components/renault/translations/id.json new file mode 100644 index 00000000000..e1b1f3fc893 --- /dev/null +++ b/homeassistant/components/renault/translations/id.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "invalid_credentials": "Autentikasi tidak valid" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + }, + "description": "Perbarui kata sandi Anda untuk {username}", + "title": "Autentikasi Ulang Integrasi" + }, + "user": { + "data": { + "password": "Kata Sandi", + "username": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/nl.json b/homeassistant/components/renault/translations/nl.json index 1ca9e0ae32b..c2e02b03166 100644 --- a/homeassistant/components/renault/translations/nl.json +++ b/homeassistant/components/renault/translations/nl.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Account is al geconfigureerd", - "kamereon_no_account": "Kan Kamereon-account niet vinden." + "kamereon_no_account": "Kan Kamereon-account niet vinden.", + "reauth_successful": "Opnieuw verifi\u00ebren is gelukt" }, "error": { "invalid_credentials": "Ongeldige authenticatie" @@ -15,7 +16,11 @@ "title": "Selecteer Kamereon-account-ID" }, "reauth_confirm": { - "description": "Werk uw wachtwoord voor {gebruikersnaam} bij" + "data": { + "password": "Wachtwoord" + }, + "description": "Werk uw wachtwoord voor {gebruikersnaam} bij", + "title": "Integratie opnieuw verifi\u00ebren" }, "user": { "data": { diff --git a/homeassistant/components/rfxtrx/translations/ca.json b/homeassistant/components/rfxtrx/translations/ca.json index d7db4107e3b..477bfa14608 100644 --- a/homeassistant/components/rfxtrx/translations/ca.json +++ b/homeassistant/components/rfxtrx/translations/ca.json @@ -23,7 +23,7 @@ }, "setup_serial_manual_path": { "data": { - "device": "Ruta del port USB del dispositiu" + "device": "Ruta del dispositiu USB" }, "title": "Ruta" }, @@ -35,6 +35,16 @@ } } }, + "device_automation": { + "action_type": { + "send_command": "Envia comanda: {subtype}", + "send_status": "Envia comanda d'estat: {subtype}" + }, + "trigger_type": { + "command": "Comanda rebuda: {subtype}", + "status": "Estat rebut: {subtype}" + } + }, "options": { "error": { "already_configured_device": "El dispositiu ja est\u00e0 configurat", diff --git a/homeassistant/components/rfxtrx/translations/de.json b/homeassistant/components/rfxtrx/translations/de.json index 7b006782d96..ee65e371330 100644 --- a/homeassistant/components/rfxtrx/translations/de.json +++ b/homeassistant/components/rfxtrx/translations/de.json @@ -35,6 +35,16 @@ } } }, + "device_automation": { + "action_type": { + "send_command": "Befehl senden: {subtype}", + "send_status": "Statusaktualisierung senden: {subtype}" + }, + "trigger_type": { + "command": "Empfangener Befehl: {subtype}", + "status": "Erhaltener Status: {subtype}" + } + }, "options": { "error": { "already_configured_device": "Ger\u00e4t ist bereits konfiguriert", diff --git a/homeassistant/components/rfxtrx/translations/en.json b/homeassistant/components/rfxtrx/translations/en.json index 69be3726865..2728c189010 100644 --- a/homeassistant/components/rfxtrx/translations/en.json +++ b/homeassistant/components/rfxtrx/translations/en.json @@ -35,6 +35,16 @@ } } }, + "device_automation": { + "action_type": { + "send_command": "Send command: {subtype}", + "send_status": "Send status update: {subtype}" + }, + "trigger_type": { + "command": "Received command: {subtype}", + "status": "Received status: {subtype}" + } + }, "options": { "error": { "already_configured_device": "Device is already configured", @@ -70,16 +80,5 @@ "title": "Configure device options" } } - }, - "device_automation": { - "action_type": { - "send_status": "Send status update: {subtype}", - "send_command": "Send command: {subtype}" - }, - "trigger_type": { - "status": "Received status: {subtype}", - "command": "Received command: {subtype}" - } - }, - "title": "Rfxtrx" -} + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/es.json b/homeassistant/components/rfxtrx/translations/es.json index c1c4d72735c..fa45fe8a777 100644 --- a/homeassistant/components/rfxtrx/translations/es.json +++ b/homeassistant/components/rfxtrx/translations/es.json @@ -35,6 +35,16 @@ } } }, + "device_automation": { + "action_type": { + "send_command": "Enviar comando: {subtype}", + "send_status": "Enviar actualizaci\u00f3n de estado: {subtype}" + }, + "trigger_type": { + "command": "Comando recibido: {subtype}", + "status": "Estado recibido: {subtype}" + } + }, "options": { "error": { "already_configured_device": "El dispositivo ya est\u00e1 configurado", diff --git a/homeassistant/components/rfxtrx/translations/et.json b/homeassistant/components/rfxtrx/translations/et.json index 662664b4454..1b414db656c 100644 --- a/homeassistant/components/rfxtrx/translations/et.json +++ b/homeassistant/components/rfxtrx/translations/et.json @@ -35,6 +35,16 @@ } } }, + "device_automation": { + "action_type": { + "send_command": "Saada k\u00e4sk: {subtype}", + "send_status": "Saada olekuv\u00e4rskendus: {subtype}" + }, + "trigger_type": { + "command": "Saabunud k\u00e4sk: {subtype}", + "status": "Saabunud olek: {subtype}" + } + }, "options": { "error": { "already_configured_device": "Seade on juba h\u00e4\u00e4lestatud", diff --git a/homeassistant/components/rfxtrx/translations/hu.json b/homeassistant/components/rfxtrx/translations/hu.json index 5b953c1260e..86242a4e973 100644 --- a/homeassistant/components/rfxtrx/translations/hu.json +++ b/homeassistant/components/rfxtrx/translations/hu.json @@ -14,7 +14,7 @@ "other": "\u00dcres", "setup_network": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "port": "Port" }, "title": "V\u00e1lassza ki a csatlakoz\u00e1si c\u00edmet" @@ -39,6 +39,16 @@ } } }, + "device_automation": { + "action_type": { + "send_command": "Parancs k\u00fcld\u00e9se: {subtype}", + "send_status": "\u00c1llapotfriss\u00edt\u00e9s k\u00fcld\u00e9se: {subtype}" + }, + "trigger_type": { + "command": "Be\u00e9rkezett parancs: {alt\u00edpus}", + "status": "Be\u00e9rkezett st\u00e1tusz: {subtype}" + } + }, "one": "\u00dcres", "options": { "error": { diff --git a/homeassistant/components/rfxtrx/translations/it.json b/homeassistant/components/rfxtrx/translations/it.json index 4d2ae4710e7..d5bb516b26b 100644 --- a/homeassistant/components/rfxtrx/translations/it.json +++ b/homeassistant/components/rfxtrx/translations/it.json @@ -39,6 +39,16 @@ } } }, + "device_automation": { + "action_type": { + "send_command": "Invia comando: {subtype}", + "send_status": "Invia aggiornamento di stato: {subtype}" + }, + "trigger_type": { + "command": "Comando ricevuto: {subtype}", + "status": "Stato ricevuto: {subtype}" + } + }, "one": "Pi\u00f9", "options": { "error": { diff --git a/homeassistant/components/rfxtrx/translations/nl.json b/homeassistant/components/rfxtrx/translations/nl.json index 1d22751ceed..92154861f15 100644 --- a/homeassistant/components/rfxtrx/translations/nl.json +++ b/homeassistant/components/rfxtrx/translations/nl.json @@ -35,6 +35,16 @@ } } }, + "device_automation": { + "action_type": { + "send_command": "Stuur commando: {subtype}", + "send_status": "Stuur status update: {subtype}" + }, + "trigger_type": { + "command": "Ontvangen commando: {subtype}", + "status": "Ontvangen status: {subtype}" + } + }, "options": { "error": { "already_configured_device": "Apparaat is al geconfigureerd", diff --git a/homeassistant/components/rfxtrx/translations/no.json b/homeassistant/components/rfxtrx/translations/no.json index 3eb9c9b83df..2f867554442 100644 --- a/homeassistant/components/rfxtrx/translations/no.json +++ b/homeassistant/components/rfxtrx/translations/no.json @@ -35,6 +35,16 @@ } } }, + "device_automation": { + "action_type": { + "send_command": "Send kommando: {subtype}", + "send_status": "Send statusoppdatering: {subtype}" + }, + "trigger_type": { + "command": "Mottatt kommando: {subtype}", + "status": "Mottatt status: {subtype}" + } + }, "options": { "error": { "already_configured_device": "Enheten er allerede konfigurert", diff --git a/homeassistant/components/rfxtrx/translations/ru.json b/homeassistant/components/rfxtrx/translations/ru.json index 5a635766d3f..4a56f37687a 100644 --- a/homeassistant/components/rfxtrx/translations/ru.json +++ b/homeassistant/components/rfxtrx/translations/ru.json @@ -35,6 +35,16 @@ } } }, + "device_automation": { + "action_type": { + "send_command": "\u041e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u043a\u043e\u043c\u0430\u043d\u0434\u0443: {subtype}", + "send_status": "\u041e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0441\u0442\u0430\u0442\u0443\u0441\u0430: {subtype}" + }, + "trigger_type": { + "command": "\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u0430\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u0430: {subtype}", + "status": "\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0439 \u0441\u0442\u0430\u0442\u0443\u0441: {subtype}" + } + }, "options": { "error": { "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.", diff --git a/homeassistant/components/rfxtrx/translations/zh-Hant.json b/homeassistant/components/rfxtrx/translations/zh-Hant.json index fbbfeb5d6a0..ec763ece1de 100644 --- a/homeassistant/components/rfxtrx/translations/zh-Hant.json +++ b/homeassistant/components/rfxtrx/translations/zh-Hant.json @@ -35,6 +35,16 @@ } } }, + "device_automation": { + "action_type": { + "send_command": "\u50b3\u9001\u547d\u4ee4\uff1a{subtype}", + "send_status": "\u50b3\u9001\u72c0\u614b\u66f4\u65b0\uff1a{subtype}" + }, + "trigger_type": { + "command": "\u63a5\u6536\u547d\u4ee4\uff1a{subtype}", + "status": "\u63a5\u6536\u72c0\u614b\uff1a{subtype}" + } + }, "options": { "error": { "already_configured_device": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", diff --git a/homeassistant/components/risco/translations/hu.json b/homeassistant/components/risco/translations/hu.json index aaa7974cd4a..198c30d2b02 100644 --- a/homeassistant/components/risco/translations/hu.json +++ b/homeassistant/components/risco/translations/hu.json @@ -27,8 +27,8 @@ "armed_home": "\u00c9les\u00edtve (otthon)", "armed_night": "\u00c9les\u00edtve (\u00e9jszakai)" }, - "description": "V\u00e1lassza ki, hogy milyen \u00e1llapotba \u00e1ll\u00edtsa a Risco riaszt\u00e1st a Home Assistant riaszt\u00e1s \u00e9les\u00edt\u00e9sekor", - "title": "A Home Assistant \u00e1llapotok megjelen\u00edt\u00e9se Risco \u00e1llapotokba" + "description": "V\u00e1lassza ki, hogy milyen \u00e1llapotba \u00e1ll\u00edtsa a Risco riaszt\u00e1st Home Assistant riaszt\u00e1s \u00e9les\u00edt\u00e9sekor", + "title": "Home Assistant \u00e1llapotok megjelen\u00edt\u00e9se Risco \u00e1llapotokba" }, "init": { "data": { diff --git a/homeassistant/components/roku/translations/hu.json b/homeassistant/components/roku/translations/hu.json index b7aa12bfb4d..101931e0d21 100644 --- a/homeassistant/components/roku/translations/hu.json +++ b/homeassistant/components/roku/translations/hu.json @@ -2,20 +2,20 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, - "flow_title": "Roku: {name}", + "flow_title": "{name}", "step": { "discovery_confirm": { "data": { "one": "Egy", "other": "Egy\u00e9b" }, - "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a(z) {name}-t?", + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?", "title": "Roku" }, "ssdp_confirm": { @@ -23,12 +23,12 @@ "one": "\u00dcres", "other": "\u00dcres" }, - "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}-t?", + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?", "title": "Roku" }, "user": { "data": { - "host": "Hoszt" + "host": "C\u00edm" }, "description": "Adja meg Roku adatait." } diff --git a/homeassistant/components/roku/translations/id.json b/homeassistant/components/roku/translations/id.json index 0e60de9b61f..3a227e80eaf 100644 --- a/homeassistant/components/roku/translations/id.json +++ b/homeassistant/components/roku/translations/id.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "Gagal terhubung" }, - "flow_title": "Roku: {name}", + "flow_title": "{name}", "step": { "discovery_confirm": { "description": "Ingin menyiapkan {name}?", diff --git a/homeassistant/components/roomba/translations/es.json b/homeassistant/components/roomba/translations/es.json index c78b66bbb87..315c8bda096 100644 --- a/homeassistant/components/roomba/translations/es.json +++ b/homeassistant/components/roomba/translations/es.json @@ -34,7 +34,7 @@ "blid": "BLID", "host": "Host" }, - "description": "No se ha descubierto ning\u00fan dispositivo Roomba ni Braava en tu red. El BLID es la parte del nombre de host del dispositivo despu\u00e9s de 'iRobot-'. Por favor, sigue los pasos descritos en la documentaci\u00f3n en: {auth_help_url}", + "description": "No se ha descubierto ning\u00fan dispositivo Roomba ni Braava en tu red.", "title": "Conectar manualmente con el dispositivo" }, "user": { diff --git a/homeassistant/components/roomba/translations/hu.json b/homeassistant/components/roomba/translations/hu.json index 0d76ce920b2..34e36f55150 100644 --- a/homeassistant/components/roomba/translations/hu.json +++ b/homeassistant/components/roomba/translations/hu.json @@ -13,7 +13,7 @@ "step": { "init": { "data": { - "host": "Hoszt" + "host": "C\u00edm" }, "description": "V\u00e1lasszon egy Roomba vagy Braava k\u00e9sz\u00fcl\u00e9ket.", "title": "Automatikus csatlakoz\u00e1s az eszk\u00f6zh\u00f6z" @@ -32,9 +32,9 @@ "manual": { "data": { "blid": "BLID", - "host": "Hoszt" + "host": "C\u00edm" }, - "description": "A h\u00e1l\u00f3zaton egyetlen Roomba vagy Braava sem ker\u00fclt el\u0151. A BLID az eszk\u00f6z hostnev\u00e9nek az `iRobot-` vagy `Roomba -` ut\u00e1ni r\u00e9sze. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3ban ismertetett l\u00e9p\u00e9seket: {auth_help_url}", + "description": "A h\u00e1l\u00f3zaton egyetlen Roomba vagy Braava sem ker\u00fclt el\u0151.", "title": "Manu\u00e1lis csatlakoz\u00e1s az eszk\u00f6zh\u00f6z" }, "user": { @@ -42,11 +42,11 @@ "blid": "BLID", "continuous": "Folyamatos", "delay": "K\u00e9sleltet\u00e9s", - "host": "Hoszt", + "host": "C\u00edm", "password": "Jelsz\u00f3" }, "description": "V\u00e1lasszon Roomba-t vagy Braava-t.", - "title": "Csatlakoz\u00e1s az eszk\u00f6zh\u00f6z" + "title": "Automatikus csatlakoz\u00e1s az eszk\u00f6zh\u00f6z" } } }, diff --git a/homeassistant/components/roomba/translations/id.json b/homeassistant/components/roomba/translations/id.json index aaffac267aa..1ade232fd70 100644 --- a/homeassistant/components/roomba/translations/id.json +++ b/homeassistant/components/roomba/translations/id.json @@ -9,7 +9,7 @@ "error": { "cannot_connect": "Gagal terhubung" }, - "flow_title": "iRobot {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "data": { @@ -19,7 +19,7 @@ "title": "Sambungkan secara otomatis ke perangkat" }, "link": { - "description": "Tekan dan tahan tombol Home pada {name} hingga perangkat mengeluarkan suara (sekitar dua detik).", + "description": "Tekan dan tahan tombol Home pada {name} hingga perangkat mengeluarkan suara (sekitar dua detik), lalu kirim dalam waktu 30 detik.", "title": "Ambil Kata Sandi" }, "link_manual": { @@ -34,7 +34,7 @@ "blid": "BLID", "host": "Host" }, - "description": "Tidak ada Roomba atau Braava yang ditemukan di jaringan Anda. BLID adalah bagian dari nama host perangkat setelah `iRobot-`. Ikuti langkah-langkah yang diuraikan dalam dokumentasi di: {auth_help_url}", + "description": "Tidak ada Roomba atau Braava yang ditemukan di jaringan Anda.", "title": "Hubungkan ke perangkat secara manual" }, "user": { @@ -45,8 +45,8 @@ "host": "Host", "password": "Kata Sandi" }, - "description": "Saat ini proses mengambil BLID dan kata sandi merupakan proses manual. Iikuti langkah-langkah yang diuraikan dalam dokumentasi di: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", - "title": "Hubungkan ke perangkat" + "description": "Pilih Roomba atau Braava.", + "title": "Sambungkan secara otomatis ke perangkat" } } }, diff --git a/homeassistant/components/roon/translations/hu.json b/homeassistant/components/roon/translations/hu.json index 56a8ade165c..09bad262c45 100644 --- a/homeassistant/components/roon/translations/hu.json +++ b/homeassistant/components/roon/translations/hu.json @@ -9,14 +9,14 @@ }, "step": { "link": { - "description": "Enged\u00e9lyeznie kell az HomeAssistantot a Roonban. Miut\u00e1n r\u00e1kattintott a K\u00fcld\u00e9s gombra, nyissa meg a Roon Core alkalmaz\u00e1st, nyissa meg a Be\u00e1ll\u00edt\u00e1sokat, \u00e9s enged\u00e9lyezze a HomeAssistant funkci\u00f3t a B\u0151v\u00edtm\u00e9nyek lapon.", - "title": "Enged\u00e9lyezze a HomeAssistant alkalmaz\u00e1st Roon-ban" + "description": "Enged\u00e9lyeznie kell az Home Assistant-ot a Roonban. Miut\u00e1n r\u00e1kattintott a K\u00fcld\u00e9s gombra, nyissa meg a Roon Core alkalmaz\u00e1st, nyissa meg a Be\u00e1ll\u00edt\u00e1sokat, \u00e9s enged\u00e9lyezze a Home Assistant funkci\u00f3t a B\u0151v\u00edtm\u00e9nyek lapon.", + "title": "Enged\u00e9lyezze a Home Assistant alkalmaz\u00e1st Roon-ban" }, "user": { "data": { - "host": "Hoszt" + "host": "C\u00edm" }, - "description": "Nem tal\u00e1lta a Roon szervert, adja meg a gazdag\u00e9p nev\u00e9t vagy IP-c\u00edm\u00e9t." + "description": "A Roon szerver nem tal\u00e1lhat\u00f3, adja meg a hosztnev\u00e9t vagy c\u00edm\u00e9t" } } } diff --git a/homeassistant/components/rpi_power/translations/hu.json b/homeassistant/components/rpi_power/translations/hu.json index feb1687037f..840ce725b8b 100644 --- a/homeassistant/components/rpi_power/translations/hu.json +++ b/homeassistant/components/rpi_power/translations/hu.json @@ -6,9 +6,9 @@ }, "step": { "confirm": { - "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?" + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" } } }, - "title": "Raspberry Pi Power Supply Checker" + "title": "Raspberry Pi t\u00e1pegys\u00e9g ellen\u0151rz\u0151" } \ No newline at end of file diff --git a/homeassistant/components/rpi_power/translations/nl.json b/homeassistant/components/rpi_power/translations/nl.json index 5529aa39f20..d9e42ef11f3 100644 --- a/homeassistant/components/rpi_power/translations/nl.json +++ b/homeassistant/components/rpi_power/translations/nl.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Wil je beginnen met instellen?" + "description": "Wilt u beginnen met instellen?" } } }, diff --git a/homeassistant/components/ruckus_unleashed/translations/hu.json b/homeassistant/components/ruckus_unleashed/translations/hu.json index 0abcc301f0c..9590d3c12be 100644 --- a/homeassistant/components/ruckus_unleashed/translations/hu.json +++ b/homeassistant/components/ruckus_unleashed/translations/hu.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } diff --git a/homeassistant/components/samsungtv/translations/bg.json b/homeassistant/components/samsungtv/translations/bg.json new file mode 100644 index 00000000000..c30e629d8ad --- /dev/null +++ b/homeassistant/components/samsungtv/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/ca.json b/homeassistant/components/samsungtv/translations/ca.json index 64e2298d141..e701bdb1d92 100644 --- a/homeassistant/components/samsungtv/translations/ca.json +++ b/homeassistant/components/samsungtv/translations/ca.json @@ -6,6 +6,7 @@ "auth_missing": "Home Assistant no est\u00e0 autenticat per connectar-se amb aquest televisor Samsung. V\u00e9s a la configuraci\u00f3 de dispositius externs del televisor per autoritzar Home Assistant.", "cannot_connect": "Ha fallat la connexi\u00f3", "id_missing": "El dispositiu Samsung no t\u00e9 cap n\u00famero de s\u00e8rie.", + "missing_config_entry": "Aquest dispositiu Samsung no t\u00e9 cap entrada de configuraci\u00f3.", "not_supported": "Actualment aquest dispositiu Samsung no \u00e9s compatible.", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", "unknown": "Error inesperat" diff --git a/homeassistant/components/samsungtv/translations/de.json b/homeassistant/components/samsungtv/translations/de.json index f59004a5dab..ec5b791626a 100644 --- a/homeassistant/components/samsungtv/translations/de.json +++ b/homeassistant/components/samsungtv/translations/de.json @@ -6,6 +6,7 @@ "auth_missing": "Home Assistant ist nicht berechtigt, eine Verbindung zu diesem Samsung TV herzustellen. \u00dcberpr\u00fcfe den Ger\u00e4teverbindungsmanager in den Einstellungen deines Fernsehger\u00e4ts, um Home Assistant zu autorisieren.", "cannot_connect": "Verbindung fehlgeschlagen", "id_missing": "Dieses Samsung-Ger\u00e4t hat keine Seriennummer.", + "missing_config_entry": "Dieses Samsung-Ger\u00e4t hat keinen Konfigurationseintrag.", "not_supported": "Dieses Samsung TV-Ger\u00e4t wird derzeit nicht unterst\u00fctzt.", "reauth_successful": "Die erneute Authentifizierung war erfolgreich", "unknown": "Unerwarteter Fehler" diff --git a/homeassistant/components/samsungtv/translations/en.json b/homeassistant/components/samsungtv/translations/en.json index 8b48de950ee..4648f930e9b 100644 --- a/homeassistant/components/samsungtv/translations/en.json +++ b/homeassistant/components/samsungtv/translations/en.json @@ -6,6 +6,7 @@ "auth_missing": "Home Assistant is not authorized to connect to this Samsung TV. Check your TV's External Device Manager settings to authorize Home Assistant.", "cannot_connect": "Failed to connect", "id_missing": "This Samsung device doesn't have a SerialNumber.", + "missing_config_entry": "This Samsung device doesn't have a configuration entry.", "not_supported": "This Samsung device is currently not supported.", "reauth_successful": "Re-authentication was successful", "unknown": "Unexpected error" @@ -16,7 +17,8 @@ "flow_title": "{device}", "step": { "confirm": { - "description": "Do you want to set up {device}? If you never connected Home Assistant before you should see a popup on your TV asking for authorization." + "description": "Do you want to set up {device}? If you never connected Home Assistant before you should see a popup on your TV asking for authorization.", + "title": "Samsung TV" }, "reauth_confirm": { "description": "After submitting, accept the the popup on {device} requesting authorization within 30 seconds." diff --git a/homeassistant/components/samsungtv/translations/es.json b/homeassistant/components/samsungtv/translations/es.json index 0228ca3101f..42b0f794e7a 100644 --- a/homeassistant/components/samsungtv/translations/es.json +++ b/homeassistant/components/samsungtv/translations/es.json @@ -6,12 +6,18 @@ "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.", - "not_supported": "Esta televisi\u00f3n Samsung actualmente no es compatible." + "missing_config_entry": "Este dispositivo de Samsung no est\u00e1 configurado.", + "not_supported": "Esta televisi\u00f3n Samsung actualmente no es compatible.", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "unknown": "Error inesperado" }, - "flow_title": "Televisor Samsung: {model}", + "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." + }, + "flow_title": "{device}", "step": { "confirm": { - "description": "\u00bfQuieres configurar la televisi\u00f3n Samsung {model}? Si nunca la has conectado a Home Assistant antes deber\u00edas ver una ventana en tu TV pidiendo autorizaci\u00f3n. Cualquier configuraci\u00f3n manual de esta TV se sobreescribir\u00e1.", + "description": "\u00bfQuieres configurar {device}? Si nunca la has conectado a Home Assistant antes deber\u00edas ver una ventana en tu TV pidiendo autorizaci\u00f3n.", "title": "Samsung TV" }, "reauth_confirm": { diff --git a/homeassistant/components/samsungtv/translations/et.json b/homeassistant/components/samsungtv/translations/et.json index 0cc9bf8ebcc..47360f4ed06 100644 --- a/homeassistant/components/samsungtv/translations/et.json +++ b/homeassistant/components/samsungtv/translations/et.json @@ -6,6 +6,7 @@ "auth_missing": "Home Assistantil pole selle Samsungi teleriga \u00fchenduse loomiseks luba. Home Assistanti autoriseerimiseks kontrolli oma teleri seadeid.", "cannot_connect": "\u00dchendamine nurjus", "id_missing": "Sellel Samsungi seadmel puudub seerianumber.", + "missing_config_entry": "Sellel Samsungi seadmel puudub seadekirje.", "not_supported": "Seda Samsungi seadet praegu ei toetata.", "reauth_successful": "Taastuvastamine \u00f5nnestus", "unknown": "Tundmatu t\u00f5rge" diff --git a/homeassistant/components/samsungtv/translations/hu.json b/homeassistant/components/samsungtv/translations/hu.json index f0aa85433a1..93c0b2bee6d 100644 --- a/homeassistant/components/samsungtv/translations/hu.json +++ b/homeassistant/components/samsungtv/translations/hu.json @@ -2,21 +2,22 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", - "auth_missing": "A Home Assistant nem jogosult csatlakozni ehhez a Samsung TV-hez. Ellen\u0151rizd a TV be\u00e1ll\u00edt\u00e1sait a Home Assistant enged\u00e9lyez\u00e9s\u00e9hez.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "auth_missing": "Home Assistant nem jogosult csatlakozni ehhez a Samsung TV-hez. Ellen\u0151rizze a TV be\u00e1ll\u00edt\u00e1sait Home Assistant enged\u00e9lyez\u00e9s\u00e9hez.", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "id_missing": "Ennek a Samsung eszk\u00f6znek nincs sorsz\u00e1ma.", + "missing_config_entry": "Ez a Samsung eszk\u00f6z nem rendelkezik konfigur\u00e1ci\u00f3s bejegyz\u00e9ssel.", "not_supported": "Ez a Samsung k\u00e9sz\u00fcl\u00e9k jelenleg nem t\u00e1mogatott.", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { - "auth_missing": "A Home Assistant nem jogosult csatlakozni ehhez a Samsung TV-hez. Ellen\u0151rizd a TV be\u00e1ll\u00edt\u00e1sait a Home Assistant enged\u00e9lyez\u00e9s\u00e9hez." + "auth_missing": "Home Assistant nem jogosult csatlakozni ehhez a Samsung TV-hez. Ellen\u0151rizze a TV be\u00e1ll\u00edt\u00e1sait Home Assistant enged\u00e9lyez\u00e9s\u00e9hez." }, "flow_title": "{device}", "step": { "confirm": { - "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a(z) {device} k\u00e9sz\u00fcl\u00e9ket? Ha kor\u00e1bban m\u00e9g csatlakoztattad a Home Assistantet, akkor meg kell jelennie egy felugr\u00f3 ablaknak a TV k\u00e9perny\u0151j\u00e9n, ami j\u00f3v\u00e1hagy\u00e1sra v\u00e1r.", + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani {device} k\u00e9sz\u00fcl\u00e9k\u00e9t? Ha kor\u00e1bban m\u00e9g sosem csatlakoztatta Home Assistant-hoz, akkor meg kell jelennie egy felugr\u00f3 ablaknak a TV k\u00e9perny\u0151j\u00e9n, ami j\u00f3v\u00e1hagy\u00e1sra v\u00e1r.", "title": "Samsung TV" }, "reauth_confirm": { @@ -24,7 +25,7 @@ }, "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "name": "N\u00e9v" }, "description": "\u00cdrd be a Samsung TV adatait. Ha m\u00e9g soha nem csatlakozott Home Assistant-hez, akkor meg kell jelennie egy felugr\u00f3 ablaknak a TV k\u00e9perny\u0151j\u00e9n, ahol hiteles\u00edt\u00e9st k\u00e9r." diff --git a/homeassistant/components/samsungtv/translations/id.json b/homeassistant/components/samsungtv/translations/id.json index 0b8bbe60150..0714af37146 100644 --- a/homeassistant/components/samsungtv/translations/id.json +++ b/homeassistant/components/samsungtv/translations/id.json @@ -3,21 +3,26 @@ "abort": { "already_configured": "Perangkat sudah dikonfigurasi", "already_in_progress": "Alur konfigurasi sedang berlangsung", - "auth_missing": "Home Assistant tidak diizinkan untuk tersambung ke TV Samsung ini. Periksa setelan TV Anda untuk mengotorisasi Home Assistant.", + "auth_missing": "Home Assistant tidak diizinkan untuk tersambung ke TV Samsung ini. Periksa Manajer Perangkat Eksternal TV Anda untuk mengotorisasi Home Assistant.", "cannot_connect": "Gagal terhubung", + "id_missing": "Perangkat Samsung ini tidak memiliki SerialNumber.", + "missing_config_entry": "Perangkat Samsung ini tidak memiliki entri konfigurasi.", "not_supported": "Perangkat TV Samsung ini saat ini tidak didukung.", "reauth_successful": "Autentikasi ulang berhasil", "unknown": "Kesalahan yang tidak diharapkan" }, "error": { - "auth_missing": "Home Assistant tidak diizinkan untuk tersambung ke TV Samsung ini. Periksa setelan TV Anda untuk mengotorisasi Home Assistant." + "auth_missing": "Home Assistant tidak diizinkan untuk tersambung ke TV Samsung ini. Periksa Manajer Perangkat Eksternal TV Anda untuk mengotorisasi Home Assistant." }, - "flow_title": "TV Samsung: {model}", + "flow_title": "{device}", "step": { "confirm": { - "description": "Apakah Anda ingin menyiapkan TV Samsung {model}? Jika Anda belum pernah menyambungkan Home Assistant sebelumnya, Anda akan melihat dialog di TV yang meminta otorisasi. Konfigurasi manual untuk TV ini akan ditimpa.", + "description": "Apakah Anda ingin menyiapkan {device}? Jika Anda belum pernah menyambungkan Home Assistant sebelumnya, Anda akan melihat dialog di TV yang meminta otorisasi.", "title": "TV Samsung" }, + "reauth_confirm": { + "description": "Setelah mengirimkan, setujui pada popup di {device} yang meminta otorisasi dalam waktu 30 detik." + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/samsungtv/translations/it.json b/homeassistant/components/samsungtv/translations/it.json index ee1219305d7..51f9b4e2ef9 100644 --- a/homeassistant/components/samsungtv/translations/it.json +++ b/homeassistant/components/samsungtv/translations/it.json @@ -6,6 +6,7 @@ "auth_missing": "Home Assistant non \u00e8 autorizzato a connettersi a questo televisore Samsung. Controlla le impostazioni di Gestione dispositivi esterni della tua TV per autorizzare Home Assistant.", "cannot_connect": "Impossibile connettersi", "id_missing": "Questo dispositivo Samsung non ha un SerialNumber.", + "missing_config_entry": "Questo dispositivo Samsung non ha una voce di configurazione.", "not_supported": "Questo dispositivo Samsung non \u00e8 attualmente supportato.", "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", "unknown": "Errore imprevisto" diff --git a/homeassistant/components/samsungtv/translations/nl.json b/homeassistant/components/samsungtv/translations/nl.json index b4478994e1c..692c70642d6 100644 --- a/homeassistant/components/samsungtv/translations/nl.json +++ b/homeassistant/components/samsungtv/translations/nl.json @@ -6,6 +6,7 @@ "auth_missing": "Home Assistant is niet gemachtigd om verbinding te maken met deze Samsung TV. Controleer de instellingen van Extern apparaatbeheer van uw tv om Home Assistant te machtigen.", "cannot_connect": "Kan geen verbinding maken", "id_missing": "Dit Samsung-apparaat heeft geen serienummer.", + "missing_config_entry": "Dit Samsung-apparaat heeft geen configuratie-invoer.", "not_supported": "Deze Samsung TV wordt momenteel niet ondersteund.", "reauth_successful": "Herauthenticatie was succesvol", "unknown": "Onverwachte fout" diff --git a/homeassistant/components/samsungtv/translations/no.json b/homeassistant/components/samsungtv/translations/no.json index 6da9787d3f6..7b7108cbf77 100644 --- a/homeassistant/components/samsungtv/translations/no.json +++ b/homeassistant/components/samsungtv/translations/no.json @@ -6,6 +6,7 @@ "auth_missing": "Home Assistant er ikke autorisert til \u00e5 koble til denne Samsung TV-en. Sjekk TV-ens innstillinger for ekstern enhetsbehandling for \u00e5 autorisere Home Assistant.", "cannot_connect": "Tilkobling mislyktes", "id_missing": "Denne Samsung-enheten har ikke serienummer.", + "missing_config_entry": "Denne Samsung -enheten har ingen konfigurasjonsoppf\u00f8ring.", "not_supported": "Denne Samsung-enheten st\u00f8ttes forel\u00f8pig ikke.", "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", "unknown": "Uventet feil" diff --git a/homeassistant/components/samsungtv/translations/ru.json b/homeassistant/components/samsungtv/translations/ru.json index 7d4c24aba45..111b30c5488 100644 --- a/homeassistant/components/samsungtv/translations/ru.json +++ b/homeassistant/components/samsungtv/translations/ru.json @@ -6,6 +6,7 @@ "auth_missing": "Home Assistant \u043d\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u044d\u0442\u043e\u043c\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Samsung TV. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 External Device Manager \u0412\u0430\u0448\u0435\u0433\u043e \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "id_missing": "\u0423 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Samsung \u043d\u0435\u0442 \u0441\u0435\u0440\u0438\u0439\u043d\u043e\u0433\u043e \u043d\u043e\u043c\u0435\u0440\u0430.", + "missing_config_entry": "\u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0437\u0430\u043f\u0438\u0441\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Samsung.", "not_supported": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Samsung \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", "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.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." diff --git a/homeassistant/components/samsungtv/translations/zh-Hant.json b/homeassistant/components/samsungtv/translations/zh-Hant.json index 950a460965b..ba828665cea 100644 --- a/homeassistant/components/samsungtv/translations/zh-Hant.json +++ b/homeassistant/components/samsungtv/translations/zh-Hant.json @@ -6,6 +6,7 @@ "auth_missing": "Home Assistant \u672a\u7372\u5f97\u9a57\u8b49\u4ee5\u9023\u7dda\u81f3\u6b64\u4e09\u661f\u96fb\u8996\u3002\u8acb\u6aa2\u67e5\u60a8\u7684\u96fb\u8996\u5916\u90e8\u88dd\u7f6e\u7ba1\u7406\u54e1\u8a2d\u5b9a\u4ee5\u9032\u884c\u9a57\u8b49\u3002", "cannot_connect": "\u9023\u7dda\u5931\u6557", "id_missing": "\u4e09\u661f\u88dd\u7f6e\u4e26\u672a\u5305\u542b\u5e8f\u865f\u3002", + "missing_config_entry": "\u6b64\u4e09\u661f\u88dd\u7f6e\u4e26\u672a\u5305\u542b\u8a2d\u5b9a\u3002", "not_supported": "\u4e0d\u652f\u63f4\u6b64\u6b3e\u4e09\u661f\u88dd\u7f6e\u3002", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" diff --git a/homeassistant/components/screenlogic/translations/hu.json b/homeassistant/components/screenlogic/translations/hu.json index 3efb8ec6e60..6781f477ab6 100644 --- a/homeassistant/components/screenlogic/translations/hu.json +++ b/homeassistant/components/screenlogic/translations/hu.json @@ -13,7 +13,7 @@ "ip_address": "IP c\u00edm", "port": "Port" }, - "description": "Add meg a ScreenLogic Gateway adatait.", + "description": "Adja meg a ScreenLogic Gateway adatait.", "title": "ScreenLogic" }, "gateway_select": { diff --git a/homeassistant/components/screenlogic/translations/id.json b/homeassistant/components/screenlogic/translations/id.json index 5af1cfbe5ef..b0052f6f0f7 100644 --- a/homeassistant/components/screenlogic/translations/id.json +++ b/homeassistant/components/screenlogic/translations/id.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Gagal terhubung" }, - "flow_title": "ScreenLogic {name}", + "flow_title": "{name}", "step": { "gateway_entry": { "data": { diff --git a/homeassistant/components/sensor/translations/el.json b/homeassistant/components/sensor/translations/el.json index ed9e92cd4b1..ff8750f52dc 100644 --- a/homeassistant/components/sensor/translations/el.json +++ b/homeassistant/components/sensor/translations/el.json @@ -3,7 +3,8 @@ "condition_type": { "is_gas": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03b1\u03ad\u03c1\u03b9\u03bf {entity_name}", "is_pm25": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03b5\u03c0\u03af\u03c0\u03b5\u03b4\u03bf \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 {entity_name} PM2.5", - "is_sulphur_dioxide": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03b5\u03c0\u03af\u03c0\u03b5\u03b4\u03bf \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 \u03b4\u03b9\u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03b8\u03b5\u03af\u03bf\u03c5 {entity_name}" + "is_sulphur_dioxide": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03b5\u03c0\u03af\u03c0\u03b5\u03b4\u03bf \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 \u03b4\u03b9\u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03b8\u03b5\u03af\u03bf\u03c5 {entity_name}", + "is_volatile_organic_compounds": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03bd \u03b5\u03c0\u03af\u03c0\u03b5\u03b4\u03bf \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 \u03c0\u03c4\u03b7\u03c4\u03b9\u03ba\u03ce\u03bd \u03bf\u03c1\u03b3\u03b1\u03bd\u03b9\u03ba\u03ce\u03bd \u03b5\u03bd\u03ce\u03c3\u03b5\u03c9\u03bd {entity_name}" }, "trigger_type": { "gas": "{entity_name} \u03bc\u03b5\u03c4\u03b1\u03b2\u03bf\u03bb\u03ad\u03c2 \u03b1\u03b5\u03c1\u03af\u03bf\u03c5", @@ -14,7 +15,8 @@ "pm1": "{entity_name} \u03bc\u03b5\u03c4\u03b1\u03b2\u03bf\u03bb\u03ad\u03c2 \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 PM1", "pm10": "{entity_name} \u03bc\u03b5\u03c4\u03b1\u03b2\u03bf\u03bb\u03ad\u03c2 \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 PM10", "pm25": "{entity_name} \u03bc\u03b5\u03c4\u03b1\u03b2\u03bf\u03bb\u03ad\u03c2 \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 PM2.5", - "sulphur_dioxide": "{entity_name} \u03bc\u03b5\u03c4\u03b1\u03b2\u03bf\u03bb\u03ad\u03c2 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 \u03b4\u03b9\u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03b8\u03b5\u03af\u03bf\u03c5" + "sulphur_dioxide": "{entity_name} \u03bc\u03b5\u03c4\u03b1\u03b2\u03bf\u03bb\u03ad\u03c2 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7\u03c2 \u03b4\u03b9\u03bf\u03be\u03b5\u03b9\u03b4\u03af\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03b8\u03b5\u03af\u03bf\u03c5", + "volatile_organic_compounds": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ad\u03c2 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03b3\u03ba\u03ad\u03bd\u03c4\u03c1\u03c9\u03c3\u03b7 \u03c4\u03c9\u03bd \u03c0\u03c4\u03b7\u03c4\u03b9\u03ba\u03ce\u03bd \u03bf\u03c1\u03b3\u03b1\u03bd\u03b9\u03ba\u03ce\u03bd \u03b5\u03bd\u03ce\u03c3\u03b5\u03c9\u03bd {entity_name}" } }, "state": { diff --git a/homeassistant/components/sensor/translations/id.json b/homeassistant/components/sensor/translations/id.json index 9af162d1357..cea3f890430 100644 --- a/homeassistant/components/sensor/translations/id.json +++ b/homeassistant/components/sensor/translations/id.json @@ -6,14 +6,24 @@ "is_carbon_monoxide": "Level konsentasi karbonmonoksida {entity_name} saat ini", "is_current": "Arus {entity_name} saat ini", "is_energy": "Energi {entity_name} saat ini", + "is_gas": "Gas {entity_name} saat ini", "is_humidity": "Kelembaban {entity_name} saat ini", "is_illuminance": "Pencahayaan {entity_name} saat ini", + "is_nitrogen_dioxide": "Tingkat konsentrasi nitrogen dioksida {entity_name} saat ini", + "is_nitrogen_monoxide": "Tingkat konsentrasi nitrogen monoksida {entity_name} saat ini", + "is_nitrous_oxide": "Tingkat konsentrasi nitrit oksida {entity_name} saat ini", + "is_ozone": "Tingkat konsentrasi ozon {entity_name} saat ini", + "is_pm1": "Tingkat konsentrasi PM1 {entity_name} saat ini", + "is_pm10": "Tingkat konsentrasi PM10 {entity_name} saat ini", + "is_pm25": "Tingkat konsentrasi PM2.5 {entity_name} saat ini", "is_power": "Daya {entity_name} saat ini", "is_power_factor": "Faktor daya {entity_name} saat ini", "is_pressure": "Tekanan {entity_name} saat ini", "is_signal_strength": "Kekuatan sinyal {entity_name} saat ini", + "is_sulphur_dioxide": "Tingkat konsentrasi sulfur dioksida {entity_name} saat ini", "is_temperature": "Suhu {entity_name} saat ini", "is_value": "Nilai {entity_name} saat ini", + "is_volatile_organic_compounds": "Tingkat konsentrasi senyawa organik volatil {entity_name} saat ini", "is_voltage": "Tegangan {entity_name} saat ini" }, "trigger_type": { @@ -22,14 +32,24 @@ "carbon_monoxide": "Perubahan konsentrasi karbonmonoksida {entity_name}", "current": "Perubahan arus {entity_name}", "energy": "Perubahan energi {entity_name}", + "gas": "Perubahan gas {entity_name}", "humidity": "Perubahan kelembaban {entity_name}", "illuminance": "Perubahan pencahayaan {entity_name}", + "nitrogen_dioxide": "Perubahan konsentrasi nitrogen dioksida {entity_name}", + "nitrogen_monoxide": "Perubahan konsentrasi nitrogen monoksida {entity_name}", + "nitrous_oxide": "Perubahan konsentrasi nitro oksida {entity_name}", + "ozone": "Perubahan konsentrasi ozon {entity_name}", + "pm1": "Perubahan konsentrasi PM1 {entity_name}", + "pm10": "Perubahan konsentrasi PM10 {entity_name}", + "pm25": "Perubahan konsentrasi PM2.5 {entity_name}", "power": "Perubahan daya {entity_name}", "power_factor": "Perubahan faktor daya {entity_name}", "pressure": "Perubahan tekanan {entity_name}", "signal_strength": "Perubahan kekuatan sinyal {entity_name}", + "sulphur_dioxide": "Perubahan konsentrasi sulfur dioksida {entity_name}", "temperature": "Perubahan suhu {entity_name}", "value": "Perubahan nilai {entity_name}", + "volatile_organic_compounds": "Perubahan konsentrasi senyawa organik volatil {entity_name}", "voltage": "Perubahan tegangan {entity_name}" } }, diff --git a/homeassistant/components/sentry/translations/hu.json b/homeassistant/components/sentry/translations/hu.json index df07c41449e..9c28d57eb5d 100644 --- a/homeassistant/components/sentry/translations/hu.json +++ b/homeassistant/components/sentry/translations/hu.json @@ -12,7 +12,7 @@ "data": { "dsn": "DSN" }, - "description": "Add meg a Sentry DSN-t", + "description": "Adja meg a Sentry DSN-t", "title": "Sentry" } } diff --git a/homeassistant/components/sharkiq/translations/ca.json b/homeassistant/components/sharkiq/translations/ca.json index 9ae6a703835..70402446062 100644 --- a/homeassistant/components/sharkiq/translations/ca.json +++ b/homeassistant/components/sharkiq/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat", + "already_configured": "El compte ja est\u00e0 configurat", "cannot_connect": "Ha fallat la connexi\u00f3", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", "unknown": "Error inesperat" diff --git a/homeassistant/components/shelly/translations/ca.json b/homeassistant/components/shelly/translations/ca.json index 13cc79ac3d8..c485d955ff2 100644 --- a/homeassistant/components/shelly/translations/ca.json +++ b/homeassistant/components/shelly/translations/ca.json @@ -33,14 +33,20 @@ "button": "Bot\u00f3", "button1": "Primer bot\u00f3", "button2": "Segon bot\u00f3", - "button3": "Tercer bot\u00f3" + "button3": "Tercer bot\u00f3", + "button4": "Quart bot\u00f3" }, "trigger_type": { + "btn_down": "Bot\u00f3 {subtype} avall", + "btn_up": "Bot\u00f3 {subtype} amunt", "double": "{subtype} clicat dues vegades", + "double_push": "{subtype} clicat dues vegades", "long": "{subtype} clicat durant una estona", + "long_push": "{subtype} clicat durant una estona", "long_single": "{subtype} clicat durant una estona i despr\u00e9s r\u00e0pid", "single": "{subtype} clicat una vegada", "single_long": "{subtype} clicat r\u00e0pid i, despr\u00e9s, durant una estona", + "single_push": "{subtype} clicat una vegada", "triple": "{subtype} clicat tres vegades" } } diff --git a/homeassistant/components/shelly/translations/cs.json b/homeassistant/components/shelly/translations/cs.json index afdfe7c8f56..e3f1215d6f2 100644 --- a/homeassistant/components/shelly/translations/cs.json +++ b/homeassistant/components/shelly/translations/cs.json @@ -33,7 +33,8 @@ "button": "Tla\u010d\u00edtko", "button1": "Prvn\u00ed tla\u010d\u00edtko", "button2": "Druh\u00e9 tla\u010d\u00edtko", - "button3": "T\u0159et\u00ed tla\u010d\u00edtko" + "button3": "T\u0159et\u00ed tla\u010d\u00edtko", + "button4": "\u010ctvrt\u00e9 tla\u010d\u00edtko" }, "trigger_type": { "double": "\"{subtype}\" stisknuto dvakr\u00e1t", diff --git a/homeassistant/components/shelly/translations/de.json b/homeassistant/components/shelly/translations/de.json index 513ff66dff1..3a943507284 100644 --- a/homeassistant/components/shelly/translations/de.json +++ b/homeassistant/components/shelly/translations/de.json @@ -33,14 +33,20 @@ "button": "Taste", "button1": "Erste Taste", "button2": "Zweite Taste", - "button3": "Dritte Taste" + "button3": "Dritte Taste", + "button4": "Vierte Taste" }, "trigger_type": { + "btn_down": "{subtype} Taste nach unten", + "btn_up": "{subtype} Taste nach oben", "double": "{subtype} zweifach bet\u00e4tigt", + "double_push": "{subtype} Doppelter Push", "long": "{subtype} gehalten", + "long_push": "{subtype} langer Push", "long_single": "{subtype} gehalten und dann einfach bet\u00e4tigt", "single": "{subtype} einfach bet\u00e4tigt", "single_long": "{subtype} einfach bet\u00e4tigt und dann gehalten", + "single_push": "{subtype} einzelner Push", "triple": "{subtype} dreifach bet\u00e4tigt" } } diff --git a/homeassistant/components/shelly/translations/en.json b/homeassistant/components/shelly/translations/en.json index 2ed09356363..b48eb630024 100644 --- a/homeassistant/components/shelly/translations/en.json +++ b/homeassistant/components/shelly/translations/en.json @@ -37,17 +37,17 @@ "button4": "Fourth button" }, "trigger_type": { + "btn_down": "{subtype} button down", + "btn_up": "{subtype} button up", "double": "{subtype} double clicked", + "double_push": "{subtype} double push", "long": " {subtype} long clicked", + "long_push": " {subtype} long push", "long_single": "{subtype} long clicked and then single clicked", "single": "{subtype} single clicked", "single_long": "{subtype} single clicked and then long clicked", - "triple": "{subtype} triple clicked", - "btn_down": "{subtype} button down", - "btn_up": "{subtype} button up", "single_push": "{subtype} single push", - "double_push": "{subtype} double push", - "long_push": " {subtype} long push" + "triple": "{subtype} triple clicked" } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/es.json b/homeassistant/components/shelly/translations/es.json index 09cc3f51378..6f5c86417d4 100644 --- a/homeassistant/components/shelly/translations/es.json +++ b/homeassistant/components/shelly/translations/es.json @@ -33,14 +33,20 @@ "button": "Bot\u00f3n", "button1": "Primer bot\u00f3n", "button2": "Segundo bot\u00f3n", - "button3": "Tercer bot\u00f3n" + "button3": "Tercer bot\u00f3n", + "button4": "Cuarto bot\u00f3n" }, "trigger_type": { + "btn_down": "Bot\u00f3n {subtype} pulsado", + "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_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", + "single_push": "Pulsaci\u00f3n simple de {subtype}", "triple": "Pulsaci\u00f3n triple de {subtype}" } } diff --git a/homeassistant/components/shelly/translations/et.json b/homeassistant/components/shelly/translations/et.json index 7059ce6b3d3..7db0eaad4ac 100644 --- a/homeassistant/components/shelly/translations/et.json +++ b/homeassistant/components/shelly/translations/et.json @@ -33,14 +33,20 @@ "button": "Nupp", "button1": "Esimene nupp", "button2": "Teine nupp", - "button3": "Kolmas nupp" + "button3": "Kolmas nupp", + "button4": "Neljas nupp" }, "trigger_type": { + "btn_down": "{subtype} nupp vajutatud", + "btn_up": "{subtype} nupp vabastatud", "double": "Nuppu {subtype} topeltkl\u00f5psati", + "double_push": "{subtype} topeltkl\u00f5ps", "long": "Nuppu \"{subtype}\" hoiti all", + "long_push": "{subtype} pikk vajutus", "long_single": "Nuppu {subtype} hoiti all ja seej\u00e4rel kl\u00f5psati", "single": "Nuppu {subtype} kl\u00f5psati", "single_long": "Nuppu {subtype} kl\u00f5psati \u00fcks kord ja seej\u00e4rel hoiti all", + "single_push": "{subtype} l\u00fchike vajutus", "triple": "Nuppu {subtype} kl\u00f5psati kolm korda" } } diff --git a/homeassistant/components/shelly/translations/he.json b/homeassistant/components/shelly/translations/he.json index a27b19c08e7..5eb76e4b55e 100644 --- a/homeassistant/components/shelly/translations/he.json +++ b/homeassistant/components/shelly/translations/he.json @@ -1,7 +1,8 @@ { "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", + "unsupported_firmware": "\u05d4\u05d4\u05ea\u05e7\u05df \u05de\u05e9\u05ea\u05de\u05e9 \u05d1\u05d2\u05d9\u05e8\u05e1\u05ea \u05e7\u05d5\u05e9\u05d7\u05d4 \u05e9\u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea." }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", @@ -22,8 +23,31 @@ "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7" - } + }, + "description": "\u05dc\u05e4\u05e0\u05d9 \u05d4\u05d4\u05ea\u05e7\u05e0\u05d4, \u05d9\u05e9 \u05dc\u05d4\u05e2\u05d9\u05e8 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d4\u05de\u05d5\u05e4\u05e2\u05dc\u05d9\u05dd \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05e1\u05d5\u05dc\u05dc\u05d4, \u05db\u05e2\u05ea \u05d1\u05d0\u05e4\u05e9\u05e8\u05d5\u05ea\u05da \u05dc\u05d4\u05e2\u05d9\u05e8 \u05d0\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05dc\u05d7\u05d9\u05e6\u05d4 \u05e2\u05dc\u05d9\u05d5." } } + }, + "device_automation": { + "trigger_subtype": { + "button": "\u05dc\u05d7\u05e6\u05df", + "button1": "\u05dc\u05d7\u05e6\u05df \u05e8\u05d0\u05e9\u05d5\u05df", + "button2": "\u05dc\u05d7\u05e6\u05df \u05e9\u05e0\u05d9", + "button3": "\u05dc\u05d7\u05e6\u05df \u05e9\u05dc\u05d9\u05e9\u05d9", + "button4": "\u05dc\u05d7\u05e6\u05df \u05e8\u05d1\u05d9\u05e2\u05d9" + }, + "trigger_type": { + "btn_down": "{subtype} \u05dc\u05d7\u05e6\u05df \u05de\u05d8\u05d4", + "btn_up": "{subtype} \u05dc\u05d7\u05e6\u05df\u05df \u05de\u05e2\u05dc\u05d4", + "double": "{subtype} \u05dc\u05d7\u05d9\u05e6\u05d4 \u05db\u05e4\u05d5\u05dc\u05d4", + "double_push": "{subtype} \u05dc\u05d7\u05d9\u05e6\u05d4 \u05db\u05e4\u05d5\u05dc\u05d4", + "long": "{subtype} \u05dc\u05d7\u05d9\u05e6\u05d4 \u05de\u05de\u05d5\u05e9\u05db\u05ea", + "long_push": "{subtype} \u05dc\u05d7\u05d9\u05e6\u05d4 \u05de\u05de\u05d5\u05e9\u05db\u05ea", + "long_single": "{subtype} \u05dc\u05d7\u05d9\u05e6\u05d4 \u05de\u05de\u05d5\u05e9\u05db\u05ea \u05d5\u05dc\u05d0\u05d7\u05e8 \u05de\u05db\u05df \u05dc\u05d7\u05d9\u05e6\u05d4 \u05d1\u05d5\u05d3\u05d3\u05ea", + "single": "{subtype} \u05dc\u05d7\u05d9\u05e6\u05d4 \u05d1\u05d5\u05d3\u05d3\u05ea", + "single_long": "{subtype} \u05dc\u05d7\u05d9\u05e6\u05d4 \u05d1\u05d5\u05d3\u05d3\u05ea \u05d5\u05dc\u05d0\u05d7\u05e8 \u05de\u05db\u05df \u05dc\u05d7\u05d9\u05e6\u05d4 \u05de\u05de\u05d5\u05e9\u05db\u05ea", + "single_push": "{subtype} \u05dc\u05d7\u05d9\u05e6\u05d4 \u05d1\u05d5\u05d3\u05d3\u05ea", + "triple": "{subtype} \u05dc\u05d7\u05d9\u05e6\u05d4 \u05de\u05e9\u05d5\u05dc\u05e9\u05ea" + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/hu.json b/homeassistant/components/shelly/translations/hu.json index 9388e26515a..bfaf591d7c2 100644 --- a/homeassistant/components/shelly/translations/hu.json +++ b/homeassistant/components/shelly/translations/hu.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "confirm_discovery": { - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani a(z) {model} a(z) {host} c\u00edmen? \n\n A jelsz\u00f3val v\u00e9dett akkumul\u00e1toros eszk\u00f6z\u00f6ket fel kell \u00e9breszteni, miel\u0151tt folytatn\u00e1 a be\u00e1ll\u00edt\u00e1st.\n Az elemmel m\u0171k\u00f6d\u0151, jelsz\u00f3val nem v\u00e9dett eszk\u00f6z\u00f6k hozz\u00e1ad\u00e1sra ker\u00fclnek, amikor az eszk\u00f6z fel\u00e9bred, most manu\u00e1lisan \u00e9bresztheti fel az eszk\u00f6zt egy rajta l\u00e9v\u0151 gombbal, vagy v\u00e1rhat a k\u00f6vetkez\u0151 adatfriss\u00edt\u00e9sre." + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani {model}-t {host} c\u00edmen? \n\nA jelsz\u00f3val v\u00e9dett akkumul\u00e1toros eszk\u00f6z\u00f6ket fel kell \u00e9breszteni, miel\u0151tt folytatn\u00e1 a be\u00e1ll\u00edt\u00e1st.\nAz elemmel m\u0171k\u00f6d\u0151, jelsz\u00f3val nem v\u00e9dett eszk\u00f6z\u00f6k hozz\u00e1ad\u00e1sra ker\u00fclnek, amikor az eszk\u00f6z fel\u00e9bred, most manu\u00e1lisan \u00e9bresztheti fel az eszk\u00f6zt egy rajta l\u00e9v\u0151 gombbal, vagy v\u00e1rhat a k\u00f6vetkez\u0151 adatfriss\u00edt\u00e9sre." }, "credentials": { "data": { @@ -22,7 +22,7 @@ }, "user": { "data": { - "host": "Hoszt" + "host": "C\u00edm" }, "description": "A be\u00e1ll\u00edt\u00e1s el\u0151tt az akkumul\u00e1toros eszk\u00f6z\u00f6ket fel kell \u00e9breszteni, most egy rajta l\u00e9v\u0151 gombbal fel\u00e9bresztheted az eszk\u00f6zt." } @@ -33,14 +33,20 @@ "button": "Gomb", "button1": "Els\u0151 gomb", "button2": "M\u00e1sodik gomb", - "button3": "Harmadik gomb" + "button3": "Harmadik gomb", + "button4": "Negyedik gomb" }, "trigger_type": { + "btn_down": "{subtype} gomb lenyomva", + "btn_up": "{subtype} gomb elengedve", "double": "{subtype} dupla kattint\u00e1s", + "double_push": "{subtype} dupla lenyom\u00e1s", "long": "{subtype} hosszan nyomva", + "long_push": "{subtype} hosszan lenyomva", "long_single": "{subtype} hosszan nyomva, majd egy kattint\u00e1s", "single": "{subtype} egy kattint\u00e1s", "single_long": "{subtype} egy kattint\u00e1s, majd hosszan nyomva", + "single_push": "{subtype} egy lenyom\u00e1s", "triple": "{subtype} tripla kattint\u00e1s" } } diff --git a/homeassistant/components/shelly/translations/id.json b/homeassistant/components/shelly/translations/id.json index 606ee473805..2f385796fd1 100644 --- a/homeassistant/components/shelly/translations/id.json +++ b/homeassistant/components/shelly/translations/id.json @@ -38,9 +38,11 @@ "trigger_type": { "double": "{subtype} diklik dua kali", "long": "{subtype} diklik lama", + "long_push": "Push lama {subtype}", "long_single": "{subtype} diklik lama kemudian diklik sekali", "single": "{subtype} diklik sekali", "single_long": "{subtype} diklik sekali kemudian diklik lama", + "single_push": "Push tunggal {subtype}", "triple": "{subtype} diklik tiga kali" } } diff --git a/homeassistant/components/shelly/translations/it.json b/homeassistant/components/shelly/translations/it.json index 051cf88dc38..c004141cac4 100644 --- a/homeassistant/components/shelly/translations/it.json +++ b/homeassistant/components/shelly/translations/it.json @@ -33,14 +33,20 @@ "button": "Pulsante", "button1": "Primo pulsante", "button2": "Secondo pulsante", - "button3": "Terzo pulsante" + "button3": "Terzo pulsante", + "button4": "Quarto pulsante" }, "trigger_type": { + "btn_down": "{subtype} pulsante in gi\u00f9", + "btn_up": "{subtype} pulsante in su", "double": "{subtype} premuto due volte", + "double_push": "{subtype} doppia pressione", "long": "{subtype} premuto a lungo", + "long_push": "{subtype} pressione prolungata", "long_single": "{subtype} premuto a lungo e poi singolarmente", "single": "{subtype} premuto singolarmente", "single_long": "{subtype} premuto singolarmente e poi a lungo", + "single_push": "{subtype} singola pressione", "triple": "{subtype} premuto tre volte" } } diff --git a/homeassistant/components/shelly/translations/nl.json b/homeassistant/components/shelly/translations/nl.json index 4a58fa31d85..0251e2e7267 100644 --- a/homeassistant/components/shelly/translations/nl.json +++ b/homeassistant/components/shelly/translations/nl.json @@ -33,14 +33,20 @@ "button": "Knop", "button1": "Eerste knop", "button2": "Tweede knop", - "button3": "Derde knop" + "button3": "Derde knop", + "button4": "Vierde knop" }, "trigger_type": { + "btn_down": "{subtype} knop omlaag", + "btn_up": "{subtype} knop omhoog", "double": "{subtype} dubbel geklikt", + "double_push": "{subtype} dubbele druk", "long": "{subtype} lang geklikt", + "long_push": " {subtype} lange druk", "long_single": "{subtype} lang geklikt en daarna \u00e9\u00e9n keer geklikt", "single": "{subtype} enkel geklikt", "single_long": "{subtype} een keer geklikt en daarna lang geklikt", + "single_push": "{subtype} een druk", "triple": "{subtype} driemaal geklikt" } } diff --git a/homeassistant/components/shelly/translations/no.json b/homeassistant/components/shelly/translations/no.json index 90cfe3ca906..dd587e56a6b 100644 --- a/homeassistant/components/shelly/translations/no.json +++ b/homeassistant/components/shelly/translations/no.json @@ -33,14 +33,20 @@ "button": "Knapp", "button1": "F\u00f8rste knapp", "button2": "Andre knapp", - "button3": "Tredje knapp" + "button3": "Tredje knapp", + "button4": "Fjerde knapp" }, "trigger_type": { + "btn_down": "{subtype}-knappen ned", + "btn_up": "{subtype} -knappen opp", "double": "{subtype} dobbeltklikket", + "double_push": "{subtype} dobbelt trykk", "long": "{subtype} lenge klikket", + "long_push": "{subtype} langt trykk", "long_single": "{subtype} lengre klikk og deretter et enkeltklikk", "single": "{subtype} enkeltklikket", "single_long": "{subtype} enkeltklikket og deretter et lengre klikk", + "single_push": "{subtype} enkelt trykk", "triple": "{subtype} trippelklikket" } } diff --git a/homeassistant/components/shelly/translations/ru.json b/homeassistant/components/shelly/translations/ru.json index 9996e347e96..d3f38aa9eeb 100644 --- a/homeassistant/components/shelly/translations/ru.json +++ b/homeassistant/components/shelly/translations/ru.json @@ -24,7 +24,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442" }, - "description": "\u041f\u0435\u0440\u0435\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u0440\u0430\u0431\u043e\u0442\u0430\u044e\u0449\u0438\u0435 \u043e\u0442 \u0431\u0430\u0442\u0430\u0440\u0435\u0438, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u0432\u0435\u0441\u0442\u0438 \u0438\u0437 \u0441\u043f\u044f\u0449\u0435\u0433\u043e \u0440\u0435\u0436\u0438\u043c\u0430, \u043d\u0430\u0436\u0430\u0432 \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435." + "description": "\u0420\u0430\u0431\u043e\u0442\u0430\u044e\u0449\u0438\u0435 \u043e\u0442 \u0431\u0430\u0442\u0430\u0440\u0435\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043f\u0435\u0440\u0435\u0434 \u043d\u0430\u0447\u0430\u043b\u043e\u043c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u044b\u0432\u0435\u0441\u0442\u0438 \u0438\u0437 \u0441\u043f\u044f\u0449\u0435\u0433\u043e \u0440\u0435\u0436\u0438\u043c\u0430. \u042d\u0442\u043e \u043c\u043e\u0436\u043d\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043a\u043d\u043e\u043f\u043a\u0438, \u0440\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u043d\u043e\u0439 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435." } } }, @@ -33,14 +33,20 @@ "button": "\u041a\u043d\u043e\u043f\u043a\u0430", "button1": "\u041f\u0435\u0440\u0432\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "button2": "\u0412\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", - "button3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430" + "button3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430" }, "trigger_type": { + "btn_down": "{subtype} \u0432 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0438 '\u0432\u043d\u0438\u0437'", + "btn_up": "{subtype} \u0432 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0438 '\u0432\u0432\u0435\u0440\u0445'", "double": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430", + "double_push": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430", "long": "{subtype} \u0434\u043e\u043b\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0430", + "long_push": "{subtype} \u0434\u043e\u043b\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0430", "long_single": "{subtype} \u0434\u043e\u043b\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0430 \u0438 \u0437\u0430\u0442\u0435\u043c \u043d\u0430\u0436\u0430\u0442\u0430 \u043e\u0434\u0438\u043d \u0440\u0430\u0437", "single": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u043e\u0434\u0438\u043d \u0440\u0430\u0437", "single_long": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u043e\u0434\u0438\u043d \u0440\u0430\u0437 \u0438 \u0437\u0430\u0442\u0435\u043c \u0434\u043e\u043b\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0430", + "single_push": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u043e\u0434\u0438\u043d \u0440\u0430\u0437", "triple": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430" } } diff --git a/homeassistant/components/shelly/translations/zh-Hant.json b/homeassistant/components/shelly/translations/zh-Hant.json index d0e255560be..bc746ccac2a 100644 --- a/homeassistant/components/shelly/translations/zh-Hant.json +++ b/homeassistant/components/shelly/translations/zh-Hant.json @@ -33,14 +33,20 @@ "button": "\u6309\u9215", "button1": "\u7b2c\u4e00\u500b\u6309\u9215", "button2": "\u7b2c\u4e8c\u500b\u6309\u9215", - "button3": "\u7b2c\u4e09\u500b\u6309\u9215" + "button3": "\u7b2c\u4e09\u500b\u6309\u9215", + "button4": "\u7b2c\u56db\u500b\u6309\u9215" }, "trigger_type": { + "btn_down": "\"{subtype}\" \u6309\u9215\u6309\u4e0b", + "btn_up": "\"{subtype}\" \u6309\u9215\u91cb\u653e", "double": "{subtype} \u96d9\u64ca", + "double_push": "{subtype} \u96d9\u6309", "long": "{subtype} \u9577\u6309", + "long_push": "{subtype} \u9577\u6309", "long_single": "{subtype} \u9577\u6309\u5f8c\u55ae\u64ca", "single": "{subtype} \u55ae\u64ca", "single_long": "{subtype} \u55ae\u64ca\u5f8c\u9577\u6309", + "single_push": "{subtype} \u55ae\u6309", "triple": "{subtype} \u4e09\u9023\u64ca" } } diff --git a/homeassistant/components/shopping_list/translations/hu.json b/homeassistant/components/shopping_list/translations/hu.json index 5f092963da3..27c984ce1ae 100644 --- a/homeassistant/components/shopping_list/translations/hu.json +++ b/homeassistant/components/shopping_list/translations/hu.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a bev\u00e1s\u00e1rl\u00f3list\u00e1t?", + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani a bev\u00e1s\u00e1rl\u00f3list\u00e1t?", "title": "Bev\u00e1s\u00e1rl\u00f3lista" } } diff --git a/homeassistant/components/sia/translations/es.json b/homeassistant/components/sia/translations/es.json index 34ff6847421..8e1bb05978d 100644 --- a/homeassistant/components/sia/translations/es.json +++ b/homeassistant/components/sia/translations/es.json @@ -6,10 +6,18 @@ "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_ping": "El intervalo de ping debe estar entre 1 y 1440 minutos.", - "invalid_zones": "Tiene que haber al menos 1 zona." + "invalid_zones": "Tiene que haber al menos 1 zona.", + "unknown": "Error inesperado" }, "step": { "additional_account": { + "data": { + "account": "ID de la cuenta", + "additional_account": "Cuentas adicionales", + "encryption_key": "Clave de encriptaci\u00f3n", + "ping_interval": "Intervalo de ping (min)", + "zones": "N\u00famero de zonas de la cuenta" + }, "title": "Agrega otra cuenta al puerto actual." }, "user": { @@ -30,7 +38,8 @@ "step": { "options": { "data": { - "ignore_timestamps": "Ignore la verificaci\u00f3n de la marca de tiempo de los eventos SIA" + "ignore_timestamps": "Ignore la verificaci\u00f3n de la marca de tiempo de los eventos SIA", + "zones": "N\u00famero de zonas de la cuenta" }, "description": "Configure las opciones para la cuenta: {account}", "title": "Opciones para la configuraci\u00f3n de SIA." diff --git a/homeassistant/components/sia/translations/id.json b/homeassistant/components/sia/translations/id.json new file mode 100644 index 00000000000..e7ab7918fb3 --- /dev/null +++ b/homeassistant/components/sia/translations/id.json @@ -0,0 +1,50 @@ +{ + "config": { + "error": { + "invalid_account_format": "Format akun ini tidak dalam nilai heksadesimal, gunakan hanya karakter 0-9 dan A-F.", + "invalid_account_length": "Panjang format akun tidak tepat, harus antara 3 dan 16 karakter.", + "invalid_key_format": "Format kunci ini tidak dalam nilai heksadesimal, gunakan hanya karakter 0-9 dan A-F.", + "invalid_key_length": "Panjang format kunci tidak tepat, harus antara 16, 25, atau 32 karakter heksadesimal.", + "invalid_ping": "Interval ping harus antara 1 dan 1440 menit.", + "invalid_zones": "Setidaknya harus ada 1 zona.", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "additional_account": { + "data": { + "account": "ID Akun", + "additional_account": "Akun lainnya", + "encryption_key": "Kunci Enkripsi", + "ping_interval": "Interval Ping (menit)", + "zones": "Jumlah zona untuk akun" + }, + "title": "Tambahkan akun lain ke port saat ini." + }, + "user": { + "data": { + "account": "ID Akun", + "additional_account": "Akun lainnya", + "encryption_key": "Kunci Enkripsi", + "ping_interval": "Interval Ping (menit)", + "port": "Port", + "protocol": "Protokol", + "zones": "Jumlah zona untuk akun" + }, + "title": "Buat koneksi untuk sistem alarm berbasis SIA." + } + } + }, + "options": { + "step": { + "options": { + "data": { + "ignore_timestamps": "Abaikan pemeriksaan stempel waktu peristiwa SIA", + "zones": "Jumlah zona untuk akun" + }, + "description": "Setel opsi untuk akun: {account}", + "title": "Opsi untuk Pengaturan SIA." + } + } + }, + "title": "Sistem Alarm SIA" +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/hu.json b/homeassistant/components/simplisafe/translations/hu.json index f7c1b5afd9d..ed0eb0b2212 100644 --- a/homeassistant/components/simplisafe/translations/hu.json +++ b/homeassistant/components/simplisafe/translations/hu.json @@ -28,7 +28,7 @@ "password": "Jelsz\u00f3", "username": "E-mail" }, - "title": "T\u00f6ltsd ki az adataid" + "title": "T\u00f6ltse ki az adatait" } } }, diff --git a/homeassistant/components/simplisafe/translations/id.json b/homeassistant/components/simplisafe/translations/id.json index 512d6a38405..c9ff0f96bb9 100644 --- a/homeassistant/components/simplisafe/translations/id.json +++ b/homeassistant/components/simplisafe/translations/id.json @@ -19,7 +19,7 @@ "data": { "password": "Kata Sandi" }, - "description": "Token akses Anda telah kedaluwarsa atau dicabut. Masukkan kata sandi Anda untuk menautkan kembali akun Anda.", + "description": "Akses Anda telah kedaluwarsa atau dicabut. Masukkan kata sandi Anda untuk menautkan kembali akun Anda.", "title": "Autentikasi Ulang Integrasi" }, "user": { diff --git a/homeassistant/components/sma/translations/hu.json b/homeassistant/components/sma/translations/hu.json index cab063cd077..f1958dbcc1f 100644 --- a/homeassistant/components/sma/translations/hu.json +++ b/homeassistant/components/sma/translations/hu.json @@ -14,7 +14,7 @@ "user": { "data": { "group": "Csoport", - "host": "Gazdag\u00e9p", + "host": "C\u00edm", "password": "Jelsz\u00f3", "ssl": "SSL tan\u00fas\u00edtv\u00e1nyt haszn\u00e1l", "verify_ssl": "Ellen\u0151rizze az SSL tan\u00fas\u00edtv\u00e1nyt" diff --git a/homeassistant/components/smappee/translations/hu.json b/homeassistant/components/smappee/translations/hu.json index 5b00dffde9c..5b4a83a74b0 100644 --- a/homeassistant/components/smappee/translations/hu.json +++ b/homeassistant/components/smappee/translations/hu.json @@ -6,7 +6,7 @@ "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_mdns": "Nem t\u00e1mogatott eszk\u00f6z a Smappee integr\u00e1ci\u00f3hoz.", - "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz." }, "flow_title": "{name}", @@ -19,9 +19,9 @@ }, "local": { "data": { - "host": "Hoszt" + "host": "C\u00edm" }, - "description": "Adja meg a gazdag\u00e9pet a Smappee helyi integr\u00e1ci\u00f3j\u00e1nak elind\u00edt\u00e1s\u00e1hoz" + "description": "Adja meg a c\u00edmet a Smappee helyi integr\u00e1ci\u00f3j\u00e1nak elind\u00edt\u00e1s\u00e1hoz" }, "pick_implementation": { "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" diff --git a/homeassistant/components/smappee/translations/id.json b/homeassistant/components/smappee/translations/id.json index b72200c34ca..66efc23dcee 100644 --- a/homeassistant/components/smappee/translations/id.json +++ b/homeassistant/components/smappee/translations/id.json @@ -9,7 +9,7 @@ "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})" }, - "flow_title": "Smappee: {name}", + "flow_title": "{name}", "step": { "environment": { "data": { diff --git a/homeassistant/components/smartthings/translations/hu.json b/homeassistant/components/smartthings/translations/hu.json index 05e99bef2ea..90ea748ae33 100644 --- a/homeassistant/components/smartthings/translations/hu.json +++ b/homeassistant/components/smartthings/translations/hu.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "invalid_webhook_url": "A Home Assistant nincs megfelel\u0151en konfigur\u00e1lva a SmartThings friss\u00edt\u00e9seinek fogad\u00e1s\u00e1ra. A webhook URL \u00e9rv\u00e9nytelen:\n > {webhook_url} \n\n K\u00e9rj\u00fck, friss\u00edtse konfigur\u00e1ci\u00f3j\u00e1t az [utas\u00edt\u00e1sok] szerint ({component_url}), ind\u00edtsa \u00fajra a Home Assistant alkalmaz\u00e1st, \u00e9s pr\u00f3b\u00e1lja \u00fajra.", + "invalid_webhook_url": "Home Assistant nincs megfelel\u0151en konfigur\u00e1lva a SmartThings friss\u00edt\u00e9seinek fogad\u00e1s\u00e1ra. A webhook URL \u00e9rv\u00e9nytelen:\n > {webhook_url} \n\nK\u00e9rj\u00fck, friss\u00edtse konfigur\u00e1ci\u00f3j\u00e1t az [utas\u00edt\u00e1sok]({component_url}) szerint, ind\u00edtsa \u00fajra a Home Assistant alkalmaz\u00e1st, \u00e9s pr\u00f3b\u00e1lja \u00fajra.", "no_available_locations": "Nincsenek be\u00e1ll\u00edthat\u00f3 SmartThings helyek a Home Assistant alkalmaz\u00e1sban." }, "error": { - "app_setup_error": "A SmartApp be\u00e1ll\u00edt\u00e1sa nem siker\u00fclt. K\u00e9rlek pr\u00f3b\u00e1ld \u00fajra.", + "app_setup_error": "A SmartApp be\u00e1ll\u00edt\u00e1sa nem siker\u00fclt. K\u00e9rem, pr\u00f3b\u00e1lja \u00fajra.", "token_forbidden": "A token nem rendelkezik a sz\u00fcks\u00e9ges OAuth-tartom\u00e1nyokkal.", "token_invalid_format": "A tokennek UID / GUID form\u00e1tumban kell lennie", "token_unauthorized": "A token \u00e9rv\u00e9nytelen vagy m\u00e1r nem enged\u00e9lyezett.", - "webhook_error": "A SmartThings nem tudta \u00e9rv\u00e9nyes\u00edteni a `base_url`-ben konfigur\u00e1lt v\u00e9gpontot. K\u00e9rlek, tekintsd \u00e1t az \u00f6sszetev\u0151 k\u00f6vetelm\u00e9nyeit." + "webhook_error": "SmartThings nem tudta \u00e9rv\u00e9nyes\u00edteni a webhook URL-t. K\u00e9rj\u00fck, ellen\u0151rizze, hogy a webhook URL el\u00e9rhet\u0151-e az internet fel\u0151l, \u00e9s pr\u00f3b\u00e1lja meg \u00fajra." }, "step": { "authorize": { @@ -30,7 +30,7 @@ "title": "Hely kiv\u00e1laszt\u00e1sa" }, "user": { - "description": "K\u00e9rlek add meg a SmartThings [Personal Access Tokent]({token_url}), amit az [instrukci\u00f3k]({component_url}) alapj\u00e1n hozt\u00e1l l\u00e9tre.", + "description": "K\u00e9rem adja meg a SmartThings [Personal Access Tokent]({token_url}), amit az [instrukci\u00f3k]({component_url}) alapj\u00e1n hozott l\u00e9tre.", "title": "Callback URL meger\u0151s\u00edt\u00e9se" } } diff --git a/homeassistant/components/smarttub/translations/ca.json b/homeassistant/components/smarttub/translations/ca.json index d00c4d26c98..4f0e9a2c502 100644 --- a/homeassistant/components/smarttub/translations/ca.json +++ b/homeassistant/components/smarttub/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat", + "already_configured": "El compte ja est\u00e0 configurat", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { diff --git a/homeassistant/components/smarttub/translations/hu.json b/homeassistant/components/smarttub/translations/hu.json index e9a45d3773f..3764b27abd2 100644 --- a/homeassistant/components/smarttub/translations/hu.json +++ b/homeassistant/components/smarttub/translations/hu.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" }, "error": { @@ -17,7 +17,7 @@ "email": "E-mail", "password": "Jelsz\u00f3" }, - "description": "Add meg SmartTub e-mail c\u00edmet \u00e9s jelsz\u00f3t a bejelentkez\u00e9shez", + "description": "Adja meg SmartTub e-mail c\u00edmet \u00e9s jelsz\u00f3t a bejelentkez\u00e9shez", "title": "Bejelentkez\u00e9s" } } diff --git a/homeassistant/components/smarttub/translations/id.json b/homeassistant/components/smarttub/translations/id.json index bf32b29d1e7..9b021f65771 100644 --- a/homeassistant/components/smarttub/translations/id.json +++ b/homeassistant/components/smarttub/translations/id.json @@ -9,6 +9,7 @@ }, "step": { "reauth_confirm": { + "description": "Integrasi SmartTub perlu mengautentikasi ulang akun Anda", "title": "Autentikasi Ulang Integrasi" }, "user": { diff --git a/homeassistant/components/smarttub/translations/ko.json b/homeassistant/components/smarttub/translations/ko.json index b68ff871d4d..ff50dadc63e 100644 --- a/homeassistant/components/smarttub/translations/ko.json +++ b/homeassistant/components/smarttub/translations/ko.json @@ -8,6 +8,9 @@ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { + "reauth_confirm": { + "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30" + }, "user": { "data": { "email": "\uc774\uba54\uc77c", diff --git a/homeassistant/components/solarlog/translations/hu.json b/homeassistant/components/solarlog/translations/hu.json index 23baa393942..ada2ab95751 100644 --- a/homeassistant/components/solarlog/translations/hu.json +++ b/homeassistant/components/solarlog/translations/hu.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "name": "A Solar-Log szenzorokhoz haszn\u00e1land\u00f3 el\u0151tag" }, "title": "Hat\u00e1rozza meg a Solar-Log kapcsolatot" diff --git a/homeassistant/components/soma/translations/hu.json b/homeassistant/components/soma/translations/hu.json index c3e572ebe0a..e7ac9d8d71c 100644 --- a/homeassistant/components/soma/translations/hu.json +++ b/homeassistant/components/soma/translations/hu.json @@ -13,7 +13,7 @@ "step": { "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "port": "Port" }, "description": "K\u00e9rj\u00fck, adja meg a SOMA Connect csatlakoz\u00e1si be\u00e1ll\u00edt\u00e1sait.", diff --git a/homeassistant/components/somfy/translations/hu.json b/homeassistant/components/somfy/translations/hu.json index ce4e94b3399..06b0894faf1 100644 --- a/homeassistant/components/somfy/translations/hu.json +++ b/homeassistant/components/somfy/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", - "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, diff --git a/homeassistant/components/somfy_mylink/translations/hu.json b/homeassistant/components/somfy_mylink/translations/hu.json index 3610a930022..fa6620859f5 100644 --- a/homeassistant/components/somfy_mylink/translations/hu.json +++ b/homeassistant/components/somfy_mylink/translations/hu.json @@ -12,7 +12,7 @@ "step": { "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "port": "Port", "system_id": "Rendszerazonos\u00edt\u00f3" }, diff --git a/homeassistant/components/somfy_mylink/translations/id.json b/homeassistant/components/somfy_mylink/translations/id.json index 0203ae421e2..c4b2269ef2c 100644 --- a/homeassistant/components/somfy_mylink/translations/id.json +++ b/homeassistant/components/somfy_mylink/translations/id.json @@ -8,7 +8,7 @@ "invalid_auth": "Autentikasi tidak valid", "unknown": "Kesalahan yang tidak diharapkan" }, - "flow_title": "Somfy MyLink {mac} ({ip})", + "flow_title": "{mac} ({ip})", "step": { "user": { "data": { diff --git a/homeassistant/components/sonarr/translations/hu.json b/homeassistant/components/sonarr/translations/hu.json index 6ebdb22404c..7ac0b621b13 100644 --- a/homeassistant/components/sonarr/translations/hu.json +++ b/homeassistant/components/sonarr/translations/hu.json @@ -19,7 +19,7 @@ "data": { "api_key": "API kulcs", "base_path": "El\u00e9r\u00e9si \u00fat az API-hoz", - "host": "Hoszt", + "host": "C\u00edm", "port": "Port", "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" diff --git a/homeassistant/components/sonarr/translations/id.json b/homeassistant/components/sonarr/translations/id.json index ffaf1d22604..9d906a07f91 100644 --- a/homeassistant/components/sonarr/translations/id.json +++ b/homeassistant/components/sonarr/translations/id.json @@ -9,7 +9,7 @@ "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid" }, - "flow_title": "Sonarr: {name}", + "flow_title": "{name}", "step": { "reauth_confirm": { "description": "Integrasi Sonarr perlu diautentikasi ulang secara manual dengan API Sonarr yang dihosting di: {host}", diff --git a/homeassistant/components/songpal/translations/hu.json b/homeassistant/components/songpal/translations/hu.json index 2bce32d0cb8..a02844a50a7 100644 --- a/homeassistant/components/songpal/translations/hu.json +++ b/homeassistant/components/songpal/translations/hu.json @@ -10,7 +10,7 @@ "flow_title": "{name} ({host})", "step": { "init": { - "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}({host})-t?" + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name} ({host})?" }, "user": { "data": { diff --git a/homeassistant/components/songpal/translations/id.json b/homeassistant/components/songpal/translations/id.json index 2b8149661bc..9e619e5bf76 100644 --- a/homeassistant/components/songpal/translations/id.json +++ b/homeassistant/components/songpal/translations/id.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Gagal terhubung" }, - "flow_title": "Sony Songpal {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "description": "Ingin menyiapkan {name} ({host})?" diff --git a/homeassistant/components/sonos/translations/he.json b/homeassistant/components/sonos/translations/he.json index 878c14a5119..64824b942ec 100644 --- a/homeassistant/components/sonos/translations/he.json +++ b/homeassistant/components/sonos/translations/he.json @@ -2,6 +2,7 @@ "config": { "abort": { "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "not_sonos_device": "\u05d4\u05ea\u05e7\u05df \u05e9\u05d4\u05ea\u05d2\u05dc\u05d4 \u05d0\u05d9\u05e0\u05d5 \u05d4\u05ea\u05e7\u05df Sonos", "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." }, "step": { diff --git a/homeassistant/components/sonos/translations/hu.json b/homeassistant/components/sonos/translations/hu.json index a928f97b3d6..a521c1e9d75 100644 --- a/homeassistant/components/sonos/translations/hu.json +++ b/homeassistant/components/sonos/translations/hu.json @@ -7,7 +7,7 @@ }, "step": { "confirm": { - "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a Sonos-t?" + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani a Sonos-t?" } } } diff --git a/homeassistant/components/sonos/translations/id.json b/homeassistant/components/sonos/translations/id.json index 145e2775e4a..d64dccf3af3 100644 --- a/homeassistant/components/sonos/translations/id.json +++ b/homeassistant/components/sonos/translations/id.json @@ -2,6 +2,7 @@ "config": { "abort": { "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "not_sonos_device": "Perangkat yang ditemukan bukan perangkat Sonos", "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." }, "step": { diff --git a/homeassistant/components/speedtestdotnet/translations/hu.json b/homeassistant/components/speedtestdotnet/translations/hu.json index cd08c3bd2d6..9e602652e5b 100644 --- a/homeassistant/components/speedtestdotnet/translations/hu.json +++ b/homeassistant/components/speedtestdotnet/translations/hu.json @@ -6,7 +6,7 @@ }, "step": { "user": { - "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?" + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" } } }, @@ -16,7 +16,7 @@ "data": { "manual": "Automatikus friss\u00edt\u00e9s letilt\u00e1sa", "scan_interval": "Friss\u00edt\u00e9si gyakoris\u00e1g (perc)", - "server_name": "V\u00e1laszd ki a teszt szervert" + "server_name": "V\u00e1lassza ki a teszt szervert" } } } diff --git a/homeassistant/components/speedtestdotnet/translations/nl.json b/homeassistant/components/speedtestdotnet/translations/nl.json index 5de8460fd77..3a112d48e9d 100644 --- a/homeassistant/components/speedtestdotnet/translations/nl.json +++ b/homeassistant/components/speedtestdotnet/translations/nl.json @@ -6,7 +6,7 @@ }, "step": { "user": { - "description": "Wil je beginnen met instellen?" + "description": "Wilt u beginnen met instellen?" } } }, diff --git a/homeassistant/components/spotify/translations/hu.json b/homeassistant/components/spotify/translations/hu.json index 8ffeadaf842..136e6185b46 100644 --- a/homeassistant/components/spotify/translations/hu.json +++ b/homeassistant/components/spotify/translations/hu.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.", + "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", "missing_configuration": "A Spotify integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.", "reauth_account_mismatch": "A Spotify-fi\u00f3kkal hiteles\u00edtett fi\u00f3k nem egyezik meg az \u00faj hiteles\u00edt\u00e9shez sz\u00fcks\u00e9ges fi\u00f3kkal." diff --git a/homeassistant/components/squeezebox/translations/hu.json b/homeassistant/components/squeezebox/translations/hu.json index a047dbca45f..5c2a3d37e85 100644 --- a/homeassistant/components/squeezebox/translations/hu.json +++ b/homeassistant/components/squeezebox/translations/hu.json @@ -14,7 +14,7 @@ "step": { "edit": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "password": "Jelsz\u00f3", "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" @@ -23,7 +23,7 @@ }, "user": { "data": { - "host": "Hoszt" + "host": "C\u00edm" } } } diff --git a/homeassistant/components/squeezebox/translations/id.json b/homeassistant/components/squeezebox/translations/id.json index 764c356ba84..02d82e872d8 100644 --- a/homeassistant/components/squeezebox/translations/id.json +++ b/homeassistant/components/squeezebox/translations/id.json @@ -10,7 +10,7 @@ "no_server_found": "Tidak dapat menemukan server secara otomatis.", "unknown": "Kesalahan yang tidak diharapkan" }, - "flow_title": "Logitech Squeezebox: {host}", + "flow_title": "{host}", "step": { "edit": { "data": { diff --git a/homeassistant/components/subaru/translations/ca.json b/homeassistant/components/subaru/translations/ca.json index 310747f613e..6b83af06bb8 100644 --- a/homeassistant/components/subaru/translations/ca.json +++ b/homeassistant/components/subaru/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat", + "already_configured": "El compte ja est\u00e0 configurat", "cannot_connect": "Ha fallat la connexi\u00f3" }, "error": { diff --git a/homeassistant/components/surepetcare/translations/ca.json b/homeassistant/components/surepetcare/translations/ca.json new file mode 100644 index 00000000000..5165473860a --- /dev/null +++ b/homeassistant/components/surepetcare/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": { + "password": "Contrasenya", + "username": "Nom d'usuari" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/surepetcare/translations/cs.json b/homeassistant/components/surepetcare/translations/cs.json new file mode 100644 index 00000000000..b6c00c05389 --- /dev/null +++ b/homeassistant/components/surepetcare/translations/cs.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/surepetcare/translations/de.json b/homeassistant/components/surepetcare/translations/de.json new file mode 100644 index 00000000000..14f319fb4d3 --- /dev/null +++ b/homeassistant/components/surepetcare/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": { + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/surepetcare/translations/en.json b/homeassistant/components/surepetcare/translations/en.json new file mode 100644 index 00000000000..a6c0889765f --- /dev/null +++ b/homeassistant/components/surepetcare/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": { + "password": "Password", + "username": "Username" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/surepetcare/translations/es.json b/homeassistant/components/surepetcare/translations/es.json new file mode 100644 index 00000000000..3d3945748cb --- /dev/null +++ b/homeassistant/components/surepetcare/translations/es.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya ha sido configurada" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/surepetcare/translations/et.json b/homeassistant/components/surepetcare/translations/et.json new file mode 100644 index 00000000000..74f668d14dc --- /dev/null +++ b/homeassistant/components/surepetcare/translations/et.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Konto on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/surepetcare/translations/he.json b/homeassistant/components/surepetcare/translations/he.json new file mode 100644 index 00000000000..454b7e1ae51 --- /dev/null +++ b/homeassistant/components/surepetcare/translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\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", + "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/surepetcare/translations/hu.json b/homeassistant/components/surepetcare/translations/hu.json new file mode 100644 index 00000000000..cc0c820facf --- /dev/null +++ b/homeassistant/components/surepetcare/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": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/surepetcare/translations/id.json b/homeassistant/components/surepetcare/translations/id.json new file mode 100644 index 00000000000..a346fab8e56 --- /dev/null +++ b/homeassistant/components/surepetcare/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": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/surepetcare/translations/it.json b/homeassistant/components/surepetcare/translations/it.json new file mode 100644 index 00000000000..aee18749ab0 --- /dev/null +++ b/homeassistant/components/surepetcare/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": { + "password": "Password", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/surepetcare/translations/nl.json b/homeassistant/components/surepetcare/translations/nl.json new file mode 100644 index 00000000000..1dd597d28b4 --- /dev/null +++ b/homeassistant/components/surepetcare/translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/surepetcare/translations/no.json b/homeassistant/components/surepetcare/translations/no.json new file mode 100644 index 00000000000..f34edbd641d --- /dev/null +++ b/homeassistant/components/surepetcare/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": { + "password": "Passord", + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/surepetcare/translations/pt-BR.json b/homeassistant/components/surepetcare/translations/pt-BR.json new file mode 100644 index 00000000000..c41610abb32 --- /dev/null +++ b/homeassistant/components/surepetcare/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/surepetcare/translations/ru.json b/homeassistant/components/surepetcare/translations/ru.json new file mode 100644 index 00000000000..c31f79d1d04 --- /dev/null +++ b/homeassistant/components/surepetcare/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": { + "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/surepetcare/translations/zh-Hant.json b/homeassistant/components/surepetcare/translations/zh-Hant.json new file mode 100644 index 00000000000..ad4530cb30f --- /dev/null +++ b/homeassistant/components/surepetcare/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": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/translations/he.json b/homeassistant/components/switch/translations/he.json index 0b70a69350b..6d41c202beb 100644 --- a/homeassistant/components/switch/translations/he.json +++ b/homeassistant/components/switch/translations/he.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "toggle": "\u05d4\u05d7\u05dc\u05e4\u05ea \u05de\u05e6\u05d1 {entity_name}", + "turn_off": "\u05db\u05d9\u05d1\u05d5\u05d9 {entity_name}", + "turn_on": "\u05d4\u05e4\u05e2\u05dc\u05ea {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} \u05db\u05d1\u05d5\u05d9", + "is_on": "{entity_name} \u05e4\u05d5\u05e2\u05dc" + }, + "trigger_type": { + "turned_off": "{entity_name} \u05db\u05d5\u05d1\u05d4", + "turned_on": "{entity_name} \u05d4\u05d5\u05e4\u05e2\u05dc" + } + }, "state": { "_": { "off": "\u05db\u05d1\u05d5\u05d9", diff --git a/homeassistant/components/switchbot/translations/ca.json b/homeassistant/components/switchbot/translations/ca.json new file mode 100644 index 00000000000..6409efcbab7 --- /dev/null +++ b/homeassistant/components/switchbot/translations/ca.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured_device": "El dispositiu ja est\u00e0 configurat", + "cannot_connect": "Ha fallat la connexi\u00f3", + "no_unconfigured_devices": "No s'han trobat dispositius no configurats.", + "switchbot_unsupported_type": "Tipus de Switchbot no compatible.", + "unknown": "Error inesperat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "mac": "Adre\u00e7a MAC del dispositiu", + "name": "Nom", + "password": "Contrasenya" + }, + "title": "Configuraci\u00f3 de dispositiu Switchbot" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "retry_count": "Nombre de reintents", + "retry_timeout": "Temps d'espera entre reintents", + "scan_timeout": "Quant de temps s'ha d'escanejar en busca de dades d'alerta", + "update_time": "Temps entre actualitzacions (segons)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/cs.json b/homeassistant/components/switchbot/translations/cs.json new file mode 100644 index 00000000000..7a44ab78d3b --- /dev/null +++ b/homeassistant/components/switchbot/translations/cs.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured_device": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "step": { + "user": { + "data": { + "password": "Heslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/de.json b/homeassistant/components/switchbot/translations/de.json new file mode 100644 index 00000000000..f499712718e --- /dev/null +++ b/homeassistant/components/switchbot/translations/de.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured_device": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", + "no_unconfigured_devices": "Keine unkonfigurierten Ger\u00e4te gefunden.", + "switchbot_unsupported_type": "Nicht unterst\u00fctzter Switchbot-Typ.", + "unknown": "Unerwarteter Fehler" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "mac": "MAC-Adresse des Ger\u00e4ts", + "name": "Name", + "password": "Passwort" + }, + "title": "Switchbot-Ger\u00e4t einrichten" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "retry_count": "Anzahl der Wiederholungen", + "retry_timeout": "Zeit\u00fcberschreitung zwischen Wiederholungsversuchen", + "scan_timeout": "Wie lange nach Anzeigendaten suchen", + "update_time": "Zeit zwischen Aktualisierungen (Sekunden)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/en.json b/homeassistant/components/switchbot/translations/en.json index 5f2c49e74f5..4ea3d21de65 100644 --- a/homeassistant/components/switchbot/translations/en.json +++ b/homeassistant/components/switchbot/translations/en.json @@ -2,18 +2,19 @@ "config": { "abort": { "already_configured_device": "Device is already configured", - "no_unconfigured_devices": "No unconfigured devices found.", - "unknown": "Unexpected error", "cannot_connect": "Failed to connect", - "switchbot_unsupported_type": "Unsupported Switchbot Type." + "no_unconfigured_devices": "No unconfigured devices found.", + "switchbot_unsupported_type": "Unsupported Switchbot Type.", + "unknown": "Unexpected error" + }, + "error": { + "cannot_connect": "Failed to connect" }, - "error": {}, "flow_title": "{name}", "step": { - "user": { "data": { - "mac": "Mac", + "mac": "Device MAC address", "name": "Name", "password": "Password" }, @@ -25,10 +26,10 @@ "step": { "init": { "data": { - "update_time": "Time between updates (seconds)", "retry_count": "Retry count", "retry_timeout": "Timeout between retries", - "scan_timeout": "How long to scan for advertisement data" + "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 new file mode 100644 index 00000000000..fe22d91e7f1 --- /dev/null +++ b/homeassistant/components/switchbot/translations/es.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured_device": "El dispositivo ya est\u00e1 configurado", + "switchbot_unsupported_type": "Tipo de Switchbot no compatible.", + "unknown": "Error inesperado" + }, + "error": { + "cannot_connect": "Fall\u00f3 al conectar" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "mac": "Direcci\u00f3n MAC del dispositivo", + "name": "Nombre", + "password": "Contrase\u00f1a" + }, + "title": "Configurar el dispositivo Switchbot" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "retry_count": "Recuento de reintentos", + "retry_timeout": "Tiempo de espera entre reintentos", + "scan_timeout": "Cu\u00e1nto tiempo se debe buscar datos de anuncio", + "update_time": "Tiempo entre actualizaciones (segundos)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/et.json b/homeassistant/components/switchbot/translations/et.json new file mode 100644 index 00000000000..cc746796195 --- /dev/null +++ b/homeassistant/components/switchbot/translations/et.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured_device": "Seade on juba h\u00e4\u00e4lestatud", + "cannot_connect": "\u00dchendamine nurjus", + "no_unconfigured_devices": "H\u00e4\u00e4lestamata seadmeid ei leitud.", + "switchbot_unsupported_type": "Toetamata Switchboti t\u00fc\u00fcp.", + "unknown": "Ootamatu t\u00f5rge" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "mac": "Seadme MAC-aadress", + "name": "Nimi", + "password": "Salas\u00f5na" + }, + "title": "Switchbot seadme seadistamine" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "retry_count": "Korduskatsete arv", + "retry_timeout": "Korduskatsete vaheline aeg", + "scan_timeout": "Kui kaua andmeid otsida", + "update_time": "V\u00e4rskenduste vaheline aeg (sekundites)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/he.json b/homeassistant/components/switchbot/translations/he.json new file mode 100644 index 00000000000..9e4d8129169 --- /dev/null +++ b/homeassistant/components/switchbot/translations/he.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured_device": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "name": "\u05e9\u05dd", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "title": "\u05d4\u05ea\u05e7\u05e0\u05ea \u05d1\u05d5\u05e8\u05e8 \u05db\u05d9\u05d5\u05d5\u05e0\u05d5\u05df" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "retry_count": "\u05e1\u05e4\u05d9\u05e8\u05ea \u05e0\u05e1\u05d9\u05d5\u05e0\u05d5\u05ea \u05d7\u05d5\u05d6\u05e8\u05d9\u05dd", + "retry_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05d1\u05d9\u05df \u05e0\u05d9\u05e1\u05d9\u05d5\u05e0\u05d5\u05ea \u05d7\u05d5\u05d6\u05e8\u05d9\u05dd", + "scan_timeout": "\u05db\u05de\u05d4 \u05d6\u05de\u05df \u05dc\u05e1\u05e8\u05d5\u05e7 \u05e0\u05ea\u05d5\u05e0\u05d9 \u05e4\u05e8\u05e1\u05d5\u05de\u05ea", + "update_time": "\u05d6\u05de\u05df \u05d1\u05d9\u05df \u05e2\u05d3\u05db\u05d5\u05e0\u05d9\u05dd (\u05e9\u05e0\u05d9\u05d5\u05ea)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/hu.json b/homeassistant/components/switchbot/translations/hu.json new file mode 100644 index 00000000000..5af1acb5d35 --- /dev/null +++ b/homeassistant/components/switchbot/translations/hu.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "A csatlakoz\u00e1s sikertelen", + "no_unconfigured_devices": "Nem tal\u00e1lhat\u00f3 konfigur\u00e1latlan eszk\u00f6z.", + "switchbot_unsupported_type": "Nem t\u00e1mogatott Switchbot t\u00edpus.", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "one": "\u00dcres", + "other": "\u00dcres" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "mac": "Eszk\u00f6z MAC-c\u00edme", + "name": "N\u00e9v", + "password": "Jelsz\u00f3" + }, + "title": "Switchbot eszk\u00f6z be\u00e1ll\u00edt\u00e1sa" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "retry_count": "\u00dajrapr\u00f3b\u00e1lkoz\u00e1sok sz\u00e1ma", + "retry_timeout": "\u00dajrapr\u00f3b\u00e1lkoz\u00e1sok k\u00f6z\u00f6tti id\u0151korl\u00e1t", + "scan_timeout": "Mennyi ideig keresse a hirdet\u00e9si adatokat", + "update_time": "Friss\u00edt\u00e9sek k\u00f6z\u00f6tti id\u0151 (m\u00e1sodperc)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/id.json b/homeassistant/components/switchbot/translations/id.json new file mode 100644 index 00000000000..af61966afa5 --- /dev/null +++ b/homeassistant/components/switchbot/translations/id.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured_device": "Perangkat sudah dikonfigurasi", + "switchbot_unsupported_type": "Jenis Switchbot yang tidak didukung.", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "mac": "Alamat MAC perangkat", + "name": "Nama", + "password": "Kata Sandi" + }, + "title": "Siapkan perangkat Switchbot" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "retry_count": "Jumlah percobaan", + "retry_timeout": "Tenggang waktu antara percobaan ulang", + "scan_timeout": "Berapa lama untuk memindai data iklan", + "update_time": "Waktu antara pembaruan (detik)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/it.json b/homeassistant/components/switchbot/translations/it.json new file mode 100644 index 00000000000..fc8296f6442 --- /dev/null +++ b/homeassistant/components/switchbot/translations/it.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured_device": "Il dispositivo \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi", + "no_unconfigured_devices": "Nessun dispositivo non configurato trovato.", + "switchbot_unsupported_type": "Tipo di Switchbot non supportato.", + "unknown": "Errore imprevisto" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "mac": "Indirizzo MAC del dispositivo", + "name": "Nome", + "password": "Password" + }, + "title": "Impostare il dispositivo Switchbot" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "retry_count": "Conteggio dei tentativi di ripetizione", + "retry_timeout": "Tempo scaduto tra i tentativi", + "scan_timeout": "Per quanto tempo eseguire la scansione dei dati pubblicitari", + "update_time": "Tempo tra gli aggiornamenti (secondi)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/nl.json b/homeassistant/components/switchbot/translations/nl.json new file mode 100644 index 00000000000..fb1e55f6b9d --- /dev/null +++ b/homeassistant/components/switchbot/translations/nl.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured_device": "Apparaat is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken", + "no_unconfigured_devices": "Geen niet-geconfigureerde apparaten gevonden.", + "switchbot_unsupported_type": "Niet-ondersteund Switchbot-type.", + "unknown": "Onverwachte fout" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "mac": "MAC-adres apparaat", + "name": "Naam", + "password": "Wachtwoord" + }, + "title": "Switchbot-apparaat instellen" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "retry_count": "Aantal herhalingen", + "retry_timeout": "Time-out tussen nieuwe pogingen", + "scan_timeout": "Hoe lang te scannen voor advertentiegegevens", + "update_time": "Tijd tussen updates (seconden)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/no.json b/homeassistant/components/switchbot/translations/no.json new file mode 100644 index 00000000000..4d8cb95061a --- /dev/null +++ b/homeassistant/components/switchbot/translations/no.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured_device": "Enheten er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes", + "no_unconfigured_devices": "Ingen ukonfigurerte enheter ble funnet.", + "switchbot_unsupported_type": "Switchbot-type st\u00f8ttes ikke.", + "unknown": "Uventet feil" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "mac": "Enhetens MAC -adresse", + "name": "Navn", + "password": "Passord" + }, + "title": "Sett opp Switchbot-enhet" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "retry_count": "Antall nye fors\u00f8k", + "retry_timeout": "Tidsavbrudd mellom fors\u00f8k", + "scan_timeout": "Hvor lenge skal jeg s\u00f8ke etter annonsedata", + "update_time": "Tid mellom oppdateringer (sekunder)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/ro.json b/homeassistant/components/switchbot/translations/ro.json new file mode 100644 index 00000000000..7668dd5e8e2 --- /dev/null +++ b/homeassistant/components/switchbot/translations/ro.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "switchbot_unsupported_type": "Tipul Switchbot neacceptat." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/ru.json b/homeassistant/components/switchbot/translations/ru.json new file mode 100644 index 00000000000..5eaa1cdbc4f --- /dev/null +++ b/homeassistant/components/switchbot/translations/ru.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "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.", + "switchbot_unsupported_type": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0439 \u0442\u0438\u043f Switchbot.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "mac": "MAC-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Switchbot" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "retry_count": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u044b\u0445 \u043f\u043e\u043f\u044b\u0442\u043e\u043a", + "retry_timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 \u043c\u0435\u0436\u0434\u0443 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u044b\u043c\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0430\u043c\u0438", + "scan_timeout": "\u041a\u0430\u043a \u0434\u043e\u043b\u0433\u043e \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0440\u0435\u043a\u043b\u0430\u043c\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", + "update_time": "\u0412\u0440\u0435\u043c\u044f \u043c\u0435\u0436\u0434\u0443 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f\u043c\u0438 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/zh-Hant.json b/homeassistant/components/switchbot/translations/zh-Hant.json new file mode 100644 index 00000000000..44fe1fe5c54 --- /dev/null +++ b/homeassistant/components/switchbot/translations/zh-Hant.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured_device": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "no_unconfigured_devices": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u8a2d\u5b9a\u88dd\u7f6e\u3002", + "switchbot_unsupported_type": "\u4e0d\u652f\u6301\u7684 Switchbot \u985e\u578b\u3002", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "mac": "\u88dd\u7f6e MAC \u4f4d\u5740", + "name": "\u540d\u7a31", + "password": "\u5bc6\u78bc" + }, + "title": "\u8a2d\u5b9a Switchbot \u88dd\u7f6e" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "retry_count": "\u91cd\u8a66\u6b21\u6578", + "retry_timeout": "\u903e\u6642", + "scan_timeout": "\u6383\u63cf\u5ee3\u544a\u6578\u64da\u7684\u6642\u9593", + "update_time": "\u66f4\u65b0\u9593\u9694\u6642\u9593\uff08\u79d2\uff09" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switcher_kis/translations/es.json b/homeassistant/components/switcher_kis/translations/es.json new file mode 100644 index 00000000000..520df7ee4cd --- /dev/null +++ b/homeassistant/components/switcher_kis/translations/es.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "No se encontraron dispositivos en la red", + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." + }, + "step": { + "confirm": { + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switcher_kis/translations/hu.json b/homeassistant/components/switcher_kis/translations/hu.json index c3be866fb85..20237758f11 100644 --- a/homeassistant/components/switcher_kis/translations/hu.json +++ b/homeassistant/components/switcher_kis/translations/hu.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "El akarja kezdeni a be\u00e1ll\u00edt\u00e1sokat?" + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1sokat?" } } } diff --git a/homeassistant/components/switcher_kis/translations/id.json b/homeassistant/components/switcher_kis/translations/id.json new file mode 100644 index 00000000000..223836a8b40 --- /dev/null +++ b/homeassistant/components/switcher_kis/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 memulai penyiapan?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switcher_kis/translations/nl.json b/homeassistant/components/switcher_kis/translations/nl.json index d11896014fd..0671f0b3674 100644 --- a/homeassistant/components/switcher_kis/translations/nl.json +++ b/homeassistant/components/switcher_kis/translations/nl.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Wil je beginnen met instellen?" + "description": "Wilt u beginnen met instellen?" } } } diff --git a/homeassistant/components/syncthru/translations/id.json b/homeassistant/components/syncthru/translations/id.json index 54d5e6f5c96..5a79e74a771 100644 --- a/homeassistant/components/syncthru/translations/id.json +++ b/homeassistant/components/syncthru/translations/id.json @@ -8,7 +8,7 @@ "syncthru_not_supported": "Perangkat tidak mendukung SyncThru", "unknown_state": "Status printer tidak diketahui, verifikasi URL dan konektivitas jaringan" }, - "flow_title": "Printer Samsung SyncThru: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/synology_dsm/translations/es.json b/homeassistant/components/synology_dsm/translations/es.json index 571dced9bc7..e143a636fb0 100644 --- a/homeassistant/components/synology_dsm/translations/es.json +++ b/homeassistant/components/synology_dsm/translations/es.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "El host ya est\u00e1 configurado." + "already_configured": "El host 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", @@ -31,10 +33,18 @@ }, "reauth": { "data": { - "password": "Contrase\u00f1a" + "password": "Contrase\u00f1a", + "username": "Usuario" }, "description": "Raz\u00f3n: {details}", - "title": "Synology DSM Volver a autenticar la integraci\u00f3n" + "title": "Volver a autenticar la integraci\u00f3n Synology DSM" + }, + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + }, + "title": "Volver a autenticar la integraci\u00f3n Synology DSM" }, "user": { "data": { diff --git a/homeassistant/components/synology_dsm/translations/hu.json b/homeassistant/components/synology_dsm/translations/hu.json index 56a5ebb994f..f3f3d4ead4c 100644 --- a/homeassistant/components/synology_dsm/translations/hu.json +++ b/homeassistant/components/synology_dsm/translations/hu.json @@ -28,7 +28,7 @@ "username": "Felhaszn\u00e1l\u00f3n\u00e9v", "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" }, - "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a {name}({host})-t?", + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name} ({host})?", "title": "Synology DSM" }, "reauth": { @@ -39,9 +39,16 @@ "description": "Indokl\u00e1s: {details}", "title": "Synology DSM Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" }, + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "title": "Synology DSM Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "password": "Jelsz\u00f3", "port": "Port", "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", diff --git a/homeassistant/components/synology_dsm/translations/id.json b/homeassistant/components/synology_dsm/translations/id.json index e614c2578d4..ca322fc518e 100644 --- a/homeassistant/components/synology_dsm/translations/id.json +++ b/homeassistant/components/synology_dsm/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": { "cannot_connect": "Gagal terhubung", @@ -10,7 +11,7 @@ "otp_failed": "Autentikasi dua langkah gagal, coba lagi dengan kode sandi baru", "unknown": "Kesalahan yang tidak diharapkan" }, - "flow_title": "Synology DSM {name} ({host})", + "flow_title": "{name} ({host})", "step": { "2sa": { "data": { @@ -29,6 +30,18 @@ "description": "Ingin menyiapkan {name} ({host})?", "title": "Synology DSM" }, + "reauth": { + "data": { + "username": "Nama Pengguna" + }, + "title": "Autentikasi Ulang Integrasi Synology DSM" + }, + "reauth_confirm": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/synology_dsm/translations/it.json b/homeassistant/components/synology_dsm/translations/it.json index a9ea23bf08a..41c535f714f 100644 --- a/homeassistant/components/synology_dsm/translations/it.json +++ b/homeassistant/components/synology_dsm/translations/it.json @@ -39,6 +39,13 @@ "description": "Motivo: {details}", "title": "Synology DSM Autenticare nuovamente l'integrazione" }, + "reauth_confirm": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "title": "Autenticare nuovamente l'integrazione Synology DSM " + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/synology_dsm/translations/nl.json b/homeassistant/components/synology_dsm/translations/nl.json index d33ed48ce10..8740308faf0 100644 --- a/homeassistant/components/synology_dsm/translations/nl.json +++ b/homeassistant/components/synology_dsm/translations/nl.json @@ -39,6 +39,13 @@ "description": "Reden: {details}", "title": "Synology DSM Verifieer de integratie opnieuw" }, + "reauth_confirm": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "title": "Synology DSM Integratie opnieuw verifi\u00ebren" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/system_bridge/translations/hu.json b/homeassistant/components/system_bridge/translations/hu.json index 50643ca5e95..31082202419 100644 --- a/homeassistant/components/system_bridge/translations/hu.json +++ b/homeassistant/components/system_bridge/translations/hu.json @@ -21,7 +21,7 @@ "user": { "data": { "api_key": "API kulcs", - "host": "Gazdag\u00e9p", + "host": "C\u00edm", "port": "Port" }, "description": "K\u00e9rj\u00fck, adja meg kapcsolati adatait." diff --git a/homeassistant/components/tasmota/translations/hu.json b/homeassistant/components/tasmota/translations/hu.json index 72a26925bc9..3a6c32dbb42 100644 --- a/homeassistant/components/tasmota/translations/hu.json +++ b/homeassistant/components/tasmota/translations/hu.json @@ -11,11 +11,11 @@ "data": { "discovery_prefix": "Felder\u00edt\u00e9si t\u00e9ma el\u0151tagja" }, - "description": "Add meg a Tasmota konfigur\u00e1ci\u00f3t.", + "description": "Adja meg a Tasmota konfigur\u00e1ci\u00f3t.", "title": "Tasmota" }, "confirm": { - "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a Tasmota-t?" + "description": "Szeretn\u00e9 b\u00e1ll\u00edtani a Tasmota-t?" } } } diff --git a/homeassistant/components/tellduslive/translations/he.json b/homeassistant/components/tellduslive/translations/he.json index d19fa6d3d31..6a1c8a23c65 100644 --- a/homeassistant/components/tellduslive/translations/he.json +++ b/homeassistant/components/tellduslive/translations/he.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", "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.", - "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4", + "unknown_authorize_url_generation": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4 \u05d1\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05e9\u05dc \u05d4\u05e8\u05e9\u05d0\u05d4." }, "error": { "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" diff --git a/homeassistant/components/tellduslive/translations/hu.json b/homeassistant/components/tellduslive/translations/hu.json index 207e9ada090..a07259b67f9 100644 --- a/homeassistant/components/tellduslive/translations/hu.json +++ b/homeassistant/components/tellduslive/translations/hu.json @@ -11,15 +11,15 @@ }, "step": { "auth": { - "description": "A TelldusLive-fi\u00f3k \u00f6sszekapcsol\u00e1sa:\n 1. Kattintson az al\u00e1bbi linkre\n 2. Jelentkezzen be a Telldus Live szolg\u00e1ltat\u00e1sba\n 3. Enged\u00e9lyezze ** {app_name} ** (kattintson a ** Yes ** gombra).\n 4. J\u00f6jj\u00f6n vissza ide, \u00e9s kattintson a ** SUBMIT ** gombra. \n\n [Link TelldusLive-fi\u00f3k] ( {auth_url} )", + "description": "A TelldusLive-fi\u00f3k \u00f6sszekapcsol\u00e1sa:\n 1. Kattintson az al\u00e1bbi linkre\n 2. Jelentkezzen be a Telldus Live szolg\u00e1ltat\u00e1sba\n 3. Enged\u00e9lyezzeie kell **{app_name}** (kattintson a ** Yes ** gombra).\n 4. J\u00f6jj\u00f6n vissza ide, \u00e9s kattintson a ** K\u00fcld\u00e9s ** gombra. \n\n [Link TelldusLive-fi\u00f3k]({auth_url})", "title": "Hiteles\u00edtsen a TelldusLive-on" }, "user": { "data": { - "host": "Hoszt" + "host": "C\u00edm" }, "description": "\u00dcres", - "title": "V\u00e1lassz v\u00e9gpontot." + "title": "V\u00e1lasszon v\u00e9gpontot." } } } diff --git a/homeassistant/components/tibber/translations/hu.json b/homeassistant/components/tibber/translations/hu.json index 6ad59022845..1ff558b7280 100644 --- a/homeassistant/components/tibber/translations/hu.json +++ b/homeassistant/components/tibber/translations/hu.json @@ -13,7 +13,7 @@ "data": { "access_token": "Hozz\u00e1f\u00e9r\u00e9si token" }, - "description": "Add meg a hozz\u00e1f\u00e9r\u00e9si tokent a https://developer.tibber.com/settings/accesstoken c\u00edmr\u0151l", + "description": "Adja meg a hozz\u00e1f\u00e9r\u00e9si tokent a https://developer.tibber.com/settings/accesstoken c\u00edmr\u0151l", "title": "Tibber" } } diff --git a/homeassistant/components/tile/translations/ca.json b/homeassistant/components/tile/translations/ca.json index 60c31b8dce6..1d70a94f7af 100644 --- a/homeassistant/components/tile/translations/ca.json +++ b/homeassistant/components/tile/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat" + "already_configured": "El compte ja est\u00e0 configurat" }, "error": { "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" diff --git a/homeassistant/components/toon/translations/he.json b/homeassistant/components/toon/translations/he.json index 431a7b32509..c4269a46b1c 100644 --- a/homeassistant/components/toon/translations/he.json +++ b/homeassistant/components/toon/translations/he.json @@ -3,7 +3,8 @@ "abort": { "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.", "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.", - "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})" + "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})", + "unknown_authorize_url_generation": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4 \u05d1\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05e9\u05dc \u05d4\u05e8\u05e9\u05d0\u05d4." } } } \ No newline at end of file diff --git a/homeassistant/components/toon/translations/hu.json b/homeassistant/components/toon/translations/hu.json index 18f333dccdf..b1a69144dd5 100644 --- a/homeassistant/components/toon/translations/hu.json +++ b/homeassistant/components/toon/translations/hu.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "A kiv\u00e1lasztott meg\u00e1llapod\u00e1s m\u00e1r konfigur\u00e1lva van.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", - "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "no_agreements": "Ennek a fi\u00f3knak nincsenek Toon kijelz\u0151i.", "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.", "unknown_authorize_url_generation": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n." @@ -17,7 +17,7 @@ "title": "V\u00e1lassza ki a meg\u00e1llapod\u00e1st" }, "pick_implementation": { - "title": "V\u00e1lassza ki a hiteles\u00edt\u00e9shez" + "title": "V\u00e1lassza ki a b\u00e9rl\u0151t a hiteles\u00edt\u00e9shez" } } } diff --git a/homeassistant/components/totalconnect/translations/ca.json b/homeassistant/components/totalconnect/translations/ca.json index cbe1d4e449c..fa42c81e1be 100644 --- a/homeassistant/components/totalconnect/translations/ca.json +++ b/homeassistant/components/totalconnect/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat", + "already_configured": "El compte ja est\u00e0 configurat", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { diff --git a/homeassistant/components/totalconnect/translations/es.json b/homeassistant/components/totalconnect/translations/es.json index c4923884c43..63d61445ef5 100644 --- a/homeassistant/components/totalconnect/translations/es.json +++ b/homeassistant/components/totalconnect/translations/es.json @@ -14,7 +14,7 @@ "location": "Localizaci\u00f3n", "usercode": "Codigo de usuario" }, - "description": "Ingrese el c\u00f3digo de usuario para este usuario en esta ubicaci\u00f3n", + "description": "Introduce el c\u00f3digo de usuario para este usuario en la ubicaci\u00f3n {location_id}", "title": "C\u00f3digos de usuario de ubicaci\u00f3n" }, "reauth_confirm": { diff --git a/homeassistant/components/totalconnect/translations/hu.json b/homeassistant/components/totalconnect/translations/hu.json index 319611fd2b1..9b55278e9a8 100644 --- a/homeassistant/components/totalconnect/translations/hu.json +++ b/homeassistant/components/totalconnect/translations/hu.json @@ -14,7 +14,7 @@ "location": "Elhelyezked\u00e9s", "usercode": "Felhaszn\u00e1l\u00f3i k\u00f3d" }, - "description": "Adja meg ennek a felhaszn\u00e1l\u00f3nak a felhaszn\u00e1l\u00f3i k\u00f3dj\u00e1t a k\u00f6vetkez\u0151 helyen: {location_id}", + "description": "Adja meg ennek a felhaszn\u00e1l\u00f3i k\u00f3dj\u00e1t a k\u00f6vetkez\u0151 helyen: {location_id}", "title": "Helyhaszn\u00e1lati k\u00f3dok" }, "reauth_confirm": { diff --git a/homeassistant/components/totalconnect/translations/id.json b/homeassistant/components/totalconnect/translations/id.json index c1bdf664994..b1bc5573021 100644 --- a/homeassistant/components/totalconnect/translations/id.json +++ b/homeassistant/components/totalconnect/translations/id.json @@ -13,7 +13,7 @@ "data": { "location": "Lokasi" }, - "description": "Masukkan kode pengguna untuk pengguna ini di lokasi ini", + "description": "Masukkan kode pengguna untuk pengguna ini di lokasi {location_id}", "title": "Lokasi Kode Pengguna" }, "reauth_confirm": { diff --git a/homeassistant/components/tplink/translations/ca.json b/homeassistant/components/tplink/translations/ca.json index 69dfc1b4b9d..4dfb749a9d7 100644 --- a/homeassistant/components/tplink/translations/ca.json +++ b/homeassistant/components/tplink/translations/ca.json @@ -1,12 +1,31 @@ { "config": { "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", "no_devices_found": "No s'han trobat dispositius a la xarxa", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "flow_title": "{name} {model} ({host})", "step": { "confirm": { "description": "Vols configurar dispositius intel\u00b7ligents TP-Link?" + }, + "discovery_confirm": { + "description": "Vols configurar {name} {model} ({host})?" + }, + "pick_device": { + "data": { + "device": "Dispositiu" + } + }, + "user": { + "data": { + "host": "Amfitri\u00f3" + }, + "description": "Si deixes l'amfitri\u00f3 buit, s'utilitzar\u00e0 el descobriment per cercar dispositius." } } } diff --git a/homeassistant/components/tplink/translations/de.json b/homeassistant/components/tplink/translations/de.json index 6f804a6eeef..4d6a07b881e 100644 --- a/homeassistant/components/tplink/translations/de.json +++ b/homeassistant/components/tplink/translations/de.json @@ -1,12 +1,31 @@ { "config": { "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "flow_title": "{name} {model} ({host})", "step": { "confirm": { "description": "M\u00f6chtest du TP-Link Smart Devices einrichten?" + }, + "discovery_confirm": { + "description": "M\u00f6chtest du {name} {model} ({host}) einrichten?" + }, + "pick_device": { + "data": { + "device": "Ger\u00e4t" + } + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Wenn du den Host leer l\u00e4sst, wird die Erkennung verwendet, um Ger\u00e4te zu finden." } } } diff --git a/homeassistant/components/tplink/translations/en.json b/homeassistant/components/tplink/translations/en.json index 0697974e708..da4681145d8 100644 --- a/homeassistant/components/tplink/translations/en.json +++ b/homeassistant/components/tplink/translations/en.json @@ -2,13 +2,17 @@ "config": { "abort": { "already_configured": "Device is already configured", - "no_devices_found": "No devices found on the network" + "no_devices_found": "No devices found on the network", + "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { "cannot_connect": "Failed to connect" }, "flow_title": "{name} {model} ({host})", "step": { + "confirm": { + "description": "Do you want to setup TP-Link smart devices?" + }, "discovery_confirm": { "description": "Do you want to setup {name} {model} ({host})?" }, diff --git a/homeassistant/components/tplink/translations/et.json b/homeassistant/components/tplink/translations/et.json index 972e581fc61..12c4f3d6f84 100644 --- a/homeassistant/components/tplink/translations/et.json +++ b/homeassistant/components/tplink/translations/et.json @@ -1,12 +1,31 @@ { "config": { "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "no_devices_found": "V\u00f5rgust ei leitud seadmeid", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "flow_title": "{name} {model} ( {host} )", "step": { "confirm": { "description": "Kas soovid seadistada TP-Linki nutiseadmeid?" + }, + "discovery_confirm": { + "description": "Kas seadistada {name}{model} ({host})?" + }, + "pick_device": { + "data": { + "device": "Seade" + } + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Kui j\u00e4tad hosti t\u00fchjaks kasutatakse seadmete leidmiseks avastamist." } } } diff --git a/homeassistant/components/tplink/translations/hu.json b/homeassistant/components/tplink/translations/hu.json index bcfb467538d..c00744d0dcf 100644 --- a/homeassistant/components/tplink/translations/hu.json +++ b/homeassistant/components/tplink/translations/hu.json @@ -1,12 +1,31 @@ { "config": { "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r be van konfigur\u00e1lva", "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." }, + "error": { + "cannot_connect": "A csatlakoz\u00e1s sikertelen" + }, + "flow_title": "{name} {model} ({host})", "step": { "confirm": { - "description": "Be szeretn\u00e9 \u00e1ll\u00edtani a TP-Link intelligens eszk\u00f6z\u00f6ket?" + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani a TP-Link intelligens eszk\u00f6zeit?" + }, + "discovery_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name} {model} ({host})?" + }, + "pick_device": { + "data": { + "device": "Eszk\u00f6z" + } + }, + "user": { + "data": { + "host": "C\u00edm" + }, + "description": "Ha nem ad meg c\u00edmet, akkor az eszk\u00f6z\u00f6k keres\u00e9se a felder\u00edt\u00e9ssel t\u00f6rt\u00e9nik." } } } diff --git a/homeassistant/components/tplink/translations/it.json b/homeassistant/components/tplink/translations/it.json index 8940b1c8ee6..3fd30d8d12c 100644 --- a/homeassistant/components/tplink/translations/it.json +++ b/homeassistant/components/tplink/translations/it.json @@ -1,12 +1,31 @@ { "config": { "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "no_devices_found": "Nessun dispositivo trovato sulla rete", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "flow_title": "{name} {model} ({host})", "step": { "confirm": { "description": "Vuoi configurare i dispositivi intelligenti TP-Link?" + }, + "discovery_confirm": { + "description": "Vuoi configurare {name} {model} ({host})?" + }, + "pick_device": { + "data": { + "device": "Dispositivo" + } + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Se si lascia vuoto l'host, l'individuazione verr\u00e0 utilizzata per trovare i dispositivi." } } } diff --git a/homeassistant/components/tplink/translations/nl.json b/homeassistant/components/tplink/translations/nl.json index 362645d9f19..f6cf6a21e72 100644 --- a/homeassistant/components/tplink/translations/nl.json +++ b/homeassistant/components/tplink/translations/nl.json @@ -1,12 +1,31 @@ { "config": { "abort": { + "already_configured": "Apparaat is al geconfigureerd", "no_devices_found": "Geen apparaten gevonden op het netwerk", "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." }, + "error": { + "cannot_connect": "Kon geen verbinding maken" + }, + "flow_title": "{name} {model} ({host})", "step": { "confirm": { "description": "Wil je TP-Link slimme apparaten instellen?" + }, + "discovery_confirm": { + "description": "Wilt u {name} {model} ({host}) instellen?" + }, + "pick_device": { + "data": { + "device": "Apparaat" + } + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Als u de host leeg laat, wordt detectie gebruikt om apparaten te vinden." } } } diff --git a/homeassistant/components/tplink/translations/no.json b/homeassistant/components/tplink/translations/no.json index 1d1d624ab40..6c7bd7dcbf4 100644 --- a/homeassistant/components/tplink/translations/no.json +++ b/homeassistant/components/tplink/translations/no.json @@ -1,12 +1,31 @@ { "config": { "abort": { + "already_configured": "Enheten er allerede konfigurert", "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": "{name} {model} ( {host} )", "step": { "confirm": { "description": "Vil du konfigurere TP-Link smart enheter?" + }, + "discovery_confirm": { + "description": "Vil du konfigurere {name} {model} ( {host} )?" + }, + "pick_device": { + "data": { + "device": "Enhet" + } + }, + "user": { + "data": { + "host": "Vert" + }, + "description": "Hvis du lar verten st\u00e5 tom, brukes automatisk oppdagelse til \u00e5 finne enheter" } } } diff --git a/homeassistant/components/tplink/translations/ru.json b/homeassistant/components/tplink/translations/ru.json index 4df755bee4f..47f1459e572 100644 --- a/homeassistant/components/tplink/translations/ru.json +++ b/homeassistant/components/tplink/translations/ru.json @@ -1,12 +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.", "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." }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "flow_title": "{name} {model} ({host})", "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 TP-Link Smart Home?" + }, + "discovery_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} {model} ({host})?" + }, + "pick_device": { + "data": { + "device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + } + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u0415\u0441\u043b\u0438 \u043d\u0435 \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430, \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0431\u0443\u0434\u0443\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438." } } } diff --git a/homeassistant/components/tplink/translations/zh-Hant.json b/homeassistant/components/tplink/translations/zh-Hant.json index 2fac2ac142d..153783b1b90 100644 --- a/homeassistant/components/tplink/translations/zh-Hant.json +++ b/homeassistant/components/tplink/translations/zh-Hant.json @@ -1,12 +1,31 @@ { "config": { "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "flow_title": "{name} {model} ({host})", "step": { "confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a TP-Link \u667a\u80fd\u88dd\u7f6e\uff1f" + }, + "discovery_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name} {model} ({host})\uff1f" + }, + "pick_device": { + "data": { + "device": "\u88dd\u7f6e" + } + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + }, + "description": "\u5047\u5982\u4e3b\u6a5f\u7aef\u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u63a2\u7d22\u6240\u6709\u53ef\u7528\u88dd\u7f6e\u3002" } } } diff --git a/homeassistant/components/traccar/translations/hu.json b/homeassistant/components/traccar/translations/hu.json index 94fc9198921..902b4ea5231 100644 --- a/homeassistant/components/traccar/translations/hu.json +++ b/homeassistant/components/traccar/translations/hu.json @@ -2,10 +2,10 @@ "config": { "abort": { "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", - "webhook_not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." + "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, "create_entry": { - "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a webhook funkci\u00f3t a Traccar-ban. \n\n Haszn\u00e1lja a k\u00f6vetkez\u0151 URL-t: `{webhook_url}`\n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t]({docs_url})." + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a webhook funkci\u00f3t a Traccar-ban. \n\nHaszn\u00e1lja a k\u00f6vetkez\u0151 URL-t: `{webhook_url}`\n\nTov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1ssa a [dokument\u00e1ci\u00f3t]({docs_url})." }, "step": { "user": { diff --git a/homeassistant/components/tractive/translations/es.json b/homeassistant/components/tractive/translations/es.json index 11aa4f1aa9c..9b252a0b2f0 100644 --- a/homeassistant/components/tractive/translations/es.json +++ b/homeassistant/components/tractive/translations/es.json @@ -1,17 +1,18 @@ { "config": { "abort": { - "already_configured": "El sistema 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 err\u00f3nea", - "unknown": "Error desconocido" + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" }, "step": { "user": { "data": { - "email": "Correo-e", - "password": "Clave" + "email": "Correo electr\u00f3nico", + "password": "Contrase\u00f1a" } } } diff --git a/homeassistant/components/tradfri/translations/fi.json b/homeassistant/components/tradfri/translations/fi.json index 31984784ee6..4946d88778f 100644 --- a/homeassistant/components/tradfri/translations/fi.json +++ b/homeassistant/components/tradfri/translations/fi.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Silta on jo m\u00e4\u00e4ritetty" }, + "error": { + "cannot_connect": "Yhdist\u00e4minen ep\u00e4onnistui" + }, "step": { "auth": { "data": { diff --git a/homeassistant/components/tradfri/translations/hu.json b/homeassistant/components/tradfri/translations/hu.json index 3bc4ec90e77..e5f749a83df 100644 --- a/homeassistant/components/tradfri/translations/hu.json +++ b/homeassistant/components/tradfri/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van." + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -12,11 +12,11 @@ "step": { "auth": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "security_code": "Biztons\u00e1gi K\u00f3d" }, "description": "A biztons\u00e1gi k\u00f3dot a Gatewayed h\u00e1toldal\u00e1n tal\u00e1lod.", - "title": "Add meg a biztons\u00e1gi k\u00f3dot" + "title": "Adja meg a biztons\u00e1gi k\u00f3dot" } } } diff --git a/homeassistant/components/transmission/translations/hu.json b/homeassistant/components/transmission/translations/hu.json index 5c968b21ed7..5e3dcfd2b6c 100644 --- a/homeassistant/components/transmission/translations/hu.json +++ b/homeassistant/components/transmission/translations/hu.json @@ -6,12 +6,12 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik" + "name_exists": "A n\u00e9v m\u00e1r foglalt" }, "step": { "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "name": "N\u00e9v", "password": "Jelsz\u00f3", "port": "Port", diff --git a/homeassistant/components/tuya/translations/af.json b/homeassistant/components/tuya/translations/af.json new file mode 100644 index 00000000000..71ac741b6b8 --- /dev/null +++ b/homeassistant/components/tuya/translations/af.json @@ -0,0 +1,8 @@ +{ + "options": { + "error": { + "dev_not_config": "Ger\u00e4tetyp nicht konfigurierbar", + "dev_not_found": "Ger\u00e4t nicht gefunden" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/ca.json b/homeassistant/components/tuya/translations/ca.json new file mode 100644 index 00000000000..6759d322484 --- /dev/null +++ b/homeassistant/components/tuya/translations/ca.json @@ -0,0 +1,79 @@ +{ + "config": { + "abort": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "error": { + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + }, + "flow_title": "Configuraci\u00f3 de Tuya", + "step": { + "login": { + "data": { + "access_id": "ID d'acc\u00e9s", + "access_secret": "Secret d'acc\u00e9s", + "country_code": "Codi de pa\u00eds", + "endpoint": "Zona de disponibilitat", + "password": "Contrasenya", + "tuya_app_type": "Aplicaci\u00f3 per a m\u00f2bil", + "username": "Compte" + }, + "description": "Introdueix la credencial de Tuya", + "title": "Tuya" + }, + "user": { + "data": { + "country_code": "El teu codi de pa\u00eds (per exemple, 1 per l'EUA o 86 per la Xina)", + "password": "Contrasenya", + "platform": "L'aplicaci\u00f3 on es registra el teu compte", + "tuya_project_type": "Tipus de projecte al n\u00favol de Tuya", + "username": "Nom d'usuari" + }, + "description": "Introdueix les teves credencial de Tuya.", + "title": "Integraci\u00f3 Tuya" + } + } + }, + "options": { + "abort": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "error": { + "dev_multi_type": "Per configurar una selecci\u00f3 de m\u00faltiples dispositius, aquests han de ser del mateix tipus", + "dev_not_config": "El tipus d'aquest dispositiu no \u00e9s configurable", + "dev_not_found": "No s'ha trobat el dispositiu." + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "Rang de brillantor utilitzat pel dispositiu", + "curr_temp_divider": "Divisor del valor de temperatura actual (0 = predeterminat)", + "max_kelvin": "Temperatura del color m\u00e0xima suportada, en Kelvin", + "max_temp": "Temperatura desitjada m\u00e0xima (utilitza min i max = 0 per defecte)", + "min_kelvin": "Temperatura del color m\u00ednima suportada, en Kelvin", + "min_temp": "Temperatura desitjada m\u00ednima (utilitza min i max = 0 per defecte)", + "set_temp_divided": "Utilitza el valor de temperatura dividit per a ordres de configuraci\u00f3 de temperatura", + "support_color": "For\u00e7a el suport de color", + "temp_divider": "Divisor del valor de temperatura (0 = predeterminat)", + "temp_step_override": "Pas de temperatura objectiu", + "tuya_max_coltemp": "Temperatura de color m\u00e0xima enviada pel dispositiu", + "unit_of_measurement": "Unitat de temperatura utilitzada pel dispositiu" + }, + "description": "Configura les opcions per ajustar la informaci\u00f3 mostrada pel dispositiu {device_type} `{device_name}`", + "title": "Configuraci\u00f3 de dispositiu Tuya" + }, + "init": { + "data": { + "discovery_interval": "Interval de sondeig del dispositiu de descoberta, en segons", + "list_devices": "Selecciona els dispositius a configurar o deixa-ho buit per desar la configuraci\u00f3", + "query_device": "Selecciona el dispositiu que utilitzar\u00e0 m\u00e8tode de consulta, per actualitzacions d'estat m\u00e9s freq\u00fcents", + "query_interval": "Interval de sondeig de consultes del dispositiu, en segons" + }, + "description": "No estableixis valors d'interval de sondeig massa baixos ja que les crides fallaran i generaran missatges d'error al registre", + "title": "Configuraci\u00f3 d'opcions de Tuya" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/cs.json b/homeassistant/components/tuya/translations/cs.json new file mode 100644 index 00000000000..1dda4ea6df7 --- /dev/null +++ b/homeassistant/components/tuya/translations/cs.json @@ -0,0 +1,60 @@ +{ + "config": { + "abort": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." + }, + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "flow_title": "Konfigurace Tuya", + "step": { + "user": { + "data": { + "country_code": "K\u00f3d zem\u011b va\u0161eho \u00fa\u010dtu (nap\u0159. 1 pro USA nebo 86 pro \u010c\u00ednu)", + "password": "Heslo", + "platform": "Aplikace, ve kter\u00e9 m\u00e1te zaregistrovan\u00fd \u00fa\u010det", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + }, + "description": "Zadejte sv\u00e9 p\u0159ihla\u0161ovac\u00ed \u00fadaje k Tuya.", + "title": "Tuya" + } + } + }, + "options": { + "abort": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "error": { + "dev_multi_type": "V\u00edce vybran\u00fdch za\u0159\u00edzen\u00ed k nastaven\u00ed mus\u00ed b\u00fdt stejn\u00e9ho typu", + "dev_not_config": "Typ za\u0159\u00edzen\u00ed nelze nastavit", + "dev_not_found": "Za\u0159\u00edzen\u00ed nenalezeno" + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "Rozsah jasu pou\u017e\u00edvan\u00fd za\u0159\u00edzen\u00edm", + "max_kelvin": "Maxim\u00e1ln\u00ed podporovan\u00e1 teplota barev v kelvinech", + "max_temp": "Maxim\u00e1ln\u00ed c\u00edlov\u00e1 teplota (pou\u017eijte min a max = 0 jako v\u00fdchoz\u00ed)", + "min_kelvin": "Maxim\u00e1ln\u00ed podporovan\u00e1 teplota barev v kelvinech", + "min_temp": "Minim\u00e1ln\u00ed c\u00edlov\u00e1 teplota (pou\u017eijte min a max = 0 jako v\u00fdchoz\u00ed)", + "support_color": "Vynutit podporu barev", + "tuya_max_coltemp": "Maxim\u00e1ln\u00ed teplota barev nahl\u00e1\u0161en\u00e1 za\u0159\u00edzen\u00edm", + "unit_of_measurement": "Jednotka teploty pou\u017e\u00edvan\u00e1 za\u0159\u00edzen\u00edm" + }, + "title": "Nastavte za\u0159\u00edzen\u00ed Tuya" + }, + "init": { + "data": { + "discovery_interval": "Interval objevov\u00e1n\u00ed za\u0159\u00edzen\u00ed v sekund\u00e1ch", + "list_devices": "Vyberte za\u0159\u00edzen\u00ed, kter\u00e1 chcete nastavit, nebo ponechte pr\u00e1zdn\u00e9, abyste konfiguraci ulo\u017eili", + "query_device": "Vyberte za\u0159\u00edzen\u00ed, kter\u00e9 bude pou\u017e\u00edvat metodu dotaz\u016f pro rychlej\u0161\u00ed aktualizaci stavu", + "query_interval": "Interval dotazov\u00e1n\u00ed za\u0159\u00edzen\u00ed v sekund\u00e1ch" + }, + "description": "Nenastavujte intervalu dotazov\u00e1n\u00ed p\u0159\u00edli\u0161 n\u00edzk\u00e9 hodnoty, jinak se dotazov\u00e1n\u00ed nezda\u0159\u00ed a bude generovat chybov\u00e9 zpr\u00e1vy do logu", + "title": "Nastavte mo\u017enosti Tuya" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/de.json b/homeassistant/components/tuya/translations/de.json new file mode 100644 index 00000000000..57439e1fa76 --- /dev/null +++ b/homeassistant/components/tuya/translations/de.json @@ -0,0 +1,79 @@ +{ + "config": { + "abort": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, + "flow_title": "Tuya Konfiguration", + "step": { + "login": { + "data": { + "access_id": "Zugangs-ID", + "access_secret": "Zugangsgeheimnis", + "country_code": "L\u00e4ndercode", + "endpoint": "Verf\u00fcgbarkeitszone", + "password": "Passwort", + "tuya_app_type": "Mobile App", + "username": "Konto" + }, + "description": "Gib deine Tuya-Anmeldedaten ein", + "title": "Tuya" + }, + "user": { + "data": { + "country_code": "L\u00e4ndercode deines Kontos (z. B. 1 f\u00fcr USA oder 86 f\u00fcr China)", + "password": "Passwort", + "platform": "Die App, in der dein Konto registriert ist", + "tuya_project_type": "Tuya Cloud Projekttyp", + "username": "Benutzername" + }, + "description": "Gib deine Tuya-Anmeldeinformationen ein.", + "title": "Tuya-Integration" + } + } + }, + "options": { + "abort": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "error": { + "dev_multi_type": "Mehrere ausgew\u00e4hlte Ger\u00e4te zur Konfiguration m\u00fcssen vom gleichen Typ sein", + "dev_not_config": "Ger\u00e4tetyp nicht konfigurierbar", + "dev_not_found": "Ger\u00e4t nicht gefunden" + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "Vom Ger\u00e4t genutzter Helligkeitsbereich", + "curr_temp_divider": "Aktueller Temperaturwert-Teiler (0 = Standard verwenden)", + "max_kelvin": "Maximal unterst\u00fctzte Farbtemperatur in Kelvin", + "max_temp": "Maximale Solltemperatur (f\u00fcr Voreinstellung min und max = 0 verwenden)", + "min_kelvin": "Minimale unterst\u00fctzte Farbtemperatur in Kelvin", + "min_temp": "Minimal Solltemperatur (f\u00fcr Voreinstellung min und max = 0 verwenden)", + "set_temp_divided": "Geteilten Temperaturwert f\u00fcr Solltemperaturbefehl verwenden", + "support_color": "Farbunterst\u00fctzung erzwingen", + "temp_divider": "Teiler f\u00fcr Temperaturwerte (0 = Standard verwenden)", + "temp_step_override": "Zieltemperaturschritt", + "tuya_max_coltemp": "Vom Ger\u00e4t gemeldete maximale Farbtemperatur", + "unit_of_measurement": "Vom Ger\u00e4t verwendete Temperatureinheit" + }, + "description": "Optionen zur Anpassung der angezeigten Informationen f\u00fcr das Ger\u00e4t `{device_name}` vom Typ: {device_type}konfigurieren", + "title": "Tuya-Ger\u00e4t konfigurieren" + }, + "init": { + "data": { + "discovery_interval": "Abfrageintervall f\u00fcr Ger\u00e4teabruf in Sekunden", + "list_devices": "W\u00e4hle die zu konfigurierenden Ger\u00e4te aus oder lasse sie leer, um die Konfiguration zu speichern", + "query_device": "W\u00e4hle ein Ger\u00e4t aus, das die Abfragemethode f\u00fcr eine schnellere Statusaktualisierung verwendet.", + "query_interval": "Ger\u00e4teabrufintervall in Sekunden" + }, + "description": "Stelle das Abfrageintervall nicht zu niedrig ein, sonst schlagen die Aufrufe fehl und erzeugen eine Fehlermeldung im Protokoll", + "title": "Tuya-Optionen konfigurieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/en.json b/homeassistant/components/tuya/translations/en.json index 631f0b7172f..c7aaee977ee 100644 --- a/homeassistant/components/tuya/translations/en.json +++ b/homeassistant/components/tuya/translations/en.json @@ -1,29 +1,79 @@ { "config": { + "abort": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, "error": { "invalid_auth": "Invalid authentication" }, "flow_title": "Tuya configuration", "step": { - "user":{ - "title":"Tuya Integration", - "data":{ - "tuya_project_type": "Tuya cloud project type" - } - }, "login": { "data": { - "endpoint": "Availability Zone", "access_id": "Access ID", "access_secret": "Access Secret", - "tuya_app_type": "Mobile App", "country_code": "Country Code", - "username": "Account", - "password": "Password" + "endpoint": "Availability Zone", + "password": "Password", + "tuya_app_type": "Mobile App", + "username": "Account" }, - "description": "Enter your Tuya credential.", + "description": "Enter your Tuya credential", "title": "Tuya" + }, + "user": { + "data": { + "country_code": "Your account country code (e.g., 1 for USA or 86 for China)", + "password": "Password", + "platform": "The app where your account is registered", + "tuya_project_type": "Tuya cloud project type", + "username": "Username" + }, + "description": "Enter your Tuya credentials.", + "title": "Tuya Integration" + } + } + }, + "options": { + "abort": { + "cannot_connect": "Failed to connect" + }, + "error": { + "dev_multi_type": "Multiple selected devices to configure must be of the same type", + "dev_not_config": "Device type not configurable", + "dev_not_found": "Device not found" + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "Brightness range used by device", + "curr_temp_divider": "Current Temperature value divider (0 = use default)", + "max_kelvin": "Max color temperature supported in kelvin", + "max_temp": "Max target temperature (use min and max = 0 for default)", + "min_kelvin": "Min color temperature supported in kelvin", + "min_temp": "Min target temperature (use min and max = 0 for default)", + "set_temp_divided": "Use divided Temperature value for set temperature command", + "support_color": "Force color support", + "temp_divider": "Temperature values divider (0 = use default)", + "temp_step_override": "Target Temperature step", + "tuya_max_coltemp": "Max color temperature reported by device", + "unit_of_measurement": "Temperature unit used by device" + }, + "description": "Configure options to adjust displayed information for {device_type} device `{device_name}`", + "title": "Configure Tuya Device" + }, + "init": { + "data": { + "discovery_interval": "Discovery device polling interval in seconds", + "list_devices": "Select the devices to configure or leave empty to save configuration", + "query_device": "Select device that will use query method for faster status update", + "query_interval": "Query device polling interval in seconds" + }, + "description": "Do not set pollings interval values too low or the calls will fail generating error message in the log", + "title": "Configure Tuya Options" } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/es.json b/homeassistant/components/tuya/translations/es.json new file mode 100644 index 00000000000..74649379a7c --- /dev/null +++ b/homeassistant/components/tuya/translations/es.json @@ -0,0 +1,79 @@ +{ + "config": { + "abort": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." + }, + "error": { + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + }, + "flow_title": "Configuraci\u00f3n Tuya", + "step": { + "login": { + "data": { + "access_id": "ID de acceso", + "access_secret": "Acceso secreto", + "country_code": "C\u00f3digo de pa\u00eds", + "endpoint": "Zona de disponibilidad", + "password": "Contrase\u00f1a", + "tuya_app_type": "Aplicaci\u00f3n m\u00f3vil", + "username": "Cuenta" + }, + "description": "Ingrese su credencial Tuya", + "title": "Tuya" + }, + "user": { + "data": { + "country_code": "C\u00f3digo de pais de tu cuenta (por ejemplo, 1 para USA o 86 para China)", + "password": "Contrase\u00f1a", + "platform": "La aplicaci\u00f3n en la cual registraste tu cuenta", + "tuya_project_type": "Tipo de proyecto en la nube de Tuya", + "username": "Usuario" + }, + "description": "Introduce tu credencial Tuya.", + "title": "Tuya" + } + } + }, + "options": { + "abort": { + "cannot_connect": "No se pudo conectar" + }, + "error": { + "dev_multi_type": "Los m\u00faltiples dispositivos seleccionados para configurar deben ser del mismo tipo", + "dev_not_config": "Tipo de dispositivo no configurable", + "dev_not_found": "Dispositivo no encontrado" + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "Rango de brillo utilizado por el dispositivo", + "curr_temp_divider": "Divisor del valor de la temperatura actual (0 = usar valor por defecto)", + "max_kelvin": "Temperatura de color m\u00e1xima admitida en kelvin", + "max_temp": "Temperatura objetivo m\u00e1xima (usa m\u00edn. y m\u00e1x. = 0 por defecto)", + "min_kelvin": "Temperatura de color m\u00ednima soportada en kelvin", + "min_temp": "Temperatura objetivo m\u00ednima (usa m\u00edn. y m\u00e1x. = 0 por defecto)", + "set_temp_divided": "Use el valor de temperatura dividido para el comando de temperatura establecida", + "support_color": "Forzar soporte de color", + "temp_divider": "Divisor de los valores de temperatura (0 = usar valor por defecto)", + "temp_step_override": "Temperatura deseada", + "tuya_max_coltemp": "Temperatura de color m\u00e1xima notificada por dispositivo", + "unit_of_measurement": "Unidad de temperatura utilizada por el dispositivo" + }, + "description": "Configura las opciones para ajustar la informaci\u00f3n mostrada para {device_type} dispositivo `{device_name}`", + "title": "Configurar dispositivo Tuya" + }, + "init": { + "data": { + "discovery_interval": "Intervalo de sondeo del descubrimiento al dispositivo en segundos", + "list_devices": "Selecciona los dispositivos a configurar o d\u00e9jalos en blanco para guardar la configuraci\u00f3n", + "query_device": "Selecciona el dispositivo que utilizar\u00e1 el m\u00e9todo de consulta para una actualizaci\u00f3n de estado m\u00e1s r\u00e1pida", + "query_interval": "Intervalo de sondeo de la consulta al dispositivo en segundos" + }, + "description": "No establezcas valores de intervalo de sondeo demasiado bajos o las llamadas fallar\u00e1n generando un mensaje de error en el registro", + "title": "Configurar opciones de Tuya" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/et.json b/homeassistant/components/tuya/translations/et.json new file mode 100644 index 00000000000..45b4e4d2639 --- /dev/null +++ b/homeassistant/components/tuya/translations/et.json @@ -0,0 +1,79 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamise viga", + "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks sidumine." + }, + "error": { + "invalid_auth": "Tuvastamise viga" + }, + "flow_title": "Tuya seaded", + "step": { + "login": { + "data": { + "access_id": "Juurdep\u00e4\u00e4su ID", + "access_secret": "API salas\u00f5na", + "country_code": "Riigi kood", + "endpoint": "Seadmete regioon", + "password": "Salas\u00f5na", + "tuya_app_type": "Mobiilirakendus", + "username": "Konto" + }, + "description": "Sisesta oma Tuya mandaat", + "title": "Tuya" + }, + "user": { + "data": { + "country_code": "Konto riigikood (nt 1 USA v\u00f5i 372 Eesti)", + "password": "Salas\u00f5na", + "platform": "\u00c4pp kus konto registreeriti", + "tuya_project_type": "Tuya pilveprojekti t\u00fc\u00fcp", + "username": "Kasutajanimi" + }, + "description": "Sisesta oma Tuya konto andmed.", + "title": "Tuya sidumine" + } + } + }, + "options": { + "abort": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "error": { + "dev_multi_type": "Mitu h\u00e4\u00e4lestatavat seadet peavad olema sama t\u00fc\u00fcpi", + "dev_not_config": "Seda t\u00fc\u00fcpi seade pole seadistatav", + "dev_not_found": "Seadet ei leitud" + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "Seadme kasutatav heledusvahemik", + "curr_temp_divider": "Praeguse temperatuuri v\u00e4\u00e4rtuse eraldaja (0 = kasuta vaikev\u00e4\u00e4rtust)", + "max_kelvin": "Maksimaalne v\u00f5imalik v\u00e4rvitemperatuur (Kelvinites)", + "max_temp": "Maksimaalne sihttemperatuur (vaikimisi kasuta min ja max = 0)", + "min_kelvin": "Minimaalne v\u00f5imalik v\u00e4rvitemperatuur (Kelvinites)", + "min_temp": "Minimaalne sihttemperatuur (vaikimisi kasuta min ja max = 0)", + "set_temp_divided": "M\u00e4\u00e4ratud temperatuuri k\u00e4su jaoks kasuta jagatud temperatuuri v\u00e4\u00e4rtust", + "support_color": "Luba v\u00e4rvuse juhtimine", + "temp_divider": "Temperatuuri v\u00e4\u00e4rtuse eraldaja (0 = kasuta vaikev\u00e4\u00e4rtust)", + "temp_step_override": "Sihttemperatuuri samm", + "tuya_max_coltemp": "Seadme teatatud maksimaalne v\u00e4rvitemperatuur", + "unit_of_measurement": "Seadme temperatuuri\u00fchik" + }, + "description": "Suvandid \u00fcksuse {device_type} {device_name} kuvatava teabe muutmiseks", + "title": "H\u00e4\u00e4lesta Tuya seade" + }, + "init": { + "data": { + "discovery_interval": "Seadme leidmisp\u00e4ringute intervall (sekundites)", + "list_devices": "Vali seadistatavad seadmed v\u00f5i j\u00e4ta s\u00e4tete salvestamiseks t\u00fchjaks", + "query_device": "Vali seade, mis kasutab oleku kiiremaks v\u00e4rskendamiseks p\u00e4ringumeetodit", + "query_interval": "P\u00e4ringute intervall (sekundites)" + }, + "description": "\u00c4ra m\u00e4\u00e4ra k\u00fcsitlusintervalli v\u00e4\u00e4rtusi liiga madalaks, vastasel korral v\u00f5ivad p\u00e4ringud logis t\u00f5rketeate genereerida", + "title": "Tuya suvandite seadistamine" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/fi.json b/homeassistant/components/tuya/translations/fi.json new file mode 100644 index 00000000000..3c74a9b8eeb --- /dev/null +++ b/homeassistant/components/tuya/translations/fi.json @@ -0,0 +1,17 @@ +{ + "config": { + "flow_title": "Tuya-asetukset", + "step": { + "user": { + "data": { + "country_code": "Tilisi maakoodi (esim. 1 Yhdysvalloissa, 358 Suomessa)", + "password": "Salasana", + "platform": "Sovellus, johon tili rekister\u00f6id\u00e4\u00e4n", + "username": "K\u00e4ytt\u00e4j\u00e4tunnus" + }, + "description": "Anna Tuya-tunnistetietosi.", + "title": "Tuya" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/fr.json b/homeassistant/components/tuya/translations/fr.json new file mode 100644 index 00000000000..b741d3f9377 --- /dev/null +++ b/homeassistant/components/tuya/translations/fr.json @@ -0,0 +1,65 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification invalide", + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "error": { + "invalid_auth": "Authentification invalide" + }, + "flow_title": "Configuration Tuya", + "step": { + "user": { + "data": { + "country_code": "Le code de pays de votre compte (par exemple, 1 pour les \u00c9tats-Unis ou 86 pour la Chine)", + "password": "Mot de passe", + "platform": "L'application dans laquelle votre compte est enregistr\u00e9", + "username": "Nom d'utilisateur" + }, + "description": "Saisissez vos informations d'identification Tuya.", + "title": "Tuya" + } + } + }, + "options": { + "abort": { + "cannot_connect": "\u00c9chec de connexion" + }, + "error": { + "dev_multi_type": "Plusieurs p\u00e9riph\u00e9riques s\u00e9lectionn\u00e9s \u00e0 configurer doivent \u00eatre du m\u00eame type", + "dev_not_config": "Type d'appareil non configurable", + "dev_not_found": "Appareil non trouv\u00e9" + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "Plage de luminosit\u00e9 utilis\u00e9e par l'appareil", + "curr_temp_divider": "Diviseur de valeur de temp\u00e9rature actuelle (0 = utiliser la valeur par d\u00e9faut)", + "max_kelvin": "Temp\u00e9rature de couleur maximale prise en charge en Kelvin", + "max_temp": "Temp\u00e9rature cible maximale (utilisez min et max = 0 par d\u00e9faut)", + "min_kelvin": "Temp\u00e9rature de couleur minimale prise en charge en kelvin", + "min_temp": "Temp\u00e9rature cible minimale (utilisez min et max = 0 par d\u00e9faut)", + "set_temp_divided": "Utilisez la valeur de temp\u00e9rature divis\u00e9e pour la commande de temp\u00e9rature d\u00e9finie", + "support_color": "Forcer la prise en charge des couleurs", + "temp_divider": "Diviseur de valeurs de temp\u00e9rature (0 = utiliser la valeur par d\u00e9faut)", + "temp_step_override": "Pas de temp\u00e9rature cible", + "tuya_max_coltemp": "Temp\u00e9rature de couleur maximale rapport\u00e9e par l'appareil", + "unit_of_measurement": "Unit\u00e9 de temp\u00e9rature utilis\u00e9e par l'appareil" + }, + "description": "Configurer les options pour ajuster les informations affich\u00e9es pour l'appareil {device_type} ` {device_name} `", + "title": "Configurer l'appareil Tuya" + }, + "init": { + "data": { + "discovery_interval": "Intervalle de d\u00e9couverte de l'appareil en secondes", + "list_devices": "S\u00e9lectionnez les appareils \u00e0 configurer ou laissez vide pour enregistrer la configuration", + "query_device": "S\u00e9lectionnez l'appareil qui utilisera la m\u00e9thode de requ\u00eate pour une mise \u00e0 jour plus rapide de l'\u00e9tat", + "query_interval": "Intervalle d'interrogation de l'appareil en secondes" + }, + "description": "Ne d\u00e9finissez pas des valeurs d'intervalle d'interrogation trop faibles ou les appels \u00e9choueront \u00e0 g\u00e9n\u00e9rer un message d'erreur dans le journal", + "title": "Configurer les options de Tuya" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/he.json b/homeassistant/components/tuya/translations/he.json new file mode 100644 index 00000000000..44a7699e511 --- /dev/null +++ b/homeassistant/components/tuya/translations/he.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "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", + "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": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "flow_title": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d8\u05d5\u05d9\u05d4", + "step": { + "user": { + "data": { + "country_code": "\u05e7\u05d5\u05d3 \u05de\u05d3\u05d9\u05e0\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05e9\u05dc\u05da (\u05dc\u05de\u05e9\u05dc, 1 \u05dc\u05d0\u05e8\u05d4\"\u05d1 \u05d0\u05d5 972 \u05dc\u05d9\u05e9\u05e8\u05d0\u05dc)", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "platform": "\u05d4\u05d9\u05d9\u05e9\u05d5\u05dd \u05d1\u05d5 \u05e8\u05e9\u05d5\u05dd \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05e9\u05dc\u05da", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + }, + "description": "\u05d4\u05d6\u05df \u05d0\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8\u05d9 \u05d8\u05d5\u05d9\u05d4 \u05e9\u05dc\u05da.", + "title": "Tuya" + } + } + }, + "options": { + "abort": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/hu.json b/homeassistant/components/tuya/translations/hu.json new file mode 100644 index 00000000000..b90d2a2ff81 --- /dev/null +++ b/homeassistant/components/tuya/translations/hu.json @@ -0,0 +1,65 @@ +{ + "config": { + "abort": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "flow_title": "Tuya konfigur\u00e1ci\u00f3", + "step": { + "user": { + "data": { + "country_code": "A fi\u00f3k orsz\u00e1gk\u00f3dja (pl. 1 USA, 36 Magyarorsz\u00e1g, vagy 86 K\u00edna)", + "password": "Jelsz\u00f3", + "platform": "Az alkalmaz\u00e1s, ahol a fi\u00f3k regisztr\u00e1lt", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "Adja meg Tuya hiteles\u00edt\u0151 adatait.", + "title": "Tuya" + } + } + }, + "options": { + "abort": { + "cannot_connect": "A kapcsol\u00f3d\u00e1s nem siker\u00fclt" + }, + "error": { + "dev_multi_type": "T\u00f6bb kiv\u00e1lasztott konfigur\u00e1land\u00f3 eszk\u00f6z eset\u00e9n, azonos t\u00edpus\u00fanak kell lennie", + "dev_not_config": "Ez az eszk\u00f6zt\u00edpus nem konfigur\u00e1lhat\u00f3", + "dev_not_found": "Eszk\u00f6z nem tal\u00e1lhat\u00f3" + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "Az eszk\u00f6z \u00e1ltal haszn\u00e1lt f\u00e9nyer\u0151 tartom\u00e1ny", + "curr_temp_divider": "Aktu\u00e1lis h\u0151m\u00e9rs\u00e9klet-\u00e9rt\u00e9k oszt\u00f3 (0 = alap\u00e9rtelmezetten)", + "max_kelvin": "Maxim\u00e1lis t\u00e1mogatott sz\u00ednh\u0151m\u00e9rs\u00e9klet kelvinben", + "max_temp": "Maxim\u00e1lis k\u00edv\u00e1nt h\u0151m\u00e9rs\u00e9klet (alap\u00e9rtelmezettnek min \u00e9s max 0)", + "min_kelvin": "Minimum t\u00e1mogatott sz\u00ednh\u0151m\u00e9rs\u00e9klet kelvinben", + "min_temp": "Minim\u00e1lis k\u00edv\u00e1nt h\u0151m\u00e9rs\u00e9klet (alap\u00e9rtelmezettnek min \u00e9s max 0)", + "set_temp_divided": "A h\u0151m\u00e9rs\u00e9klet be\u00e1ll\u00edt\u00e1s\u00e1hoz osztott h\u0151m\u00e9rs\u00e9kleti \u00e9rt\u00e9ket haszn\u00e1ljon", + "support_color": "Sz\u00ednt\u00e1mogat\u00e1s k\u00e9nyszer\u00edt\u00e9se", + "temp_divider": "Sz\u00ednh\u0151m\u00e9rs\u00e9klet-\u00e9rt\u00e9kek oszt\u00f3ja (0 = alap\u00e9rtelmezett)", + "temp_step_override": "C\u00e9lh\u0151m\u00e9rs\u00e9klet l\u00e9pcs\u0151", + "tuya_max_coltemp": "Az eszk\u00f6z \u00e1ltal megadott maxim\u00e1lis sz\u00ednh\u0151m\u00e9rs\u00e9klet", + "unit_of_measurement": "Az eszk\u00f6z \u00e1ltal haszn\u00e1lt h\u0151m\u00e9rs\u00e9kleti egys\u00e9g" + }, + "description": "Konfigur\u00e1l\u00e1si lehet\u0151s\u00e9gek a(z) {device_type} t\u00edpus\u00fa `{device_name}` eszk\u00f6z megjelen\u00edtett inform\u00e1ci\u00f3inak be\u00e1ll\u00edt\u00e1s\u00e1hoz", + "title": "Tuya eszk\u00f6z konfigur\u00e1l\u00e1sa" + }, + "init": { + "data": { + "discovery_interval": "Felfedez\u0151 eszk\u00f6z lek\u00e9rdez\u00e9si intervalluma m\u00e1sodpercben", + "list_devices": "V\u00e1lassza ki a konfigur\u00e1lni k\u00edv\u00e1nt eszk\u00f6z\u00f6ket, vagy hagyja \u00fcresen a konfigur\u00e1ci\u00f3 ment\u00e9s\u00e9hez", + "query_device": "V\u00e1lassza ki azt az eszk\u00f6zt, amely a lek\u00e9rdez\u00e9si m\u00f3dszert haszn\u00e1lja a gyorsabb \u00e1llapotfriss\u00edt\u00e9shez", + "query_interval": "Eszk\u00f6z lek\u00e9rdez\u00e9si id\u0151k\u00f6ze m\u00e1sodpercben" + }, + "description": "Ne \u00e1ll\u00edtsd t\u00fal alacsonyra a lek\u00e9rdez\u00e9si intervallum \u00e9rt\u00e9keit, k\u00fcl\u00f6nben a h\u00edv\u00e1sok nem fognak hiba\u00fczenetet gener\u00e1lni a napl\u00f3ban", + "title": "Tuya be\u00e1ll\u00edt\u00e1sok konfigur\u00e1l\u00e1sa" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/id.json b/homeassistant/components/tuya/translations/id.json new file mode 100644 index 00000000000..8b7f196b5a2 --- /dev/null +++ b/homeassistant/components/tuya/translations/id.json @@ -0,0 +1,65 @@ +{ + "config": { + "abort": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "invalid_auth": "Autentikasi tidak valid" + }, + "flow_title": "Konfigurasi Tuya", + "step": { + "user": { + "data": { + "country_code": "Kode negara akun Anda (mis., 1 untuk AS atau 86 untuk China)", + "password": "Kata Sandi", + "platform": "Aplikasi tempat akun Anda terdaftar", + "username": "Nama Pengguna" + }, + "description": "Masukkan kredensial Tuya Anda.", + "title": "Tuya" + } + } + }, + "options": { + "abort": { + "cannot_connect": "Gagal terhubung" + }, + "error": { + "dev_multi_type": "Untuk konfigurasi sekaligus, beberapa perangkat yang dipilih harus berjenis sama", + "dev_not_config": "Jenis perangkat tidak dapat dikonfigurasi", + "dev_not_found": "Perangkat tidak ditemukan" + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "Rentang kecerahan yang digunakan oleh perangkat", + "curr_temp_divider": "Pembagi nilai suhu saat ini (0 = gunakan bawaan)", + "max_kelvin": "Suhu warna maksimal yang didukung dalam Kelvin", + "max_temp": "Suhu target maksimal (gunakan min dan maks = 0 untuk bawaan)", + "min_kelvin": "Suhu warna minimal yang didukung dalam Kelvin", + "min_temp": "Suhu target minimal (gunakan min dan maks = 0 untuk bawaan)", + "set_temp_divided": "Gunakan nilai suhu terbagi untuk mengirimkan perintah mengatur suhu", + "support_color": "Paksa dukungan warna", + "temp_divider": "Pembagi nilai suhu (0 = gunakan bawaan)", + "temp_step_override": "Langkah Suhu Target", + "tuya_max_coltemp": "Suhu warna maksimal yang dilaporkan oleh perangkat", + "unit_of_measurement": "Satuan suhu yang digunakan oleh perangkat" + }, + "description": "Konfigurasikan opsi untuk menyesuaikan informasi yang ditampilkan untuk perangkat {device_type} `{device_name}`", + "title": "Konfigurasi Perangkat Tuya" + }, + "init": { + "data": { + "discovery_interval": "Interval polling penemuan perangkat dalam detik", + "list_devices": "Pilih perangkat yang akan dikonfigurasi atau biarkan kosong untuk menyimpan konfigurasi", + "query_device": "Pilih perangkat yang akan menggunakan metode kueri untuk pembaruan status lebih cepat", + "query_interval": "Interval polling perangkat kueri dalam detik" + }, + "description": "Jangan atur nilai interval polling terlalu rendah karena panggilan akan gagal menghasilkan pesan kesalahan dalam log", + "title": "Konfigurasikan Opsi Tuya" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/it.json b/homeassistant/components/tuya/translations/it.json new file mode 100644 index 00000000000..3baed47661c --- /dev/null +++ b/homeassistant/components/tuya/translations/it.json @@ -0,0 +1,79 @@ +{ + "config": { + "abort": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "error": { + "invalid_auth": "Autenticazione non valida" + }, + "flow_title": "Configurazione di Tuya", + "step": { + "login": { + "data": { + "access_id": "ID di accesso", + "access_secret": "Accesso segreto", + "country_code": "Prefisso internazionale", + "endpoint": "Zona di disponibilit\u00e0", + "password": "Password", + "tuya_app_type": "App per dispositivi mobili", + "username": "Account" + }, + "description": "Inserisci le tue credenziali Tuya", + "title": "Tuya" + }, + "user": { + "data": { + "country_code": "Prefisso internazionale del tuo account (ad es. 1 per gli Stati Uniti o 86 per la Cina)", + "password": "Password", + "platform": "L'app in cui \u00e8 registrato il tuo account", + "tuya_project_type": "Tipo di progetto Tuya cloud", + "username": "Nome utente" + }, + "description": "Inserisci le tue credenziali Tuya.", + "title": "Integrazione Tuya" + } + } + }, + "options": { + "abort": { + "cannot_connect": "Impossibile connettersi" + }, + "error": { + "dev_multi_type": "Pi\u00f9 dispositivi selezionati da configurare devono essere dello stesso tipo", + "dev_not_config": "Tipo di dispositivo non configurabile", + "dev_not_found": "Dispositivo non trovato" + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "Intervallo di luminosit\u00e0 utilizzato dal dispositivo", + "curr_temp_divider": "Divisore del valore della temperatura corrente (0 = usa il valore predefinito)", + "max_kelvin": "Temperatura colore massima supportata in kelvin", + "max_temp": "Temperatura di destinazione massima (utilizzare min e max = 0 per impostazione predefinita)", + "min_kelvin": "Temperatura colore minima supportata in kelvin", + "min_temp": "Temperatura di destinazione minima (utilizzare min e max = 0 per impostazione predefinita)", + "set_temp_divided": "Utilizzare il valore temperatura diviso per impostare il comando temperatura", + "support_color": "Forza il supporto del colore", + "temp_divider": "Divisore dei valori di temperatura (0 = utilizzare il valore predefinito)", + "temp_step_override": "Passo della temperatura da raggiungere", + "tuya_max_coltemp": "Temperatura di colore massima riportata dal dispositivo", + "unit_of_measurement": "Unit\u00e0 di temperatura utilizzata dal dispositivo" + }, + "description": "Configura le opzioni per regolare le informazioni visualizzate per il dispositivo {device_type} `{device_name}`", + "title": "Configura il dispositivo Tuya" + }, + "init": { + "data": { + "discovery_interval": "Intervallo di scansione di rilevamento dispositivo in secondi", + "list_devices": "Selezionare i dispositivi da configurare o lasciare vuoto per salvare la configurazione", + "query_device": "Selezionare il dispositivo che utilizzer\u00e0 il metodo di interrogazione per un pi\u00f9 rapido aggiornamento dello stato", + "query_interval": "Intervallo di scansione di interrogazione dispositivo in secondi" + }, + "description": "Non impostare valori dell'intervallo di scansione troppo bassi o le chiamate non riusciranno a generare un messaggio di errore nel registro", + "title": "Configura le opzioni Tuya" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/ka.json b/homeassistant/components/tuya/translations/ka.json new file mode 100644 index 00000000000..7c80ef1ffba --- /dev/null +++ b/homeassistant/components/tuya/translations/ka.json @@ -0,0 +1,37 @@ +{ + "options": { + "error": { + "dev_multi_type": "\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d8\u10e1\u10d7\u10d5\u10d8\u10e1 \u10e8\u10d4\u10e0\u10e9\u10d4\u10e3\u10da\u10d8 \u10db\u10e0\u10d0\u10d5\u10da\u10dd\u10d1\u10d8\u10d7\u10d8 \u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10e3\u10dc\u10d3\u10d0 \u10d8\u10e7\u10dd\u10e1 \u10d4\u10e0\u10d7\u10dc\u10d0\u10d8\u10e0\u10d8 \u10e2\u10d8\u10de\u10d8\u10e1", + "dev_not_config": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10e2\u10d8\u10de\u10d8 \u10d0\u10e0 \u10d0\u10e0\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10d0\u10d3\u10d8", + "dev_not_found": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10d8\u10eb\u10d4\u10d1\u10dc\u10d0" + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10db\u10d8\u10d4\u10e0 \u10d2\u10d0\u10db\u10dd\u10e7\u10d4\u10dc\u10d4\u10d1\u10e3\u10da\u10d8 \u10e1\u10d8\u10d9\u10d0\u10e8\u10d9\u10d0\u10e8\u10d8\u10e1 \u10d3\u10d8\u10d0\u10de\u10d0\u10d6\u10dd\u10dc\u10d8", + "curr_temp_divider": "\u10db\u10d8\u10db\u10d3\u10d8\u10dc\u10d0\u10e0\u10d4 \u10e2\u10d4\u10db\u10d4\u10de\u10e0\u10d0\u10e2\u10e3\u10e0\u10d8\u10e1 \u10db\u10dc\u10d8\u10e8\u10d5\u10dc\u10d4\u10da\u10d8\u10e1 \u10d2\u10d0\u10db\u10e7\u10dd\u10e4\u10d8 (0 - \u10dc\u10d0\u10d2\u10e3\u10da\u10d8\u10e1\u10ee\u10db\u10d4\u10d5\u10d8)", + "max_kelvin": "\u10db\u10ee\u10d0\u10e0\u10d3\u10d0\u10ed\u10d4\u10e0\u10d8\u10da\u10d8 \u10db\u10d0\u10e5\u10e1\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8 \u10e2\u10d4\u10db\u10de\u10d4\u10e0\u10d0\u10e2\u10e3\u10e0\u10d8\u10e1 \u10e4\u10d4\u10e0\u10d8 \u10d9\u10d4\u10da\u10d5\u10d8\u10dc\u10d4\u10d1\u10e8\u10d8", + "max_temp": "\u10db\u10d0\u10e5\u10e1\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8 \u10db\u10d8\u10d6\u10dc\u10dd\u10d1\u10e0\u10d8\u10d5\u10d8 \u10e2\u10d4\u10db\u10de\u10d4\u10e0\u10d0\u10e2\u10e3\u10e0\u10d0 (\u10db\u10d8\u10dc\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8 \u10d3\u10d0 \u10db\u10d0\u10e5\u10e1\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8\u10e1\u10d0\u10d7\u10d5\u10d8\u10e1 \u10dc\u10d0\u10d2\u10e3\u10da\u10d8\u10e1\u10ee\u10db\u10d4\u10d5\u10d8\u10d0 0)", + "min_kelvin": "\u10db\u10ee\u10d0\u10e0\u10d3\u10d0\u10ed\u10d4\u10e0\u10d8\u10da\u10d8 \u10db\u10d8\u10dc\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8 \u10e2\u10d4\u10db\u10de\u10d4\u10e0\u10d0\u10e2\u10e3\u10e0\u10d8\u10e1 \u10e4\u10d4\u10e0\u10d8 \u10d9\u10d4\u10da\u10d5\u10d8\u10dc\u10d4\u10d1\u10e8\u10d8", + "min_temp": "\u10db\u10d8\u10dc\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8 \u10db\u10d8\u10d6\u10dc\u10dd\u10d1\u10e0\u10d8\u10d5\u10d8 \u10e2\u10d4\u10db\u10de\u10d4\u10e0\u10d0\u10e2\u10e3\u10e0\u10d0 (\u10db\u10d8\u10dc\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8 \u10d3\u10d0 \u10db\u10d0\u10e5\u10e1\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8\u10e1\u10d0\u10d7\u10d5\u10d8\u10e1 \u10dc\u10d0\u10d2\u10e3\u10da\u10d8\u10e1\u10ee\u10db\u10d4\u10d5\u10d8\u10d0 0)", + "support_color": "\u10e4\u10d4\u10e0\u10d8\u10e1 \u10db\u10ee\u10d0\u10e0\u10d3\u10d0\u10ed\u10d4\u10e0\u10d0 \u10d8\u10eb\u10e3\u10da\u10d4\u10d1\u10d8\u10d7", + "temp_divider": "\u10e2\u10d4\u10db\u10de\u10d4\u10e0\u10d0\u10e2\u10e3\u10e0\u10e3\u10da\u10d8 \u10db\u10dc\u10d8\u10e8\u10d5\u10dc\u10d4\u10da\u10d8\u10e1 \u10d2\u10d0\u10db\u10e7\u10dd\u10e4\u10d8 (0 = \u10dc\u10d0\u10d2\u10e3\u10da\u10d8\u10e1\u10ee\u10db\u10d4\u10d5\u10d8)", + "tuya_max_coltemp": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10db\u10d8\u10d4\u10e0 \u10db\u10dd\u10ec\u10dd\u10d3\u10d4\u10d1\u10e3\u10da\u10d8 \u10e4\u10d4\u10e0\u10d8\u10e1 \u10db\u10d0\u10e5\u10e1\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8 \u10e2\u10d4\u10db\u10de\u10d4\u10e0\u10d0\u10e2\u10e3\u10e0\u10d0", + "unit_of_measurement": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10db\u10d8\u10d4\u10e0 \u10d2\u10d0\u10db\u10dd\u10e7\u10d4\u10dc\u10d4\u10d1\u10e3\u10da\u10d8 \u10e2\u10d4\u10db\u10de\u10d4\u10e0\u10d0\u10e2\u10e3\u10e0\u10e3\u10da\u10d8 \u10d4\u10e0\u10d7\u10d4\u10e3\u10da\u10d8" + }, + "description": "\u10d3\u10d0\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d3 {device_type} `{device_name}` \u10de\u10d0\u10e0\u10d0\u10db\u10d4\u10e2\u10d4\u10e0\u10d1\u10d8 \u10d8\u10dc\u10e4\u10dd\u10e0\u10db\u10d0\u10ea\u10d8\u10d8\u10e1 \u10e9\u10d5\u10d4\u10dc\u10d4\u10d1\u10d8\u10e1 \u10db\u10dd\u10e1\u10d0\u10e0\u10d2\u10d4\u10d1\u10d0\u10d3", + "title": "Tuya-\u10e1 \u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10d0" + }, + "init": { + "data": { + "discovery_interval": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10d0\u10e6\u10db\u10dd\u10e9\u10d4\u10dc\u10d8\u10e1 \u10d2\u10d0\u10db\u10dd\u10d9\u10d8\u10d7\u10ee\u10d5\u10d8\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10d5\u10d0\u10da\u10d8 \u10ec\u10d0\u10db\u10d4\u10d1\u10e8\u10d8", + "list_devices": "\u10d0\u10d8\u10e0\u10e9\u10d8\u10d4\u10d7 \u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d8\u10e1\u10d7\u10d5\u10d8\u10e1 \u10d0\u10dc \u10d3\u10d0\u10e2\u10dd\u10d5\u10d4\u10d7 \u10ea\u10d0\u10e0\u10d8\u10d4\u10da\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d8\u10e1 \u10e8\u10d4\u10e1\u10d0\u10dc\u10d0\u10ee\u10d0\u10d3", + "query_device": "\u10d0\u10d8\u10e0\u10e9\u10d8\u10d4\u10d7 \u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0, \u10e0\u10dd\u10db\u10d4\u10da\u10d8\u10ea \u10d2\u10d0\u10db\u10dd\u10d8\u10e7\u10d4\u10dc\u10d4\u10d1\u10e1 \u10db\u10dd\u10d7\u10ee\u10dd\u10d5\u10dc\u10d8\u10e1 \u10db\u10d4\u10d7\u10dd\u10d3\u10e1 \u10e1\u10e2\u10d0\u10e2\u10e3\u10e1\u10d8\u10e1 \u10e1\u10ec\u10e0\u10d0\u10e4\u10d8 \u10d2\u10d0\u10dc\u10d0\u10ee\u10da\u10d4\u10d1\u10d8\u10e1\u10d7\u10d5\u10d8\u10e1", + "query_interval": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10d2\u10d0\u10db\u10dd\u10d9\u10d8\u10d7\u10ee\u10d5\u10d8\u10e1 \u10db\u10dd\u10d7\u10ee\u10dd\u10d5\u10dc\u10d8\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10d5\u10d0\u10da\u10d8 \u10ec\u10d0\u10db\u10d4\u10d1\u10e8\u10d8" + }, + "description": "\u10d0\u10e0 \u10d3\u10d0\u10d0\u10e7\u10d4\u10dc\u10dd\u10d7 \u10d2\u10d0\u10db\u10dd\u10d9\u10d8\u10d7\u10ee\u10d5\u10d8\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10d5\u10d0\u10da\u10d8\u10e1 \u10db\u10dc\u10d8\u10e8\u10d5\u10dc\u10d4\u10da\u10dd\u10d1\u10d4\u10d1\u10d8 \u10eb\u10d0\u10da\u10d8\u10d0\u10dc \u10db\u10ea\u10d8\u10e0\u10d4 \u10d7\u10dd\u10e0\u10d4\u10d1 \u10d2\u10d0\u10db\u10dd\u10eb\u10d0\u10ee\u10d4\u10d1\u10d4\u10d1\u10d8 \u10d3\u10d0\u10d0\u10d2\u10d4\u10dc\u10d4\u10e0\u10d8\u10e0\u10d4\u10d1\u10d4\u10dc \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d4\u10d1\u10e1 \u10da\u10dd\u10d2\u10e8\u10d8", + "title": "Tuya-\u10e1 \u10de\u10d0\u10e0\u10d0\u10db\u10d4\u10e2\u10e0\u10d4\u10d1\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10d0" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/ko.json b/homeassistant/components/tuya/translations/ko.json new file mode 100644 index 00000000000..afa2541e7b9 --- /dev/null +++ b/homeassistant/components/tuya/translations/ko.json @@ -0,0 +1,65 @@ +{ + "config": { + "abort": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + }, + "error": { + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "flow_title": "Tuya \uad6c\uc131\ud558\uae30", + "step": { + "user": { + "data": { + "country_code": "\uacc4\uc815 \uad6d\uac00 \ucf54\ub4dc (\uc608 : \ubbf8\uad6d\uc758 \uacbd\uc6b0 1, \uc911\uad6d\uc758 \uacbd\uc6b0 86)", + "password": "\ube44\ubc00\ubc88\ud638", + "platform": "\uacc4\uc815\uc774 \ub4f1\ub85d\ub41c \uc571", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "Tuya \uc790\uaca9 \uc99d\uba85\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "Tuya" + } + } + }, + "options": { + "abort": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "dev_multi_type": "\uc120\ud0dd\ud55c \uc5ec\ub7ec \uae30\uae30\ub97c \uad6c\uc131\ud558\ub824\uba74 \uc720\ud615\uc774 \ub3d9\uc77c\ud574\uc57c \ud569\ub2c8\ub2e4", + "dev_not_config": "\uae30\uae30 \uc720\ud615\uc744 \uad6c\uc131\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "dev_not_found": "\uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "\uae30\uae30\uc5d0\uc11c \uc0ac\uc6a9\ud558\ub294 \ubc1d\uae30 \ubc94\uc704", + "curr_temp_divider": "\ud604\uc7ac \uc628\ub3c4 \uac12 \ubd84\ud560 (0 = \uae30\ubcf8\uac12 \uc0ac\uc6a9)", + "max_kelvin": "\uce98\ube48 \ub2e8\uc704\uc758 \ucd5c\ub300 \uc0c9\uc628\ub3c4", + "max_temp": "\ucd5c\ub300 \ubaa9\ud45c \uc628\ub3c4 (\uae30\ubcf8\uac12\uc758 \uacbd\uc6b0 \ucd5c\uc19f\uac12 \ubc0f \ucd5c\ub313\uac12 = 0)", + "min_kelvin": "\uce98\ube48 \ub2e8\uc704\uc758 \ucd5c\uc18c \uc0c9\uc628\ub3c4", + "min_temp": "\ucd5c\uc18c \ubaa9\ud45c \uc628\ub3c4 (\uae30\ubcf8\uac12\uc758 \uacbd\uc6b0 \ucd5c\uc19f\uac12 \ubc0f \ucd5c\ub313\uac12 = 0)", + "set_temp_divided": "\uc124\uc815 \uc628\ub3c4 \uba85\ub839\uc5d0 \ubd84\ud560\ub41c \uc628\ub3c4 \uac12 \uc0ac\uc6a9\ud558\uae30", + "support_color": "\uc0c9\uc0c1 \uc9c0\uc6d0 \uac15\uc81c \uc801\uc6a9\ud558\uae30", + "temp_divider": "\uc628\ub3c4 \uac12 \ubd84\ud560 (0 = \uae30\ubcf8\uac12 \uc0ac\uc6a9)", + "temp_step_override": "\ud76c\ub9dd \uc628\ub3c4 \ub2e8\uacc4", + "tuya_max_coltemp": "\uae30\uae30\uc5d0\uc11c \ubcf4\uace0\ud55c \ucd5c\ub300 \uc0c9\uc628\ub3c4", + "unit_of_measurement": "\uae30\uae30\uc5d0\uc11c \uc0ac\uc6a9\ud558\ub294 \uc628\ub3c4 \ub2e8\uc704" + }, + "description": "{device_type} `{device_name}` \uae30\uae30\uc5d0 \ub300\ud574 \ud45c\uc2dc\ub418\ub294 \uc815\ubcf4\ub97c \uc870\uc815\ud558\ub294 \uc635\uc158 \uad6c\uc131\ud558\uae30", + "title": "Tuya \uae30\uae30 \uad6c\uc131\ud558\uae30" + }, + "init": { + "data": { + "discovery_interval": "\uae30\uae30 \uac80\uc0c9 \ud3f4\ub9c1 \uac04\uaca9 (\ucd08)", + "list_devices": "\uad6c\uc131\uc744 \uc800\uc7a5\ud558\ub824\uba74 \uad6c\uc131\ud560 \uae30\uae30\ub97c \uc120\ud0dd\ud558\uac70\ub098 \ube44\uc6cc \ub450\uc138\uc694", + "query_device": "\ube60\ub978 \uc0c1\ud0dc \uc5c5\ub370\uc774\ud2b8\ub97c \uc704\ud574 \ucffc\ub9ac \ubc29\ubc95\uc744 \uc0ac\uc6a9\ud560 \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694", + "query_interval": "\uae30\uae30 \ucffc\ub9ac \ud3f4\ub9c1 \uac04\uaca9 (\ucd08)" + }, + "description": "\ud3f4\ub9c1 \uac04\uaca9 \uac12\uc744 \ub108\ubb34 \ub0ae\uac8c \uc124\uc815\ud558\uc9c0 \ub9d0\uc544 \uc8fc\uc138\uc694. \uadf8\ub807\uc9c0 \uc54a\uc73c\uba74 \ud638\ucd9c\uc5d0 \uc2e4\ud328\ud558\uace0 \ub85c\uadf8\uc5d0 \uc624\ub958 \uba54\uc2dc\uc9c0\uac00 \uc0dd\uc131\ub429\ub2c8\ub2e4.", + "title": "Tuya \uc635\uc158 \uad6c\uc131\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/lb.json b/homeassistant/components/tuya/translations/lb.json new file mode 100644 index 00000000000..0000f9ef6e6 --- /dev/null +++ b/homeassistant/components/tuya/translations/lb.json @@ -0,0 +1,59 @@ +{ + "config": { + "abort": { + "cannot_connect": "Feeler beim verbannen", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech." + }, + "error": { + "invalid_auth": "Ong\u00eblteg Authentifikatioun" + }, + "flow_title": "Tuya Konfiguratioun", + "step": { + "user": { + "data": { + "country_code": "De L\u00e4nner Code fir d\u00e4i Kont (beispill 1 fir USA oder 86 fir China)", + "password": "Passwuert", + "platform": "d'App wou den Kont registr\u00e9iert ass", + "username": "Benotzernumm" + }, + "description": "F\u00ebll deng Tuya Umeldungs Informatiounen aus.", + "title": "Tuya" + } + } + }, + "options": { + "abort": { + "cannot_connect": "Feeler beim verbannen" + }, + "error": { + "dev_multi_type": "Multiple ausgewielte Ger\u00e4ter fir ze konfigur\u00e9ieren musse vum selwechten Typ sinn", + "dev_not_config": "Typ vun Apparat net konfigur\u00e9ierbar", + "dev_not_found": "Apparat net fonnt" + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "Hellegkeetsber\u00e4ich vum Apparat", + "curr_temp_divider": "Aktuell Temperatur W\u00e4erter Deeler (0= benotz Standard)", + "max_kelvin": "Maximal Faarftemperatur \u00ebnnerst\u00ebtzt a Kelvin", + "max_temp": "Maximal Zil Temperatur (benotz min a max = 0 fir standard)", + "min_kelvin": "Minimal Faarftemperatur \u00ebnnerst\u00ebtzt a Kelvin", + "min_temp": "Minimal Zil Temperatur (benotz min a max = 0 fir standard)", + "support_color": "Forc\u00e9ier Faarf \u00cbnnerst\u00ebtzung", + "temp_divider": "Temperatur W\u00e4erter Deeler (0= benotz Standard)", + "tuya_max_coltemp": "Max Faarftemperatur vum Apparat gemellt", + "unit_of_measurement": "Temperatur Eenheet vum Apparat" + }, + "description": "Konfigur\u00e9ier Optioune fir ugewisen Informatioune fir {device_type} Apparat `{device_name}` unzepassen", + "title": "Tuya Apparat ariichten" + }, + "init": { + "data": { + "list_devices": "Wiel d'Apparater fir ze konfigur\u00e9ieren aus oder loss se eidel fir d'Konfiguratioun ze sp\u00e4icheren" + }, + "title": "Tuya Optioune konfigur\u00e9ieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/nl.json b/homeassistant/components/tuya/translations/nl.json new file mode 100644 index 00000000000..be405db1a08 --- /dev/null +++ b/homeassistant/components/tuya/translations/nl.json @@ -0,0 +1,78 @@ +{ + "config": { + "abort": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "single_instance_allowed": "Al geconfigureerd. Er is maar een configuratie mogelijk." + }, + "error": { + "invalid_auth": "Ongeldige authenticatie" + }, + "flow_title": "Tuya-configuratie", + "step": { + "login": { + "data": { + "access_id": "Toegangs-ID", + "country_code": "Landcode", + "endpoint": "Beschikbaarheidszone", + "password": "Wachtwoord", + "tuya_app_type": "Mobiele app", + "username": "Account" + }, + "description": "Voer uw Tuya-inloggegevens in", + "title": "Tuya" + }, + "user": { + "data": { + "country_code": "De landcode van uw account (bijvoorbeeld 1 voor de VS of 86 voor China)", + "password": "Wachtwoord", + "platform": "De app waar uw account is geregistreerd", + "tuya_project_type": "Tuya cloud project type", + "username": "Gebruikersnaam" + }, + "description": "Voer uw Tuya-inloggegevens in.", + "title": "Tuya" + } + } + }, + "options": { + "abort": { + "cannot_connect": "Kan geen verbinding maken" + }, + "error": { + "dev_multi_type": "Meerdere geselecteerde apparaten om te configureren moeten van hetzelfde type zijn", + "dev_not_config": "Apparaattype kan niet worden geconfigureerd", + "dev_not_found": "Apparaat niet gevonden" + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "Helderheidsbereik gebruikt door apparaat", + "curr_temp_divider": "Huidige temperatuurwaarde deler (0 = standaardwaarde)", + "max_kelvin": "Max kleurtemperatuur in kelvin", + "max_temp": "Maximale doeltemperatuur (gebruik min en max = 0 voor standaardwaarde)", + "min_kelvin": "Minimaal ondersteunde kleurtemperatuur in kelvin", + "min_temp": "Min. gewenste temperatuur (gebruik min en max = 0 voor standaard)", + "set_temp_divided": "Gedeelde temperatuurwaarde gebruiken voor ingestelde temperatuuropdracht", + "support_color": "Forceer kleurenondersteuning", + "temp_divider": "Temperatuurwaarde deler (0 = standaardwaarde)", + "temp_step_override": "Doeltemperatuur stap", + "tuya_max_coltemp": "Max. kleurtemperatuur gerapporteerd door apparaat", + "unit_of_measurement": "Temperatuureenheid gebruikt door apparaat" + }, + "description": "Configureer opties om weergegeven informatie aan te passen voor {device_type} apparaat `{device_name}`", + "title": "Configureer Tuya Apparaat" + }, + "init": { + "data": { + "discovery_interval": "Polling-interval van nieuwe apparaten in seconden", + "list_devices": "Selecteer de te configureren apparaten of laat leeg om de configuratie op te slaan", + "query_device": "Selecteer apparaat dat query-methode zal gebruiken voor snellere statusupdate", + "query_interval": "Peilinginterval van het apparaat in seconden" + }, + "description": "Stel de waarden voor het pollinginterval niet te laag in, anders zullen de oproepen geen foutmelding in het logboek genereren", + "title": "Configureer Tuya opties" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/no.json b/homeassistant/components/tuya/translations/no.json new file mode 100644 index 00000000000..eedf24be696 --- /dev/null +++ b/homeassistant/components/tuya/translations/no.json @@ -0,0 +1,65 @@ +{ + "config": { + "abort": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "error": { + "invalid_auth": "Ugyldig godkjenning" + }, + "flow_title": "Tuya konfigurasjon", + "step": { + "user": { + "data": { + "country_code": "Din landskode for kontoen din (f.eks. 1 for USA eller 86 for Kina)", + "password": "Passord", + "platform": "Appen der kontoen din er registrert", + "username": "Brukernavn" + }, + "description": "Angi Tuya-legitimasjonen din.", + "title": "" + } + } + }, + "options": { + "abort": { + "cannot_connect": "Tilkobling mislyktes" + }, + "error": { + "dev_multi_type": "Flere valgte enheter som skal konfigureres, m\u00e5 v\u00e6re av samme type", + "dev_not_config": "Enhetstype kan ikke konfigureres", + "dev_not_found": "Finner ikke enheten" + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "Lysstyrkeomr\u00e5de som brukes av enheten", + "curr_temp_divider": "N\u00e5v\u00e6rende temperaturverdi (0 = bruk standard)", + "max_kelvin": "Maks fargetemperatur st\u00f8ttet i kelvin", + "max_temp": "Maks m\u00e5ltemperatur (bruk min og maks = 0 for standard)", + "min_kelvin": "Min fargetemperatur st\u00f8ttet i kelvin", + "min_temp": "Min m\u00e5ltemperatur (bruk min og maks = 0 for standard)", + "set_temp_divided": "Bruk delt temperaturverdi for innstilt temperaturkommando", + "support_color": "Tving fargest\u00f8tte", + "temp_divider": "Deler temperaturverdier (0 = bruk standard)", + "temp_step_override": "Trinn for m\u00e5ltemperatur", + "tuya_max_coltemp": "Maks fargetemperatur rapportert av enheten", + "unit_of_measurement": "Temperaturenhet som brukes av enheten" + }, + "description": "Konfigurer alternativer for \u00e5 justere vist informasjon for {device_type} device ` {device_name} `", + "title": "Konfigurere Tuya-enhet" + }, + "init": { + "data": { + "discovery_interval": "Avsp\u00f8rringsintervall for discovery-enheten i l\u00f8pet av sekunder", + "list_devices": "Velg enhetene du vil konfigurere, eller la de v\u00e6re tomme for \u00e5 lagre konfigurasjonen", + "query_device": "Velg enhet som skal bruke sp\u00f8rringsmetode for raskere statusoppdatering", + "query_interval": "Sp\u00f8rringsintervall for intervall i sekunder" + }, + "description": "Ikke angi pollingsintervallverdiene for lave, ellers vil ikke anropene generere feilmelding i loggen", + "title": "Konfigurer Tuya-alternativer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/pl.json b/homeassistant/components/tuya/translations/pl.json new file mode 100644 index 00000000000..92ced00e733 --- /dev/null +++ b/homeassistant/components/tuya/translations/pl.json @@ -0,0 +1,65 @@ +{ + "config": { + "abort": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "error": { + "invalid_auth": "Niepoprawne uwierzytelnienie" + }, + "flow_title": "Konfiguracja integracji Tuya", + "step": { + "user": { + "data": { + "country_code": "Kod kraju twojego konta (np. 1 dla USA lub 86 dla Chin)", + "password": "Has\u0142o", + "platform": "Aplikacja, w kt\u00f3rej zarejestrowane jest Twoje konto", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce", + "title": "Tuya" + } + } + }, + "options": { + "abort": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "error": { + "dev_multi_type": "Wybrane urz\u0105dzenia do skonfigurowania musz\u0105 by\u0107 tego samego typu", + "dev_not_config": "Typ urz\u0105dzenia nie jest konfigurowalny", + "dev_not_found": "Nie znaleziono urz\u0105dzenia" + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "Zakres jasno\u015bci u\u017cywany przez urz\u0105dzenie", + "curr_temp_divider": "Dzielnik aktualnej warto\u015bci temperatury (0 = u\u017cyj warto\u015bci domy\u015blnej)", + "max_kelvin": "Maksymalna obs\u0142ugiwana temperatura barwy w kelwinach", + "max_temp": "Maksymalna temperatura docelowa (u\u017cyj min i max = 0 dla warto\u015bci domy\u015blnej)", + "min_kelvin": "Minimalna obs\u0142ugiwana temperatura barwy w kelwinach", + "min_temp": "Minimalna temperatura docelowa (u\u017cyj min i max = 0 dla warto\u015bci domy\u015blnej)", + "set_temp_divided": "U\u017cyj podzielonej warto\u015bci temperatury dla polecenia ustawienia temperatury", + "support_color": "Wymu\u015b obs\u0142ug\u0119 kolor\u00f3w", + "temp_divider": "Dzielnik warto\u015bci temperatury (0 = u\u017cyj warto\u015bci domy\u015blnej)", + "temp_step_override": "Krok docelowej temperatury", + "tuya_max_coltemp": "Maksymalna temperatura barwy raportowana przez urz\u0105dzenie", + "unit_of_measurement": "Jednostka temperatury u\u017cywana przez urz\u0105dzenie" + }, + "description": "Skonfiguruj opcje, aby dostosowa\u0107 wy\u015bwietlane informacje dla urz\u0105dzenia {device_type} `{device_name}'", + "title": "Konfiguracja urz\u0105dzenia Tuya" + }, + "init": { + "data": { + "discovery_interval": "Cz\u0119stotliwo\u015b\u0107 skanowania nowych urz\u0105dze\u0144 (w sekundach)", + "list_devices": "Wybierz urz\u0105dzenia do skonfigurowania lub pozostaw puste, aby zapisa\u0107 konfiguracj\u0119", + "query_device": "Wybierz urz\u0105dzenie, kt\u00f3re b\u0119dzie u\u017cywa\u0107 metody odpytywania w celu szybszej aktualizacji statusu", + "query_interval": "Cz\u0119stotliwo\u015b\u0107 skanowania odpytywanego urz\u0105dzenia w sekundach" + }, + "description": "Nie ustawiaj zbyt niskich warto\u015bci skanowania, bo zako\u0144cz\u0105 si\u0119 niepowodzeniem, generuj\u0105c komunikat o b\u0142\u0119dzie w logu", + "title": "Konfiguracja opcji Tuya" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/pt-BR.json b/homeassistant/components/tuya/translations/pt-BR.json new file mode 100644 index 00000000000..8dc537e7549 --- /dev/null +++ b/homeassistant/components/tuya/translations/pt-BR.json @@ -0,0 +1,17 @@ +{ + "config": { + "flow_title": "Configura\u00e7\u00e3o Tuya", + "step": { + "user": { + "data": { + "country_code": "O c\u00f3digo do pa\u00eds da sua conta (por exemplo, 1 para os EUA ou 86 para a China)", + "password": "Senha", + "platform": "O aplicativo onde sua conta \u00e9 registrada", + "username": "Nome de usu\u00e1rio" + }, + "description": "Digite sua credencial Tuya.", + "title": "Tuya" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/pt.json b/homeassistant/components/tuya/translations/pt.json new file mode 100644 index 00000000000..566746538c0 --- /dev/null +++ b/homeassistant/components/tuya/translations/pt.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + }, + "options": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/ru.json b/homeassistant/components/tuya/translations/ru.json new file mode 100644 index 00000000000..8e00eee568c --- /dev/null +++ b/homeassistant/components/tuya/translations/ru.json @@ -0,0 +1,79 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "error": { + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." + }, + "flow_title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Tuya", + "step": { + "login": { + "data": { + "access_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0434\u043e\u0441\u0442\u0443\u043f\u0430", + "access_secret": "\u0421\u0435\u043a\u0440\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u0430", + "country_code": "\u041a\u043e\u0434 \u0441\u0442\u0440\u0430\u043d\u044b", + "endpoint": "\u0417\u043e\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e\u0441\u0442\u0438", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "tuya_app_type": "\u041c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435", + "username": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Tuya.", + "title": "Tuya" + }, + "user": { + "data": { + "country_code": "\u041a\u043e\u0434 \u0441\u0442\u0440\u0430\u043d\u044b \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430 (1 \u0434\u043b\u044f \u0421\u0428\u0410 \u0438\u043b\u0438 86 \u0434\u043b\u044f \u041a\u0438\u0442\u0430\u044f)", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "platform": "\u041f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c", + "tuya_project_type": "\u0422\u0438\u043f \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0435\u043a\u0442\u0430 Tuya", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Tuya.", + "title": "Tuya" + } + } + }, + "options": { + "abort": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "error": { + "dev_multi_type": "\u041d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u043e\u0434\u043d\u043e\u0433\u043e \u0442\u0438\u043f\u0430.", + "dev_not_config": "\u0422\u0438\u043f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", + "dev_not_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e." + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "\u0414\u0438\u0430\u043f\u0430\u0437\u043e\u043d \u044f\u0440\u043a\u043e\u0441\u0442\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c", + "curr_temp_divider": "\u0414\u0435\u043b\u0438\u0442\u0435\u043b\u044c \u0442\u0435\u043a\u0443\u0449\u0435\u0433\u043e \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b (0 = \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e)", + "max_kelvin": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0446\u0432\u0435\u0442\u043e\u0432\u0430\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u0432 \u043a\u0435\u043b\u044c\u0432\u0438\u043d\u0430\u0445)", + "max_temp": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f \u0446\u0435\u043b\u0435\u0432\u0430\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 min \u0438 max = 0)", + "min_kelvin": "\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0446\u0432\u0435\u0442\u043e\u0432\u0430\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u0432 \u043a\u0435\u043b\u044c\u0432\u0438\u043d\u0430\u0445)", + "min_temp": "\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f \u0446\u0435\u043b\u0435\u0432\u0430\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 min \u0438 max = 0)", + "set_temp_divided": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u043d\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b \u0434\u043b\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u044b \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b", + "support_color": "\u041f\u0440\u0438\u043d\u0443\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0430 \u0446\u0432\u0435\u0442\u0430", + "temp_divider": "\u0414\u0435\u043b\u0438\u0442\u0435\u043b\u044c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b (0 = \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e)", + "temp_step_override": "\u0428\u0430\u0433 \u0446\u0435\u043b\u0435\u0432\u043e\u0439 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b", + "tuya_max_coltemp": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f \u0446\u0432\u0435\u0442\u043e\u0432\u0430\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430, \u0441\u043e\u043e\u0431\u0449\u0430\u0435\u043c\u0430\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c", + "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0430\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c" + }, + "description": "\u041e\u043f\u0446\u0438\u0438 \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u043c\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u0434\u043b\u044f {device_type} \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 `{device_name}`", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Tuya" + }, + "init": { + "data": { + "discovery_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0440\u043e\u0441\u0430 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", + "list_devices": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043b\u0438 \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c \u0434\u043b\u044f \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438", + "query_device": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043c\u0435\u0442\u043e\u0434 \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0434\u043b\u044f \u0431\u043e\u043b\u0435\u0435 \u0431\u044b\u0441\u0442\u0440\u043e\u0433\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u0442\u0430\u0442\u0443\u0441\u0430", + "query_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0440\u043e\u0441\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + }, + "description": "\u041d\u0435 \u0443\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0439\u0442\u0435 \u0441\u043b\u0438\u0448\u043a\u043e\u043c \u043d\u0438\u0437\u043a\u0438\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b\u0430 \u043e\u043f\u0440\u043e\u0441\u0430, \u0438\u043d\u0430\u0447\u0435 \u0432\u044b\u0437\u043e\u0432\u044b \u043d\u0435 \u0431\u0443\u0434\u0443\u0442 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u043e\u0431 \u043e\u0448\u0438\u0431\u043a\u0435 \u0432 \u0436\u0443\u0440\u043d\u0430\u043b\u0435.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Tuya" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/sl.json b/homeassistant/components/tuya/translations/sl.json new file mode 100644 index 00000000000..b07ad70adac --- /dev/null +++ b/homeassistant/components/tuya/translations/sl.json @@ -0,0 +1,11 @@ +{ + "options": { + "abort": { + "cannot_connect": "Povezovanje ni uspelo." + }, + "error": { + "dev_not_config": "Vrsta naprave ni nastavljiva", + "dev_not_found": "Naprave ni mogo\u010de najti" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/sv.json b/homeassistant/components/tuya/translations/sv.json new file mode 100644 index 00000000000..85cc9c57fd3 --- /dev/null +++ b/homeassistant/components/tuya/translations/sv.json @@ -0,0 +1,17 @@ +{ + "config": { + "flow_title": "Tuya-konfiguration", + "step": { + "user": { + "data": { + "country_code": "Landskod f\u00f6r ditt konto (t.ex. 1 f\u00f6r USA eller 86 f\u00f6r Kina)", + "password": "L\u00f6senord", + "platform": "Appen d\u00e4r ditt konto registreras", + "username": "Anv\u00e4ndarnamn" + }, + "description": "Ange dina Tuya anv\u00e4ndaruppgifter.", + "title": "Tuya" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/tr.json b/homeassistant/components/tuya/translations/tr.json new file mode 100644 index 00000000000..2edf3276b6c --- /dev/null +++ b/homeassistant/components/tuya/translations/tr.json @@ -0,0 +1,60 @@ +{ + "config": { + "abort": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "flow_title": "Tuya yap\u0131land\u0131rmas\u0131", + "step": { + "user": { + "data": { + "country_code": "Hesap \u00fclke kodunuz (\u00f6r. ABD i\u00e7in 1 veya \u00c7in i\u00e7in 86)", + "password": "Parola", + "platform": "Hesab\u0131n\u0131z\u0131n kay\u0131tl\u0131 oldu\u011fu uygulama", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "Tuya kimlik bilgilerinizi girin.", + "title": "Tuya" + } + } + }, + "options": { + "abort": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "error": { + "dev_not_config": "Cihaz t\u00fcr\u00fc yap\u0131land\u0131r\u0131lamaz", + "dev_not_found": "Cihaz bulunamad\u0131" + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "Cihaz\u0131n kulland\u0131\u011f\u0131 parlakl\u0131k aral\u0131\u011f\u0131", + "max_temp": "Maksimum hedef s\u0131cakl\u0131k (varsay\u0131lan olarak min ve maks = 0 kullan\u0131n)", + "min_kelvin": "Kelvin destekli min renk s\u0131cakl\u0131\u011f\u0131", + "min_temp": "Minimum hedef s\u0131cakl\u0131k (varsay\u0131lan i\u00e7in min ve maks = 0 kullan\u0131n)", + "support_color": "Vurgu rengi", + "temp_divider": "S\u0131cakl\u0131k de\u011ferleri ay\u0131r\u0131c\u0131 (0 = varsay\u0131lan\u0131 kullan)", + "tuya_max_coltemp": "Cihaz taraf\u0131ndan bildirilen maksimum renk s\u0131cakl\u0131\u011f\u0131", + "unit_of_measurement": "Cihaz\u0131n kulland\u0131\u011f\u0131 s\u0131cakl\u0131k birimi" + }, + "description": "{device_type} ayg\u0131t\u0131 '{device_name}' i\u00e7in g\u00f6r\u00fcnt\u00fclenen bilgileri ayarlamak i\u00e7in se\u00e7enekleri yap\u0131land\u0131r\u0131n", + "title": "Tuya Cihaz\u0131n\u0131 Yap\u0131land\u0131r\u0131n" + }, + "init": { + "data": { + "discovery_interval": "Cihaz\u0131 yoklama aral\u0131\u011f\u0131 saniye cinsinden", + "list_devices": "Yap\u0131land\u0131rmay\u0131 kaydetmek i\u00e7in yap\u0131land\u0131r\u0131lacak veya bo\u015f b\u0131rak\u0131lacak cihazlar\u0131 se\u00e7in", + "query_device": "Daha h\u0131zl\u0131 durum g\u00fcncellemesi i\u00e7in sorgu y\u00f6ntemini kullanacak cihaz\u0131 se\u00e7in", + "query_interval": "Ayg\u0131t yoklama aral\u0131\u011f\u0131 saniye cinsinden" + }, + "description": "Yoklama aral\u0131\u011f\u0131 de\u011ferlerini \u00e7ok d\u00fc\u015f\u00fck ayarlamay\u0131n, aksi takdirde \u00e7a\u011fr\u0131lar g\u00fcnl\u00fckte hata mesaj\u0131 olu\u015fturarak ba\u015far\u0131s\u0131z olur", + "title": "Tuya Se\u00e7eneklerini Konfig\u00fcre Et" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/uk.json b/homeassistant/components/tuya/translations/uk.json new file mode 100644 index 00000000000..1d2709d260a --- /dev/null +++ b/homeassistant/components/tuya/translations/uk.json @@ -0,0 +1,63 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "flow_title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Tuya", + "step": { + "user": { + "data": { + "country_code": "\u041a\u043e\u0434 \u043a\u0440\u0430\u0457\u043d\u0438 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 (1 \u0434\u043b\u044f \u0421\u0428\u0410 \u0430\u0431\u043e 86 \u0434\u043b\u044f \u041a\u0438\u0442\u0430\u044e)", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "platform": "\u0414\u043e\u0434\u0430\u0442\u043e\u043a, \u0432 \u044f\u043a\u043e\u043c\u0443 \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u043e\u0432\u0430\u043d\u043e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 Tuya.", + "title": "Tuya" + } + } + }, + "options": { + "abort": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "error": { + "dev_multi_type": "\u041a\u0456\u043b\u044c\u043a\u0430 \u043e\u0431\u0440\u0430\u043d\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 \u0434\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0431\u0443\u0442\u0438 \u043e\u0434\u043d\u043e\u0433\u043e \u0442\u0438\u043f\u0443.", + "dev_not_config": "\u0422\u0438\u043f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u043d\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f.", + "dev_not_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e." + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "\u0414\u0456\u0430\u043f\u0430\u0437\u043e\u043d \u044f\u0441\u043a\u0440\u0430\u0432\u043e\u0441\u0442\u0456, \u044f\u043a\u0438\u0439 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c", + "curr_temp_divider": "\u0414\u0456\u043b\u044c\u043d\u0438\u043a \u043f\u043e\u0442\u043e\u0447\u043d\u043e\u0433\u043e \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0438 (0 = \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c)", + "max_kelvin": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0430 \u043a\u043e\u043b\u0456\u0440\u043d\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u0432 \u043a\u0435\u043b\u044c\u0432\u0456\u043d\u0430\u0445)", + "max_temp": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430 \u0446\u0456\u043b\u044c\u043e\u0432\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 min \u0456 max = 0)", + "min_kelvin": "\u041c\u0456\u043d\u0456\u043c\u0430\u043b\u044c\u043d\u0430 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0430 \u043a\u043e\u043b\u0456\u0440\u043d\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u0432 \u043a\u0435\u043b\u044c\u0432\u0456\u043d\u0430\u0445)", + "min_temp": "\u041c\u0456\u043d\u0456\u043c\u0430\u043b\u044c\u043d\u0430 \u0446\u0456\u043b\u044c\u043e\u0432\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 min \u0456 max = 0)", + "support_color": "\u041f\u0440\u0438\u043c\u0443\u0441\u043e\u0432\u0430 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u043a\u0430 \u043a\u043e\u043b\u044c\u043e\u0440\u0443", + "temp_divider": "\u0414\u0456\u043b\u044c\u043d\u0438\u043a \u0437\u043d\u0430\u0447\u0435\u043d\u044c \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0438 (0 = \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c)", + "tuya_max_coltemp": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430 \u043a\u043e\u043b\u0456\u0440\u043d\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430, \u044f\u043a\u0430 \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u044f\u0454\u0442\u044c\u0441\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c", + "unit_of_measurement": "\u041e\u0434\u0438\u043d\u0438\u0446\u044f \u0432\u0438\u043c\u0456\u0440\u0443 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0438, \u044f\u043a\u0430 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c" + }, + "description": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0434\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0438\u0434\u0438\u043c\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u0434\u043b\u044f {device_type} \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e '{device_name}'", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Tuya" + }, + "init": { + "data": { + "discovery_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0438\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", + "list_devices": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u0434\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0430\u0431\u043e \u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c \u0434\u043b\u044f \u0437\u0431\u0435\u0440\u0435\u0436\u0435\u043d\u043d\u044f \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457", + "query_device": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439, \u044f\u043a\u0438\u0439 \u0431\u0443\u0434\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430\u043f\u0438\u0442\u0443 \u0434\u043b\u044f \u0431\u0456\u043b\u044c\u0448 \u0448\u0432\u0438\u0434\u043a\u043e\u0433\u043e \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0441\u0442\u0430\u0442\u0443\u0441\u0443", + "query_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0438\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + }, + "description": "\u041d\u0435 \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u044e\u0439\u0442\u0435 \u0437\u0430\u043d\u0430\u0434\u0442\u043e \u043d\u0438\u0437\u044c\u043a\u0456 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0456\u043d\u0442\u0435\u0440\u0432\u0430\u043b\u0443 \u043e\u043f\u0438\u0442\u0443\u0432\u0430\u043d\u043d\u044f, \u0456\u043d\u0430\u043a\u0448\u0435 \u0432\u0438\u043a\u043b\u0438\u043a\u0438 \u043d\u0435 \u0431\u0443\u0434\u0443\u0442\u044c \u0433\u0435\u043d\u0435\u0440\u0443\u0432\u0430\u0442\u0438 \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u043d\u044f \u043f\u0440\u043e \u043f\u043e\u043c\u0438\u043b\u043a\u0443 \u0432 \u0436\u0443\u0440\u043d\u0430\u043b\u0456.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Tuya" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/zh-Hans.json b/homeassistant/components/tuya/translations/zh-Hans.json index e1acb5453aa..ff3887c840d 100644 --- a/homeassistant/components/tuya/translations/zh-Hans.json +++ b/homeassistant/components/tuya/translations/zh-Hans.json @@ -1,29 +1,60 @@ { "config": { - "error": { - "invalid_auth": "身份认证无效" + "abort": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_auth": "\u8eab\u4efd\u8ba4\u8bc1\u65e0\u6548", + "single_instance_allowed": "\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86\uff0c\u4e14\u53ea\u80fd\u914d\u7f6e\u4e00\u6b21\u3002" }, - "flow_title": "涂鸦配置", + "error": { + "invalid_auth": "\u8eab\u4efd\u8ba4\u8bc1\u65e0\u6548" + }, + "flow_title": "\u6d82\u9e26\u914d\u7f6e", "step": { - "user":{ - "title":"Tuya插件", - "data":{ - "tuya_project_type": "涂鸦云项目类型" - } - }, - "login": { + "user": { "data": { - "endpoint": "可用区域", - "access_id": "Access ID", - "access_secret": "Access Secret", - "tuya_app_type": "移动应用", - "country_code": "国家码", - "username": "账号", - "password": "密码" + "country_code": "\u60a8\u7684\u5e10\u6237\u56fd\u5bb6(\u5730\u533a)\u4ee3\u7801\uff08\u4f8b\u5982\u4e2d\u56fd\u4e3a 86\uff0c\u7f8e\u56fd\u4e3a 1\uff09", + "password": "\u5bc6\u7801", + "platform": "\u60a8\u6ce8\u518c\u5e10\u6237\u7684\u5e94\u7528", + "username": "\u7528\u6237\u540d" }, - "description": "请输入涂鸦账户信息。", - "title": "涂鸦" + "description": "\u8bf7\u8f93\u5165\u6d82\u9e26\u8d26\u6237\u4fe1\u606f\u3002", + "title": "\u6d82\u9e26" + } + } + }, + "options": { + "abort": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25" + }, + "error": { + "dev_multi_type": "\u591a\u4e2a\u8981\u914d\u7f6e\u7684\u8bbe\u5907\u5fc5\u987b\u5177\u6709\u76f8\u540c\u7684\u7c7b\u578b", + "dev_not_config": "\u8bbe\u5907\u7c7b\u578b\u4e0d\u53ef\u914d\u7f6e", + "dev_not_found": "\u672a\u627e\u5230\u8bbe\u5907" + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "\u8bbe\u5907\u4f7f\u7528\u7684\u4eae\u5ea6\u8303\u56f4", + "max_kelvin": "\u6700\u9ad8\u652f\u6301\u8272\u6e29\uff08\u5f00\u6c0f\uff09", + "max_temp": "\u6700\u9ad8\u76ee\u6807\u6e29\u5ea6\uff08min \u548c max \u4e3a 0 \u65f6\u4f7f\u7528\u9ed8\u8ba4\uff09", + "min_kelvin": "\u6700\u4f4e\u652f\u6301\u8272\u6e29\uff08\u5f00\u6c0f\uff09", + "min_temp": "\u6700\u4f4e\u76ee\u6807\u6e29\u5ea6\uff08min \u548c max \u4e3a 0 \u65f6\u4f7f\u7528\u9ed8\u8ba4\uff09", + "support_color": "\u5f3a\u5236\u652f\u6301\u8c03\u8272", + "tuya_max_coltemp": "\u8bbe\u5907\u62a5\u544a\u7684\u6700\u9ad8\u8272\u6e29", + "unit_of_measurement": "\u8bbe\u5907\u4f7f\u7528\u7684\u6e29\u5ea6\u5355\u4f4d" + }, + "title": "\u914d\u7f6e\u6d82\u9e26\u8bbe\u5907" + }, + "init": { + "data": { + "discovery_interval": "\u53d1\u73b0\u8bbe\u5907\u8f6e\u8be2\u95f4\u9694\uff08\u4ee5\u79d2\u4e3a\u5355\u4f4d\uff09", + "list_devices": "\u8bf7\u9009\u62e9\u8981\u914d\u7f6e\u7684\u8bbe\u5907\uff0c\u6216\u7559\u7a7a\u4ee5\u4fdd\u5b58\u914d\u7f6e", + "query_device": "\u8bf7\u9009\u62e9\u4f7f\u7528\u67e5\u8be2\u65b9\u6cd5\u7684\u8bbe\u5907\uff0c\u4ee5\u4fbf\u66f4\u5feb\u5730\u66f4\u65b0\u72b6\u6001", + "query_interval": "\u67e5\u8be2\u8bbe\u5907\u8f6e\u8be2\u95f4\u9694\uff08\u4ee5\u79d2\u4e3a\u5355\u4f4d\uff09" + }, + "description": "\u8bf7\u4e0d\u8981\u5c06\u8f6e\u8be2\u95f4\u9694\u8bbe\u7f6e\u5f97\u592a\u4f4e\uff0c\u5426\u5219\u5c06\u8c03\u7528\u5931\u8d25\u5e76\u5728\u65e5\u5fd7\u751f\u6210\u9519\u8bef\u6d88\u606f", + "title": "\u914d\u7f6e\u6d82\u9e26\u9009\u9879" } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/zh-Hant.json b/homeassistant/components/tuya/translations/zh-Hant.json new file mode 100644 index 00000000000..e747e50d2c7 --- /dev/null +++ b/homeassistant/components/tuya/translations/zh-Hant.json @@ -0,0 +1,79 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "error": { + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "flow_title": "Tuya \u8a2d\u5b9a", + "step": { + "login": { + "data": { + "access_id": "Access ID", + "access_secret": "Access Secret", + "country_code": "\u570b\u78bc", + "endpoint": "\u53ef\u7528\u5340\u57df", + "password": "\u5bc6\u78bc", + "tuya_app_type": "\u624b\u6a5f App", + "username": "\u5e33\u865f" + }, + "description": "\u8f38\u5165 Tuya \u6191\u8b49", + "title": "Tuya" + }, + "user": { + "data": { + "country_code": "\u5e33\u865f\u570b\u5bb6\u4ee3\u78bc\uff08\u4f8b\u5982\uff1a\u7f8e\u570b 1 \u6216\u4e2d\u570b 86\uff09", + "password": "\u5bc6\u78bc", + "platform": "\u5e33\u6236\u8a3b\u518a\u6240\u5728\u4f4d\u7f6e", + "tuya_project_type": "Tuya \u96f2\u5c08\u6848\u985e\u578b", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8f38\u5165 Tuya \u6191\u8b49\u3002", + "title": "Tuya \u6574\u5408" + } + } + }, + "options": { + "abort": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "error": { + "dev_multi_type": "\u591a\u91cd\u9078\u64c7\u8a2d\u88dd\u7f6e\u4ee5\u8a2d\u5b9a\u4f7f\u7528\u76f8\u540c\u985e\u578b", + "dev_not_config": "\u88dd\u7f6e\u985e\u578b\u7121\u6cd5\u8a2d\u5b9a", + "dev_not_found": "\u627e\u4e0d\u5230\u88dd\u7f6e" + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "\u88dd\u7f6e\u6240\u4f7f\u7528\u4e4b\u4eae\u5ea6\u7bc4\u570d", + "curr_temp_divider": "\u76ee\u524d\u8272\u6eab\u503c\u5206\u914d\u5668\uff080 = \u4f7f\u7528\u9810\u8a2d\uff09", + "max_kelvin": "Kelvin \u652f\u63f4\u6700\u9ad8\u8272\u6eab", + "max_temp": "\u6700\u9ad8\u76ee\u6a19\u8272\u6eab\uff08\u4f7f\u7528\u6700\u4f4e\u8207\u6700\u9ad8 = 0 \u4f7f\u7528\u9810\u8a2d\uff09", + "min_kelvin": "Kelvin \u652f\u63f4\u6700\u4f4e\u8272\u6eab", + "min_temp": "\u6700\u4f4e\u76ee\u6a19\u8272\u6eab\uff08\u4f7f\u7528\u6700\u4f4e\u8207\u6700\u9ad8 = 0 \u4f7f\u7528\u9810\u8a2d\uff09", + "set_temp_divided": "\u4f7f\u7528\u5206\u9694\u865f\u6eab\u5ea6\u503c\u4ee5\u57f7\u884c\u8a2d\u5b9a\u6eab\u5ea6\u6307\u4ee4", + "support_color": "\u5f37\u5236\u8272\u6eab\u652f\u63f4", + "temp_divider": "\u8272\u6eab\u503c\u5206\u914d\u5668\uff080 = \u4f7f\u7528\u9810\u8a2d\uff09", + "temp_step_override": "\u76ee\u6a19\u6eab\u5ea6\u8a2d\u5b9a", + "tuya_max_coltemp": "\u88dd\u7f6e\u56de\u5831\u6700\u9ad8\u8272\u6eab", + "unit_of_measurement": "\u88dd\u7f6e\u6240\u4f7f\u7528\u4e4b\u6eab\u5ea6\u55ae\u4f4d" + }, + "description": "\u8a2d\u5b9a\u9078\u9805\u4ee5\u8abf\u6574 {device_type} \u88dd\u7f6e `{device_name}` \u986f\u793a\u8cc7\u8a0a", + "title": "\u8a2d\u5b9a Tuya \u88dd\u7f6e" + }, + "init": { + "data": { + "discovery_interval": "\u63a2\u7d22\u88dd\u7f6e\u66f4\u65b0\u79d2\u9593\u8ddd", + "list_devices": "\u9078\u64c7\u88dd\u7f6e\u4ee5\u8a2d\u5b9a\u3001\u6216\u4fdd\u6301\u7a7a\u767d\u4ee5\u5132\u5b58\u8a2d\u5b9a", + "query_device": "\u9078\u64c7\u88dd\u7f6e\u5c07\u4f7f\u7528\u67e5\u8a62\u65b9\u5f0f\u4ee5\u7372\u5f97\u66f4\u5feb\u7684\u72c0\u614b\u66f4\u65b0", + "query_interval": "\u67e5\u8a62\u88dd\u7f6e\u66f4\u65b0\u79d2\u9593\u8ddd" + }, + "description": "\u66f4\u65b0\u9593\u8ddd\u4e0d\u8981\u8a2d\u5b9a\u7684\u904e\u4f4e\u3001\u53ef\u80fd\u6703\u5c0e\u81f4\u65bc\u65e5\u8a8c\u4e2d\u7522\u751f\u932f\u8aa4\u8a0a\u606f", + "title": "\u8a2d\u5b9a Tuya \u9078\u9805" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/translations/hu.json b/homeassistant/components/twilio/translations/hu.json index cd60890dab3..512296463e4 100644 --- a/homeassistant/components/twilio/translations/hu.json +++ b/homeassistant/components/twilio/translations/hu.json @@ -2,14 +2,14 @@ "config": { "abort": { "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", - "webhook_not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." + "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, "create_entry": { - "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a [Webhooks Twilio-val]({twilio_url}) alkalmaz\u00e1st. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/x-www-form-urlencoded \n\n L\u00e1sd [a dokument\u00e1ci\u00f3]({docs_url}), hogyan konfigur\u00e1lhatja az automatiz\u00e1l\u00e1sokat a bej\u00f6v\u0151 adatok kezel\u00e9s\u00e9re." + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a [Webhooks Twilio-val]({twilio_url}) alkalmaz\u00e1st. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/x-www-form-urlencoded \n\n L\u00e1sd [a dokument\u00e1ci\u00f3]({docs_url}), hogyan konfigur\u00e1lhatja az automatizmusokat a bej\u00f6v\u0151 adatok kezel\u00e9s\u00e9re." }, "step": { "user": { - "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?", + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?", "title": "A Twilio Webhook be\u00e1ll\u00edt\u00e1sa" } } diff --git a/homeassistant/components/twilio/translations/nl.json b/homeassistant/components/twilio/translations/nl.json index 0d5d33a727e..3d31175d2de 100644 --- a/homeassistant/components/twilio/translations/nl.json +++ b/homeassistant/components/twilio/translations/nl.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Wil je beginnen met instellen?", + "description": "Wilt u beginnen met instellen?", "title": "Stel de Twilio Webhook in" } } diff --git a/homeassistant/components/twinkly/translations/hu.json b/homeassistant/components/twinkly/translations/hu.json index d5cd872bbd0..9c5137c30a4 100644 --- a/homeassistant/components/twinkly/translations/hu.json +++ b/homeassistant/components/twinkly/translations/hu.json @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "host": "A Twinkly eszk\u00f6z gazdag\u00e9pe (vagy IP-c\u00edme)" + "host": "A Twinkly eszk\u00f6z c\u00edme" }, "description": "\u00c1ll\u00edtsa be a Twinkly led-karakterl\u00e1nc\u00e1t", "title": "Twinkly" diff --git a/homeassistant/components/unifi/translations/hu.json b/homeassistant/components/unifi/translations/hu.json index 22904c8ec7b..9564b211043 100644 --- a/homeassistant/components/unifi/translations/hu.json +++ b/homeassistant/components/unifi/translations/hu.json @@ -14,10 +14,10 @@ "step": { "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "password": "Jelsz\u00f3", "port": "Port", - "site": "Site azonos\u00edt\u00f3", + "site": "Hely azonos\u00edt\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v", "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" }, @@ -31,10 +31,10 @@ "data": { "block_client": "H\u00e1l\u00f3zathozz\u00e1f\u00e9r\u00e9s vez\u00e9relt \u00fcgyfelek", "dpi_restrictions": "Enged\u00e9lyezze a DPI restrikci\u00f3s csoportok vez\u00e9rl\u00e9s\u00e9t", - "poe_clients": "Enged\u00e9lyezze az \u00fcgyfelek POE-vez\u00e9rl\u00e9s\u00e9t" + "poe_clients": "Enged\u00e9lyezze a POE-vez\u00e9rl\u00e9s\u00e9t" }, "description": "Konfigur\u00e1lja a klienseket\n\n Hozzon l\u00e9tre kapcsol\u00f3kat azokhoz a sorsz\u00e1mokhoz, amelyeknek vez\u00e9relni k\u00edv\u00e1nja a h\u00e1l\u00f3zati hozz\u00e1f\u00e9r\u00e9st.", - "title": "UniFi lehet\u0151s\u00e9gek 2/3" + "title": "UniFi be\u00e1ll\u00edt\u00e1sok 2/3" }, "device_tracker": { "data": { @@ -46,7 +46,7 @@ "track_wired_clients": "Vegyen fel vezet\u00e9kes h\u00e1l\u00f3zati \u00fcgyfeleket" }, "description": "Eszk\u00f6zk\u00f6vet\u00e9s konfigur\u00e1l\u00e1sa", - "title": "UniFi lehet\u0151s\u00e9gek 1/3" + "title": "UniFi be\u00e1ll\u00edt\u00e1sok 1/3" }, "init": { "data": { @@ -64,11 +64,11 @@ }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "S\u00e1vsz\u00e9less\u00e9g-haszn\u00e1lati \u00e9rz\u00e9kel\u0151k l\u00e9trehoz\u00e1sa a h\u00e1l\u00f3zati \u00fcgyfelek sz\u00e1m\u00e1ra", + "allow_bandwidth_sensors": "Kliensenk\u00e9nti s\u00e1vsz\u00e9less\u00e9g-haszn\u00e1lati \u00e9rz\u00e9kel\u0151k l\u00e9trehoz\u00e1sa", "allow_uptime_sensors": "\u00dczemid\u0151-\u00e9rz\u00e9kel\u0151k h\u00e1l\u00f3zati \u00fcgyfelek sz\u00e1m\u00e1ra" }, "description": "Statisztikai \u00e9rz\u00e9kel\u0151k konfigur\u00e1l\u00e1sa", - "title": "UniFi lehet\u0151s\u00e9gek 3/3" + "title": "UniFi be\u00e1ll\u00edt\u00e1sok 3/3" } } } diff --git a/homeassistant/components/unifi/translations/id.json b/homeassistant/components/unifi/translations/id.json index 7a707b28aa0..ec023fa7363 100644 --- a/homeassistant/components/unifi/translations/id.json +++ b/homeassistant/components/unifi/translations/id.json @@ -10,7 +10,7 @@ "service_unavailable": "Gagal terhubung", "unknown_client_mac": "Tidak ada klien yang tersedia di alamat MAC tersebut" }, - "flow_title": "UniFi Network {site} ({host})", + "flow_title": "{site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/ru.json b/homeassistant/components/unifi/translations/ru.json index 8a34f9fc17e..a2c9bfe8061 100644 --- a/homeassistant/components/unifi/translations/ru.json +++ b/homeassistant/components/unifi/translations/ru.json @@ -38,7 +38,7 @@ }, "device_tracker": { "data": { - "detection_time": "\u0412\u0440\u0435\u043c\u044f \u043e\u0442 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0433\u043e \u0441\u0435\u0430\u043d\u0441\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c (\u0441\u0435\u043a.), \u043f\u043e \u0438\u0441\u0442\u0435\u0447\u0435\u043d\u0438\u044e \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u0442\u0430\u0442\u0443\u0441 \"\u041d\u0435 \u0434\u043e\u043c\u0430\".", + "detection_time": "\u0412\u0440\u0435\u043c\u044f \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445), \u043f\u043e \u0438\u0441\u0442\u0435\u0447\u0435\u043d\u0438\u044e \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0441\u0447\u0438\u0442\u0430\u0442\u044c \u0447\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0434\u043e\u043c\u0430", "ignore_wired_bug": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043b\u043e\u0433\u0438\u043a\u0443 \u043e\u0448\u0438\u0431\u043a\u0438 \u0434\u043b\u044f \u043d\u0435 \u0431\u0435\u0441\u043f\u0440\u043e\u0432\u043e\u0434\u043d\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 UniFi", "ssid_filter": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 SSID \u0434\u043b\u044f \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u044f \u0431\u0435\u0441\u043f\u0440\u043e\u0432\u043e\u0434\u043d\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432", "track_clients": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u0441\u0435\u0442\u0438", diff --git a/homeassistant/components/updater/translations/hu.json b/homeassistant/components/updater/translations/hu.json index 52b2c972559..e862dcb360c 100644 --- a/homeassistant/components/updater/translations/hu.json +++ b/homeassistant/components/updater/translations/hu.json @@ -1,3 +1,3 @@ { - "title": "Friss\u00edt\u00e9sek" + "title": "Friss\u00edt\u0151" } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/fi.json b/homeassistant/components/upnp/translations/fi.json index dcd927ffd24..aaf44e6c730 100644 --- a/homeassistant/components/upnp/translations/fi.json +++ b/homeassistant/components/upnp/translations/fi.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Laite on jo m\u00e4\u00e4ritetty" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/upnp/translations/hu.json b/homeassistant/components/upnp/translations/hu.json index 8ef3ff8dcc0..46c6bd2de1f 100644 --- a/homeassistant/components/upnp/translations/hu.json +++ b/homeassistant/components/upnp/translations/hu.json @@ -13,7 +13,7 @@ "step": { "init": { "one": "\u00dcres", - "other": "" + "other": "\u00dcres" }, "ssdp_confirm": { "description": "Szeretn\u00e9 be\u00e1ll\u00edtani ezt az UPnP/IGD eszk\u00f6zt?" diff --git a/homeassistant/components/upnp/translations/id.json b/homeassistant/components/upnp/translations/id.json index 3a953ba62a9..f70fca145e8 100644 --- a/homeassistant/components/upnp/translations/id.json +++ b/homeassistant/components/upnp/translations/id.json @@ -5,7 +5,7 @@ "incomplete_discovery": "Proses penemuan tidak selesai", "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" }, - "flow_title": "UPnP/IGD: {name}", + "flow_title": "{name}", "step": { "ssdp_confirm": { "description": "Ingin menyiapkan perangkat UPnP/IGD ini?" diff --git a/homeassistant/components/uptimerobot/translations/ca.json b/homeassistant/components/uptimerobot/translations/ca.json index a3bccb98295..b845e271666 100644 --- a/homeassistant/components/uptimerobot/translations/ca.json +++ b/homeassistant/components/uptimerobot/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat", + "already_configured": "El compte ja est\u00e0 configurat", "reauth_failed_existing": "No s'ha pogut actualitzar l'entrada de configuraci\u00f3, elimina la integraci\u00f3 i torna-la a instal\u00b7lar.", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", "unknown": "Error inesperat" diff --git a/homeassistant/components/uptimerobot/translations/es.json b/homeassistant/components/uptimerobot/translations/es.json index 1f04109611f..455c96cd644 100644 --- a/homeassistant/components/uptimerobot/translations/es.json +++ b/homeassistant/components/uptimerobot/translations/es.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "already_configured": "La cuenta ya est\u00e1 configurada", + "already_configured": "La cuenta ya ha sido configurada", "reauth_failed_existing": "No se pudo actualizar la entrada de configuraci\u00f3n, elimine la integraci\u00f3n y config\u00farela nuevamente.", - "reauth_successful": "La reautenticaci\u00f3n fue exitosa", - "unknown": "Error desconocido" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "unknown": "Error inesperado" }, "error": { "cannot_connect": "No se pudo conectar", - "invalid_api_key": "Clave de la API err\u00f3nea", + "invalid_api_key": "Clave API no v\u00e1lida", "reauth_failed_matching_account": "La clave de API que has proporcionado no coincide con el ID de cuenta para la configuraci\u00f3n existente.", - "unknown": "Error desconocido" + "unknown": "Error inesperado" }, "step": { "reauth_confirm": { @@ -22,7 +22,7 @@ }, "user": { "data": { - "api_key": "Clave de la API" + "api_key": "Clave API" }, "description": "Debes proporcionar una clave API de solo lectura de robot de tiempo de actividad/funcionamiento" } diff --git a/homeassistant/components/uptimerobot/translations/id.json b/homeassistant/components/uptimerobot/translations/id.json new file mode 100644 index 00000000000..e107b1fcac6 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/id.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "reauth_successful": "Autentikasi ulang berhasil", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_api_key": "Kunci API tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Kunci API" + }, + "title": "Autentikasi Ulang Integrasi" + }, + "user": { + "data": { + "api_key": "Kunci API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vera/translations/hu.json b/homeassistant/components/vera/translations/hu.json index 1f1e22b9ed8..4e9639b0258 100644 --- a/homeassistant/components/vera/translations/hu.json +++ b/homeassistant/components/vera/translations/hu.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "cannot_connect": "Nem siker\u00fclt csatlakozni a {base_url}" + "cannot_connect": "Nem siker\u00fclt csatlakozni: {base_url}" }, "step": { "user": { "data": { - "exclude": "Vera eszk\u00f6zazonos\u00edt\u00f3k kiz\u00e1r\u00e1sa a HomeAssistantb\u00f3l.", - "lights": "A Vera kapcsol\u00f3eszk\u00f6z-azonos\u00edt\u00f3k f\u00e9nyk\u00e9nt kezelhet\u0151k a HomeAssistant alkalmaz\u00e1sban.", + "exclude": "Vera eszk\u00f6zazonos\u00edt\u00f3k kiz\u00e1r\u00e1sa Home Assistant-b\u00f3l.", + "lights": "A Vera kapcsol\u00f3eszk\u00f6z-azonos\u00edt\u00f3k f\u00e9nyk\u00e9nt kezelhet\u0151k a Home Assistant alkalmaz\u00e1sban.", "vera_controller_url": "Vez\u00e9rl\u0151 URL" }, - "description": "Adja meg a Vera vez\u00e9rl\u0151 URL-j\u00e9t al\u00e1bb. Ennek \u00edgy kell kin\u00e9znie: http://192.168.1.161:3480.", + "description": "Adja meg a Vera vez\u00e9rl\u0151 URL-j\u00e9t al\u00e1bb. Hasonl\u00f3k\u00e9ppen kell kin\u00e9znie: http://192.168.1.161:3480.", "title": "Vera vez\u00e9rl\u0151 be\u00e1ll\u00edt\u00e1sa" } } @@ -19,8 +19,8 @@ "step": { "init": { "data": { - "exclude": "Vera eszk\u00f6zazonos\u00edt\u00f3k kiz\u00e1r\u00e1sa a HomeAssistantb\u00f3l.", - "lights": "A Vera kapcsol\u00f3eszk\u00f6z-azonos\u00edt\u00f3k f\u00e9nyk\u00e9nt kezelhet\u0151k a HomeAssistant alkalmaz\u00e1sban." + "exclude": "Vera eszk\u00f6zazonos\u00edt\u00f3k kiz\u00e1r\u00e1sa Home Assistant-b\u00f3l.", + "lights": "A Vera kapcsol\u00f3eszk\u00f6z-azonos\u00edt\u00f3k f\u00e9nyk\u00e9nt kezelhet\u0151k a Home Assistant alkalmaz\u00e1sban." }, "description": "Az opcion\u00e1lis param\u00e9terekr\u0151l a vera dokument\u00e1ci\u00f3j\u00e1ban olvashat: https://www.home-assistant.io/integrations/vera/. Megjegyz\u00e9s: Az itt v\u00e9grehajtott v\u00e1ltoztat\u00e1sokhoz \u00fajra kell ind\u00edtani a h\u00e1zi asszisztens szervert. Az \u00e9rt\u00e9kek t\u00f6rl\u00e9s\u00e9hez adjon meg egy sz\u00f3k\u00f6zt.", "title": "Vera vez\u00e9rl\u0151 opci\u00f3k" diff --git a/homeassistant/components/verisure/translations/ca.json b/homeassistant/components/verisure/translations/ca.json index 0ddcf9513f4..c27943b35f3 100644 --- a/homeassistant/components/verisure/translations/ca.json +++ b/homeassistant/components/verisure/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat", + "already_configured": "El compte ja est\u00e0 configurat", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { diff --git a/homeassistant/components/verisure/translations/hu.json b/homeassistant/components/verisure/translations/hu.json index f071872b81c..89ff19bd1fe 100644 --- a/homeassistant/components/verisure/translations/hu.json +++ b/homeassistant/components/verisure/translations/hu.json @@ -13,7 +13,7 @@ "data": { "giid": "Telep\u00edt\u00e9s" }, - "description": "A Home Assistant t\u00f6bb Verisure telep\u00edt\u00e9st tal\u00e1lt a Saj\u00e1t oldalak fi\u00f3kodban. K\u00e9rj\u00fck, v\u00e1lassza ki azt a telep\u00edt\u00e9st, amelyet hozz\u00e1 k\u00edv\u00e1n adni a Home Assistant programhoz." + "description": "Home Assistant t\u00f6bb Verisure telep\u00edt\u00e9st tal\u00e1lt a Saj\u00e1t oldalak fi\u00f3kj\u00e1ban. K\u00e9rj\u00fck, v\u00e1lassza ki azt a telep\u00edt\u00e9st, amelyet hozz\u00e1 k\u00edv\u00e1nja adni a Home Assistant p\u00e9ld\u00e1ny\u00e1hoz." }, "reauth_confirm": { "data": { diff --git a/homeassistant/components/vilfo/translations/hu.json b/homeassistant/components/vilfo/translations/hu.json index 4e2ab47a476..157a6cdbabd 100644 --- a/homeassistant/components/vilfo/translations/hu.json +++ b/homeassistant/components/vilfo/translations/hu.json @@ -12,7 +12,7 @@ "user": { "data": { "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", - "host": "Hoszt" + "host": "C\u00edm" }, "description": "\u00c1ll\u00edtsa be a Vilfo Router integr\u00e1ci\u00f3t. Sz\u00fcks\u00e9ge van a Vilfo Router gazdag\u00e9pnev\u00e9re/IP -c\u00edm\u00e9re \u00e9s egy API hozz\u00e1f\u00e9r\u00e9si jogkivonatra. Ha tov\u00e1bbi inform\u00e1ci\u00f3ra van sz\u00fcks\u00e9ge az integr\u00e1ci\u00f3r\u00f3l \u00e9s a r\u00e9szletekr\u0151l, l\u00e1togasson el a k\u00f6vetkez\u0151 webhelyre: https://www.home-assistant.io/integrations/vilfo", "title": "Csatlakoz\u00e1s a Vilfo routerhez" diff --git a/homeassistant/components/vizio/translations/hu.json b/homeassistant/components/vizio/translations/hu.json index edc91cdb31c..3708bfbc379 100644 --- a/homeassistant/components/vizio/translations/hu.json +++ b/homeassistant/components/vizio/translations/hu.json @@ -19,18 +19,18 @@ "title": "V\u00e9gezze el a p\u00e1ros\u00edt\u00e1si folyamatot" }, "pairing_complete": { - "description": "A VIZIO SmartCast Eszk\u00f6z mostant\u00f3l csatlakozik a Home Assistant-hoz.", + "description": "A VIZIO SmartCast Eszk\u00f6z mostant\u00f3l csatlakozik Home Assistant-hoz.", "title": "P\u00e1ros\u00edt\u00e1s k\u00e9sz" }, "pairing_complete_import": { - "description": "A VIZIO SmartCast Eszk\u00f6z mostant\u00f3l csatlakozik a Home Assistant szolg\u00e1ltat\u00e1shoz. \n\n A Hozz\u00e1f\u00e9r\u00e9si token a \u201e** {access_token} **\u201d.", + "description": "A VIZIO SmartCast Eszk\u00f6z mostant\u00f3l csatlakozik a Home Assistant szolg\u00e1ltat\u00e1shoz. \n\nA Hozz\u00e1f\u00e9r\u00e9si token a `**{access_token}**`.", "title": "P\u00e1ros\u00edt\u00e1s k\u00e9sz" }, "user": { "data": { "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", "device_class": "Eszk\u00f6zt\u00edpus", - "host": "Hoszt", + "host": "C\u00edm", "name": "N\u00e9v" }, "description": "A Hozz\u00e1f\u00e9r\u00e9si token csak t\u00e9v\u00e9khez sz\u00fcks\u00e9ges. Ha TV -t konfigur\u00e1l, \u00e9s m\u00e9g nincs Hozz\u00e1f\u00e9r\u00e9si token , hagyja \u00fcresen a p\u00e1ros\u00edt\u00e1si folyamathoz.", diff --git a/homeassistant/components/volumio/translations/hu.json b/homeassistant/components/volumio/translations/hu.json index e58f0666039..209a892af3d 100644 --- a/homeassistant/components/volumio/translations/hu.json +++ b/homeassistant/components/volumio/translations/hu.json @@ -10,12 +10,12 @@ }, "step": { "discovery_confirm": { - "description": "Szeretn\u00e9d hozz\u00e1adni a Volumio (`{name}`)-t a Home Assistant-hoz?", + "description": "Szeretn\u00e9 hozz\u00e1adni a Volumio (`{name}`)-t a Home Assistant-hoz?", "title": "Felfedezett Volumio" }, "user": { "data": { - "host": "Hoszt", + "host": "C\u00edm", "port": "Port" } } diff --git a/homeassistant/components/wallbox/translations/es.json b/homeassistant/components/wallbox/translations/es.json index 2b22ece4860..1252e5eaca1 100644 --- a/homeassistant/components/wallbox/translations/es.json +++ b/homeassistant/components/wallbox/translations/es.json @@ -1,13 +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": { "password": "Contrase\u00f1a", - "station": "N\u00famero de serie de la estaci\u00f3n" + "station": "N\u00famero de serie de la estaci\u00f3n", + "username": "Usuario" } } } diff --git a/homeassistant/components/wallbox/translations/id.json b/homeassistant/components/wallbox/translations/id.json index 8fa55e63051..becbcbe817f 100644 --- a/homeassistant/components/wallbox/translations/id.json +++ b/homeassistant/components/wallbox/translations/id.json @@ -11,9 +11,11 @@ "step": { "user": { "data": { - "password": "Kata Sandi" + "password": "Kata Sandi", + "username": "Nama Pengguna" } } } - } + }, + "title": "Wallbox" } \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/ca.json b/homeassistant/components/watttime/translations/ca.json new file mode 100644 index 00000000000..09a0360fab3 --- /dev/null +++ b/homeassistant/components/watttime/translations/ca.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat", + "unknown_coordinates": "No hi ha dades de latitud/longitud" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud" + }, + "description": "Introdueix la latitud i la longitud a monitoritzar:" + }, + "location": { + "data": { + "location_type": "Ubicaci\u00f3" + }, + "description": "Tria una ubicaci\u00f3 a monitoritzar:" + }, + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Introdueix el nom d'usuari i contrasenya:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/cs.json b/homeassistant/components/watttime/translations/cs.json new file mode 100644 index 00000000000..b4df21bd3a1 --- /dev/null +++ b/homeassistant/components/watttime/translations/cs.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka" + } + }, + "location": { + "data": { + "location_type": "Um\u00edst\u011bn\u00ed" + } + }, + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/de.json b/homeassistant/components/watttime/translations/de.json new file mode 100644 index 00000000000..c6bf9641c13 --- /dev/null +++ b/homeassistant/components/watttime/translations/de.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler", + "unknown_coordinates": "Keine Daten f\u00fcr Breitengrad/L\u00e4ngengrad" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad" + }, + "description": "Gib den zu \u00fcberwachenden Breitengrad und L\u00e4ngengrad ein:" + }, + "location": { + "data": { + "location_type": "Standort" + }, + "description": "W\u00e4hle einen Standort f\u00fcr die \u00dcberwachung:" + }, + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "description": "Gib deinen Benutzernamen und dein Passwort ein:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/es.json b/homeassistant/components/watttime/translations/es.json new file mode 100644 index 00000000000..922aed60d97 --- /dev/null +++ b/homeassistant/components/watttime/translations/es.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya se ha configurado" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado", + "unknown_coordinates": "No hay datos para esa latitud/longitud" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud" + }, + "description": "Introduzca la latitud y longitud a monitorizar:" + }, + "location": { + "data": { + "location_type": "Ubicaci\u00f3n" + }, + "description": "Escoja una ubicaci\u00f3n para monitorizar:" + }, + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "description": "Introduzca su nombre de usuario y contrase\u00f1a:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/et.json b/homeassistant/components/watttime/translations/et.json new file mode 100644 index 00000000000..c9f47756021 --- /dev/null +++ b/homeassistant/components/watttime/translations/et.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge", + "unknown_coordinates": "Laius- ja/v\u00f5i pikkuskraadi andmed puuduvad" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad" + }, + "description": "Sisesta j\u00e4lgitav laius- ja pikkuskraad:" + }, + "location": { + "data": { + "location_type": "Asukoht" + }, + "description": "Vali j\u00e4lgiv asukoht:" + }, + "user": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + }, + "description": "Sisesta oma kasutajanimi ja salas\u00f5na:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/he.json b/homeassistant/components/watttime/translations/he.json new file mode 100644 index 00000000000..bbc82f5fa86 --- /dev/null +++ b/homeassistant/components/watttime/translations/he.json @@ -0,0 +1,30 @@ +{ + "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": { + "coordinates": { + "data": { + "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" + } + }, + "location": { + "data": { + "location_type": "\u05de\u05d9\u05e7\u05d5\u05dd" + } + }, + "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/watttime/translations/hu.json b/homeassistant/components/watttime/translations/hu.json new file mode 100644 index 00000000000..a106416f4b9 --- /dev/null +++ b/homeassistant/components/watttime/translations/hu.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", + "unknown_coordinates": "Nincs adat a megadott sz\u00e9less\u00e9g/hossz\u00fas\u00e1g vonatkoz\u00e1s\u00e1ban" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g" + }, + "description": "Adja meg a sz\u00e9less\u00e9gi \u00e9s a hossz\u00fas\u00e1gi fokot a monitoroz\u00e1shoz:" + }, + "location": { + "data": { + "location_type": "Elhelyezked\u00e9s" + }, + "description": "V\u00e1lasszon egy helyet a monitoroz\u00e1shoz:" + }, + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "Adja meg felhaszn\u00e1l\u00f3nev\u00e9t \u00e9s jelszav\u00e1t:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/id.json b/homeassistant/components/watttime/translations/id.json new file mode 100644 index 00000000000..2549bd6f4ff --- /dev/null +++ b/homeassistant/components/watttime/translations/id.json @@ -0,0 +1,26 @@ +{ + "config": { + "step": { + "coordinates": { + "data": { + "latitude": "Lintang", + "longitude": "Bujur" + }, + "description": "Masukkan lintang dan bujur untuk dipantau:" + }, + "location": { + "data": { + "location_type": "Lokasi" + }, + "description": "Pilih lokasi untuk dipantau:" + }, + "user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "description": "Masukkan nama pengguna dan kata sandi Anda:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/it.json b/homeassistant/components/watttime/translations/it.json new file mode 100644 index 00000000000..40f41c1d046 --- /dev/null +++ b/homeassistant/components/watttime/translations/it.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto", + "unknown_coordinates": "Nessun dato per latitudine/longitudine" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Latitudine", + "longitude": "Logitudine" + }, + "description": "Immettere la latitudine e la longitudine da monitorare:" + }, + "location": { + "data": { + "location_type": "Posizione" + }, + "description": "Scegli una posizione da monitorare:" + }, + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "description": "Inserisci il tuo nome utente e password:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/nl.json b/homeassistant/components/watttime/translations/nl.json new file mode 100644 index 00000000000..f6776744cbb --- /dev/null +++ b/homeassistant/components/watttime/translations/nl.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout", + "unknown_coordinates": "Geen gegevens voor lengte-/breedtegraad" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Breedtegraad", + "longitude": "Lengtegraad" + }, + "description": "Voer de breedtegraad en de lengtegraad in die u wilt monitoren:" + }, + "location": { + "data": { + "location_type": "Locatie" + }, + "description": "Kies een locatie om te monitoren:" + }, + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "description": "Voer uw gebruikersnaam en wachtwoord in:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/no.json b/homeassistant/components/watttime/translations/no.json new file mode 100644 index 00000000000..a58a29ff052 --- /dev/null +++ b/homeassistant/components/watttime/translations/no.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil", + "unknown_coordinates": "Ingen data for breddegrad/lengdegrad" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Breddegrad", + "longitude": "Lengdegrad" + }, + "description": "Skriv inn breddegrad og lengdegrad som skal overv\u00e5kes:" + }, + "location": { + "data": { + "location_type": "Plassering" + }, + "description": "Velg et sted \u00e5 overv\u00e5ke:" + }, + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Skriv inn brukernavn og passord:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/ru.json b/homeassistant/components/watttime/translations/ru.json new file mode 100644 index 00000000000..d7e67187d9d --- /dev/null +++ b/homeassistant/components/watttime/translations/ru.json @@ -0,0 +1,34 @@ +{ + "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.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", + "unknown_coordinates": "\u041d\u0435\u0442 \u0434\u0430\u043d\u043d\u044b\u0445 \u043f\u043e \u0448\u0438\u0440\u043e\u0442\u0435 \u0438 \u0434\u043e\u043b\u0433\u043e\u0442\u0435." + }, + "step": { + "coordinates": { + "data": { + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0448\u0438\u0440\u043e\u0442\u0443 \u0438 \u0434\u043e\u043b\u0433\u043e\u0442\u0443 \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430:" + }, + "location": { + "data": { + "location_type": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430:" + }, + "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" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438 \u043f\u0430\u0440\u043e\u043b\u044c:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/zh-Hant.json b/homeassistant/components/watttime/translations/zh-Hant.json new file mode 100644 index 00000000000..898dfc05dd7 --- /dev/null +++ b/homeassistant/components/watttime/translations/zh-Hant.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4", + "unknown_coordinates": "\u6c92\u6709\u7d93\u5ea6/\u7def\u5ea6\u8cc7\u6599" + }, + "step": { + "coordinates": { + "data": { + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6" + }, + "description": "\u8f38\u5165\u6240\u8981\u76e3\u63a7\u7684\u7d93\u5ea6\u8207\u7def\u5ea6\uff1a" + }, + "location": { + "data": { + "location_type": "\u5ea7\u6a19" + }, + "description": "\u9078\u64c7\u6240\u8981\u76e3\u63a7\u7684\u4f4d\u7f6e\uff1a" + }, + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8f38\u5165\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\uff1a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/waze_travel_time/translations/id.json b/homeassistant/components/waze_travel_time/translations/id.json index 587e959fe7e..3f3cd02aaf6 100644 --- a/homeassistant/components/waze_travel_time/translations/id.json +++ b/homeassistant/components/waze_travel_time/translations/id.json @@ -10,6 +10,7 @@ "user": { "data": { "destination": "Tujuan", + "name": "Nama", "origin": "Asal", "region": "Wilayah" }, diff --git a/homeassistant/components/wemo/translations/hu.json b/homeassistant/components/wemo/translations/hu.json index ff9f4dc5f75..f27d566ceb9 100644 --- a/homeassistant/components/wemo/translations/hu.json +++ b/homeassistant/components/wemo/translations/hu.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Be szeretn\u00e9 \u00e1ll\u00edtani a Wemo-t?" + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani Wemo-t?" } } }, diff --git a/homeassistant/components/wemo/translations/id.json b/homeassistant/components/wemo/translations/id.json index af0b3128cb9..831b0822f64 100644 --- a/homeassistant/components/wemo/translations/id.json +++ b/homeassistant/components/wemo/translations/id.json @@ -9,5 +9,10 @@ "description": "Ingin menyiapkan Wemo?" } } + }, + "device_automation": { + "trigger_type": { + "long_press": "Tombol Wemo ditekan selama 2 detik" + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/ca.json b/homeassistant/components/whirlpool/translations/ca.json new file mode 100644 index 00000000000..f844476e4c6 --- /dev/null +++ b/homeassistant/components/whirlpool/translations/ca.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/cs.json b/homeassistant/components/whirlpool/translations/cs.json new file mode 100644 index 00000000000..c0841233cb7 --- /dev/null +++ b/homeassistant/components/whirlpool/translations/cs.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/de.json b/homeassistant/components/whirlpool/translations/de.json new file mode 100644 index 00000000000..57f62e0da32 --- /dev/null +++ b/homeassistant/components/whirlpool/translations/de.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/en.json b/homeassistant/components/whirlpool/translations/en.json index 407d41d6736..74817db9ba7 100644 --- a/homeassistant/components/whirlpool/translations/en.json +++ b/homeassistant/components/whirlpool/translations/en.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "Device is already configured" - }, "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", @@ -11,12 +8,10 @@ "step": { "user": { "data": { - "host": "Host", "password": "Password", "username": "Username" } } } - }, - "title": "Whirlpool Sixth Sense" + } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/es.json b/homeassistant/components/whirlpool/translations/es.json new file mode 100644 index 00000000000..d26c25c3548 --- /dev/null +++ b/homeassistant/components/whirlpool/translations/es.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/et.json b/homeassistant/components/whirlpool/translations/et.json new file mode 100644 index 00000000000..983f599c870 --- /dev/null +++ b/homeassistant/components/whirlpool/translations/et.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/he.json b/homeassistant/components/whirlpool/translations/he.json new file mode 100644 index 00000000000..96803e13b33 --- /dev/null +++ b/homeassistant/components/whirlpool/translations/he.json @@ -0,0 +1,17 @@ +{ + "config": { + "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": { + "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/whirlpool/translations/hu.json b/homeassistant/components/whirlpool/translations/hu.json new file mode 100644 index 00000000000..e1cc19c9c30 --- /dev/null +++ b/homeassistant/components/whirlpool/translations/hu.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "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/whirlpool/translations/id.json b/homeassistant/components/whirlpool/translations/id.json new file mode 100644 index 00000000000..7244ccf8912 --- /dev/null +++ b/homeassistant/components/whirlpool/translations/id.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "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/whirlpool/translations/it.json b/homeassistant/components/whirlpool/translations/it.json new file mode 100644 index 00000000000..eb5545ca85a --- /dev/null +++ b/homeassistant/components/whirlpool/translations/it.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/nl.json b/homeassistant/components/whirlpool/translations/nl.json new file mode 100644 index 00000000000..a4954b83866 --- /dev/null +++ b/homeassistant/components/whirlpool/translations/nl.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/no.json b/homeassistant/components/whirlpool/translations/no.json new file mode 100644 index 00000000000..4bcac3aada8 --- /dev/null +++ b/homeassistant/components/whirlpool/translations/no.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/pt-BR.json b/homeassistant/components/whirlpool/translations/pt-BR.json new file mode 100644 index 00000000000..efdc82ab438 --- /dev/null +++ b/homeassistant/components/whirlpool/translations/pt-BR.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/ru.json b/homeassistant/components/whirlpool/translations/ru.json new file mode 100644 index 00000000000..994a287efd7 --- /dev/null +++ b/homeassistant/components/whirlpool/translations/ru.json @@ -0,0 +1,17 @@ +{ + "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.", + "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/whirlpool/translations/zh-Hant.json b/homeassistant/components/whirlpool/translations/zh-Hant.json new file mode 100644 index 00000000000..a3784595b65 --- /dev/null +++ b/homeassistant/components/whirlpool/translations/zh-Hant.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wilight/translations/hu.json b/homeassistant/components/wilight/translations/hu.json index 3b769a88b8f..9ef669f1ed3 100644 --- a/homeassistant/components/wilight/translations/hu.json +++ b/homeassistant/components/wilight/translations/hu.json @@ -8,7 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Be szeretn\u00e9 \u00e1ll\u00edtani a WiLight {name} ? \n\n T\u00e1mogatja: {components}", + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani a WiLight {name}-t ? \n\nT\u00e1mogatja: {components}", "title": "WiLight" } } diff --git a/homeassistant/components/wilight/translations/id.json b/homeassistant/components/wilight/translations/id.json index dae7b0bd16a..06616b29e35 100644 --- a/homeassistant/components/wilight/translations/id.json +++ b/homeassistant/components/wilight/translations/id.json @@ -5,7 +5,7 @@ "not_supported_device": "Perangkat WiLight ini saat ini tidak didukung.", "not_wilight_device": "Perangkat ini bukan perangkat WiLight" }, - "flow_title": "WiLight: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Apakah Anda ingin menyiapkan WiLight {name}?\n\nIni mendukung: {components}", diff --git a/homeassistant/components/withings/translations/ca.json b/homeassistant/components/withings/translations/ca.json index b548735d426..c75e08e0234 100644 --- a/homeassistant/components/withings/translations/ca.json +++ b/homeassistant/components/withings/translations/ca.json @@ -10,7 +10,7 @@ "default": "Autenticaci\u00f3 exitosa amb Withings." }, "error": { - "already_configured": "El compte ja ha estat configurat" + "already_configured": "El compte ja est\u00e0 configurat" }, "flow_title": "{profile}", "step": { diff --git a/homeassistant/components/withings/translations/hu.json b/homeassistant/components/withings/translations/hu.json index e26cff027fc..1a64a95de5e 100644 --- a/homeassistant/components/withings/translations/hu.json +++ b/homeassistant/components/withings/translations/hu.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "A profil konfigur\u00e1ci\u00f3ja friss\u00edtve.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", - "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz." }, "create_entry": { @@ -19,9 +19,9 @@ }, "profile": { "data": { - "profile": "Profil" + "profile": "Profil n\u00e9v" }, - "description": "Melyik profilt v\u00e1lasztottad ki a Withings weboldalon? Fontos, hogy a profilok egyeznek, k\u00fcl\u00f6nben az adatok helytelen c\u00edmk\u00e9vel lesznek ell\u00e1tva.", + "description": "K\u00e9rem, adjon meg egy egyedi profilnevet. Ez \u00e1ltal\u00e1ban az el\u0151z\u0151 l\u00e9p\u00e9sben kiv\u00e1lasztott profil neve.", "title": "Felhaszn\u00e1l\u00f3i profil." }, "reauth": { diff --git a/homeassistant/components/withings/translations/id.json b/homeassistant/components/withings/translations/id.json index e254e61d91e..eb21a0d3352 100644 --- a/homeassistant/components/withings/translations/id.json +++ b/homeassistant/components/withings/translations/id.json @@ -12,7 +12,7 @@ "error": { "already_configured": "Akun sudah dikonfigurasi" }, - "flow_title": "Withings: {profile}", + "flow_title": "{profile}", "step": { "pick_implementation": { "title": "Pilih Metode Autentikasi" diff --git a/homeassistant/components/wled/translations/hu.json b/homeassistant/components/wled/translations/hu.json index 769573bfc89..2e0ac08d3cb 100644 --- a/homeassistant/components/wled/translations/hu.json +++ b/homeassistant/components/wled/translations/hu.json @@ -7,16 +7,16 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, - "flow_title": "WLED: {name}", + "flow_title": "{name}", "step": { "user": { "data": { - "host": "Hoszt" + "host": "C\u00edm" }, - "description": "\u00c1ll\u00edtsd be a WLED-et a Home Assistant-ba val\u00f3 integr\u00e1l\u00e1shoz." + "description": "\u00c1ll\u00edtsa be a WLED-et Home Assistant-ba val\u00f3 integr\u00e1l\u00e1shoz." }, "zeroconf_confirm": { - "description": "Szeretn\u00e9d hozz\u00e1adni a(z) `{name}` WLED-et a Home Assistant-hoz?", + "description": "Szeretn\u00e9 hozz\u00e1adni a(z) `{name}` WLED-et Home Assistant-hoz?", "title": "Felfedezett WLED eszk\u00f6z" } } diff --git a/homeassistant/components/wled/translations/id.json b/homeassistant/components/wled/translations/id.json index 6437dfaf83e..122cfd9da0b 100644 --- a/homeassistant/components/wled/translations/id.json +++ b/homeassistant/components/wled/translations/id.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Gagal terhubung" }, - "flow_title": "WLED: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/xbox/translations/hu.json b/homeassistant/components/xbox/translations/hu.json index b35b1b8e2fc..24c46bb8ab0 100644 --- a/homeassistant/components/xbox/translations/hu.json +++ b/homeassistant/components/xbox/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", - "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t.", + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "create_entry": { diff --git a/homeassistant/components/xiaomi_aqara/translations/hu.json b/homeassistant/components/xiaomi_aqara/translations/hu.json index 675ef24af3b..a6139c8851b 100644 --- a/homeassistant/components/xiaomi_aqara/translations/hu.json +++ b/homeassistant/components/xiaomi_aqara/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", "not_xiaomi_aqara": "Nem egy Xiaomi Aqara Gateway, a felfedezett eszk\u00f6z nem egyezett az ismert \u00e1tj\u00e1r\u00f3kkal" }, "error": { diff --git a/homeassistant/components/xiaomi_aqara/translations/id.json b/homeassistant/components/xiaomi_aqara/translations/id.json index 5a2acfa330a..eeab548f681 100644 --- a/homeassistant/components/xiaomi_aqara/translations/id.json +++ b/homeassistant/components/xiaomi_aqara/translations/id.json @@ -12,7 +12,7 @@ "invalid_key": "Kunci gateway tidak valid", "invalid_mac": "Alamat MAC Tidak Valid" }, - "flow_title": "Xiaomi Aqara Gateway: {name}", + "flow_title": "{name}", "step": { "select": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/es.json b/homeassistant/components/xiaomi_miio/translations/es.json index 5403e98f25f..d70445d2aa5 100644 --- a/homeassistant/components/xiaomi_miio/translations/es.json +++ b/homeassistant/components/xiaomi_miio/translations/es.json @@ -4,7 +4,8 @@ "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n para este dispositivo Xiaomi Miio ya est\u00e1 en marcha.", "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." + "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", @@ -53,6 +54,10 @@ "title": "Conectar con un Xiaomi Gateway" }, "manual": { + "data": { + "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.", "title": "Con\u00e9ctate a un dispositivo Xiaomi Miio o una puerta de enlace Xiaomi" }, diff --git a/homeassistant/components/xiaomi_miio/translations/hu.json b/homeassistant/components/xiaomi_miio/translations/hu.json index 1747b51c61a..6d53aad6f56 100644 --- a/homeassistant/components/xiaomi_miio/translations/hu.json +++ b/homeassistant/components/xiaomi_miio/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", "incomplete_info": "Az eszk\u00f6z be\u00e1ll\u00edt\u00e1s\u00e1hoz sz\u00fcks\u00e9ges inform\u00e1ci\u00f3k hi\u00e1nyosak, nincs megadva \u00e1llom\u00e1s vagy token.", "not_xiaomi_miio": "Az eszk\u00f6zt (m\u00e9g) nem t\u00e1mogatja a Xiaomi Miio integr\u00e1ci\u00f3.", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" @@ -41,7 +41,7 @@ "name": "Eszk\u00f6z neve", "token": "API Token" }, - "description": "Sz\u00fcks\u00e9ged lesz a 32 karakteres API Tokenre, k\u00f6vesd a https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token oldal instrukci\u00f3it. Vedd figyelembe, hogy ez az API Token k\u00fcl\u00f6nb\u00f6zik a Xiaomi Aqara integr\u00e1ci\u00f3 \u00e1ltal haszn\u00e1lt kulcst\u00f3l.", + "description": "Sz\u00fcks\u00e9ge lesz a 32 karakteres API Tokenre, k\u00f6vesse a https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token oldal instrukci\u00f3it. Vegye figyelembe, hogy ez az API Token k\u00fcl\u00f6nb\u00f6zik a Xiaomi Aqara integr\u00e1ci\u00f3 \u00e1ltal haszn\u00e1lt kulcst\u00f3l.", "title": "Csatlakoz\u00e1s Xiaomi Miio eszk\u00f6zh\u00f6z vagy Xiaomi Gateway-hez" }, "gateway": { diff --git a/homeassistant/components/xiaomi_miio/translations/id.json b/homeassistant/components/xiaomi_miio/translations/id.json index f893f7b06aa..a6217b52eb1 100644 --- a/homeassistant/components/xiaomi_miio/translations/id.json +++ b/homeassistant/components/xiaomi_miio/translations/id.json @@ -2,14 +2,15 @@ "config": { "abort": { "already_configured": "Perangkat sudah dikonfigurasi", - "already_in_progress": "Alur konfigurasi sedang berlangsung" + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "cannot_connect": "Gagal terhubung", "no_device_selected": "Tidak ada perangkat yang dipilih, pilih satu perangkat.", "unknown_device": "Model perangkat tidak diketahui, tidak dapat menyiapkan perangkat menggunakan alur konfigurasi." }, - "flow_title": "Xiaomi Miio: {name}", + "flow_title": "{name}", "step": { "cloud": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/select.id.json b/homeassistant/components/xiaomi_miio/translations/select.id.json new file mode 100644 index 00000000000..178bc06301c --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/select.id.json @@ -0,0 +1,9 @@ +{ + "state": { + "xiaomi_miio__led_brightness": { + "bright": "Terang", + "dim": "Redup", + "off": "Mati" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/ca.json b/homeassistant/components/yale_smart_alarm/translations/ca.json index ab77170999b..04e894afe1b 100644 --- a/homeassistant/components/yale_smart_alarm/translations/ca.json +++ b/homeassistant/components/yale_smart_alarm/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat" + "already_configured": "El compte ja est\u00e0 configurat" }, "error": { "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" diff --git a/homeassistant/components/yale_smart_alarm/translations/el.json b/homeassistant/components/yale_smart_alarm/translations/el.json new file mode 100644 index 00000000000..676d0889008 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/el.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "area_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ae\u03c2" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/es.json b/homeassistant/components/yale_smart_alarm/translations/es.json index 367454a7d21..178b8209af7 100644 --- a/homeassistant/components/yale_smart_alarm/translations/es.json +++ b/homeassistant/components/yale_smart_alarm/translations/es.json @@ -1,18 +1,18 @@ { "config": { "abort": { - "already_configured": "La cuenta ya est\u00e1 configurada" + "already_configured": "La cuenta ya ha sido configurada" }, "error": { - "invalid_auth": "Autenticaci\u00f3n err\u00f3nea" + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { "reauth_confirm": { "data": { - "area_id": "ID de \u00c1rea", + "area_id": "ID de \u00e1rea", "name": "Nombre", - "password": "Clave", - "username": "Nombre de usuario" + "password": "Contrase\u00f1a", + "username": "Usuario" } }, "user": { @@ -20,7 +20,7 @@ "area_id": "ID de \u00e1rea", "name": "Nombre", "password": "Contrase\u00f1a", - "username": "Nombre de usuario" + "username": "Usuario" } } } diff --git a/homeassistant/components/yale_smart_alarm/translations/id.json b/homeassistant/components/yale_smart_alarm/translations/id.json new file mode 100644 index 00000000000..ee24f03a33c --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/id.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi" + }, + "error": { + "invalid_auth": "Autentikasi tidak valid" + }, + "step": { + "reauth_confirm": { + "data": { + "area_id": "ID Area", + "name": "Nama", + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + }, + "user": { + "data": { + "area_id": "ID Area", + "name": "Nama", + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + } + } + } +} \ 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 185176e7b39..46f8a02f33d 100644 --- a/homeassistant/components/yamaha_musiccast/translations/es.json +++ b/homeassistant/components/yamaha_musiccast/translations/es.json @@ -1,6 +1,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." }, "error": { @@ -8,6 +9,9 @@ }, "flow_title": "MusicCast: {name}", "step": { + "confirm": { + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" + }, "user": { "data": { "host": "Anfitri\u00f3n" diff --git a/homeassistant/components/yamaha_musiccast/translations/hu.json b/homeassistant/components/yamaha_musiccast/translations/hu.json index 9ddf75ca732..fc2672f5839 100644 --- a/homeassistant/components/yamaha_musiccast/translations/hu.json +++ b/homeassistant/components/yamaha_musiccast/translations/hu.json @@ -10,11 +10,11 @@ "flow_title": "MusicCast: {name}", "step": { "confirm": { - "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?" + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" }, "user": { "data": { - "host": "Hoszt" + "host": "C\u00edm" }, "description": "\u00c1ll\u00edtsa be a MusicCast-ot a Homeassistanttal val\u00f3 integr\u00e1ci\u00f3hoz." } diff --git a/homeassistant/components/yamaha_musiccast/translations/nl.json b/homeassistant/components/yamaha_musiccast/translations/nl.json index e1e31149c06..8cb8265a1f0 100644 --- a/homeassistant/components/yamaha_musiccast/translations/nl.json +++ b/homeassistant/components/yamaha_musiccast/translations/nl.json @@ -10,7 +10,7 @@ "flow_title": "MusicCast: {name}", "step": { "confirm": { - "description": "Wil je beginnen met instellen?" + "description": "Wilt u beginnen met instellen?" }, "user": { "data": { diff --git a/homeassistant/components/yeelight/translations/es.json b/homeassistant/components/yeelight/translations/es.json index 044a10c695d..c58d863fa13 100644 --- a/homeassistant/components/yeelight/translations/es.json +++ b/homeassistant/components/yeelight/translations/es.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "No se pudo conectar" }, - "flow_title": "{model} {host}", + "flow_title": "{model} {id} ({host})", "step": { "discovery_confirm": { "description": "\u00bfQuieres configurar {model} ({host})?" diff --git a/homeassistant/components/yeelight/translations/hu.json b/homeassistant/components/yeelight/translations/hu.json index 26dc6cb5ba1..6cf10422c28 100644 --- a/homeassistant/components/yeelight/translations/hu.json +++ b/homeassistant/components/yeelight/translations/hu.json @@ -7,10 +7,10 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, - "flow_title": "{model} {host}", + "flow_title": "{model} {id} ({host})", "step": { "discovery_confirm": { - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani a {model} ( {host} ) szolg\u00e1ltat\u00e1st?" + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani a {model} ({host}) szolg\u00e1ltat\u00e1st?" }, "pick_device": { "data": { @@ -19,9 +19,9 @@ }, "user": { "data": { - "host": "Hoszt" + "host": "C\u00edm" }, - "description": "Ha a gazdag\u00e9pet \u00fcresen hagyja, felder\u00edt\u00e9sre ker\u00fcl automatikusan." + "description": "Ha nem ad meg c\u00edmet, akkor az eszk\u00f6z\u00f6k keres\u00e9se a felder\u00edt\u00e9ssel t\u00f6rt\u00e9nik." } } }, diff --git a/homeassistant/components/yeelight/translations/id.json b/homeassistant/components/yeelight/translations/id.json index 3b2f0273ae3..d9795662689 100644 --- a/homeassistant/components/yeelight/translations/id.json +++ b/homeassistant/components/yeelight/translations/id.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Gagal terhubung" }, - "flow_title": "{model} {host}", + "flow_title": "{model} {id} ({host})", "step": { "discovery_confirm": { "description": "Ingin menyiapkan {model} ({host})?" @@ -29,7 +29,7 @@ "step": { "init": { "data": { - "model": "Model (Opsional)", + "model": "Model (opsional)", "nightlight_switch": "Gunakan Sakelar Lampu Malam", "save_on_change": "Simpan Status Saat Berubah", "transition": "Waktu Transisi (milidetik)", diff --git a/homeassistant/components/youless/translations/es.json b/homeassistant/components/youless/translations/es.json index 72a56cc5608..77837bb25ce 100644 --- a/homeassistant/components/youless/translations/es.json +++ b/homeassistant/components/youless/translations/es.json @@ -6,7 +6,7 @@ "step": { "user": { "data": { - "host": "Anfitri\u00f3n", + "host": "Host", "name": "Nombre" } } diff --git a/homeassistant/components/youless/translations/hu.json b/homeassistant/components/youless/translations/hu.json index 21c7a7ebe4b..31913b7fa6f 100644 --- a/homeassistant/components/youless/translations/hu.json +++ b/homeassistant/components/youless/translations/hu.json @@ -6,7 +6,7 @@ "step": { "user": { "data": { - "host": "H\u00e1zigazda", + "host": "C\u00edm", "name": "N\u00e9v" } } diff --git a/homeassistant/components/youless/translations/id.json b/homeassistant/components/youless/translations/id.json new file mode 100644 index 00000000000..fd6c2bc2491 --- /dev/null +++ b/homeassistant/components/youless/translations/id.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nama" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zerproc/translations/hu.json b/homeassistant/components/zerproc/translations/hu.json index 6c61530acbe..a56ebbfc906 100644 --- a/homeassistant/components/zerproc/translations/hu.json +++ b/homeassistant/components/zerproc/translations/hu.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?" + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" } } } diff --git a/homeassistant/components/zerproc/translations/nl.json b/homeassistant/components/zerproc/translations/nl.json index d11896014fd..0671f0b3674 100644 --- a/homeassistant/components/zerproc/translations/nl.json +++ b/homeassistant/components/zerproc/translations/nl.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Wil je beginnen met instellen?" + "description": "Wilt u beginnen met instellen?" } } } diff --git a/homeassistant/components/zha/translations/es.json b/homeassistant/components/zha/translations/es.json index 4753834a493..f04614f1f72 100644 --- a/homeassistant/components/zha/translations/es.json +++ b/homeassistant/components/zha/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." + "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" }, "error": { "cannot_connect": "No se pudo conectar" diff --git a/homeassistant/components/zha/translations/hu.json b/homeassistant/components/zha/translations/hu.json index c65eaea4325..cc480bb413e 100644 --- a/homeassistant/components/zha/translations/hu.json +++ b/homeassistant/components/zha/translations/hu.json @@ -11,7 +11,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Be szeretn\u00e9 \u00e1ll\u00edtani a {name}-t?" + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" }, "pick_radio": { "data": { diff --git a/homeassistant/components/zha/translations/id.json b/homeassistant/components/zha/translations/id.json index 4198352aae8..5a10e3d01af 100644 --- a/homeassistant/components/zha/translations/id.json +++ b/homeassistant/components/zha/translations/id.json @@ -1,13 +1,18 @@ { "config": { "abort": { - "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + "not_zha_device": "Perangkat ini bukan perangkat zha", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", + "usb_probe_failed": "Gagal mendeteksi perangkat usb" }, "error": { "cannot_connect": "Gagal terhubung" }, - "flow_title": "ZHA: {name}", + "flow_title": "{name}", "step": { + "confirm": { + "description": "Ingin menyiapkan {name}?" + }, "pick_radio": { "data": { "radio_type": "Jenis Radio" @@ -43,6 +48,7 @@ "zha_options": { "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", "title": "Opsi Global" } diff --git a/homeassistant/components/zoneminder/translations/hu.json b/homeassistant/components/zoneminder/translations/hu.json index a449464e27f..e01f032925d 100644 --- a/homeassistant/components/zoneminder/translations/hu.json +++ b/homeassistant/components/zoneminder/translations/hu.json @@ -19,7 +19,7 @@ "step": { "user": { "data": { - "host": "Host \u00e9s Port (pl. 10.10.0.4:8010)", + "host": "C\u00edm \u00e9s Port (pl. 10.10.0.4:8010)", "password": "Jelsz\u00f3", "path": "ZM \u00fatvonal", "path_zms": "ZMS el\u00e9r\u00e9si \u00fat", diff --git a/homeassistant/components/zwave/translations/ca.json b/homeassistant/components/zwave/translations/ca.json index 9a5ef2b5010..4b9e8953b8a 100644 --- a/homeassistant/components/zwave/translations/ca.json +++ b/homeassistant/components/zwave/translations/ca.json @@ -11,7 +11,7 @@ "user": { "data": { "network_key": "Clau de xarxa (deixa-ho en blanc per generar-la autom\u00e0ticament)", - "usb_path": "Ruta del port USB del dispositiu" + "usb_path": "Ruta del dispositiu USB" }, "description": "Aquesta integraci\u00f3 ja no s'actualitzar\u00e0. Utilitza Z-Wave JS per a instal\u00b7lacions noves.\n\nConsulta https://www.home-assistant.io/docs/z-wave/installation/ per a m\u00e9s informaci\u00f3 sobre les variables de configuraci\u00f3" } diff --git a/homeassistant/components/zwave/translations/hu.json b/homeassistant/components/zwave/translations/hu.json index 7269ee32daf..4d0c6adff59 100644 --- a/homeassistant/components/zwave/translations/hu.json +++ b/homeassistant/components/zwave/translations/hu.json @@ -13,19 +13,19 @@ "network_key": "H\u00e1l\u00f3zati kulcs (hagyja \u00fcresen az automatikus gener\u00e1l\u00e1shoz)", "usb_path": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" }, - "description": "A konfigur\u00e1ci\u00f3s v\u00e1ltoz\u00f3kr\u00f3l az inform\u00e1ci\u00f3kat l\u00e1sd a https://www.home-assistant.io/docs/z-wave/installation/ oldalon." + "description": "Ezt az integr\u00e1ci\u00f3t m\u00e1r nem tartj\u00e1k fenn. \u00daj telep\u00edt\u00e9sek eset\u00e9n haszn\u00e1lja helyette a Z-Wave JS-t.\n\nA konfigur\u00e1ci\u00f3s v\u00e1ltoz\u00f3kkal kapcsolatos inform\u00e1ci\u00f3k\u00e9rt l\u00e1sd https://www.home-assistant.io/docs/z-wave/installation/." } } }, "state": { "_": { - "dead": "Halott", + "dead": "Nem ad \u00e9letjelet", "initializing": "Inicializ\u00e1l\u00e1s", "ready": "K\u00e9sz", "sleeping": "Alv\u00e1s" }, "query_stage": { - "dead": "Halott", + "dead": "Nem ad \u00e9letjelet", "initializing": "Inicializ\u00e1l\u00e1s" } } diff --git a/homeassistant/components/zwave_js/translations/ca.json b/homeassistant/components/zwave_js/translations/ca.json index ac18c44b489..d0861d44f89 100644 --- a/homeassistant/components/zwave_js/translations/ca.json +++ b/homeassistant/components/zwave_js/translations/ca.json @@ -27,7 +27,11 @@ "configure_addon": { "data": { "network_key": "Clau de xarxa", - "usb_path": "Ruta del port USB del dispositiu" + "s0_legacy_key": "Clau d'S0 (est\u00e0ndard)", + "s2_access_control_key": "Clau de control d'acc\u00e9s d'S2", + "s2_authenticated_key": "Clau d'S2 autenticat", + "s2_unauthenticated_key": "Clau d'S2 no autenticat", + "usb_path": "Ruta del dispositiu USB" }, "title": "Introdueix la configuraci\u00f3 del complement Z-Wave JS" }, @@ -58,6 +62,15 @@ } }, "device_automation": { + "action_type": { + "clear_lock_usercode": "Esborra codi d'usuari de {entity_name}", + "ping": "Sondeja dispositiu", + "refresh_value": "Actualitza el/s valor/s de {entity_name}", + "reset_meter": "Reinicialitza comptadors de {subtype}", + "set_config_parameter": "Estableix el valor del par\u00e0metre de configuraci\u00f3 {subtype}", + "set_lock_usercode": "Estableix codi d'usuari a {entity_name}", + "set_value": "Estableix el valor d'un valor Z-Wave" + }, "condition_type": { "config_parameter": "Configura el valor del par\u00e0metre {subtype}", "node_status": "Estat del node", @@ -100,7 +113,11 @@ "emulate_hardware": "Emula maquinari", "log_level": "Nivell dels registres", "network_key": "Clau de xarxa", - "usb_path": "Ruta del port USB del dispositiu" + "s0_legacy_key": "Clau d'S0 (est\u00e0ndard)", + "s2_access_control_key": "Clau de control d'acc\u00e9s d'S2", + "s2_authenticated_key": "Clau d'S2 autenticat", + "s2_unauthenticated_key": "Clau d'S2 no autenticat", + "usb_path": "Ruta del dispositiu USB" }, "title": "Introdueix la configuraci\u00f3 del complement Z-Wave JS" }, diff --git a/homeassistant/components/zwave_js/translations/de.json b/homeassistant/components/zwave_js/translations/de.json index 8d9634c3f46..8bdf7a78237 100644 --- a/homeassistant/components/zwave_js/translations/de.json +++ b/homeassistant/components/zwave_js/translations/de.json @@ -27,6 +27,10 @@ "configure_addon": { "data": { "network_key": "Netzwerk-Schl\u00fcssel", + "s0_legacy_key": "S0 Schl\u00fcssel (Legacy)", + "s2_access_control_key": "S2 Zugangskontrollschl\u00fcssel", + "s2_authenticated_key": "S2 Authentifizierter Schl\u00fcssel", + "s2_unauthenticated_key": "S2 Nicht authentifizierter Schl\u00fcssel", "usb_path": "USB-Ger\u00e4te-Pfad" }, "title": "Gib die Konfiguration des Z-Wave JS Add-ons ein" @@ -58,6 +62,15 @@ } }, "device_automation": { + "action_type": { + "clear_lock_usercode": "Nutzercode f\u00fcr {entity_name} l\u00f6schen", + "ping": "Ger\u00e4t anpingen", + "refresh_value": "Aktualisieren der Wert(e) f\u00fcr {entity_name}", + "reset_meter": "Z\u00e4hler von {subtype} zur\u00fccksetzen", + "set_config_parameter": "Wert des Konfigurationsparameters {subtype} festlegen", + "set_lock_usercode": "Einen Nutzercode f\u00fcr {entity_name} festlegen", + "set_value": "Wert eines Z-Wave-Werts einstellen" + }, "condition_type": { "config_parameter": "Wert des Konfigurationsparameters {subtype}", "node_status": "Status des Knotens", @@ -100,6 +113,10 @@ "emulate_hardware": "Hardware emulieren", "log_level": "Protokollstufe", "network_key": "Netzwerkschl\u00fcssel", + "s0_legacy_key": "S0 Schl\u00fcssel (Legacy)", + "s2_access_control_key": "S2 Zugangskontrollschl\u00fcssel", + "s2_authenticated_key": "S2 Authentifizierter Schl\u00fcssel", + "s2_unauthenticated_key": "S2 Nicht authentifizierter Schl\u00fcssel", "usb_path": "USB-Ger\u00e4te-Pfad" }, "title": "Gib die Konfiguration des Z-Wave JS-Add-ons ein" diff --git a/homeassistant/components/zwave_js/translations/el.json b/homeassistant/components/zwave_js/translations/el.json index 00149f5a0d0..21ba61a6af1 100644 --- a/homeassistant/components/zwave_js/translations/el.json +++ b/homeassistant/components/zwave_js/translations/el.json @@ -4,6 +4,7 @@ "discovery_requires_supervisor": "\u0397 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7 \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03c4\u03bf\u03bd \u03b5\u03c0\u03cc\u03c0\u03c4\u03b7.", "not_zwave_device": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c0\u03bf\u03c5 \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Z-Wave." }, + "flow_title": "{name}", "step": { "usb_confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} \u03bc\u03b5 \u03c4\u03bf \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf Z-Wave JS;" diff --git a/homeassistant/components/zwave_js/translations/en.json b/homeassistant/components/zwave_js/translations/en.json index b24d4f31b06..46650ca5439 100644 --- a/homeassistant/components/zwave_js/translations/en.json +++ b/homeassistant/components/zwave_js/translations/en.json @@ -26,6 +26,7 @@ "step": { "configure_addon": { "data": { + "network_key": "Network Key", "s0_legacy_key": "S0 Key (Legacy)", "s2_access_control_key": "S2 Access Control Key", "s2_authenticated_key": "S2 Authenticated Key", @@ -111,6 +112,7 @@ "data": { "emulate_hardware": "Emulate Hardware", "log_level": "Log level", + "network_key": "Network Key", "s0_legacy_key": "S0 Key (Legacy)", "s2_access_control_key": "S2 Access Control Key", "s2_authenticated_key": "S2 Authenticated Key", @@ -138,5 +140,6 @@ "title": "The Z-Wave JS add-on is starting." } } - } + }, + "title": "Z-Wave JS" } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/es.json b/homeassistant/components/zwave_js/translations/es.json index caebf4f4ecb..e1a9cd081ba 100644 --- a/homeassistant/components/zwave_js/translations/es.json +++ b/homeassistant/components/zwave_js/translations/es.json @@ -16,6 +16,7 @@ "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.", "start_addon": "Espere mientras se completa el inicio del complemento Z-Wave JS. Esto puede tardar unos segundos." @@ -99,6 +100,11 @@ "install_addon": { "title": "La instalaci\u00f3n del complemento Z-Wave JS ha comenzado" }, + "manual": { + "data": { + "url": "URL" + } + }, "on_supervisor": { "title": "Selecciona el m\u00e9todo de conexi\u00f3n" } diff --git a/homeassistant/components/zwave_js/translations/et.json b/homeassistant/components/zwave_js/translations/et.json index efed557fe73..10a813aad85 100644 --- a/homeassistant/components/zwave_js/translations/et.json +++ b/homeassistant/components/zwave_js/translations/et.json @@ -27,6 +27,10 @@ "configure_addon": { "data": { "network_key": "V\u00f5rgu v\u00f5ti", + "s0_legacy_key": "S0 vana t\u00fc\u00fcpi v\u00f5ti", + "s2_access_control_key": "S2 juurdep\u00e4\u00e4suv\u00f5ti", + "s2_authenticated_key": "Autenditud S2 v\u00f5ti", + "s2_unauthenticated_key": "Autentimata S2 v\u00f5ti", "usb_path": "USB-seadme asukoha rada" }, "title": "Sisesta Z-Wave JS lisandmooduli seaded" @@ -58,6 +62,15 @@ } }, "device_automation": { + "action_type": { + "clear_lock_usercode": "Kustutaolemi {entity_name} kasutajakood", + "ping": "K\u00fcsitle seadet", + "refresh_value": "Olemi {entity_name} v\u00e4\u00e4rtuste v\u00e4rskendamine", + "reset_meter": "L\u00e4htesta arvesti {subtype}", + "set_config_parameter": "Seadeparameetri {subtype} v\u00e4\u00e4rtuse omistamine", + "set_lock_usercode": "Olemi {entity_name} kasutaja koodi m\u00e4\u00e4ramine", + "set_value": "Z-Wave v\u00e4\u00e4rtuse m\u00e4\u00e4ramine" + }, "condition_type": { "config_parameter": "Seadeparameeteri {subtype} v\u00e4\u00e4rtus", "node_status": "S\u00f5lme olek", @@ -100,6 +113,10 @@ "emulate_hardware": "Riistvara emuleerimine", "log_level": "Logimise tase", "network_key": "V\u00f5rgu v\u00f5ti", + "s0_legacy_key": "S0 vana t\u00fc\u00fcpi v\u00f5ti", + "s2_access_control_key": "S2 juurdep\u00e4\u00e4suv\u00f5ti", + "s2_authenticated_key": "Autenditud S2 v\u00f5ti", + "s2_unauthenticated_key": "Autentimata S2 v\u00f5ti", "usb_path": "USB-seadme asukoha rada" }, "title": "Sisesta Z-Wave JS lisandmooduli seaded" diff --git a/homeassistant/components/zwave_js/translations/hu.json b/homeassistant/components/zwave_js/translations/hu.json index cf5521f3e04..bf541fce26a 100644 --- a/homeassistant/components/zwave_js/translations/hu.json +++ b/homeassistant/components/zwave_js/translations/hu.json @@ -7,7 +7,7 @@ "addon_set_config_failed": "Nem siker\u00fclt be\u00e1ll\u00edtani a Z-Wave JS konfigur\u00e1ci\u00f3t.", "addon_start_failed": "Nem siker\u00fclt elind\u00edtani a Z-Wave JS b\u0151v\u00edtm\u00e9nyt.", "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "discovery_requires_supervisor": "A felfedez\u00e9shez a fel\u00fcgyel\u0151re van sz\u00fcks\u00e9g.", "not_zwave_device": "A felfedezett eszk\u00f6z nem Z-Wave eszk\u00f6z." @@ -46,8 +46,8 @@ "data": { "use_addon": "Haszn\u00e1ld a Z-Wave JS Supervisor b\u0151v\u00edtm\u00e9nyt" }, - "description": "Szeretn\u00e9d haszn\u00e1lni az Z-Wave JS Supervisor b\u0151v\u00edtm\u00e9nyt?", - "title": "V\u00e1laszd ki a csatlakoz\u00e1si m\u00f3dot" + "description": "Szeretn\u00e9 haszn\u00e1lni az Z-Wave JS Supervisor b\u0151v\u00edtm\u00e9nyt?", + "title": "V\u00e1lassza ki a csatlakoz\u00e1si m\u00f3dot" }, "start_addon": { "title": "Indul a Z-Wave JS b\u0151v\u00edtm\u00e9ny." @@ -58,6 +58,15 @@ } }, "device_automation": { + "action_type": { + "clear_lock_usercode": "{entity_name} felhaszn\u00e1l\u00f3i k\u00f3dj\u00e1nak t\u00f6rl\u00e9se", + "ping": "Eszk\u00f6z pinget\u00e9se", + "refresh_value": "{entity_name} \u00e9rt\u00e9keinek friss\u00edt\u00e9se", + "reset_meter": "{subtype} m\u00e9r\u00e9sek alaphelyzetbe \u00e1ll\u00edt\u00e1sa", + "set_config_parameter": "{subtype} konfigur\u00e1ci\u00f3s param\u00e9ter \u00e9rt\u00e9k\u00e9nek be\u00e1ll\u00edt\u00e1sa", + "set_lock_usercode": "{entity_name} felhaszn\u00e1l\u00f3i k\u00f3dj\u00e1nak be\u00e1ll\u00edt\u00e1sa", + "set_value": "Z-Wave \u00e9rt\u00e9k be\u00e1ll\u00edt\u00e1sa" + }, "condition_type": { "config_parameter": "Konfigur\u00e1lja a(z) {subtype} param\u00e9ter \u00e9rt\u00e9k\u00e9t", "node_status": "Csom\u00f3pont \u00e1llapota", @@ -117,7 +126,7 @@ "use_addon": "Haszn\u00e1lja a Z-Wave JS Supervisor b\u0151v\u00edtm\u00e9nyt" }, "description": "Szeretn\u00e9 haszn\u00e1lni a Z-Wave JS Supervisor b\u0151v\u00edtm\u00e9nyt?", - "title": "V\u00e1laszd ki a csatlakoz\u00e1si m\u00f3dot" + "title": "V\u00e1lassza ki a csatlakoz\u00e1si m\u00f3dot" }, "start_addon": { "title": "Indul a Z-Wave JS b\u0151v\u00edtm\u00e9ny." diff --git a/homeassistant/components/zwave_js/translations/id.json b/homeassistant/components/zwave_js/translations/id.json index 61ea6762c7d..2004be7238f 100644 --- a/homeassistant/components/zwave_js/translations/id.json +++ b/homeassistant/components/zwave_js/translations/id.json @@ -8,7 +8,9 @@ "addon_start_failed": "Gagal memulai add-on Z-Wave JS.", "already_configured": "Perangkat sudah dikonfigurasi", "already_in_progress": "Alur konfigurasi sedang berlangsung", - "cannot_connect": "Gagal terhubung" + "cannot_connect": "Gagal terhubung", + "discovery_requires_supervisor": "Fitur penemuan membutuhkan supervisor.", + "not_zwave_device": "Perangkat yang ditemukan bukanperangkat Z-Wave." }, "error": { "addon_start_failed": "Gagal memulai add-on Z-Wave JS. Periksa konfigurasi.", @@ -16,6 +18,7 @@ "invalid_ws_url": "URL websocket tidak valid", "unknown": "Kesalahan yang tidak diharapkan" }, + "flow_title": "{name}", "progress": { "install_addon": "Harap tunggu hingga penginstalan add-on Z-Wave JS selesai. Ini bisa memakan waktu beberapa saat.", "start_addon": "Harap tunggu hingga add-on Z-Wave JS selesai. Ini mungkin perlu waktu beberapa saat." @@ -51,6 +54,16 @@ } } }, + "device_automation": { + "condition_type": { + "config_parameter": "Nilai parameter konfigurasi {subtype}", + "node_status": "Status node", + "value": "Nilai saat ini dari Nilai Z-Wave" + }, + "trigger_type": { + "state.node_status": "Status node berubah" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "Gagal mendapatkan info penemuan add-on Z-Wave JS.", diff --git a/homeassistant/components/zwave_js/translations/it.json b/homeassistant/components/zwave_js/translations/it.json index 165849e9387..af3416ed9a9 100644 --- a/homeassistant/components/zwave_js/translations/it.json +++ b/homeassistant/components/zwave_js/translations/it.json @@ -27,6 +27,10 @@ "configure_addon": { "data": { "network_key": "Chiave di rete", + "s0_legacy_key": "Chiave S0 (Obsoleta)", + "s2_access_control_key": "Chiave di controllo di accesso S2", + "s2_authenticated_key": "Chiave S2 autenticata", + "s2_unauthenticated_key": "Chiave S2 non autenticata", "usb_path": "Percorso del dispositivo USB" }, "title": "Accedi alla configurazione del componente aggiuntivo Z-Wave JS" @@ -58,6 +62,15 @@ } }, "device_automation": { + "action_type": { + "clear_lock_usercode": "Cancella codice utente su {entity_name}", + "ping": "Dispositivo ping", + "refresh_value": "Aggiorna il/i valore/i per {entity_name}", + "reset_meter": "Azzerare i contatori su {subtype}", + "set_config_parameter": "Imposta il valore del parametro di configurazione {subtype}", + "set_lock_usercode": "Imposta un codice utente su {entity_name}", + "set_value": "Imposta un valore Z-Wave" + }, "condition_type": { "config_parameter": "Valore del parametro di configurazione {subtype}", "node_status": "Stato del nodo", @@ -100,6 +113,10 @@ "emulate_hardware": "Emulare l'hardware", "log_level": "Livello di registro", "network_key": "Chiave di rete", + "s0_legacy_key": "Chiave S0 (Obsoleta)", + "s2_access_control_key": "Chiave di controllo di accesso S2", + "s2_authenticated_key": "Chiave S2 autenticata", + "s2_unauthenticated_key": "Chiave S2 non autenticata", "usb_path": "Percorso del dispositivo USB" }, "title": "Entra nella configurazione del componente aggiuntivo Z-Wave JS" diff --git a/homeassistant/components/zwave_js/translations/nl.json b/homeassistant/components/zwave_js/translations/nl.json index 23d185f1ded..b7a3a68fe6b 100644 --- a/homeassistant/components/zwave_js/translations/nl.json +++ b/homeassistant/components/zwave_js/translations/nl.json @@ -53,11 +53,20 @@ "title": "De add-on Z-Wave JS wordt gestart." }, "usb_confirm": { - "description": "Wilt u {naam} instellen met de Z-Wave JS add-on?" + "description": "Wilt u {name} instellen met de Z-Wave JS add-on?" } } }, "device_automation": { + "action_type": { + "clear_lock_usercode": "Wis gebruikerscode van {entity_name}", + "ping": "Ping apparaat", + "refresh_value": "Ververs de waarde(s) voor {entity_name}", + "reset_meter": "Reset meters op {subtype}", + "set_config_parameter": "Stel waarde in voor configuratieparameter {subtype}", + "set_lock_usercode": "Stel gebruikerscode in voor {entity_name}", + "set_value": "Waarde van een Z-Wave waarde instellen" + }, "condition_type": { "config_parameter": "Config parameter {subtype} waarde", "node_status": "Knooppuntstatus", diff --git a/homeassistant/components/zwave_js/translations/no.json b/homeassistant/components/zwave_js/translations/no.json index b69b1cb4f7a..9ddf12a3b85 100644 --- a/homeassistant/components/zwave_js/translations/no.json +++ b/homeassistant/components/zwave_js/translations/no.json @@ -58,6 +58,15 @@ } }, "device_automation": { + "action_type": { + "clear_lock_usercode": "Fjern brukerkoden p\u00e5 {entity_name}", + "ping": "Ping -enhet", + "refresh_value": "Oppdater verdien (e) for {entity_name}", + "reset_meter": "Tilbakestill m\u00e5lere p\u00e5 {subtype}", + "set_config_parameter": "Angi verdien til konfigurasjonsparameteren {subtype}", + "set_lock_usercode": "Angi en brukerkode p\u00e5 {entity_name}", + "set_value": "Angi verdien for en Z-Wave-verdi" + }, "condition_type": { "config_parameter": "Konfigurer parameter {subtype} verdi", "node_status": "Nodestatus", diff --git a/homeassistant/components/zwave_js/translations/ru.json b/homeassistant/components/zwave_js/translations/ru.json index 994bfb54cfc..9ae79edb32d 100644 --- a/homeassistant/components/zwave_js/translations/ru.json +++ b/homeassistant/components/zwave_js/translations/ru.json @@ -27,6 +27,10 @@ "configure_addon": { "data": { "network_key": "\u041a\u043b\u044e\u0447 \u0441\u0435\u0442\u0438", + "s0_legacy_key": "\u041a\u043b\u044e\u0447 S0 (\u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u0439)", + "s2_access_control_key": "\u041a\u043b\u044e\u0447 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u0430 S2", + "s2_authenticated_key": "\u041a\u043b\u044e\u0447 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 S2", + "s2_unauthenticated_key": "\u041a\u043b\u044e\u0447 \u0431\u0435\u0437 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 S2", "usb_path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" }, "title": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS" @@ -58,6 +62,15 @@ } }, "device_automation": { + "action_type": { + "clear_lock_usercode": "\u041e\u0447\u0438\u0441\u0442\u0438\u0442\u044c \u043a\u043e\u0434 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043d\u0430 {entity_name}", + "ping": "\u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u0441\u0432\u044f\u0437\u044c \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c", + "refresh_value": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0434\u043b\u044f {entity_name}", + "reset_meter": "\u0421\u0431\u0440\u043e\u0441\u0438\u0442\u044c \u0441\u0447\u0435\u0442\u0447\u0438\u043a\u0438 \u043d\u0430 {subtype}", + "set_config_parameter": "\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 {subtype}", + "set_lock_usercode": "\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u043a\u043e\u0434 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043d\u0430 {entity_name}", + "set_value": "\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 Z-Wave Value" + }, "condition_type": { "config_parameter": "\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 {subtype}", "node_status": "\u0421\u0442\u0430\u0442\u0443\u0441 \u0443\u0437\u043b\u0430", @@ -100,6 +113,10 @@ "emulate_hardware": "\u042d\u043c\u0443\u043b\u044f\u0446\u0438\u044f \u043e\u0431\u043e\u0440\u0443\u0434\u043e\u0432\u0430\u043d\u0438\u044f", "log_level": "\u0423\u0440\u043e\u0432\u0435\u043d\u044c \u0436\u0443\u0440\u043d\u0430\u043b\u0430", "network_key": "\u041a\u043b\u044e\u0447 \u0441\u0435\u0442\u0438", + "s0_legacy_key": "\u041a\u043b\u044e\u0447 S0 (\u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u0439)", + "s2_access_control_key": "\u041a\u043b\u044e\u0447 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u0430 S2", + "s2_authenticated_key": "\u041a\u043b\u044e\u0447 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 S2", + "s2_unauthenticated_key": "\u041a\u043b\u044e\u0447 \u0431\u0435\u0437 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 S2", "usb_path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" }, "title": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS" diff --git a/homeassistant/components/zwave_js/translations/zh-Hant.json b/homeassistant/components/zwave_js/translations/zh-Hant.json index e9038ed9a00..7b495ed0ca0 100644 --- a/homeassistant/components/zwave_js/translations/zh-Hant.json +++ b/homeassistant/components/zwave_js/translations/zh-Hant.json @@ -27,6 +27,10 @@ "configure_addon": { "data": { "network_key": "\u7db2\u8def\u5bc6\u9470", + "s0_legacy_key": "S0 \u5bc6\u9470\uff08\u820a\u7248\uff09", + "s2_access_control_key": "S2 \u5b58\u53d6\u63a7\u5236\u5bc6\u9470", + "s2_authenticated_key": "S2 \u9a57\u8b49\u5bc6\u9470", + "s2_unauthenticated_key": "S2 \u672a\u9a57\u8b49\u5bc6\u9470", "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" }, "title": "\u8f38\u5165 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a" @@ -58,6 +62,15 @@ } }, "device_automation": { + "action_type": { + "clear_lock_usercode": "\u6e05\u9664 {entity_name} usercode", + "ping": "Ping \u88dd\u7f6e", + "refresh_value": "\u66f4\u65b0 {entity_name} \u6578\u503c", + "reset_meter": "\u91cd\u7f6e {subtype} \u8a08\u91cf", + "set_config_parameter": "\u8a2d\u5b9a {subtype} \u8a2d\u5b9a\u8b8a\u6578", + "set_lock_usercode": "\u8a2d\u5b9a {entity_name} usercode", + "set_value": "\u8a2d\u5b9a Z-Wave \u6578\u503c" + }, "condition_type": { "config_parameter": "\u8a2d\u5b9a\u53c3\u6578 {subtype} \u6578\u503c", "node_status": "\u7bc0\u9ede\u72c0\u614b", @@ -100,6 +113,10 @@ "emulate_hardware": "\u6a21\u64ec\u786c\u9ad4", "log_level": "\u65e5\u8a8c\u8a18\u9304\u7b49\u7d1a", "network_key": "\u7db2\u8def\u5bc6\u9470", + "s0_legacy_key": "S0 \u5bc6\u9470\uff08\u820a\u7248\uff09", + "s2_access_control_key": "S2 \u5b58\u53d6\u63a7\u5236\u5bc6\u9470", + "s2_authenticated_key": "S2 \u9a57\u8b49\u5bc6\u9470", + "s2_unauthenticated_key": "S2 \u672a\u9a57\u8b49\u5bc6\u9470", "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" }, "title": "\u8f38\u5165 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a" From 62f1a169182008eda4ee1e0ddce544b96ae7ae69 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sat, 2 Oct 2021 16:31:23 +0200 Subject: [PATCH 0041/1038] Add sleep_period to log for easier debugging (#56949) --- homeassistant/components/shelly/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index ad0ad5f4387..b0df4d4cb7f 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -320,9 +320,11 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): async def _async_update_data(self) -> None: """Fetch data.""" - if self.entry.data.get("sleep_period"): + if sleep_period := self.entry.data.get("sleep_period"): # Sleeping device, no point polling it, just mark it unavailable - raise update_coordinator.UpdateFailed("Sleeping device did not update") + raise update_coordinator.UpdateFailed( + f"Sleeping device did not update within {sleep_period} seconds interval" + ) _LOGGER.debug("Polling Shelly Block Device - %s", self.name) try: From 1d00bc8a62338c3df3d9151e0b57a49503ae7f49 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 2 Oct 2021 16:46:57 +0200 Subject: [PATCH 0042/1038] Use NamedTuple - iqvia Rating (#56943) --- homeassistant/components/iqvia/sensor.py | 30 ++++++++++++++---------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index adf53a9cc05..ab311800bdf 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations from statistics import mean +from typing import NamedTuple import numpy as np @@ -53,12 +54,21 @@ API_CATEGORY_MAPPING = { TYPE_DISEASE_TODAY: TYPE_DISEASE_INDEX, } -RATING_MAPPING = [ - {"label": "Low", "minimum": 0.0, "maximum": 2.4}, - {"label": "Low/Medium", "minimum": 2.5, "maximum": 4.8}, - {"label": "Medium", "minimum": 4.9, "maximum": 7.2}, - {"label": "Medium/High", "minimum": 7.3, "maximum": 9.6}, - {"label": "High", "minimum": 9.7, "maximum": 12}, + +class Rating(NamedTuple): + """Assign label to value range.""" + + label: str + minimum: float + maximum: float + + +RATING_MAPPING: list[Rating] = [ + Rating(label="Low", minimum=0.0, maximum=2.4), + Rating(label="Low/Medium", minimum=2.5, maximum=4.8), + Rating(label="Medium", minimum=4.9, maximum=7.2), + Rating(label="Medium/High", minimum=7.3, maximum=9.6), + Rating(label="High", minimum=9.7, maximum=12), ] @@ -182,9 +192,7 @@ class ForecastSensor(IQVIAEntity, SensorEntity): indices = [p["Index"] for p in data["periods"]] average = round(mean(indices), 1) [rating] = [ - i["label"] - for i in RATING_MAPPING - if i["minimum"] <= average <= i["maximum"] + i.label for i in RATING_MAPPING if i.minimum <= average <= i.maximum ] self._attr_native_value = average @@ -247,9 +255,7 @@ class IndexSensor(IQVIAEntity, SensorEntity): return [rating] = [ - i["label"] - for i in RATING_MAPPING - if i["minimum"] <= period["Index"] <= i["maximum"] + i.label for i in RATING_MAPPING if i.minimum <= period["Index"] <= i.maximum ] self._attr_extra_state_attributes.update( From 2874ca2e08fa190885372a66b434a84ccb540134 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Sat, 2 Oct 2021 16:55:16 +0200 Subject: [PATCH 0043/1038] Log when Nanoleaf is unavailable (#56921) --- homeassistant/components/nanoleaf/light.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py index 414b2079485..b1d206bd4dc 100644 --- a/homeassistant/components/nanoleaf/light.py +++ b/homeassistant/components/nanoleaf/light.py @@ -1,6 +1,7 @@ """Support for Nanoleaf Lights.""" from __future__ import annotations +import logging import math from typing import Any @@ -46,6 +47,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) +_LOGGER = logging.getLogger(__name__) + async def async_setup_platform( hass: HomeAssistant, @@ -187,6 +190,10 @@ class NanoleafLight(LightEntity): try: await self._nanoleaf.get_info() except Unavailable: + if self.available: + _LOGGER.warning("Could not connect to %s", self.name) self._attr_available = False return + if not self.available: + _LOGGER.info("Fetching %s data recovered", self.name) self._attr_available = True From 80c97a2416620fe022eb8327d92180ef033f6b99 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 2 Oct 2021 10:45:33 -0600 Subject: [PATCH 0044/1038] Remove injected logger in Ambient PWS and OpenUV (#56920) * Remove injected log in OpenUV * Add Ambient --- homeassistant/components/ambient_station/__init__.py | 1 - homeassistant/components/openuv/__init__.py | 1 - 2 files changed, 2 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 1f1b21b4346..482f526430a 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -75,7 +75,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b config_entry.data[CONF_API_KEY], config_entry.data[CONF_APP_KEY], session=session, - logger=LOGGER, ), ) hass.loop.create_task(ambient.ws_connect()) diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index 5d165c498e2..d14760d6cb1 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -66,7 +66,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b config_entry.data.get(CONF_LONGITUDE, hass.config.longitude), altitude=config_entry.data.get(CONF_ELEVATION, hass.config.elevation), session=websession, - logger=LOGGER, ), ) await openuv.async_update() From a95c6b10f7fb483b21f472cc6d39d7dcf6f280c0 Mon Sep 17 00:00:00 2001 From: icemanch Date: Sat, 2 Oct 2021 13:19:36 -0400 Subject: [PATCH 0045/1038] Flux led config flow (#56354) Co-authored-by: Milan Meulemans Co-authored-by: Erik Montnemery Co-authored-by: J. Nick Koston --- .coveragerc | 1 - .strict-typing | 1 + CODEOWNERS | 1 + homeassistant/components/flux_led/__init__.py | 143 +++- .../components/flux_led/config_flow.py | 270 ++++++++ homeassistant/components/flux_led/const.py | 51 ++ homeassistant/components/flux_led/light.py | 488 +++++++------ .../components/flux_led/manifest.json | 23 +- .../components/flux_led/services.yaml | 38 + .../components/flux_led/strings.json | 36 + .../components/flux_led/translations/en.json | 35 + homeassistant/generated/config_flows.py | 1 + homeassistant/generated/dhcp.py | 15 + mypy.ini | 11 + requirements_test_all.txt | 3 + tests/components/flux_led/__init.py__ | 1 + tests/components/flux_led/__init__.py | 57 ++ tests/components/flux_led/test_config_flow.py | 456 ++++++++++++ tests/components/flux_led/test_init.py | 58 ++ tests/components/flux_led/test_light.py | 654 ++++++++++++++++++ 20 files changed, 2142 insertions(+), 201 deletions(-) create mode 100644 homeassistant/components/flux_led/config_flow.py create mode 100644 homeassistant/components/flux_led/const.py create mode 100644 homeassistant/components/flux_led/services.yaml create mode 100644 homeassistant/components/flux_led/strings.json create mode 100644 homeassistant/components/flux_led/translations/en.json create mode 100644 tests/components/flux_led/__init.py__ create mode 100644 tests/components/flux_led/__init__.py create mode 100644 tests/components/flux_led/test_config_flow.py create mode 100644 tests/components/flux_led/test_init.py create mode 100644 tests/components/flux_led/test_light.py diff --git a/.coveragerc b/.coveragerc index a5d7eec9115..1a716ef4e5c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -343,7 +343,6 @@ omit = homeassistant/components/flume/sensor.py homeassistant/components/flunearyou/__init__.py homeassistant/components/flunearyou/sensor.py - homeassistant/components/flux_led/light.py homeassistant/components/folder/sensor.py homeassistant/components/folder_watcher/* homeassistant/components/foobot/sensor.py diff --git a/.strict-typing b/.strict-typing index 50841c49f2f..8e957faabf0 100644 --- a/.strict-typing +++ b/.strict-typing @@ -41,6 +41,7 @@ homeassistant.components.energy.* homeassistant.components.fastdotcom.* homeassistant.components.fitbit.* homeassistant.components.flunearyou.* +homeassistant.components.flux_led.* homeassistant.components.forecast_solar.* homeassistant.components.fritzbox.* homeassistant.components.frontend.* diff --git a/CODEOWNERS b/CODEOWNERS index 70fb56f73f4..58225f4e36a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -174,6 +174,7 @@ homeassistant/components/flo/* @dmulcahey homeassistant/components/flock/* @fabaff homeassistant/components/flume/* @ChrisMandich @bdraco homeassistant/components/flunearyou/* @bachya +homeassistant/components/flux_led/* @icemanch homeassistant/components/forecast_solar/* @klaasnicolaas @frenck homeassistant/components/forked_daapd/* @uvjustin homeassistant/components/fortios/* @kimfrellsen diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index 572d6e3c983..df7334a8ebc 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -1 +1,142 @@ -"""The flux_led component.""" +"""The Flux LED/MagicLight integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Any, Final + +from flux_led import BulbScanner, WifiLedBulb + +from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + DISCOVER_SCAN_TIMEOUT, + DOMAIN, + FLUX_LED_DISCOVERY, + FLUX_LED_EXCEPTIONS, + STARTUP_SCAN_TIMEOUT, +) + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS: Final = ["light"] +DISCOVERY_INTERVAL: Final = timedelta(minutes=15) +REQUEST_REFRESH_DELAY: Final = 0.65 + + +async def async_wifi_bulb_for_host(hass: HomeAssistant, host: str) -> WifiLedBulb: + """Create a WifiLedBulb from a host.""" + return await hass.async_add_executor_job(WifiLedBulb, host) + + +async def async_discover_devices( + hass: HomeAssistant, timeout: int +) -> list[dict[str, str]]: + """Discover flux led devices.""" + + def _scan_with_timeout() -> list[dict[str, str]]: + scanner = BulbScanner() + discovered: list[dict[str, str]] = scanner.scan(timeout=timeout) + return discovered + + return await hass.async_add_executor_job(_scan_with_timeout) + + +@callback +def async_trigger_discovery( + hass: HomeAssistant, + discovered_devices: list[dict[str, Any]], +) -> 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_DISCOVERY}, + data=device, + ) + ) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the flux_led component.""" + domain_data = hass.data[DOMAIN] = {} + domain_data[FLUX_LED_DISCOVERY] = await async_discover_devices( + hass, STARTUP_SCAN_TIMEOUT + ) + + async def _async_discovery(*_: Any) -> None: + async_trigger_discovery( + hass, await async_discover_devices(hass, DISCOVER_SCAN_TIMEOUT) + ) + + async_trigger_discovery(hass, domain_data[FLUX_LED_DISCOVERY]) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _async_discovery) + async_track_time_interval(hass, _async_discovery, DISCOVERY_INTERVAL) + return True + + +async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Flux LED/MagicLight from a config entry.""" + + coordinator = FluxLedUpdateCoordinator(hass, entry.data[CONF_HOST]) + await coordinator.async_config_entry_first_refresh() + hass.data[DOMAIN][entry.entry_id] = coordinator + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(async_update_listener)) + + 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 FluxLedUpdateCoordinator(DataUpdateCoordinator): + """DataUpdateCoordinator to gather data for a specific flux_led device.""" + + def __init__( + self, + hass: HomeAssistant, + host: str, + ) -> None: + """Initialize DataUpdateCoordinator to gather data for specific device.""" + self.host = host + self.device: WifiLedBulb | None = None + update_interval = timedelta(seconds=5) + super().__init__( + hass, + _LOGGER, + name=host, + update_interval=update_interval, + # We don't want an immediate refresh since the device + # takes a moment to reflect the state change + request_refresh_debouncer=Debouncer( + hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False + ), + ) + + async def _async_update_data(self) -> None: + """Fetch all device and sensor data from api.""" + try: + if not self.device: + self.device = await async_wifi_bulb_for_host(self.hass, self.host) + else: + await self.hass.async_add_executor_job(self.device.update_state) + except FLUX_LED_EXCEPTIONS as ex: + raise UpdateFailed(ex) from ex diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py new file mode 100644 index 00000000000..206c8f91433 --- /dev/null +++ b/homeassistant/components/flux_led/config_flow.py @@ -0,0 +1,270 @@ +"""Config flow for Flux LED/MagicLight.""" +from __future__ import annotations + +import logging +from typing import Any, Final + +from flux_led import WifiLedBulb +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_MODE, CONF_NAME, CONF_PROTOCOL +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import device_registry as dr +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import DiscoveryInfoType + +from . import async_discover_devices, async_wifi_bulb_for_host +from .const import ( + CONF_CUSTOM_EFFECT_COLORS, + CONF_CUSTOM_EFFECT_SPEED_PCT, + CONF_CUSTOM_EFFECT_TRANSITION, + DEFAULT_EFFECT_SPEED, + DISCOVER_SCAN_TIMEOUT, + DOMAIN, + FLUX_HOST, + FLUX_LED_EXCEPTIONS, + FLUX_MAC, + FLUX_MODEL, + MODE_AUTO, + MODE_RGB, + MODE_RGBW, + MODE_WHITE, + TRANSITION_GRADUAL, + TRANSITION_JUMP, + TRANSITION_STROBE, +) + +CONF_DEVICE: Final = "device" + + +_LOGGER = logging.getLogger(__name__) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for FluxLED/MagicHome Integration.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovered_devices: dict[str, dict[str, Any]] = {} + self._discovered_device: dict[str, Any] = {} + + @staticmethod + @callback + def async_get_options_flow(config_entry: config_entries.ConfigEntry) -> OptionsFlow: + """Get the options flow for the Flux LED component.""" + return OptionsFlow(config_entry) + + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: + """Handle configuration via YAML import.""" + _LOGGER.debug("Importing configuration from YAML for flux_led") + host = user_input[CONF_HOST] + self._async_abort_entries_match({CONF_HOST: host}) + if mac := user_input[CONF_MAC]: + await self.async_set_unique_id(dr.format_mac(mac), raise_on_progress=False) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + return self.async_create_entry( + title=user_input[CONF_NAME], + data={ + CONF_HOST: host, + CONF_NAME: user_input[CONF_NAME], + CONF_PROTOCOL: user_input.get(CONF_PROTOCOL), + }, + options={ + CONF_MODE: user_input[CONF_MODE], + CONF_CUSTOM_EFFECT_COLORS: user_input[CONF_CUSTOM_EFFECT_COLORS], + CONF_CUSTOM_EFFECT_SPEED_PCT: user_input[CONF_CUSTOM_EFFECT_SPEED_PCT], + CONF_CUSTOM_EFFECT_TRANSITION: user_input[ + CONF_CUSTOM_EFFECT_TRANSITION + ], + }, + ) + + async def async_step_dhcp(self, discovery_info: DiscoveryInfoType) -> FlowResult: + """Handle discovery via dhcp.""" + self._discovered_device = { + FLUX_HOST: discovery_info[IP_ADDRESS], + FLUX_MODEL: discovery_info[HOSTNAME], + FLUX_MAC: discovery_info[MAC_ADDRESS].replace(":", ""), + } + return await self._async_handle_discovery() + + async def async_step_discovery( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: + """Handle discovery.""" + self._discovered_device = discovery_info + return await self._async_handle_discovery() + + async def _async_handle_discovery(self) -> FlowResult: + """Handle any discovery.""" + device = self._discovered_device + mac = dr.format_mac(device[FLUX_MAC]) + host = device[FLUX_HOST] + await self.async_set_unique_id(mac) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + for entry in self._async_current_entries(include_ignore=False): + if entry.data[CONF_HOST] == host and not entry.unique_id: + name = f"{device[FLUX_MODEL]} {device[FLUX_MAC]}" + self.hass.config_entries.async_update_entry( + entry, + data={**entry.data, CONF_NAME: name}, + title=name, + unique_id=mac, + ) + return self.async_abort(reason="already_configured") + self.context[CONF_HOST] = host + for progress in self._async_in_progress(): + if progress.get("context", {}).get(CONF_HOST) == host: + return self.async_abort(reason="already_in_progress") + 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: + return self._async_create_entry_from_device(self._discovered_device) + + self._set_confirm_only() + placeholders = self._discovered_device + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="discovery_confirm", description_placeholders=placeholders + ) + + @callback + def _async_create_entry_from_device(self, device: dict[str, Any]) -> FlowResult: + """Create a config entry from a device.""" + self._async_abort_entries_match({CONF_HOST: device[FLUX_HOST]}) + if device.get(FLUX_MAC): + name = f"{device[FLUX_MODEL]} {device[FLUX_MAC]}" + else: + name = device[FLUX_HOST] + return self.async_create_entry( + title=name, + data={ + CONF_HOST: device[FLUX_HOST], + CONF_NAME: name, + }, + ) + + 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: + if not (host := user_input[CONF_HOST]): + return await self.async_step_pick_device() + try: + await self._async_try_connect(host) + except FLUX_LED_EXCEPTIONS: + errors["base"] = "cannot_connect" + else: + return self._async_create_entry_from_device( + {FLUX_MAC: None, FLUX_MODEL: None, FLUX_HOST: host} + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Optional(CONF_HOST, default=""): str}), + errors=errors, + ) + + 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: + mac = user_input[CONF_DEVICE] + await self.async_set_unique_id(mac, raise_on_progress=False) + return self._async_create_entry_from_device(self._discovered_devices[mac]) + + current_unique_ids = self._async_current_ids() + current_hosts = { + entry.data[CONF_HOST] + for entry in self._async_current_entries(include_ignore=False) + } + discovered_devices = await async_discover_devices( + self.hass, DISCOVER_SCAN_TIMEOUT + ) + self._discovered_devices = { + dr.format_mac(device[FLUX_MAC]): device for device in discovered_devices + } + devices_name = { + mac: f"{device[FLUX_MODEL]} {mac} ({device[FLUX_HOST]})" + for mac, device in self._discovered_devices.items() + if mac not in current_unique_ids and device[FLUX_HOST] not in current_hosts + } + # Check if there is at least one device + if not devices_name: + return self.async_abort(reason="no_devices_found") + return self.async_show_form( + step_id="pick_device", + data_schema=vol.Schema({vol.Required(CONF_DEVICE): vol.In(devices_name)}), + ) + + async def _async_try_connect(self, host: str) -> WifiLedBulb: + """Try to connect.""" + self._async_abort_entries_match({CONF_HOST: host}) + return await async_wifi_bulb_for_host(self.hass, host) + + +class OptionsFlow(config_entries.OptionsFlow): + """Handle flux_led options.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize the flux_led options flow.""" + self._config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Configure the options.""" + errors: dict[str, str] = {} + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + options = self._config_entry.options + options_schema = vol.Schema( + { + vol.Required( + CONF_MODE, default=options.get(CONF_MODE, MODE_AUTO) + ): vol.All( + cv.string, + vol.In( + [ + MODE_AUTO, + MODE_RGBW, + MODE_RGB, + MODE_WHITE, + ] + ), + ), + vol.Optional( + CONF_CUSTOM_EFFECT_COLORS, + default=options.get(CONF_CUSTOM_EFFECT_COLORS, ""), + ): str, + vol.Optional( + CONF_CUSTOM_EFFECT_SPEED_PCT, + default=options.get( + CONF_CUSTOM_EFFECT_SPEED_PCT, DEFAULT_EFFECT_SPEED + ), + ): vol.All(vol.Coerce(int), vol.Range(min=1, max=100)), + vol.Optional( + CONF_CUSTOM_EFFECT_TRANSITION, + default=options.get( + CONF_CUSTOM_EFFECT_TRANSITION, TRANSITION_GRADUAL + ), + ): vol.In([TRANSITION_GRADUAL, TRANSITION_JUMP, TRANSITION_STROBE]), + } + ) + + return self.async_show_form( + step_id="init", data_schema=options_schema, errors=errors + ) diff --git a/homeassistant/components/flux_led/const.py b/homeassistant/components/flux_led/const.py new file mode 100644 index 00000000000..6b2c4a8dace --- /dev/null +++ b/homeassistant/components/flux_led/const.py @@ -0,0 +1,51 @@ +"""Constants of the FluxLed/MagicHome Integration.""" + +import socket +from typing import Final + +DOMAIN: Final = "flux_led" + +API: Final = "flux_api" + + +CONF_AUTOMATIC_ADD: Final = "automatic_add" +DEFAULT_NETWORK_SCAN_INTERVAL: Final = 120 +DEFAULT_SCAN_INTERVAL: Final = 5 +DEFAULT_EFFECT_SPEED: Final = 50 + +FLUX_LED_DISCOVERY: Final = "flux_led_discovery" + +FLUX_LED_EXCEPTIONS: Final = (socket.timeout, BrokenPipeError) + +STARTUP_SCAN_TIMEOUT: Final = 5 +DISCOVER_SCAN_TIMEOUT: Final = 10 + +CONF_DEVICES: Final = "devices" +CONF_CUSTOM_EFFECT: Final = "custom_effect" +CONF_MODEL: Final = "model" + +MODE_AUTO: Final = "auto" +MODE_RGB: Final = "rgb" +MODE_RGBW: Final = "rgbw" + + +# This mode enables white value to be controlled by brightness. +# RGB value is ignored when this mode is specified. +MODE_WHITE: Final = "w" + +TRANSITION_GRADUAL: Final = "gradual" +TRANSITION_JUMP: Final = "jump" +TRANSITION_STROBE: Final = "strobe" + +CONF_COLORS: Final = "colors" +CONF_SPEED_PCT: Final = "speed_pct" +CONF_TRANSITION: Final = "transition" + + +CONF_CUSTOM_EFFECT_COLORS: Final = "custom_effect_colors" +CONF_CUSTOM_EFFECT_SPEED_PCT: Final = "custom_effect_speed_pct" +CONF_CUSTOM_EFFECT_TRANSITION: Final = "custom_effect_transition" + +FLUX_HOST: Final = "ipaddr" +FLUX_MAC: Final = "id" +FLUX_MODEL: Final = "model" diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 2f8d2cc5536..581d5fbaab6 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -1,10 +1,16 @@ -"""Support for Flux lights.""" +"""Support for FluxLED/MagicHome lights.""" +from __future__ import annotations + +import ast +from functools import partial import logging import random +from typing import Any, Final, cast -from flux_led import BulbScanner, WifiLedBulb +from flux_led import WifiLedBulb import voluptuous as vol +from homeassistant import config_entries from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -21,56 +27,84 @@ from homeassistant.components.light import ( SUPPORT_WHITE_VALUE, LightEntity, ) -from homeassistant.const import ATTR_MODE, CONF_DEVICES, CONF_NAME, CONF_PROTOCOL +from homeassistant.const import ( + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODE, + ATTR_MODEL, + ATTR_NAME, + CONF_DEVICES, + CONF_HOST, + CONF_MAC, + CONF_MODE, + CONF_NAME, + CONF_PROTOCOL, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv +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 import homeassistant.util.color as color_util +from . import FluxLedUpdateCoordinator +from .const import ( + CONF_AUTOMATIC_ADD, + CONF_COLORS, + CONF_CUSTOM_EFFECT, + CONF_CUSTOM_EFFECT_COLORS, + CONF_CUSTOM_EFFECT_SPEED_PCT, + CONF_CUSTOM_EFFECT_TRANSITION, + CONF_SPEED_PCT, + CONF_TRANSITION, + DEFAULT_EFFECT_SPEED, + DOMAIN, + FLUX_HOST, + FLUX_LED_DISCOVERY, + FLUX_MAC, + MODE_AUTO, + MODE_RGB, + MODE_RGBW, + MODE_WHITE, + TRANSITION_GRADUAL, + TRANSITION_JUMP, + TRANSITION_STROBE, +) + _LOGGER = logging.getLogger(__name__) -CONF_AUTOMATIC_ADD = "automatic_add" -CONF_CUSTOM_EFFECT = "custom_effect" -CONF_COLORS = "colors" -CONF_SPEED_PCT = "speed_pct" -CONF_TRANSITION = "transition" +SUPPORT_FLUX_LED: Final = SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | SUPPORT_COLOR -DOMAIN = "flux_led" - -SUPPORT_FLUX_LED = SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | SUPPORT_COLOR - -MODE_RGB = "rgb" -MODE_RGBW = "rgbw" - -# This mode enables white value to be controlled by brightness. -# RGB value is ignored when this mode is specified. -MODE_WHITE = "w" # Constant color temp values for 2 flux_led special modes # Warm-white and Cool-white modes -COLOR_TEMP_WARM_VS_COLD_WHITE_CUT_OFF = 285 +COLOR_TEMP_WARM_VS_COLD_WHITE_CUT_OFF: Final = 285 # List of supported effects which aren't already declared in LIGHT -EFFECT_RED_FADE = "red_fade" -EFFECT_GREEN_FADE = "green_fade" -EFFECT_BLUE_FADE = "blue_fade" -EFFECT_YELLOW_FADE = "yellow_fade" -EFFECT_CYAN_FADE = "cyan_fade" -EFFECT_PURPLE_FADE = "purple_fade" -EFFECT_WHITE_FADE = "white_fade" -EFFECT_RED_GREEN_CROSS_FADE = "rg_cross_fade" -EFFECT_RED_BLUE_CROSS_FADE = "rb_cross_fade" -EFFECT_GREEN_BLUE_CROSS_FADE = "gb_cross_fade" -EFFECT_COLORSTROBE = "colorstrobe" -EFFECT_RED_STROBE = "red_strobe" -EFFECT_GREEN_STROBE = "green_strobe" -EFFECT_BLUE_STROBE = "blue_strobe" -EFFECT_YELLOW_STROBE = "yellow_strobe" -EFFECT_CYAN_STROBE = "cyan_strobe" -EFFECT_PURPLE_STROBE = "purple_strobe" -EFFECT_WHITE_STROBE = "white_strobe" -EFFECT_COLORJUMP = "colorjump" -EFFECT_CUSTOM = "custom" +EFFECT_RED_FADE: Final = "red_fade" +EFFECT_GREEN_FADE: Final = "green_fade" +EFFECT_BLUE_FADE: Final = "blue_fade" +EFFECT_YELLOW_FADE: Final = "yellow_fade" +EFFECT_CYAN_FADE: Final = "cyan_fade" +EFFECT_PURPLE_FADE: Final = "purple_fade" +EFFECT_WHITE_FADE: Final = "white_fade" +EFFECT_RED_GREEN_CROSS_FADE: Final = "rg_cross_fade" +EFFECT_RED_BLUE_CROSS_FADE: Final = "rb_cross_fade" +EFFECT_GREEN_BLUE_CROSS_FADE: Final = "gb_cross_fade" +EFFECT_COLORSTROBE: Final = "colorstrobe" +EFFECT_RED_STROBE: Final = "red_strobe" +EFFECT_GREEN_STROBE: Final = "green_strobe" +EFFECT_BLUE_STROBE: Final = "blue_strobe" +EFFECT_YELLOW_STROBE: Final = "yellow_strobe" +EFFECT_CYAN_STROBE: Final = "cyan_strobe" +EFFECT_PURPLE_STROBE: Final = "purple_strobe" +EFFECT_WHITE_STROBE: Final = "white_strobe" +EFFECT_COLORJUMP: Final = "colorjump" +EFFECT_CUSTOM: Final = "custom" -EFFECT_MAP = { +EFFECT_MAP: Final = { EFFECT_COLORLOOP: 0x25, EFFECT_RED_FADE: 0x26, EFFECT_GREEN_FADE: 0x27, @@ -92,39 +126,36 @@ EFFECT_MAP = { EFFECT_WHITE_STROBE: 0x37, EFFECT_COLORJUMP: 0x38, } -EFFECT_CUSTOM_CODE = 0x60 +EFFECT_ID_NAME: Final = {v: k for k, v in EFFECT_MAP.items()} +EFFECT_CUSTOM_CODE: Final = 0x60 -TRANSITION_GRADUAL = "gradual" -TRANSITION_JUMP = "jump" -TRANSITION_STROBE = "strobe" +WHITE_MODES: Final = {MODE_RGBW} -FLUX_EFFECT_LIST = sorted(EFFECT_MAP) + [EFFECT_RANDOM] +FLUX_EFFECT_LIST: Final = sorted(EFFECT_MAP) + [EFFECT_RANDOM] -CUSTOM_EFFECT_SCHEMA = vol.Schema( - { - vol.Required(CONF_COLORS): vol.All( - cv.ensure_list, - vol.Length(min=1, max=16), - [ - vol.All( - vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple) - ) - ], - ), - vol.Optional(CONF_SPEED_PCT, default=50): vol.All( - vol.Range(min=0, max=100), vol.Coerce(int) - ), - vol.Optional(CONF_TRANSITION, default=TRANSITION_GRADUAL): vol.All( - cv.string, vol.In([TRANSITION_GRADUAL, TRANSITION_JUMP, TRANSITION_STROBE]) - ), - } -) +SERVICE_CUSTOM_EFFECT: Final = "set_custom_effect" -DEVICE_SCHEMA = vol.Schema( +CUSTOM_EFFECT_DICT: Final = { + vol.Required(CONF_COLORS): vol.All( + cv.ensure_list, + vol.Length(min=1, max=16), + [vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple))], + ), + vol.Optional(CONF_SPEED_PCT, default=50): vol.All( + vol.Range(min=0, max=100), vol.Coerce(int) + ), + vol.Optional(CONF_TRANSITION, default=TRANSITION_GRADUAL): vol.All( + cv.string, vol.In([TRANSITION_GRADUAL, TRANSITION_JUMP, TRANSITION_STROBE]) + ), +} + +CUSTOM_EFFECT_SCHEMA: Final = vol.Schema(CUSTOM_EFFECT_DICT) + +DEVICE_SCHEMA: Final = vol.Schema( { vol.Optional(CONF_NAME): cv.string, - vol.Optional(ATTR_MODE, default=MODE_RGBW): vol.All( - cv.string, vol.In([MODE_RGBW, MODE_RGB, MODE_WHITE]) + vol.Optional(ATTR_MODE, default=MODE_AUTO): vol.All( + cv.string, vol.In([MODE_AUTO, MODE_RGBW, MODE_RGB, MODE_WHITE]) ), vol.Optional(CONF_PROTOCOL): vol.All(cv.string, vol.In(["ledenet"])), vol.Optional(CONF_CUSTOM_EFFECT): CUSTOM_EFFECT_SCHEMA, @@ -139,160 +170,206 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> bool: + """Set up the flux led platform.""" + domain_data = hass.data[DOMAIN] + discovered_mac_by_host = { + device[FLUX_HOST]: device[FLUX_MAC] + for device in domain_data[FLUX_LED_DISCOVERY] + } + for host, device_config in config.get(CONF_DEVICES, {}).items(): + _LOGGER.warning( + "Configuring flux_led via yaml is deprecated; the configuration for" + " %s has been migrated to a config entry and can be safely removed", + host, + ) + custom_effects = device_config.get(CONF_CUSTOM_EFFECT, {}) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_HOST: host, + CONF_MAC: discovered_mac_by_host.get(host), + CONF_NAME: device_config[CONF_NAME], + CONF_PROTOCOL: device_config.get(CONF_PROTOCOL), + CONF_MODE: device_config.get(ATTR_MODE, MODE_AUTO), + CONF_CUSTOM_EFFECT_COLORS: str(custom_effects.get(CONF_COLORS)), + CONF_CUSTOM_EFFECT_SPEED_PCT: custom_effects.get( + CONF_SPEED_PCT, DEFAULT_EFFECT_SPEED + ), + CONF_CUSTOM_EFFECT_TRANSITION: custom_effects.get( + CONF_TRANSITION, TRANSITION_GRADUAL + ), + }, + ) + ) + return True + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the Flux lights.""" - lights = [] - light_ips = [] + coordinator: FluxLedUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - for ipaddr, device_config in config.get(CONF_DEVICES, {}).items(): - device = {} - device["name"] = device_config[CONF_NAME] - device["ipaddr"] = ipaddr - device[CONF_PROTOCOL] = device_config.get(CONF_PROTOCOL) - device[ATTR_MODE] = device_config[ATTR_MODE] - device[CONF_CUSTOM_EFFECT] = device_config.get(CONF_CUSTOM_EFFECT) - light = FluxLight(device) - lights.append(light) - light_ips.append(ipaddr) + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + SERVICE_CUSTOM_EFFECT, + CUSTOM_EFFECT_DICT, + "set_custom_effect", + ) + options = entry.options - if not config.get(CONF_AUTOMATIC_ADD, False): - add_entities(lights, True) - return + try: + custom_effect_colors = ast.literal_eval( + options.get(CONF_CUSTOM_EFFECT_COLORS) or "[]" + ) + except (ValueError, TypeError, SyntaxError, MemoryError) as ex: + _LOGGER.warning( + "Could not parse custom effect colors for %s: %s", entry.unique_id, ex + ) + custom_effect_colors = [] - # Find the bulbs on the LAN - scanner = BulbScanner() - scanner.scan(timeout=10) - for device in scanner.getBulbInfo(): - ipaddr = device["ipaddr"] - if ipaddr in light_ips: - continue - device["name"] = f"{device['id']} {ipaddr}" - device[ATTR_MODE] = None - device[CONF_PROTOCOL] = None - device[CONF_CUSTOM_EFFECT] = None - light = FluxLight(device) - lights.append(light) - - add_entities(lights, True) + async_add_entities( + [ + FluxLight( + coordinator, + entry.unique_id, + entry.data[CONF_NAME], + options.get(CONF_MODE) or MODE_AUTO, + list(custom_effect_colors), + options.get(CONF_CUSTOM_EFFECT_SPEED_PCT, DEFAULT_EFFECT_SPEED), + options.get(CONF_CUSTOM_EFFECT_TRANSITION, TRANSITION_GRADUAL), + ) + ] + ) -class FluxLight(LightEntity): +class FluxLight(CoordinatorEntity, LightEntity): """Representation of a Flux light.""" - def __init__(self, device): + coordinator: FluxLedUpdateCoordinator + + def __init__( + self, + coordinator: FluxLedUpdateCoordinator, + unique_id: str | None, + name: str, + mode: str, + custom_effect_colors: list[tuple[int, int, int]], + custom_effect_speed_pct: int, + custom_effect_transition: str, + ) -> None: """Initialize the light.""" - self._name = device["name"] - self._ipaddr = device["ipaddr"] - self._protocol = device[CONF_PROTOCOL] - self._mode = device[ATTR_MODE] - self._custom_effect = device[CONF_CUSTOM_EFFECT] - self._bulb = None - self._error_reported = False - - def _connect(self): - """Connect to Flux light.""" - - self._bulb = WifiLedBulb(self._ipaddr, timeout=5) - if self._protocol: - self._bulb.setProtocol(self._protocol) - - # After bulb object is created the status is updated. We can - # now set the correct mode if it was not explicitly defined. - if not self._mode: - if self._bulb.rgbwcapable: - self._mode = MODE_RGBW - else: - self._mode = MODE_RGB - - def _disconnect(self): - """Disconnect from Flux light.""" - self._bulb = None + super().__init__(coordinator) + self._bulb: WifiLedBulb = coordinator.device + self._name = name + self._unique_id = unique_id + self._ip_address = coordinator.host + self._mode = mode + self._custom_effect_colors = custom_effect_colors + self._custom_effect_speed_pct = custom_effect_speed_pct + self._custom_effect_transition = custom_effect_transition @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._bulb is not None + def unique_id(self) -> str | None: + """Return the unique ID of the light.""" + return self._unique_id @property - def name(self): - """Return the name of the device if any.""" + def name(self) -> str: + """Return the name of the device.""" return self._name @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" - return self._bulb.isOn() + return cast(bool, self._bulb.is_on) @property - def brightness(self): + def brightness(self) -> int: """Return the brightness of this light between 0..255.""" if self._mode == MODE_WHITE: return self.white_value - - return self._bulb.brightness + return cast(int, self._bulb.brightness) @property - def hs_color(self): + def hs_color(self) -> tuple[float, float] | None: """Return the color property.""" return color_util.color_RGB_to_hs(*self._bulb.getRgb()) @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" - if self._mode == MODE_RGBW: - return SUPPORT_FLUX_LED | SUPPORT_WHITE_VALUE | SUPPORT_COLOR_TEMP - if self._mode == MODE_WHITE: return SUPPORT_BRIGHTNESS - + if self._mode in WHITE_MODES: + return SUPPORT_FLUX_LED | SUPPORT_WHITE_VALUE | SUPPORT_COLOR_TEMP return SUPPORT_FLUX_LED @property - def white_value(self): + def white_value(self) -> int: """Return the white value of this light between 0..255.""" - return self._bulb.getRgbw()[3] + return cast(int, self._bulb.getRgbw()[3]) @property - def effect_list(self): + def effect_list(self) -> list[str]: """Return the list of supported effects.""" - if self._custom_effect: + if self._custom_effect_colors: return FLUX_EFFECT_LIST + [EFFECT_CUSTOM] - return FLUX_EFFECT_LIST @property - def effect(self): + def effect(self) -> str | None: """Return the current effect.""" - current_mode = self._bulb.raw_state[3] - - if current_mode == EFFECT_CUSTOM_CODE: + if (current_mode := self._bulb.raw_state[3]) == EFFECT_CUSTOM_CODE: return EFFECT_CUSTOM + return EFFECT_ID_NAME.get(current_mode) - for effect, code in EFFECT_MAP.items(): - if current_mode == code: - return effect + @property + def extra_state_attributes(self) -> dict[str, str]: + """Return the attributes.""" + return { + "ip_address": self._ip_address, + } - return None + @property + def device_info(self) -> DeviceInfo: + """Return the device information.""" + assert self._unique_id is not None + return { + ATTR_IDENTIFIERS: {(DOMAIN, self._unique_id)}, + ATTR_NAME: self._name, + ATTR_MANUFACTURER: "FluxLED/Magic Home", + ATTR_MODEL: "LED Lights", + } - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the specified or all lights on.""" + await self.hass.async_add_executor_job(partial(self._turn_on, **kwargs)) + await self.coordinator.async_request_refresh() + + def _turn_on(self, **kwargs: Any) -> None: """Turn the specified or all lights on.""" if not self.is_on: self._bulb.turnOn() - hs_color = kwargs.get(ATTR_HS_COLOR) - - if hs_color: - rgb = color_util.color_hs_to_RGB(*hs_color) + if hs_color := kwargs.get(ATTR_HS_COLOR): + rgb: tuple[int, int, int] | None = color_util.color_hs_to_RGB(*hs_color) else: rgb = None brightness = kwargs.get(ATTR_BRIGHTNESS) - effect = kwargs.get(ATTR_EFFECT) - white = kwargs.get(ATTR_WHITE_VALUE) - color_temp = kwargs.get(ATTR_COLOR_TEMP) - # handle special modes - if color_temp is not None: + if (color_temp := kwargs.get(ATTR_COLOR_TEMP)) is not None: if brightness is None: brightness = self.brightness if color_temp > COLOR_TEMP_WARM_VS_COLD_WHITE_CUT_OFF: @@ -301,6 +378,8 @@ class FluxLight(LightEntity): self._bulb.setRgbw(w2=brightness) return + white = kwargs.get(ATTR_WHITE_VALUE) + effect = kwargs.get(ATTR_EFFECT) # Show warning if effect set with rgb, brightness, or white level if effect and (brightness or white or rgb): _LOGGER.warning( @@ -315,12 +394,13 @@ class FluxLight(LightEntity): ) return + # Custom effect if effect == EFFECT_CUSTOM: - if self._custom_effect: + if self._custom_effect_colors: self._bulb.setCustomPattern( - self._custom_effect[CONF_COLORS], - self._custom_effect[CONF_SPEED_PCT], - self._custom_effect[CONF_TRANSITION], + self._custom_effect_colors, + self._custom_effect_speed_pct, + self._custom_effect_transition, ) return @@ -333,42 +413,58 @@ class FluxLight(LightEntity): if brightness is None: brightness = self.brightness + # handle W only mode (use brightness instead of white value) + if self._mode == MODE_WHITE: + self._bulb.setRgbw(0, 0, 0, w=brightness) + return + + if white is None and self._mode in WHITE_MODES: + white = self.white_value + # Preserve color on brightness/white level change if rgb is None: rgb = self._bulb.getRgb() - if white is None and self._mode == MODE_RGBW: - white = self.white_value - - # handle W only mode (use brightness instead of white value) - if self._mode == MODE_WHITE: - self._bulb.setRgbw(0, 0, 0, w=brightness) - # handle RGBW mode - elif self._mode == MODE_RGBW: + if self._mode == MODE_RGBW: self._bulb.setRgbw(*tuple(rgb), w=white, brightness=brightness) + return # handle RGB mode - else: - self._bulb.setRgb(*tuple(rgb), brightness=brightness) + self._bulb.setRgb(*tuple(rgb), brightness=brightness) - def turn_off(self, **kwargs): + def set_custom_effect( + self, colors: list[tuple[int, int, int]], speed_pct: int, transition: str + ) -> None: + """Set a custom effect on the bulb.""" + self._bulb.setCustomPattern( + colors, + speed_pct, + transition, + ) + + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the specified or all lights off.""" - self._bulb.turnOff() + await self.hass.async_add_executor_job(self._bulb.turnOff) + await self.coordinator.async_request_refresh() - def update(self): - """Synchronize state with bulb.""" - if not self.available: - try: - self._connect() - self._error_reported = False - except OSError: - self._disconnect() - if not self._error_reported: - _LOGGER.warning( - "Failed to connect to bulb %s, %s", self._ipaddr, self._name - ) - self._error_reported = True - return + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + if self._mode and self._mode != MODE_AUTO: + return - self._bulb.update_state(retry=2) + if self._bulb.mode == "ww": + self._mode = MODE_WHITE + elif self._bulb.rgbwcapable: + self._mode = MODE_RGBW + else: + self._mode = MODE_RGB + _LOGGER.debug( + "Detected mode for %s (%s) with raw_state=%s rgbwcapable=%s is %s", + self._name, + self.unique_id, + self._bulb.raw_state, + self._bulb.rgbwcapable, + self._mode, + ) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 0c6d8ae8db1..279ea05e3d2 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -1,8 +1,25 @@ { "domain": "flux_led", - "name": "Flux LED/MagicLight", + "name": "Flux LED/MagicHome", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", "requirements": ["flux_led==0.22"], - "codeowners": [], - "iot_class": "local_polling" + "codeowners": ["@icemanch"], + "iot_class": "local_polling", + "dhcp": [ + { + "macaddress": "18B905*", + "hostname": "[ba][lk]*" + }, + { + "macaddress": "249494*", + "hostname": "[ba][lk]*" + }, + { + "macaddress": "B4E842*", + "hostname": "[ba][lk]*" + } + ] } + + diff --git a/homeassistant/components/flux_led/services.yaml b/homeassistant/components/flux_led/services.yaml new file mode 100644 index 00000000000..f1dae55560b --- /dev/null +++ b/homeassistant/components/flux_led/services.yaml @@ -0,0 +1,38 @@ +set_custom_effect: + description: Set a custom light effect. + target: + entity: + integration: flux_led + domain: light + fields: + colors: + description: List of colors for the custom effect (RGB). (Max 16 Colors) + example: | + - [255,0,0] + - [0,255,0] + - [0,0,255] + required: true + selector: + object: + speed_pct: + description: Effect speed for the custom effect (0-100). + example: 80 + default: 50 + required: false + selector: + number: + min: 1 + step: 1 + max: 100 + unit_of_measurement: "%" + transition: + description: Effect transition. + example: 'jump' + default: 'gradual' + required: false + selector: + select: + options: + - "gradual" + - "jump" + - "strobe" diff --git a/homeassistant/components/flux_led/strings.json b/homeassistant/components/flux_led/strings.json new file mode 100644 index 00000000000..f311f559589 --- /dev/null +++ b/homeassistant/components/flux_led/strings.json @@ -0,0 +1,36 @@ +{ + "config": { + "flow_title": "{model} {id} ({ipaddr})", + "step": { + "user": { + "description": "If you leave the host empty, discovery will be used to find devices.", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "discovery_confirm": { + "description": "Do you want to setup {model} {id} ({ipaddr})?" + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" + } + }, + "options": { + "step": { + "init": { + "data": { + "mode": "The chosen brightness mode.", + "custom_effect_colors": "Custom Effect: List of 1 to 16 [R,G,B] colors. Example: [255,0,255],[60,128,0]", + "custom_effect_speed_pct": "Custom Effect: Speed in percents for the effect that switch colors.", + "custom_effect_transition": "Custom Effect: Type of transition between the colors." + } + } + } + } +} diff --git a/homeassistant/components/flux_led/translations/en.json b/homeassistant/components/flux_led/translations/en.json new file mode 100644 index 00000000000..3445e3e6764 --- /dev/null +++ b/homeassistant/components/flux_led/translations/en.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "no_devices_found": "No devices found on the network" + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "flow_title": "{model} {id} ({ipaddr})", + "step": { + "discovery_confirm": { + "description": "Do you want to setup {model} {id} ({ipaddr})?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "If you leave the host empty, discovery will be used to find devices." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "custom_effect_colors": "Custom Effect: List of 1 to 16 [R,G,B] colors. Example: [255,0,255],[60,128,0]", + "custom_effect_speed_pct": "Custom Effect: Speed in percents for the effect that switch colors.", + "custom_effect_transition": "Custom Effect: Type of transition between the colors.", + "mode": "The chosen brightness mode." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 0bd6edaf146..2a04ec39478 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -87,6 +87,7 @@ FLOWS = [ "flo", "flume", "flunearyou", + "flux_led", "forecast_solar", "forked_daapd", "foscam", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 34b0a468fc1..de74a41cff4 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -71,6 +71,21 @@ DHCP = [ "domain": "flume", "hostname": "flume-gw-*" }, + { + "domain": "flux_led", + "macaddress": "18B905*", + "hostname": "[ba][lk]*" + }, + { + "domain": "flux_led", + "macaddress": "249494*", + "hostname": "[ba][lk]*" + }, + { + "domain": "flux_led", + "macaddress": "B4E842*", + "hostname": "[ba][lk]*" + }, { "domain": "goalzero", "hostname": "yeti*" diff --git a/mypy.ini b/mypy.ini index afaf7dc6c21..8f9e49702fc 100644 --- a/mypy.ini +++ b/mypy.ini @@ -462,6 +462,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.flux_led.*] +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.forecast_solar.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1a3c20ffb65..61cd8bd909a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -377,6 +377,9 @@ fjaraskupan==1.0.1 # homeassistant.components.flipr flipr-api==1.4.1 +# homeassistant.components.flux_led +flux_led==0.22 + # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/tests/components/flux_led/__init.py__ b/tests/components/flux_led/__init.py__ new file mode 100644 index 00000000000..57af0b3751a --- /dev/null +++ b/tests/components/flux_led/__init.py__ @@ -0,0 +1 @@ +"""Tests for the flux_led integration.""" \ No newline at end of file diff --git a/tests/components/flux_led/__init__.py b/tests/components/flux_led/__init__.py new file mode 100644 index 00000000000..49b9db491f3 --- /dev/null +++ b/tests/components/flux_led/__init__.py @@ -0,0 +1,57 @@ +"""Tests for the flux_led integration.""" +from __future__ import annotations + +import socket +from unittest.mock import MagicMock, patch + +from flux_led import WifiLedBulb + +from homeassistant.components.dhcp import ( + HOSTNAME as DHCP_HOSTNAME, + IP_ADDRESS as DHCP_IP_ADDRESS, + MAC_ADDRESS as DHCP_MAC_ADDRESS, +) +from homeassistant.components.flux_led.const import FLUX_HOST, FLUX_MAC, FLUX_MODEL + +MODULE = "homeassistant.components.flux_led" +MODULE_CONFIG_FLOW = "homeassistant.components.flux_led.config_flow" +IP_ADDRESS = "127.0.0.1" +MODEL = "AZ120444" +MAC_ADDRESS = "aa:bb:cc:dd:ee:ff" +FLUX_MAC_ADDRESS = "aabbccddeeff" + +DEFAULT_ENTRY_TITLE = f"{MODEL} {FLUX_MAC_ADDRESS}" + +DHCP_DISCOVERY = { + DHCP_HOSTNAME: MODEL, + DHCP_IP_ADDRESS: IP_ADDRESS, + DHCP_MAC_ADDRESS: MAC_ADDRESS, +} +FLUX_DISCOVERY = {FLUX_HOST: IP_ADDRESS, FLUX_MODEL: MODEL, FLUX_MAC: FLUX_MAC_ADDRESS} + + +def _mocked_bulb() -> WifiLedBulb: + bulb = MagicMock(auto_spec=WifiLedBulb) + bulb.getRgb = MagicMock(return_value=[255, 0, 0]) + bulb.getRgbw = MagicMock(return_value=[255, 0, 0, 50]) + bulb.brightness = 128 + bulb.rgbwcapable = True + return bulb + + +def _patch_discovery(device=None, no_device=False): + def _discovery(*args, **kwargs): + if no_device: + return [] + return [FLUX_DISCOVERY] + + return patch("homeassistant.components.flux_led.BulbScanner.scan", new=_discovery) + + +def _patch_wifibulb(device=None, no_device=False): + def _wifi_led_bulb(*args, **kwargs): + if no_device: + raise socket.timeout + return device if device else _mocked_bulb() + + return patch("homeassistant.components.flux_led.WifiLedBulb", new=_wifi_led_bulb) diff --git a/tests/components/flux_led/test_config_flow.py b/tests/components/flux_led/test_config_flow.py new file mode 100644 index 00000000000..1c239108f41 --- /dev/null +++ b/tests/components/flux_led/test_config_flow.py @@ -0,0 +1,456 @@ +"""Define tests for the Flux LED/Magic Home config flow.""" +from __future__ import annotations + +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries, setup +from homeassistant.components.flux_led.const import ( + CONF_CUSTOM_EFFECT_COLORS, + CONF_CUSTOM_EFFECT_SPEED_PCT, + CONF_CUSTOM_EFFECT_TRANSITION, + DOMAIN, + MODE_AUTO, + MODE_RGB, + TRANSITION_JUMP, + TRANSITION_STROBE, +) +from homeassistant.const import ( + CONF_DEVICE, + CONF_HOST, + CONF_MAC, + CONF_MODE, + CONF_NAME, + CONF_PROTOCOL, +) +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM + +from . import ( + DEFAULT_ENTRY_TITLE, + DHCP_DISCOVERY, + DHCP_HOSTNAME, + DHCP_IP_ADDRESS, + DHCP_MAC_ADDRESS, + FLUX_DISCOVERY, + IP_ADDRESS, + MAC_ADDRESS, + MODULE, + _patch_discovery, + _patch_wifibulb, +) + +from tests.common import MockConfigEntry + + +async def test_discovery(hass: HomeAssistant): + """Test setting up discovery.""" + with _patch_discovery(), _patch_wifibulb(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + assert result2["type"] == "form" + assert result2["step_id"] == "pick_device" + assert not result2["errors"] + + # test we can try again + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + assert result2["type"] == "form" + assert result2["step_id"] == "pick_device" + assert not result2["errors"] + + with _patch_discovery(), _patch_wifibulb(), patch( + f"{MODULE}.async_setup", return_value=True + ) as mock_setup, patch( + f"{MODULE}.async_setup_entry", return_value=True + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_DEVICE: MAC_ADDRESS}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "create_entry" + assert result3["title"] == DEFAULT_ENTRY_TITLE + assert result3["data"] == {CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE} + mock_setup.assert_called_once() + mock_setup_entry.assert_called_once() + + # ignore configured devices + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + with _patch_discovery(), _patch_wifibulb(): + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "no_devices_found" + + +async def test_discovery_with_existing_device_present(hass: HomeAssistant): + """Test setting up discovery.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.2"}, unique_id="dd:dd:dd:dd:dd:dd" + ) + config_entry.add_to_hass(hass) + + with _patch_discovery(), _patch_wifibulb(no_device=True): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + with _patch_discovery(), _patch_wifibulb(): + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["step_id"] == "pick_device" + assert not result2["errors"] + + # Now abort and make sure we can start over + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + with _patch_discovery(), _patch_wifibulb(): + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["step_id"] == "pick_device" + assert not result2["errors"] + + with _patch_discovery(), _patch_wifibulb(), patch( + f"{MODULE}.async_setup_entry", return_value=True + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_DEVICE: MAC_ADDRESS} + ) + assert result3["type"] == "create_entry" + assert result3["title"] == DEFAULT_ENTRY_TITLE + assert result3["data"] == { + CONF_HOST: IP_ADDRESS, + CONF_NAME: DEFAULT_ENTRY_TITLE, + } + await hass.async_block_till_done() + + mock_setup_entry.assert_called_once() + + # ignore configured devices + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + with _patch_discovery(), _patch_wifibulb(): + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "no_devices_found" + + +async def test_discovery_no_device(hass: HomeAssistant): + """Test discovery without device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with _patch_discovery(no_device=True), _patch_wifibulb(): + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "no_devices_found" + + +async def test_import(hass: HomeAssistant): + """Test import from yaml.""" + config = { + CONF_HOST: IP_ADDRESS, + CONF_MAC: MAC_ADDRESS, + CONF_NAME: "floor lamp", + CONF_PROTOCOL: "ledenet", + CONF_MODE: MODE_RGB, + CONF_CUSTOM_EFFECT_COLORS: "[255,0,0], [0,0,255]", + CONF_CUSTOM_EFFECT_SPEED_PCT: 30, + CONF_CUSTOM_EFFECT_TRANSITION: TRANSITION_STROBE, + } + + # Success + with _patch_discovery(), _patch_wifibulb(), patch( + f"{MODULE}.async_setup", return_value=True + ) as mock_setup, patch( + f"{MODULE}.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=config + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == "floor lamp" + assert result["data"] == { + CONF_HOST: IP_ADDRESS, + CONF_NAME: "floor lamp", + CONF_PROTOCOL: "ledenet", + } + assert result["options"] == { + CONF_MODE: MODE_RGB, + CONF_CUSTOM_EFFECT_COLORS: "[255,0,0], [0,0,255]", + CONF_CUSTOM_EFFECT_SPEED_PCT: 30, + CONF_CUSTOM_EFFECT_TRANSITION: TRANSITION_STROBE, + } + mock_setup.assert_called_once() + mock_setup_entry.assert_called_once() + + # Duplicate + with _patch_discovery(), _patch_wifibulb(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_manual(hass: HomeAssistant): + """Test manually setup.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + # Cannot connect (timeout) + with _patch_discovery(no_device=True), _patch_wifibulb(no_device=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: IP_ADDRESS} + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "cannot_connect"} + + # Success + with _patch_discovery(), _patch_wifibulb(), patch( + f"{MODULE}.async_setup", return_value=True + ), patch(f"{MODULE}.async_setup_entry", return_value=True): + result4 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: IP_ADDRESS} + ) + await hass.async_block_till_done() + assert result4["type"] == "create_entry" + assert result4["title"] == IP_ADDRESS + assert result4["data"] == {CONF_HOST: IP_ADDRESS, CONF_NAME: IP_ADDRESS} + + # Duplicate + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + with _patch_discovery(no_device=True), _patch_wifibulb(no_device=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: IP_ADDRESS} + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" + + +async def test_manual_no_discovery_data(hass: HomeAssistant): + """Test manually setup without discovery data.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + with _patch_discovery(no_device=True), _patch_wifibulb(), patch( + f"{MODULE}.async_setup", return_value=True + ), patch(f"{MODULE}.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: IP_ADDRESS} + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["data"] == {CONF_HOST: IP_ADDRESS, CONF_NAME: IP_ADDRESS} + + +async def test_discovered_by_discovery_and_dhcp(hass): + """Test we get the form with discovery and abort for dhcp source when we get both.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with _patch_discovery(), _patch_wifibulb(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DISCOVERY}, + data=FLUX_DISCOVERY, + ) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with _patch_discovery(), _patch_wifibulb(): + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=DHCP_DISCOVERY, + ) + await hass.async_block_till_done() + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "already_in_progress" + + with _patch_discovery(), _patch_wifibulb(): + result3 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={ + DHCP_HOSTNAME: "any", + DHCP_IP_ADDRESS: IP_ADDRESS, + DHCP_MAC_ADDRESS: "00:00:00:00:00:00", + }, + ) + await hass.async_block_till_done() + assert result3["type"] == RESULT_TYPE_ABORT + assert result3["reason"] == "already_in_progress" + + +@pytest.mark.parametrize( + "source, data", + [ + (config_entries.SOURCE_DHCP, DHCP_DISCOVERY), + (config_entries.SOURCE_DISCOVERY, FLUX_DISCOVERY), + ], +) +async def test_discovered_by_dhcp_or_discovery(hass, source, data): + """Test we can setup when discovered from dhcp or discovery.""" + + await setup.async_setup_component(hass, "persistent_notification", {}) + + 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"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with _patch_discovery(), _patch_wifibulb(), patch( + f"{MODULE}.async_setup", return_value=True + ) as mock_async_setup, patch( + f"{MODULE}.async_setup_entry", return_value=True + ) as mock_async_setup_entry: + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["data"] == {CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE} + assert mock_async_setup.called + assert mock_async_setup_entry.called + + +@pytest.mark.parametrize( + "source, data", + [ + (config_entries.SOURCE_DHCP, DHCP_DISCOVERY), + (config_entries.SOURCE_DISCOVERY, FLUX_DISCOVERY), + ], +) +async def test_discovered_by_dhcp_or_discovery_adds_missing_unique_id( + hass, source, data +): + """Test we can setup when discovered from dhcp or discovery.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}) + config_entry.add_to_hass(hass) + await setup.async_setup_component(hass, "persistent_notification", {}) + + 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"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + assert config_entry.unique_id == MAC_ADDRESS + + +async def test_options(hass: HomeAssistant): + """Test options flow.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + options={ + CONF_MODE: MODE_RGB, + CONF_CUSTOM_EFFECT_COLORS: "[255,0,0], [0,0,255]", + CONF_CUSTOM_EFFECT_SPEED_PCT: 30, + CONF_CUSTOM_EFFECT_TRANSITION: TRANSITION_STROBE, + }, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + + with _patch_discovery(), _patch_wifibulb(): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == "form" + assert result["step_id"] == "init" + + user_input = { + CONF_MODE: MODE_AUTO, + CONF_CUSTOM_EFFECT_COLORS: "[0,0,255], [255,0,0]", + CONF_CUSTOM_EFFECT_SPEED_PCT: 50, + CONF_CUSTOM_EFFECT_TRANSITION: TRANSITION_JUMP, + } + with _patch_discovery(), _patch_wifibulb(): + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], user_input + ) + await hass.async_block_till_done() + assert result2["type"] == "create_entry" + assert result2["data"] == user_input + assert result2["data"] == config_entry.options + assert hass.states.get("light.az120444_aabbccddeeff") is not None diff --git a/tests/components/flux_led/test_init.py b/tests/components/flux_led/test_init.py new file mode 100644 index 00000000000..7b01088d1f6 --- /dev/null +++ b/tests/components/flux_led/test_init.py @@ -0,0 +1,58 @@ +"""Tests for the flux_led component.""" +from __future__ import annotations + +from unittest.mock import patch + +from homeassistant.components import flux_led +from homeassistant.components.flux_led.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow + +from . import FLUX_DISCOVERY, IP_ADDRESS, MAC_ADDRESS, _patch_discovery, _patch_wifibulb + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_configuring_flux_led_causes_discovery(hass: HomeAssistant) -> None: + """Test that specifying empty config does discovery.""" + with patch("homeassistant.components.flux_led.BulbScanner.scan") as discover: + discover.return_value = [FLUX_DISCOVERY] + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + assert len(discover.mock_calls) == 1 + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert len(discover.mock_calls) == 2 + + async_fire_time_changed(hass, utcnow() + flux_led.DISCOVERY_INTERVAL) + await hass.async_block_till_done() + assert len(discover.mock_calls) == 3 + + +async def test_config_entry_reload(hass: HomeAssistant) -> None: + """Test that a config entry can be reloaded.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=MAC_ADDRESS) + config_entry.add_to_hass(hass) + with _patch_discovery(), _patch_wifibulb(): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.LOADED + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.NOT_LOADED + + +async def test_config_entry_retry(hass: HomeAssistant) -> None: + """Test that a config entry can be retried.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=MAC_ADDRESS + ) + config_entry.add_to_hass(hass) + with _patch_discovery(no_device=True), _patch_wifibulb(no_device=True): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.SETUP_RETRY diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py new file mode 100644 index 00000000000..c2e98158b0b --- /dev/null +++ b/tests/components/flux_led/test_light.py @@ -0,0 +1,654 @@ +"""Tests for light platform.""" +from datetime import timedelta + +import pytest + +from homeassistant.components import flux_led +from homeassistant.components.flux_led.const import ( + CONF_COLORS, + CONF_CUSTOM_EFFECT, + CONF_CUSTOM_EFFECT_COLORS, + CONF_CUSTOM_EFFECT_SPEED_PCT, + CONF_CUSTOM_EFFECT_TRANSITION, + CONF_DEVICES, + CONF_SPEED_PCT, + CONF_TRANSITION, + DOMAIN, + MODE_AUTO, + TRANSITION_JUMP, +) +from homeassistant.components.flux_led.light import EFFECT_CUSTOM_CODE, FLUX_EFFECT_LIST +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, + ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_EFFECT_LIST, + ATTR_HS_COLOR, + ATTR_SUPPORTED_COLOR_MODES, + DOMAIN as LIGHT_DOMAIN, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_HOST, + CONF_MODE, + CONF_NAME, + CONF_PLATFORM, + CONF_PROTOCOL, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow + +from . import ( + DEFAULT_ENTRY_TITLE, + IP_ADDRESS, + MAC_ADDRESS, + _mocked_bulb, + _patch_discovery, + _patch_wifibulb, +) + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_light_unique_id(hass: HomeAssistant) -> None: + """Test a light unique id.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.az120444_aabbccddeeff" + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == MAC_ADDRESS + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + +async def test_light_no_unique_id(hass: HomeAssistant) -> None: + """Test a light without a unique id.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE} + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + with _patch_discovery(no_device=True), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.az120444_aabbccddeeff" + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id) is None + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + +async def test_rgb_light(hass: HomeAssistant) -> None: + """Test an rgb light.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.rgbwcapable = False + bulb.protocol = None + with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.az120444_aabbccddeeff" + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 128 + assert attributes[ATTR_COLOR_MODE] == "hs" + assert attributes[ATTR_EFFECT_LIST] == FLUX_EFFECT_LIST + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["hs"] + assert attributes[ATTR_HS_COLOR] == (0, 100) + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.turnOff.assert_called_once() + + bulb.is_on = False + async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_OFF + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.turnOn.assert_called_once() + bulb.turnOn.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + bulb.setRgb.assert_called_with(255, 0, 0, brightness=100) + bulb.setRgb.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, + blocking=True, + ) + bulb.setRgb.assert_called_with(255, 191, 178, brightness=128) + bulb.setRgb.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"}, + blocking=True, + ) + bulb.setRgb.assert_called_once() + bulb.setRgb.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"}, + blocking=True, + ) + bulb.setPresetPattern.assert_called_with(43, 50) + bulb.setPresetPattern.reset_mock() + + +async def test_rgbw_light(hass: HomeAssistant) -> None: + """Test an rgbw light.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.az120444_aabbccddeeff" + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 128 + assert attributes[ATTR_COLOR_MODE] == "rgbw" + assert attributes[ATTR_EFFECT_LIST] == FLUX_EFFECT_LIST + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "hs", "rgbw"] + assert attributes[ATTR_HS_COLOR] == (0, 100) + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.turnOff.assert_called_once() + + bulb.is_on = False + async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_OFF + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.turnOn.assert_called_once() + bulb.turnOn.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + bulb.setRgbw.assert_called_with(255, 0, 0, w=50, brightness=100) + bulb.setRgbw.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 150}, + blocking=True, + ) + bulb.setRgbw.assert_called_with(w2=128) + bulb.setRgbw.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 290}, + blocking=True, + ) + bulb.setRgbw.assert_called_with(w=128) + bulb.setRgbw.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, + blocking=True, + ) + bulb.setRgbw.assert_called_with(255, 191, 178, w=50, brightness=128) + bulb.setRgbw.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"}, + blocking=True, + ) + bulb.setRgb.assert_called_once() + bulb.setRgb.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"}, + blocking=True, + ) + bulb.setPresetPattern.assert_called_with(43, 50) + bulb.setPresetPattern.reset_mock() + + +async def test_rgbcw_light(hass: HomeAssistant) -> None: + """Test an rgbcw light.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.raw_state = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + bulb.raw_state[9] = 1 + bulb.raw_state[11] = 2 + + with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.az120444_aabbccddeeff" + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 128 + assert attributes[ATTR_COLOR_MODE] == "rgbw" + assert attributes[ATTR_EFFECT_LIST] == FLUX_EFFECT_LIST + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "hs", "rgbw"] + assert attributes[ATTR_HS_COLOR] == (0, 100) + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.turnOff.assert_called_once() + + bulb.is_on = False + async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_OFF + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.turnOn.assert_called_once() + bulb.turnOn.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + bulb.setRgbw.assert_called_with(255, 0, 0, w=50, brightness=100) + bulb.setRgbw.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 150}, + blocking=True, + ) + bulb.setRgbw.assert_called_with(w2=128) + bulb.setRgbw.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 290}, + blocking=True, + ) + bulb.setRgbw.assert_called_with(w=128) + bulb.setRgbw.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, + blocking=True, + ) + bulb.setRgbw.assert_called_with(255, 191, 178, w=50, brightness=128) + bulb.setRgbw.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"}, + blocking=True, + ) + bulb.setRgb.assert_called_once() + bulb.setRgb.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"}, + blocking=True, + ) + bulb.setPresetPattern.assert_called_with(43, 50) + bulb.setPresetPattern.reset_mock() + + +async def test_white_light(hass: HomeAssistant) -> None: + """Test a white light.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.mode = "ww" + bulb.protocol = None + with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.az120444_aabbccddeeff" + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 50 + assert attributes[ATTR_COLOR_MODE] == "brightness" + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["brightness"] + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.turnOff.assert_called_once() + + bulb.is_on = False + async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_OFF + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.turnOn.assert_called_once() + bulb.turnOn.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + bulb.setRgbw.assert_called_with(0, 0, 0, w=100) + bulb.setRgbw.reset_mock() + + +async def test_rgb_light_custom_effects( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test an rgb light with a custom effect.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + options={ + CONF_MODE: MODE_AUTO, + CONF_CUSTOM_EFFECT_COLORS: "[0,0,255], [255,0,0]", + CONF_CUSTOM_EFFECT_SPEED_PCT: 88, + CONF_CUSTOM_EFFECT_TRANSITION: TRANSITION_JUMP, + }, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.az120444_aabbccddeeff" + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 128 + assert attributes[ATTR_COLOR_MODE] == "rgbw" + assert attributes[ATTR_EFFECT_LIST] == [*FLUX_EFFECT_LIST, "custom"] + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "hs", "rgbw"] + assert attributes[ATTR_HS_COLOR] == (0, 100) + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.turnOff.assert_called_once() + + bulb.is_on = False + async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_OFF + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "custom"}, + blocking=True, + ) + bulb.setCustomPattern.assert_called_with([[0, 0, 255], [255, 0, 0]], 88, "jump") + bulb.setCustomPattern.reset_mock() + bulb.raw_state = [0, 0, 0, EFFECT_CUSTOM_CODE, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + bulb.is_on = True + async_fire_time_changed(hass, utcnow() + timedelta(seconds=20)) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attributes = state.attributes + assert attributes[ATTR_EFFECT] == "custom" + + caplog.clear() + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 55, ATTR_EFFECT: "custom"}, + blocking=True, + ) + bulb.setCustomPattern.assert_called_with([[0, 0, 255], [255, 0, 0]], 88, "jump") + bulb.setCustomPattern.reset_mock() + bulb.raw_state = [0, 0, 0, EFFECT_CUSTOM_CODE, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + bulb.is_on = True + async_fire_time_changed(hass, utcnow() + timedelta(seconds=20)) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attributes = state.attributes + assert attributes[ATTR_EFFECT] == "custom" + assert "RGB, brightness and white level are ignored when" in caplog.text + + +async def test_rgb_light_custom_effects_invalid_colors(hass: HomeAssistant) -> None: + """Test an rgb light with a invalid effect.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + options={ + CONF_MODE: MODE_AUTO, + CONF_CUSTOM_EFFECT_COLORS: ":: CANNOT BE PARSED ::", + CONF_CUSTOM_EFFECT_SPEED_PCT: 88, + CONF_CUSTOM_EFFECT_TRANSITION: TRANSITION_JUMP, + }, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.az120444_aabbccddeeff" + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 128 + assert attributes[ATTR_COLOR_MODE] == "rgbw" + assert attributes[ATTR_EFFECT_LIST] == FLUX_EFFECT_LIST + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "hs", "rgbw"] + assert attributes[ATTR_HS_COLOR] == (0, 100) + + +async def test_rgb_light_custom_effect_via_service( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test an rgb light with a custom effect set via the service.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.az120444_aabbccddeeff" + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 128 + assert attributes[ATTR_COLOR_MODE] == "rgbw" + assert attributes[ATTR_EFFECT_LIST] == [*FLUX_EFFECT_LIST] + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "hs", "rgbw"] + assert attributes[ATTR_HS_COLOR] == (0, 100) + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.turnOff.assert_called_once() + + bulb.is_on = False + async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_OFF + + await hass.services.async_call( + DOMAIN, + "set_custom_effect", + { + ATTR_ENTITY_ID: entity_id, + CONF_COLORS: [[0, 0, 255], [255, 0, 0]], + CONF_SPEED_PCT: 30, + CONF_TRANSITION: "jump", + }, + blocking=True, + ) + bulb.setCustomPattern.assert_called_with([(0, 0, 255), (255, 0, 0)], 30, "jump") + bulb.setCustomPattern.reset_mock() + + +async def test_rgbw_detection_without_protocol(hass: HomeAssistant) -> None: + """Test an rgbw detection without protocol.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.protocol = None + bulb.rgbwprotocol = None + bulb.rgbwcapable = True + with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.az120444_aabbccddeeff" + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 128 + assert attributes[ATTR_COLOR_MODE] == "rgbw" + assert attributes[ATTR_EFFECT_LIST] == FLUX_EFFECT_LIST + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "hs", "rgbw"] + assert attributes[ATTR_HS_COLOR] == (0, 100) + + +async def test_migrate_from_yaml(hass: HomeAssistant) -> None: + """Test migrate from yaml.""" + config = { + LIGHT_DOMAIN: [ + { + CONF_PLATFORM: DOMAIN, + CONF_DEVICES: { + IP_ADDRESS: { + CONF_NAME: "flux_lamppost", + CONF_PROTOCOL: "ledenet", + CONF_CUSTOM_EFFECT: { + CONF_SPEED_PCT: 30, + CONF_TRANSITION: "strobe", + CONF_COLORS: [[255, 0, 0], [255, 255, 0], [0, 255, 0]], + }, + } + }, + } + ], + } + with _patch_discovery(), _patch_wifibulb(): + await async_setup_component(hass, LIGHT_DOMAIN, config) + await hass.async_block_till_done() + await hass.async_block_till_done() + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + + migrated_entry = None + for entry in entries: + if entry.unique_id == MAC_ADDRESS: + migrated_entry = entry + break + + assert migrated_entry is not None + assert migrated_entry.data == { + CONF_HOST: IP_ADDRESS, + CONF_NAME: "flux_lamppost", + CONF_PROTOCOL: "ledenet", + } + assert migrated_entry.options == { + CONF_MODE: "auto", + CONF_CUSTOM_EFFECT_COLORS: "[(255, 0, 0), (255, 255, 0), (0, 255, 0)]", + CONF_CUSTOM_EFFECT_SPEED_PCT: 30, + CONF_CUSTOM_EFFECT_TRANSITION: "strobe", + } From 7d6b4a985dd59e99f6bd95d4e464231480c875e0 Mon Sep 17 00:00:00 2001 From: RenierM26 <66512715+RenierM26@users.noreply.github.com> Date: Sat, 2 Oct 2021 22:51:53 +0200 Subject: [PATCH 0046/1038] Fix Switchbot unsupported SB types (#56928) --- homeassistant/components/switchbot/config_flow.py | 2 +- tests/components/switchbot/conftest.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index eba40d46058..2d4e61bada5 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -112,7 +112,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): unconfigured_devices = { device["mac_address"]: f"{device['mac_address']} {device['modelName']}" for device in self._discovered_devices.values() - if device["modelName"] in SUPPORTED_MODEL_TYPES + if device.get("modelName") in SUPPORTED_MODEL_TYPES and device["mac_address"] not in configured_devices } diff --git a/tests/components/switchbot/conftest.py b/tests/components/switchbot/conftest.py index 8e90547a18f..52e5fd4fa15 100644 --- a/tests/components/switchbot/conftest.py +++ b/tests/components/switchbot/conftest.py @@ -41,6 +41,12 @@ class MocGetSwitchbotDevices: "model": "c", "modelName": "WoCurtain", }, + "ffffff19ffff": { + "mac_address": "ff:ff:ff:19:ff:ff", + "Flags": "06", + "Manufacturer": "5900ffffff19ffff", + "Complete 128b Services": "cba20d00-224d-11e6-9fb8-0002a5d5c51b", + }, } self._curtain_all_services_data = { "mac_address": "e7:89:43:90:90:90", From 2d174d0cbb1a3a0a00242e68d369bd8972543035 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 2 Oct 2021 22:52:28 +0200 Subject: [PATCH 0047/1038] Set unique id while SSDP discovery of Synology DSM (#56914) --- homeassistant/components/synology_dsm/config_flow.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index ae24adc7960..ed0ee8e9125 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -239,8 +239,12 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): # Synology NAS can broadcast on multiple IP addresses, since they can be connected to multiple ethernets. # The serial of the NAS is actually its MAC address. + await self.async_set_unique_id(discovered_mac) existing_entry = self._async_get_existing_entry(discovered_mac) + if not existing_entry: + self._abort_if_unique_id_configured() + if existing_entry and existing_entry.data[CONF_HOST] != parsed_url.hostname: _LOGGER.debug( "Update host from '%s' to '%s' for NAS '%s' via SSDP discovery", @@ -253,6 +257,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): data={**existing_entry.data, CONF_HOST: parsed_url.hostname}, ) return self.async_abort(reason="reconfigure_successful") + if existing_entry: return self.async_abort(reason="already_configured") From b563a41482e32b2ebace6cf842577e0c387a2ac3 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Sat, 2 Oct 2021 22:53:19 +0200 Subject: [PATCH 0048/1038] Update pypoint to use v5 of backend API (#56934) --- homeassistant/components/point/manifest.json | 2 +- homeassistant/components/point/sensor.py | 8 -------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/point/manifest.json b/homeassistant/components/point/manifest.json index fffb1b07f25..13a1ac5ce23 100644 --- a/homeassistant/components/point/manifest.json +++ b/homeassistant/components/point/manifest.json @@ -3,7 +3,7 @@ "name": "Minut Point", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/point", - "requirements": ["pypoint==2.1.0"], + "requirements": ["pypoint==2.2.0"], "dependencies": ["webhook", "http"], "codeowners": ["@fredrike"], "quality_scale": "gold", diff --git a/homeassistant/components/point/sensor.py b/homeassistant/components/point/sensor.py index 8d4ee69fca2..bb98ccb53d9 100644 --- a/homeassistant/components/point/sensor.py +++ b/homeassistant/components/point/sensor.py @@ -11,10 +11,8 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, PERCENTAGE, - PRESSURE_HPA, SOUND_PRESSURE_WEIGHTED_DBA, TEMP_CELSIUS, ) @@ -50,12 +48,6 @@ SENSOR_TYPES: tuple[MinutPointSensorEntityDescription, ...] = ( device_class=DEVICE_CLASS_TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, ), - MinutPointSensorEntityDescription( - key="pressure", - precision=0, - device_class=DEVICE_CLASS_PRESSURE, - native_unit_of_measurement=PRESSURE_HPA, - ), MinutPointSensorEntityDescription( key="humidity", precision=1, diff --git a/requirements_all.txt b/requirements_all.txt index 155c5410376..b5a15598c25 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1723,7 +1723,7 @@ pypjlink2==1.2.1 pyplaato==0.0.15 # homeassistant.components.point -pypoint==2.1.0 +pypoint==2.2.0 # homeassistant.components.profiler pyprof2calltree==1.4.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 61cd8bd909a..c9e35a2e56a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1024,7 +1024,7 @@ pypck==0.7.10 pyplaato==0.0.15 # homeassistant.components.point -pypoint==2.1.0 +pypoint==2.2.0 # homeassistant.components.profiler pyprof2calltree==1.4.5 From 2f35cadba7c9560f69923c60dfbf25c5579353d4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 2 Oct 2021 15:53:37 -0500 Subject: [PATCH 0049/1038] Add dhcp discovery for TPLink EP10 (#56955) --- homeassistant/components/tplink/manifest.json | 4 ++++ homeassistant/generated/dhcp.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 6712da00d0e..a24d95bbc75 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -45,6 +45,10 @@ "hostname": "hs*", "macaddress": "C006C3*" }, + { + "hostname": "ep*", + "macaddress": "003192*" + }, { "hostname": "k[lp]*", "macaddress": "003192*" diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index de74a41cff4..9036832c2f1 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -324,6 +324,11 @@ DHCP = [ "hostname": "hs*", "macaddress": "C006C3*" }, + { + "domain": "tplink", + "hostname": "ep*", + "macaddress": "003192*" + }, { "domain": "tplink", "hostname": "k[lp]*", From ad6129c505579ce0663819d79e819e5bd2618cd1 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 2 Oct 2021 23:16:29 +0200 Subject: [PATCH 0050/1038] Update frontend to 20211002.0 (#56963) --- 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 cf1f8f052af..d6d38faab27 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20210930.0" + "home-assistant-frontend==20211002.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f8c047e81f0..e7f0cb6a5c3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==3.4.8 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20210930.0 +home-assistant-frontend==20211002.0 httpx==0.19.0 ifaddr==0.1.7 jinja2==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index b5a15598c25..05784241afe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -810,7 +810,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20210930.0 +home-assistant-frontend==20211002.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c9e35a2e56a..72528f615f5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -488,7 +488,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20210930.0 +home-assistant-frontend==20211002.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 06602e6cc5ac054c983eb611b4fe6e7e26b05e90 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 3 Oct 2021 00:13:50 +0000 Subject: [PATCH 0051/1038] [ci skip] Translation update --- .../apple_tv/translations/zh-Hans.json | 32 +++++++++++++++-- .../coolmaster/translations/he.json | 1 + .../components/dlna_dmr/translations/he.json | 18 ++++++++++ .../dlna_dmr/translations/zh-Hans.json | 12 +++++++ .../esphome/translations/zh-Hans.json | 18 ++++++++-- .../components/fan/translations/he.json | 8 +++++ .../components/flux_led/translations/de.json | 36 +++++++++++++++++++ .../components/flux_led/translations/en.json | 1 + .../huawei_lte/translations/zh-Hans.json | 36 ++++++++++++++++++- .../nmap_tracker/translations/zh-Hans.json | 5 ++- .../opengarage/translations/he.json | 22 ++++++++++++ .../opengarage/translations/zh-Hans.json | 12 +++++++ .../components/switchbot/translations/he.json | 1 + .../components/tplink/translations/he.json | 14 ++++++++ .../components/tuya/translations/he.json | 36 +++++++++++++++++-- .../xiaomi_aqara/translations/he.json | 29 ++++++++++++--- .../components/zwave_js/translations/he.json | 3 +- 17 files changed, 269 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/dlna_dmr/translations/he.json create mode 100644 homeassistant/components/dlna_dmr/translations/zh-Hans.json create mode 100644 homeassistant/components/flux_led/translations/de.json create mode 100644 homeassistant/components/opengarage/translations/he.json create mode 100644 homeassistant/components/opengarage/translations/zh-Hans.json diff --git a/homeassistant/components/apple_tv/translations/zh-Hans.json b/homeassistant/components/apple_tv/translations/zh-Hans.json index 54095a0a633..4b178c75fce 100644 --- a/homeassistant/components/apple_tv/translations/zh-Hans.json +++ b/homeassistant/components/apple_tv/translations/zh-Hans.json @@ -1,18 +1,46 @@ { "config": { + "abort": { + "already_configured_device": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e", + "already_in_progress": "\u914d\u7f6e\u6d41\u7a0b\u5df2\u5728\u8fdb\u884c\u4e2d", + "backoff": "\u8bbe\u5907\u76ee\u524d\u6682\u4e0d\u63a5\u53d7\u914d\u5bf9\u8bf7\u6c42\uff08\u53ef\u80fd\u591a\u6b21\u8f93\u5165\u65e0\u6548 PIN \u7801\uff09\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5\u3002", + "invalid_config": "\u6b64\u8bbe\u5907\u7684\u914d\u7f6e\u4fe1\u606f\u4e0d\u5b8c\u6574\u3002\u8bf7\u5c1d\u8bd5\u91cd\u65b0\u6dfb\u52a0\u3002", + "no_devices_found": "\u672a\u5728\u6b64\u7f51\u7edc\u53d1\u73b0\u76f8\u5173\u8bbe\u5907", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "error": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e", + "invalid_auth": "\u51ed\u636e\u65e0\u6548", + "no_devices_found": "\u672a\u5728\u6b64\u7f51\u7edc\u53d1\u73b0\u76f8\u5173\u8bbe\u5907", + "no_usable_service": "\u5df2\u76f8\u5173\u627e\u5230\u8bbe\u5907\uff0c\u4f46\u65e0\u6cd5\u8bc6\u522b\u5e76\u4e0e\u5176\u5efa\u7acb\u8fde\u63a5\u3002\u82e5\u60a8\u4e00\u76f4\u6536\u5230\u6b64\u8b66\u544a\u6d88\u606f\uff0c\u8bf7\u5c1d\u8bd5\u4e3a\u5176\u6307\u5b9a\u56fa\u5b9a IP \u5730\u5740\u6216\u91cd\u65b0\u542f\u52a8\u60a8\u7684 Apple TV\u3002", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, "step": { "confirm": { - "description": "\u60a8\u5373\u5c06\u6dfb\u52a0 Apple TV (\u540d\u79f0\u4e3a\u201c{name}\u201d)\u5230 Home Assistant\u3002 \n\n **\u8981\u5b8c\u6210\u6b64\u8fc7\u7a0b\uff0c\u53ef\u80fd\u9700\u8981\u8f93\u5165\u591a\u4e2a PIN \u7801\u3002** \n\n\u8bf7\u6ce8\u610f\uff0c\u6b64\u96c6\u6210*\u4e0d\u80fd*\u5173\u95ed Apple TV \u7684\u7535\u6e90\uff0c\u53ea\u4f1a\u5173\u95ed Home Assistant \u4e2d\u7684\u5a92\u4f53\u64ad\u653e\u5668\uff01" + "description": "\u60a8\u5373\u5c06\u6dfb\u52a0 Apple TV (\u540d\u79f0\u4e3a\u201c{name}\u201d)\u5230 Home Assistant\u3002 \n\n **\u8981\u5b8c\u6210\u6b64\u8fc7\u7a0b\uff0c\u53ef\u80fd\u9700\u8981\u8f93\u5165\u591a\u4e2a PIN \u7801\u3002** \n\n\u8bf7\u6ce8\u610f\uff0c\u6b64\u96c6\u6210*\u4e0d\u80fd*\u5173\u95ed Apple TV \u7684\u7535\u6e90\uff0c\u53ea\u4f1a\u5173\u95ed Home Assistant \u4e2d\u7684\u5a92\u4f53\u64ad\u653e\u5668\uff01", + "title": "\u786e\u8ba4\u6dfb\u52a0 Apple TV" }, "pair_no_pin": { + "description": "`{protocol}` \u670d\u52a1\u9700\u8981\u914d\u5bf9\u3002\u8bf7\u5728\u60a8\u7684 Apple TV \u4e0a\u8f93\u5165 PIN {pin}", "title": "\u914d\u5bf9\u4e2d" }, "pair_with_pin": { "data": { "pin": "PIN\u7801" - } + }, + "title": "\u914d\u5bf9\u4e2d" + }, + "reconfigure": { + "description": "\u8be5 Apple TV \u9047\u5230\u4e00\u4e9b\u8fde\u63a5\u95ee\u9898\uff0c\u987b\u91cd\u65b0\u914d\u7f6e\u3002", + "title": "\u8bbe\u5907\u91cd\u65b0\u914d\u7f6e" + }, + "service_problem": { + "title": "\u6dfb\u52a0\u670d\u52a1\u5931\u8d25" }, "user": { + "data": { + "device_input": "\u8bbe\u5907\u5730\u5740" + }, "description": "\u8981\u5f00\u59cb\uff0c\u8bf7\u8f93\u5165\u8981\u6dfb\u52a0\u7684 Apple TV \u7684\u8bbe\u5907\u540d\u79f0\u6216 IP \u5730\u5740\u3002\u5728\u7f51\u7edc\u4e0a\u81ea\u52a8\u53d1\u73b0\u7684\u8bbe\u5907\u4f1a\u663e\u793a\u5728\u4e0b\u65b9\u3002 \n\n\u5982\u679c\u6ca1\u6709\u53d1\u73b0\u8bbe\u5907\u6216\u9047\u5230\u4efb\u4f55\u95ee\u9898\uff0c\u8bf7\u5c1d\u8bd5\u6307\u5b9a\u8bbe\u5907 IP \u5730\u5740\u3002 \n\n {devices}", "title": "\u8bbe\u7f6e\u65b0\u7684 Apple TV" } diff --git a/homeassistant/components/coolmaster/translations/he.json b/homeassistant/components/coolmaster/translations/he.json index 5903faf3c72..1fd17b10134 100644 --- a/homeassistant/components/coolmaster/translations/he.json +++ b/homeassistant/components/coolmaster/translations/he.json @@ -7,6 +7,7 @@ "step": { "user": { "data": { + "fan_only": "\u05ea\u05de\u05d9\u05db\u05d4 \u05d1\u05de\u05e6\u05d1 \u05de\u05d0\u05d5\u05d5\u05e8\u05e8 \u05d1\u05dc\u05d1\u05d3", "host": "\u05de\u05d0\u05e8\u05d7" } } diff --git a/homeassistant/components/dlna_dmr/translations/he.json b/homeassistant/components/dlna_dmr/translations/he.json new file mode 100644 index 00000000000..fbdaa0403f4 --- /dev/null +++ b/homeassistant/components/dlna_dmr/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" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + }, + "user": { + "data": { + "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/zh-Hans.json b/homeassistant/components/dlna_dmr/translations/zh-Hans.json new file mode 100644 index 00000000000..909a38b4b74 --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/zh-Hans.json @@ -0,0 +1,12 @@ +{ + "config": { + "error": { + "could_not_connect": "\u65e0\u6cd5\u8fde\u63a5\u5230 DLNA \u8bbe\u5907" + } + }, + "options": { + "error": { + "invalid_url": "\u65e0\u6548\u7f51\u5740" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/zh-Hans.json b/homeassistant/components/esphome/translations/zh-Hans.json index b1911b90fde..d0c54f6afb1 100644 --- a/homeassistant/components/esphome/translations/zh-Hans.json +++ b/homeassistant/components/esphome/translations/zh-Hans.json @@ -2,11 +2,13 @@ "config": { "abort": { "already_configured": "\u8bbe\u5907\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86", - "already_in_progress": "\u914d\u7f6e\u6d41\u7a0b\u5df2\u5728\u8fdb\u884c\u4e2d" + "already_in_progress": "\u914d\u7f6e\u6d41\u7a0b\u5df2\u5728\u8fdb\u884c\u4e2d", + "reauth_successful": "\u91cd\u9a8c\u8bc1\u6210\u529f" }, "error": { "connection_error": "\u65e0\u6cd5\u8fde\u63a5\u5230 ESP\u3002\u8bf7\u786e\u8ba4\u60a8\u7684 YAML \u6587\u4ef6\u4e2d\u5305\u542b 'api:' \u884c\u3002", "invalid_auth": "\u8eab\u4efd\u8ba4\u8bc1\u65e0\u6548", + "invalid_psk": "\u4f20\u8f93\u52a0\u5bc6\u5bc6\u94a5\u65e0\u6548\u3002\u8bf7\u786e\u4fdd\u5b83\u4e0e\u60a8\u7684\u914d\u7f6e\u4e00\u81f4\u3002", "resolve_error": "\u65e0\u6cd5\u89e3\u6790 ESP \u7684\u5730\u5740\u3002\u5982\u679c\u6b64\u9519\u8bef\u6301\u7eed\u5b58\u5728\uff0c\u8bf7\u8bbe\u7f6e\u9759\u6001IP\u5730\u5740\uff1ahttps://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, "flow_title": "ESPHome: {name}", @@ -21,9 +23,21 @@ "description": "\u662f\u5426\u8981\u5c06 ESPHome \u8282\u70b9 `{name}` \u6dfb\u52a0\u5230 Home Assistant\uff1f", "title": "\u53d1\u73b0\u4e86 ESPHome \u8282\u70b9" }, + "encryption_key": { + "data": { + "noise_psk": "\u52a0\u5bc6\u5bc6\u94a5" + }, + "description": "\u8bf7\u8f93\u5165\u60a8\u5728\u914d\u7f6e\u4e2d\u8bbe\u5907 {name} \u6240\u8bbe\u7f6e\u7684\u52a0\u5bc6\u5bc6\u94a5\u3002" + }, + "reauth_confirm": { + "data": { + "noise_psk": "\u52a0\u5bc6\u5bc6\u94a5" + }, + "description": "ESPHome \u8bbe\u5907 {name} \u5df2\u542f\u7528\u6216\u66f4\u6539\u4f20\u8f93\u52a0\u5bc6\u5bc6\u94a5\u3002\u8bf7\u8f93\u5165\u66f4\u65b0\u540e\u7684\u5bc6\u94a5\u4fe1\u606f\u3002" + }, "user": { "data": { - "host": "\u4e3b\u673a", + "host": "\u4e3b\u673a\u5730\u5740", "port": "\u7aef\u53e3" }, "description": "\u8bf7\u8f93\u5165\u60a8\u7684 [ESPHome](https://esphomelib.com/) \u8282\u70b9\u7684\u8fde\u63a5\u8bbe\u7f6e\u3002" diff --git a/homeassistant/components/fan/translations/he.json b/homeassistant/components/fan/translations/he.json index db876480dfc..92e38a79918 100644 --- a/homeassistant/components/fan/translations/he.json +++ b/homeassistant/components/fan/translations/he.json @@ -1,8 +1,16 @@ { "device_automation": { + "action_type": { + "turn_off": "\u05db\u05d9\u05d1\u05d5\u05d9 {entity_name}", + "turn_on": "\u05d4\u05e4\u05e2\u05dc\u05ea {entity_name}" + }, "condition_type": { "is_off": "{entity_name} \u05db\u05d1\u05d5\u05d9", "is_on": "{entity_name} \u05e4\u05d5\u05e2\u05dc" + }, + "trigger_type": { + "turned_off": "{entity_name} \u05db\u05d5\u05d1\u05d4", + "turned_on": "{entity_name} \u05d4\u05d5\u05e4\u05e2\u05dc" } }, "state": { diff --git a/homeassistant/components/flux_led/translations/de.json b/homeassistant/components/flux_led/translations/de.json new file mode 100644 index 00000000000..f036e7bd913 --- /dev/null +++ b/homeassistant/components/flux_led/translations/de.json @@ -0,0 +1,36 @@ +{ + "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" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "flow_title": "{model} {id} ({ipaddr})", + "step": { + "discovery_confirm": { + "description": "M\u00f6chtest du {model} {id} ({ipaddr})?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Wenn du den Host leer l\u00e4sst, wird die Erkennung verwendet, um Ger\u00e4te zu finden." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "custom_effect_colors": "Benutzerdefinierter Effekt: Liste mit 1 bis 16 [R,G,B]-Farben. Beispiel: [255,0,255],[60,128,0]", + "custom_effect_speed_pct": "Benutzerdefinierter Effekt: Geschwindigkeit in Prozent f\u00fcr den Effekt, der die Farbe wechselt.", + "custom_effect_transition": "Benutzerdefinierter Effekt: Art des \u00dcbergangs zwischen den Farben.", + "mode": "Der gew\u00e4hlte Helligkeitsmodus." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/en.json b/homeassistant/components/flux_led/translations/en.json index 3445e3e6764..9a988408c30 100644 --- a/homeassistant/components/flux_led/translations/en.json +++ b/homeassistant/components/flux_led/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" }, "error": { diff --git a/homeassistant/components/huawei_lte/translations/zh-Hans.json b/homeassistant/components/huawei_lte/translations/zh-Hans.json index 4fb447403d6..a63ff964b62 100644 --- a/homeassistant/components/huawei_lte/translations/zh-Hans.json +++ b/homeassistant/components/huawei_lte/translations/zh-Hans.json @@ -1,8 +1,42 @@ { "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e", + "already_in_progress": "\u914d\u7f6e\u6d41\u7a0b\u5df2\u5728\u8fdb\u884c", + "not_huawei_lte": "\u8be5\u8bbe\u5907\u4e0d\u662f\u534e\u4e3a LTE \u8bbe\u5907" + }, "error": { + "connection_timeout": "\u8fde\u63a5\u8d85\u65f6", + "incorrect_password": "\u5bc6\u7801\u9519\u8bef", "incorrect_username": "\u7528\u6237\u540d\u9519\u8bef", - "login_attempts_exceeded": "\u5df2\u8d85\u8fc7\u6700\u5927\u767b\u5f55\u6b21\u6570\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5" + "invalid_auth": "\u51ed\u8bc1\u65e0\u6548", + "invalid_url": "\u65e0\u6548\u7f51\u5740", + "login_attempts_exceeded": "\u5df2\u8d85\u8fc7\u6700\u5927\u767b\u5f55\u6b21\u6570\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5", + "response_error": "\u8bbe\u5907\u51fa\u73b0\u672a\u77e5\u9519\u8bef", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "step": { + "user": { + "data": { + "url": "\u4e3b\u673a\u5730\u5740", + "username": "\u7528\u6237\u540d" + }, + "description": "\u8f93\u5165\u8bbe\u5907\u76f8\u5173\u4fe1\u606f\u4ee5\u4fbf\u8fde\u63a5\u81f3\u8be5\u8bbe\u5907", + "title": "\u914d\u7f6e\u534e\u4e3aLTE" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "\u63a8\u9001\u670d\u52a1\u540d\u79f0\uff08\u66f4\u6539\u540e\u9700\u8981\u91cd\u8f7d\uff09", + "recipient": "\u77ed\u4fe1\u901a\u77e5\u6536\u4ef6\u4eba", + "track_new_devices": "\u8ddf\u8e2a\u65b0\u8bbe\u5907", + "track_wired_clients": "\u8ddf\u8e2a\u6709\u7ebf\u7f51\u7edc\u5ba2\u6237\u7aef", + "unauthenticated_mode": "\u672a\u7ecf\u8eab\u4efd\u9a8c\u8bc1\u7684\u6a21\u5f0f\uff08\u66f4\u6539\u540e\u9700\u8981\u91cd\u8f7d\uff09" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/zh-Hans.json b/homeassistant/components/nmap_tracker/translations/zh-Hans.json index e0ca0563b7a..5b1be2f497d 100644 --- a/homeassistant/components/nmap_tracker/translations/zh-Hans.json +++ b/homeassistant/components/nmap_tracker/translations/zh-Hans.json @@ -22,10 +22,13 @@ "step": { "init": { "data": { + "consider_home": "\u7b49\u5f85\u591a\u5c11\u79d2\u540e\u5219\u5224\u5b9a\u8bbe\u5907\u79bb\u5f00", "exclude": "\u4ece\u626b\u63cf\u4e2d\u6392\u9664\u7684\u7f51\u7edc\u5730\u5740\uff08\u4ee5\u9017\u53f7\u5206\u9694\uff09", "home_interval": "\u626b\u63cf\u8bbe\u5907\u7684\u6700\u5c0f\u95f4\u9694\u5206\u949f\u6570\uff08\u7528\u4e8e\u8282\u7701\u7535\u91cf\uff09", "hosts": "\u8981\u626b\u63cf\u7684\u7f51\u7edc\u5730\u5740\uff08\u4ee5\u9017\u53f7\u5206\u9694\uff09", - "scan_options": "Nmap \u7684\u539f\u59cb\u53ef\u914d\u7f6e\u626b\u63cf\u9009\u9879" + "interval_seconds": "\u626b\u63cf\u95f4\u9694\uff08\u79d2\uff09", + "scan_options": "Nmap \u7684\u539f\u59cb\u53ef\u914d\u7f6e\u626b\u63cf\u9009\u9879", + "track_new_devices": "\u8ddf\u8e2a\u65b0\u8bbe\u5907" }, "description": "\u914d\u7f6e\u901a\u8fc7 Nmap \u626b\u63cf\u7684\u4e3b\u673a\u3002\u7f51\u7edc\u5730\u5740\u548c\u6392\u9664\u9879\u53ef\u4ee5\u662f IP \u5730\u5740 (192.168.1.1)\u3001IP \u5730\u5740\u5757 (192.168.0.0/24) \u6216 IP \u8303\u56f4 (192.168.1.0-32)\u3002" } diff --git a/homeassistant/components/opengarage/translations/he.json b/homeassistant/components/opengarage/translations/he.json new file mode 100644 index 00000000000..7f9a4197d54 --- /dev/null +++ b/homeassistant/components/opengarage/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" + }, + "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": { + "device_key": "\u05de\u05e4\u05ea\u05d7 \u05d4\u05ea\u05e7\u05df", + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4", + "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opengarage/translations/zh-Hans.json b/homeassistant/components/opengarage/translations/zh-Hans.json new file mode 100644 index 00000000000..4f99ec0f978 --- /dev/null +++ b/homeassistant/components/opengarage/translations/zh-Hans.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_auth": "\u51ed\u8bc1\u65e0\u6548", + "unknown": "\u672a\u77e5\u9519\u8bef" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/he.json b/homeassistant/components/switchbot/translations/he.json index 9e4d8129169..09f62069706 100644 --- a/homeassistant/components/switchbot/translations/he.json +++ b/homeassistant/components/switchbot/translations/he.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured_device": "\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", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "error": { diff --git a/homeassistant/components/tplink/translations/he.json b/homeassistant/components/tplink/translations/he.json index 053fc43039a..621ee6bebc9 100644 --- a/homeassistant/components/tplink/translations/he.json +++ b/homeassistant/components/tplink/translations/he.json @@ -1,12 +1,26 @@ { "config": { "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "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" + }, "step": { "confirm": { "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d7\u05db\u05de\u05d9\u05dd \u05e9\u05dc TP-Link ?" + }, + "pick_device": { + "data": { + "device": "\u05d4\u05ea\u05e7\u05df" + } + }, + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } } } } diff --git a/homeassistant/components/tuya/translations/he.json b/homeassistant/components/tuya/translations/he.json index 44a7699e511..548e623533a 100644 --- a/homeassistant/components/tuya/translations/he.json +++ b/homeassistant/components/tuya/translations/he.json @@ -8,23 +8,53 @@ "error": { "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" }, - "flow_title": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d8\u05d5\u05d9\u05d4", + "flow_title": "\u05ea\u05e6\u05d5\u05e8\u05ea Tuya", "step": { + "login": { + "data": { + "access_id": "\u05de\u05d6\u05d4\u05d4 \u05d2\u05d9\u05e9\u05d4", + "access_secret": "\u05e1\u05d5\u05d3 \u05d2\u05d9\u05e9\u05d4", + "country_code": "\u05e7\u05d5\u05d3 \u05de\u05d3\u05d9\u05e0\u05d4", + "endpoint": "\u05d0\u05d6\u05d5\u05e8 \u05d6\u05de\u05d9\u05e0\u05d5\u05ea", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "tuya_app_type": "\u05d9\u05d9\u05e9\u05d5\u05dd \u05dc\u05e0\u05d9\u05d9\u05d3", + "username": "\u05d7\u05e9\u05d1\u05d5\u05df" + }, + "description": "\u05d4\u05d6\u05e0\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 \u05d4-Tuya \u05e9\u05dc\u05da", + "title": "Tuya" + }, "user": { "data": { "country_code": "\u05e7\u05d5\u05d3 \u05de\u05d3\u05d9\u05e0\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05e9\u05dc\u05da (\u05dc\u05de\u05e9\u05dc, 1 \u05dc\u05d0\u05e8\u05d4\"\u05d1 \u05d0\u05d5 972 \u05dc\u05d9\u05e9\u05e8\u05d0\u05dc)", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "platform": "\u05d4\u05d9\u05d9\u05e9\u05d5\u05dd \u05d1\u05d5 \u05e8\u05e9\u05d5\u05dd \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05e9\u05dc\u05da", + "tuya_project_type": "\u05e1\u05d5\u05d2 \u05e4\u05e8\u05d5\u05d9\u05d9\u05e7\u05d8 \u05d4\u05e2\u05e0\u05df \u05e9\u05dc Tuya", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" }, - "description": "\u05d4\u05d6\u05df \u05d0\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8\u05d9 \u05d8\u05d5\u05d9\u05d4 \u05e9\u05dc\u05da.", - "title": "Tuya" + "description": "\u05d4\u05d6\u05e0\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8\u05d9 \u05d4-Tuya \u05e9\u05dc\u05da.", + "title": "\u05e9\u05d9\u05dc\u05d5\u05d1 Tuya" } } }, "options": { "abort": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "error": { + "dev_multi_type": "\u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05e0\u05d1\u05d7\u05e8\u05d9\u05dd \u05de\u05e8\u05d5\u05d1\u05d9\u05dd \u05dc\u05e7\u05d1\u05d9\u05e2\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05d7\u05d9\u05d9\u05d1\u05d9\u05dd \u05dc\u05d4\u05d9\u05d5\u05ea \u05de\u05d0\u05d5\u05ea\u05d5 \u05e1\u05d5\u05d2", + "dev_not_config": "\u05e1\u05d5\u05d2 \u05d4\u05d4\u05ea\u05e7\u05df \u05d0\u05d9\u05e0\u05d5 \u05e0\u05d9\u05ea\u05df \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4", + "dev_not_found": "\u05d4\u05d4\u05ea\u05e7\u05df \u05dc\u05d0 \u05e0\u05de\u05e6\u05d0" + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "\u05d8\u05d5\u05d5\u05d7 \u05d1\u05d4\u05d9\u05e8\u05d5\u05ea \u05d4\u05de\u05e9\u05de\u05e9 \u05d0\u05ea \u05d4\u05d4\u05ea\u05e7\u05df", + "max_kelvin": "\u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05ea \u05e6\u05d1\u05e2 \u05de\u05e8\u05d1\u05d9\u05ea \u05d4\u05e0\u05ea\u05de\u05db\u05ea \u05d1\u05e7\u05dc\u05d5\u05d5\u05d9\u05df", + "min_kelvin": "\u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05ea \u05e6\u05d1\u05e2 \u05de\u05d9\u05e0\u05d9\u05de\u05dc\u05d9\u05ea \u05d4\u05e0\u05ea\u05de\u05db\u05ea \u05d1\u05e7\u05dc\u05d5\u05d5\u05d9\u05df", + "support_color": "\u05db\u05e4\u05d4 \u05ea\u05de\u05d9\u05db\u05d4 \u05d1\u05e6\u05d1\u05e2", + "unit_of_measurement": "\u05d9\u05d7\u05d9\u05d3\u05ea \u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05d4 \u05d4\u05de\u05e9\u05de\u05e9\u05ea \u05d0\u05ea \u05d4\u05d4\u05ea\u05e7\u05df" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/he.json b/homeassistant/components/xiaomi_aqara/translations/he.json index 5a12ddc3b9e..7450bbd463c 100644 --- a/homeassistant/components/xiaomi_aqara/translations/he.json +++ b/homeassistant/components/xiaomi_aqara/translations/he.json @@ -2,22 +2,41 @@ "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" + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "not_xiaomi_aqara": "\u05dc\u05d0 \u05e9\u05e2\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9 \u05d0\u05e7\u05d0\u05e8\u05d4, \u05d4\u05ea\u05e7\u05df \u05e9\u05d4\u05ea\u05d2\u05dc\u05d4 \u05dc\u05d0 \u05ea\u05d0\u05dd \u05dc\u05e9\u05e2\u05e8\u05d9\u05dd \u05d9\u05d3\u05d5\u05e2\u05d9\u05dd" + }, + "error": { + "discovery_error": "\u05d2\u05d9\u05dc\u05d5\u05d9 \u05e9\u05e2\u05e8 \u05e9\u05d9\u05d5\u05d0\u05de\u05d9 \u05d0\u05e7\u05d0\u05e8\u05d4 \u05e0\u05db\u05e9\u05dc, \u05e0\u05e1\u05d4 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1-IP \u05e9\u05dc \u05d4\u05d4\u05ea\u05e7\u05df \u05d1\u05d5 \u05e4\u05d5\u05e2\u05dc HomeAssistant \u05db\u05de\u05de\u05e9\u05e7", + "invalid_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd, \u05e8\u05d0\u05d4 https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", + "invalid_interface": "\u05de\u05de\u05e9\u05e7 \u05e8\u05e9\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "invalid_key": "\u05de\u05e4\u05ea\u05d7 \u05e9\u05e2\u05e8 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "invalid_mac": "\u05db\u05ea\u05d5\u05d1\u05ea Mac \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05ea" }, "flow_title": "{name}", "step": { "select": { "data": { "select_ip": "\u05db\u05ea\u05d5\u05d1\u05ea IP" - } + }, + "description": "\u05d9\u05e9 \u05dc\u05d4\u05e4\u05e2\u05d9\u05dc \u05d0\u05ea \u05d4\u05d4\u05ea\u05e7\u05e0\u05d4 \u05e9\u05d5\u05d1 \u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d7\u05d1\u05e8 \u05e9\u05e2\u05e8\u05d9\u05dd \u05e0\u05d5\u05e1\u05e4\u05d9\u05dd", + "title": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05e9\u05e2\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9 \u05d0\u05e7\u05d0\u05e8\u05d4 \u05e9\u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d7\u05d1\u05e8" }, "settings": { - "description": "\u05e0\u05d9\u05ea\u05df \u05dc\u05d0\u05d7\u05d6\u05e8 \u05d0\u05ea \u05d4\u05de\u05e4\u05ea\u05d7 (\u05e1\u05d9\u05e1\u05de\u05d4) \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05d4\u05d3\u05e8\u05db\u05d4 \u05d6\u05d5: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. \u05d0\u05dd \u05d4\u05de\u05e4\u05ea\u05d7 \u05d0\u05d9\u05e0\u05d5 \u05de\u05e1\u05d5\u05e4\u05e7, \u05e8\u05e7 \u05d7\u05d9\u05d9\u05e9\u05e0\u05d9\u05dd \u05d9\u05d4\u05d9\u05d5 \u05e0\u05d2\u05d9\u05e9\u05d9\u05dd" + "data": { + "key": "\u05de\u05e4\u05ea\u05d7 \u05d4\u05e9\u05e2\u05e8 \u05e9\u05dc\u05da", + "name": "\u05e9\u05dd \u05d4\u05e9\u05e2\u05e8" + }, + "description": "\u05e0\u05d9\u05ea\u05df \u05dc\u05d0\u05d7\u05d6\u05e8 \u05d0\u05ea \u05d4\u05de\u05e4\u05ea\u05d7 (\u05e1\u05d9\u05e1\u05de\u05d4) \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05d4\u05d3\u05e8\u05db\u05d4 \u05d6\u05d5: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. \u05d0\u05dd \u05d4\u05de\u05e4\u05ea\u05d7 \u05d0\u05d9\u05e0\u05d5 \u05de\u05e1\u05d5\u05e4\u05e7, \u05e8\u05e7 \u05d7\u05d9\u05d9\u05e9\u05e0\u05d9\u05dd \u05d9\u05d4\u05d9\u05d5 \u05e0\u05d2\u05d9\u05e9\u05d9\u05dd", + "title": "\u05e9\u05e2\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9 \u05d0\u05e7\u05d0\u05e8\u05d4, \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9\u05d5\u05ea" }, "user": { "data": { - "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)" - } + "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)", + "interface": "\u05de\u05de\u05e9\u05e7 \u05d4\u05e8\u05e9\u05ea \u05d1\u05d5 \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9", + "mac": "\u05db\u05ea\u05d5\u05d1\u05ea Mac (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)" + }, + "description": "\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05dc\u05e9\u05e2\u05e8 \u05e9\u05d9\u05d5\u05d0\u05de\u05d9 \u05d0\u05e7\u05d0\u05e8\u05d4 \u05e9\u05dc\u05da, \u05d0\u05dd \u05db\u05ea\u05d5\u05d1\u05d5\u05ea \u05d4-IP \u05d5\u05d4-MAC \u05d9\u05d5\u05d5\u05ea\u05e8\u05d5 \u05e8\u05d9\u05e7\u05d5\u05ea, \u05e0\u05e2\u05e9\u05d4 \u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d2\u05d9\u05dc\u05d5\u05d9 \u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9", + "title": "\u05e9\u05e2\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9 \u05d0\u05e7\u05d0\u05e8\u05d4" } } } diff --git a/homeassistant/components/zwave_js/translations/he.json b/homeassistant/components/zwave_js/translations/he.json index fae03188b81..041e1cafec6 100644 --- a/homeassistant/components/zwave_js/translations/he.json +++ b/homeassistant/components/zwave_js/translations/he.json @@ -64,5 +64,6 @@ "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05d4\u05e8\u05d7\u05d1\u05d4 \u05de\u05e4\u05e7\u05d7 Z-Wave JS?" } } - } + }, + "title": "Z-Wave JS" } \ No newline at end of file From 6c2a18c3e5046ec070dfef755eac32df507024a9 Mon Sep 17 00:00:00 2001 From: Oliver Ou Date: Sun, 3 Oct 2021 08:41:31 +0800 Subject: [PATCH 0052/1038] Fix Tuya v2 fan percentage (#56954) * fix:Some fans do not have a fan_speed_percent key * fix comment format issue Co-authored-by: erchuan --- homeassistant/components/tuya/fan.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index dcfde0ded0f..15a8e553a10 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -211,7 +211,7 @@ class TuyaHaFan(TuyaHaEntity, FanEntity): return self.tuya_device.status[DPCODE_MODE] @property - def percentage(self) -> int: + def percentage(self) -> int | None: """Return the current speed.""" if not self.is_on: return 0 @@ -228,7 +228,8 @@ class TuyaHaFan(TuyaHaEntity, FanEntity): self.tuya_device.status[DPCODE_AP_FAN_SPEED_ENUM], ) - return self.tuya_device.status[DPCODE_FAN_SPEED] + # some type may not have the fan_speed_percent key + return self.tuya_device.status.get(DPCODE_FAN_SPEED) @property def speed_count(self) -> int: From 0e1630e46d83097c1a920ca032c8a99f48ea0dbd Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 2 Oct 2021 18:58:10 -0600 Subject: [PATCH 0053/1038] Fix incorrect handling of hass.data in WattTime setup (#56971) --- homeassistant/components/watttime/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/watttime/__init__.py b/homeassistant/components/watttime/__init__.py index 6d23182c011..8b3a83aa8d1 100644 --- a/homeassistant/components/watttime/__init__.py +++ b/homeassistant/components/watttime/__init__.py @@ -27,7 +27,8 @@ PLATFORMS: list[str] = ["sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up WattTime from a config entry.""" - hass.data.setdefault(DOMAIN, {entry.entry_id: {DATA_COORDINATOR: {}}}) + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = {} session = aiohttp_client.async_get_clientsession(hass) From 6a1d7c7ad920a26aed73123bd4a52cb09f72d577 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 3 Oct 2021 02:59:38 +0200 Subject: [PATCH 0054/1038] Remove icon if device_class is defined - homekit_controller (#56946) --- .../components/homekit_controller/sensor.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index cac61d59ac4..aaddadd2c53 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -29,9 +29,6 @@ from homeassistant.core import callback from . import KNOWN_DEVICES, CharacteristicEntity, HomeKitEntity -HUMIDITY_ICON = "mdi:water-percent" -TEMP_C_ICON = "mdi:thermometer" -BRIGHTNESS_ICON = "mdi:brightness-6" CO2_ICON = "mdi:molecule-co2" @@ -148,11 +145,6 @@ class HomeKitHumiditySensor(HomeKitEntity, SensorEntity): """Return the name of the device.""" return f"{super().name} Humidity" - @property - def icon(self): - """Return the sensor icon.""" - return HUMIDITY_ICON - @property def native_value(self): """Return the current humidity.""" @@ -174,11 +166,6 @@ class HomeKitTemperatureSensor(HomeKitEntity, SensorEntity): """Return the name of the device.""" return f"{super().name} Temperature" - @property - def icon(self): - """Return the sensor icon.""" - return TEMP_C_ICON - @property def native_value(self): """Return the current temperature in Celsius.""" @@ -200,11 +187,6 @@ class HomeKitLightSensor(HomeKitEntity, SensorEntity): """Return the name of the device.""" return f"{super().name} Light Level" - @property - def icon(self): - """Return the sensor icon.""" - return BRIGHTNESS_ICON - @property def native_value(self): """Return the current light level in lux.""" From 7e5a991de5953dfc8cb4f5317e49ba6d691c37b9 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Sun, 3 Oct 2021 03:05:41 +0200 Subject: [PATCH 0055/1038] Sia package update to 3.0.2 (#56896) * update SIA package * update SIA package to 3.0.2 --- homeassistant/components/sia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sia/manifest.json b/homeassistant/components/sia/manifest.json index 438d63a1830..c6a8e491217 100644 --- a/homeassistant/components/sia/manifest.json +++ b/homeassistant/components/sia/manifest.json @@ -3,7 +3,7 @@ "name": "SIA Alarm Systems", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sia", - "requirements": ["pysiaalarm==3.0.1"], + "requirements": ["pysiaalarm==3.0.2"], "codeowners": ["@eavanvalkenburg"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 05784241afe..411f78865a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1790,7 +1790,7 @@ pysesame2==1.0.1 pysher==1.0.1 # homeassistant.components.sia -pysiaalarm==3.0.1 +pysiaalarm==3.0.2 # homeassistant.components.signal_messenger pysignalclirestapi==0.3.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 72528f615f5..5d5d6f132d7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1058,7 +1058,7 @@ pyserial-asyncio==0.5 pyserial==3.5 # homeassistant.components.sia -pysiaalarm==3.0.1 +pysiaalarm==3.0.2 # homeassistant.components.signal_messenger pysignalclirestapi==0.3.4 From d0827a9129252e53363ca276561eaa0bece3954f Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 2 Oct 2021 21:57:49 -0400 Subject: [PATCH 0056/1038] ZHA support for additional entities on ElectricalMeasurement ZCL cluster (#56909) * Add electrical measurement type state attribute. * Add active_power_max attribute * Skip unsupported attributes on entity update * Fix tests * Create sensor only if the main attribute is supported * Refactor ElectricalMeasurement sensor to use attr specific divisor and multiplier * Multiple entities for electrical measurement cluster * Update discovery tests * Sensor clean up * update tests * Pylint --- .../zha/core/channels/homeautomation.py | 92 +++++- .../components/zha/core/registries.py | 1 - homeassistant/components/zha/sensor.py | 91 ++++-- tests/components/zha/test_channels.py | 13 +- tests/components/zha/test_sensor.py | 287 ++++++++++++++++-- tests/components/zha/zha_devices_list.py | 132 ++++++++ 6 files changed, 546 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py index fc00db4f2d4..e25cc3eb0da 100644 --- a/homeassistant/components/zha/core/channels/homeautomation.py +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -1,12 +1,15 @@ """Home automation channels module for Zigbee Home Automation.""" from __future__ import annotations +import enum + from zigpy.zcl.clusters import homeautomation from .. import registries from ..const import ( CHANNEL_ELECTRICAL_MEASUREMENT, REPORT_CONFIG_DEFAULT, + REPORT_CONFIG_OP, SIGNAL_ATTR_UPDATED, ) from .base import ZigbeeChannel @@ -46,11 +49,36 @@ class ElectricalMeasurementChannel(ZigbeeChannel): CHANNEL_NAME = CHANNEL_ELECTRICAL_MEASUREMENT - REPORT_CONFIG = ({"attr": "active_power", "config": REPORT_CONFIG_DEFAULT},) + class MeasurementType(enum.IntFlag): + """Measurement types.""" + + ACTIVE_MEASUREMENT = 1 + REACTIVE_MEASUREMENT = 2 + APPARENT_MEASUREMENT = 4 + PHASE_A_MEASUREMENT = 8 + PHASE_B_MEASUREMENT = 16 + PHASE_C_MEASUREMENT = 32 + DC_MEASUREMENT = 64 + HARMONICS_MEASUREMENT = 128 + POWER_QUALITY_MEASUREMENT = 256 + + REPORT_CONFIG = ( + {"attr": "active_power", "config": REPORT_CONFIG_OP}, + {"attr": "active_power_max", "config": REPORT_CONFIG_DEFAULT}, + {"attr": "rms_current", "config": REPORT_CONFIG_OP}, + {"attr": "rms_current_max", "config": REPORT_CONFIG_DEFAULT}, + {"attr": "rms_voltage", "config": REPORT_CONFIG_OP}, + {"attr": "rms_voltage_max", "config": REPORT_CONFIG_DEFAULT}, + ) ZCL_INIT_ATTRS = { + "ac_current_divisor": True, + "ac_current_multiplier": True, "ac_power_divisor": True, - "power_divisor": True, "ac_power_multiplier": True, + "ac_voltage_divisor": True, + "ac_voltage_multiplier": True, + "measurement_type": True, + "power_divisor": True, "power_multiplier": True, } @@ -59,29 +87,65 @@ class ElectricalMeasurementChannel(ZigbeeChannel): self.debug("async_update") # This is a polling channel. Don't allow cache. - result = await self.get_attribute_value("active_power", from_cache=False) - if result is not None: - self.async_send_signal( - f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", - 0x050B, - "active_power", - result, - ) + attrs = [ + a["attr"] + for a in self.REPORT_CONFIG + if a["attr"] not in self.cluster.unsupported_attributes + ] + result = await self.get_attributes(attrs, from_cache=False) + if result: + for attr, value in result.items(): + self.async_send_signal( + f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", + self.cluster.attridx.get(attr, attr), + attr, + value, + ) @property - def divisor(self) -> int | None: + def ac_current_divisor(self) -> int: + """Return ac current divisor.""" + return self.cluster.get("ac_current_divisor") or 1 + + @property + def ac_current_multiplier(self) -> int: + """Return ac current multiplier.""" + return self.cluster.get("ac_current_multiplier") or 1 + + @property + def ac_voltage_divisor(self) -> int: + """Return ac voltage divisor.""" + return self.cluster.get("ac_voltage_divisor") or 1 + + @property + def ac_voltage_multiplier(self) -> int: + """Return ac voltage multiplier.""" + return self.cluster.get("ac_voltage_multiplier") or 1 + + @property + def ac_power_divisor(self) -> int: """Return active power divisor.""" return self.cluster.get( - "ac_power_divisor", self.cluster.get("power_divisor", 1) + "ac_power_divisor", self.cluster.get("power_divisor") or 1 ) @property - def multiplier(self) -> int | None: + def ac_power_multiplier(self) -> int: """Return active power divisor.""" return self.cluster.get( - "ac_power_multiplier", self.cluster.get("power_multiplier", 1) + "ac_power_multiplier", self.cluster.get("power_multiplier") or 1 ) + @property + def measurement_type(self) -> str | None: + """Return Measurement type.""" + meas_type = self.cluster.get("measurement_type") + if meas_type is None: + return None + + meas_type = self.MeasurementType(meas_type) + return ", ".join(m.name for m in self.MeasurementType if m in meas_type) + @registries.ZIGBEE_CHANNEL_REGISTRY.register( homeautomation.MeterIdentification.cluster_id diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 203867db17d..2bf324e3007 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -73,7 +73,6 @@ SINGLE_INPUT_CLUSTER_DEVICE_CLASS = { zcl.clusters.general.MultistateInput.cluster_id: SENSOR, zcl.clusters.general.OnOff.cluster_id: SWITCH, zcl.clusters.general.PowerConfiguration.cluster_id: SENSOR, - zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id: SENSOR, zcl.clusters.hvac.Fan.cluster_id: FAN, zcl.clusters.measurement.CarbonDioxideConcentration.cluster_id: SENSOR, zcl.clusters.measurement.CarbonMonoxideConcentration.cluster_id: SENSOR, diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 342fbd58d89..b2cc414ad5f 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -9,6 +9,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_CO, DEVICE_CLASS_CO2, + DEVICE_CLASS_CURRENT, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, @@ -24,6 +25,8 @@ from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, DEVICE_CLASS_ENERGY, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, LIGHT_LUX, PERCENTAGE, @@ -129,6 +132,24 @@ class Sensor(ZhaEntity, SensorEntity): super().__init__(unique_id, zha_device, channels, **kwargs) self._channel: ChannelType = channels[0] + @classmethod + def create_entity( + cls, + unique_id: str, + zha_device: ZhaDeviceType, + channels: list[ChannelType], + **kwargs, + ) -> ZhaEntity | None: + """Entity Factory. + + Return entity if it is a supported configuration, otherwise return None + """ + channel = channels[0] + if cls.SENSOR_ATTR in channel.cluster.unsupported_attributes: + return None + + return cls(unique_id, zha_device, channels, **kwargs) + async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" await super().async_added_to_hass() @@ -220,7 +241,7 @@ class Battery(Sensor): return state_attrs -@STRICT_MATCH(channel_names=CHANNEL_ELECTRICAL_MEASUREMENT) +@MULTI_MATCH(channel_names=CHANNEL_ELECTRICAL_MEASUREMENT) class ElectricalMeasurement(Sensor): """Active power measurement.""" @@ -228,16 +249,32 @@ class ElectricalMeasurement(Sensor): _device_class = DEVICE_CLASS_POWER _state_class = STATE_CLASS_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.""" + attrs = {} + if self._channel.measurement_type is not None: + attrs["measurement_type"] = self._channel.measurement_type + + max_attr_name = f"{self.SENSOR_ATTR}_max" + if (max_v := self._channel.cluster.get(max_attr_name)) is not None: + attrs[max_attr_name] = str(self.formatter(max_v)) + + return attrs + def formatter(self, value: int) -> int | float: """Return 'normalized' value.""" - value = value * self._channel.multiplier / self._channel.divisor - if value < 100 and self._channel.divisor > 1: + multiplier = getattr(self._channel, f"{self._div_mul_prefix}_multiplier") + divisor = getattr(self._channel, f"{self._div_mul_prefix}_divisor") + value = float(value * multiplier) / divisor + if value < 100 and divisor > 1: return round(value, self._decimals) return round(value) @@ -248,6 +285,36 @@ class ElectricalMeasurement(Sensor): await super().async_update() +@MULTI_MATCH(channel_names=CHANNEL_ELECTRICAL_MEASUREMENT) +class ElectricalMeasurementRMSCurrent(ElectricalMeasurement, id_suffix="rms_current"): + """RMS current measurement.""" + + SENSOR_ATTR = "rms_current" + _device_class = DEVICE_CLASS_CURRENT + _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"): + """RMS Voltage measurement.""" + + SENSOR_ATTR = "rms_voltage" + _device_class = DEVICE_CLASS_CURRENT + _unit = ELECTRIC_POTENTIAL_VOLT + _div_mul_prefix = "ac_voltage" + + @property + def should_poll(self) -> bool: + """Poll indirectly by ElectricalMeasurementSensor.""" + return False + + @STRICT_MATCH(generic_ids=CHANNEL_ST_HUMIDITY_CLUSTER) @STRICT_MATCH(channel_names=CHANNEL_HUMIDITY) class Humidity(Sensor): @@ -298,24 +365,6 @@ class SmartEnergyMetering(Sensor): 0x0C: f"MJ/{TIME_SECONDS}", } - @classmethod - def create_entity( - cls, - unique_id: str, - zha_device: ZhaDeviceType, - channels: list[ChannelType], - **kwargs, - ) -> ZhaEntity | None: - """Entity Factory. - - Return entity if it is a supported configuration, otherwise return None - """ - se_channel = channels[0] - if cls.SENSOR_ATTR in se_channel.cluster.unsupported_attributes: - return None - - return cls(unique_id, zha_device, channels, **kwargs) - def formatter(self, value: int) -> int | float: """Pass through channel formatter.""" return self._channel.demand_formatter(value) diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index c1e60db31dd..e2543181a1a 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -152,7 +152,18 @@ async def poll_control_device(zha_device_restored, zigpy_device_mock): (0x0405, 1, {"measured_value"}), (0x0406, 1, {"occupancy"}), (0x0702, 1, {"instantaneous_demand"}), - (0x0B04, 1, {"active_power"}), + ( + 0x0B04, + 1, + { + "active_power", + "active_power_max", + "rms_current", + "rms_current_max", + "rms_voltage", + "rms_voltage_max", + }, + ), ], ) async def test_in_channel_config( diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index 21731da72e6..918876fe448 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -1,5 +1,5 @@ """Test zha sensor.""" -from unittest import mock +import math import pytest import zigpy.profiles.zha @@ -9,6 +9,7 @@ import zigpy.zcl.clusters.measurement as measurement import zigpy.zcl.clusters.smartenergy as smartenergy from homeassistant.components.sensor import DOMAIN +from homeassistant.components.zha.core.const import ZHA_CHANNEL_READS_PER_REQ import homeassistant.config as config_util from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -17,6 +18,8 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, DEVICE_CLASS_ENERGY, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, LIGHT_LUX, PERCENTAGE, @@ -30,6 +33,7 @@ from homeassistant.const import ( VOLUME_CUBIC_METERS, ) from homeassistant.helpers import restore_state +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.util import dt as dt_util from .common import ( @@ -40,11 +44,52 @@ from .common import ( send_attribute_report, send_attributes_report, ) -from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE +from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE ENTITY_ID_PREFIX = "sensor.fakemanufacturer_fakemodel_e769900a_{}" +@pytest.fixture +async def elec_measurement_zigpy_dev(hass, zigpy_device_mock): + """Electric Measurement zigpy device.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [ + general.Basic.cluster_id, + homeautomation.ElectricalMeasurement.cluster_id, + ], + SIG_EP_OUTPUT: [], + SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.SIMPLE_SENSOR, + SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, + } + }, + ) + zigpy_device.node_desc.mac_capability_flags |= 0b_0000_0100 + zigpy_device.endpoints[1].electrical_measurement.PLUGGED_ATTR_READS = { + "ac_current_divisor": 10, + "ac_current_multiplier": 1, + "ac_power_divisor": 10, + "ac_power_multiplier": 1, + "ac_voltage_divisor": 10, + "ac_voltage_multiplier": 1, + "measurement_type": 8, + "power_divisor": 10, + "power_multiplier": 1, + } + return zigpy_device + + +@pytest.fixture +async def elec_measurement_zha_dev(elec_measurement_zigpy_dev, zha_device_joined): + """Electric Measurement ZHA device.""" + + zha_dev = await zha_device_joined(elec_measurement_zigpy_dev) + zha_dev.available = True + return zha_dev + + async def async_test_humidity(hass, cluster, entity_id): """Test humidity sensor.""" await send_attributes_report(hass, cluster, {1: 1, 0: 1000, 2: 100}) @@ -109,26 +154,60 @@ async def async_test_smart_energy_summation(hass, cluster, entity_id): async def async_test_electrical_measurement(hass, cluster, entity_id): """Test electrical measurement sensor.""" - with mock.patch( - ( - "homeassistant.components.zha.core.channels.homeautomation" - ".ElectricalMeasurementChannel.divisor" - ), - new_callable=mock.PropertyMock, - ) as divisor_mock: - divisor_mock.return_value = 1 - await send_attributes_report(hass, cluster, {0: 1, 1291: 100, 10: 1000}) - assert_state(hass, entity_id, "100", POWER_WATT) + # update divisor cached value + await send_attributes_report(hass, cluster, {"ac_power_divisor": 1}) + await send_attributes_report(hass, cluster, {0: 1, 1291: 100, 10: 1000}) + assert_state(hass, entity_id, "100", POWER_WATT) - await send_attributes_report(hass, cluster, {0: 1, 1291: 99, 10: 1000}) - assert_state(hass, entity_id, "99", POWER_WATT) + await send_attributes_report(hass, cluster, {0: 1, 1291: 99, 10: 1000}) + assert_state(hass, entity_id, "99", POWER_WATT) - divisor_mock.return_value = 10 - await send_attributes_report(hass, cluster, {0: 1, 1291: 1000, 10: 5000}) - assert_state(hass, entity_id, "100", POWER_WATT) + await send_attributes_report(hass, cluster, {"ac_power_divisor": 10}) + await send_attributes_report(hass, cluster, {0: 1, 1291: 1000, 10: 5000}) + assert_state(hass, entity_id, "100", POWER_WATT) - await send_attributes_report(hass, cluster, {0: 1, 1291: 99, 10: 5000}) - assert_state(hass, entity_id, "9.9", POWER_WATT) + await send_attributes_report(hass, cluster, {0: 1, 1291: 99, 10: 5000}) + assert_state(hass, entity_id, "9.9", POWER_WATT) + + assert "active_power_max" not in hass.states.get(entity_id).attributes + await send_attributes_report(hass, cluster, {0: 1, 0x050D: 88, 10: 5000}) + assert hass.states.get(entity_id).attributes["active_power_max"] == "8.8" + + +async def async_test_em_rms_current(hass, cluster, entity_id): + """Test electrical measurement RMS Current sensor.""" + + await send_attributes_report(hass, cluster, {0: 1, 0x0508: 1234, 10: 1000}) + assert_state(hass, entity_id, "1.2", ELECTRIC_CURRENT_AMPERE) + + await send_attributes_report(hass, cluster, {"ac_current_divisor": 10}) + await send_attributes_report(hass, cluster, {0: 1, 0x0508: 236, 10: 1000}) + assert_state(hass, entity_id, "23.6", ELECTRIC_CURRENT_AMPERE) + + await send_attributes_report(hass, cluster, {0: 1, 0x0508: 1236, 10: 1000}) + assert_state(hass, entity_id, "124", ELECTRIC_CURRENT_AMPERE) + + assert "rms_current_max" not in hass.states.get(entity_id).attributes + await send_attributes_report(hass, cluster, {0: 1, 0x050A: 88, 10: 5000}) + assert hass.states.get(entity_id).attributes["rms_current_max"] == "8.8" + + +async def async_test_em_rms_voltage(hass, cluster, entity_id): + """Test electrical measurement RMS Voltage sensor.""" + + await send_attributes_report(hass, cluster, {0: 1, 0x0505: 1234, 10: 1000}) + assert_state(hass, entity_id, "123", ELECTRIC_POTENTIAL_VOLT) + + await send_attributes_report(hass, cluster, {0: 1, 0x0505: 234, 10: 1000}) + assert_state(hass, entity_id, "23.4", ELECTRIC_POTENTIAL_VOLT) + + await send_attributes_report(hass, cluster, {"ac_voltage_divisor": 100}) + await send_attributes_report(hass, cluster, {0: 1, 0x0505: 2236, 10: 1000}) + assert_state(hass, entity_id, "22.4", ELECTRIC_POTENTIAL_VOLT) + + assert "rms_voltage_max" not in hass.states.get(entity_id).attributes + await send_attributes_report(hass, cluster, {0: 1, 0x0507: 888, 10: 5000}) + assert hass.states.get(entity_id).attributes["rms_voltage_max"] == "8.9" async def async_test_powerconfiguration(hass, cluster, entity_id): @@ -211,9 +290,25 @@ async def async_test_powerconfiguration(hass, cluster, entity_id): homeautomation.ElectricalMeasurement.cluster_id, "electrical_measurement", async_test_electrical_measurement, - 1, - None, - None, + 6, + {"ac_power_divisor": 1000, "ac_power_multiplier": 1}, + {"rms_current", "rms_voltage"}, + ), + ( + homeautomation.ElectricalMeasurement.cluster_id, + "electrical_measurement_rms_current", + async_test_em_rms_current, + 6, + {"ac_current_divisor": 1000, "ac_current_multiplier": 1}, + {"active_power", "rms_voltage"}, + ), + ( + homeautomation.ElectricalMeasurement.cluster_id, + "electrical_measurement_rms_voltage", + async_test_em_rms_voltage, + 6, + {"ac_voltage_divisor": 10, "ac_voltage_multiplier": 1}, + {"active_power", "rms_current"}, ), ( general.PowerConfiguration.cluster_id, @@ -255,7 +350,10 @@ async def test_sensor( if unsupported_attrs: for attr in unsupported_attrs: cluster.add_unsupported_attribute(attr) - if cluster_id == smartenergy.Metering.cluster_id: + if cluster_id in ( + smartenergy.Metering.cluster_id, + homeautomation.ElectricalMeasurement.cluster_id, + ): # this one is mains powered zigpy_device.node_desc.mac_capability_flags |= 0b_0000_0100 cluster.PLUGGED_ATTR_READS = read_plug @@ -432,35 +530,60 @@ async def test_electrical_measurement_init( assert int(hass.states.get(entity_id).state) == 100 channel = zha_device.channels.pools[0].all_channels["1:0x0b04"] - assert channel.divisor == 1 - assert channel.multiplier == 1 + assert channel.ac_power_divisor == 1 + assert channel.ac_power_multiplier == 1 # update power divisor await send_attributes_report(hass, cluster, {0: 1, 1291: 20, 0x0403: 5, 10: 1000}) - assert channel.divisor == 5 - assert channel.multiplier == 1 + assert channel.ac_power_divisor == 5 + assert channel.ac_power_multiplier == 1 assert hass.states.get(entity_id).state == "4.0" await send_attributes_report(hass, cluster, {0: 1, 1291: 30, 0x0605: 10, 10: 1000}) - assert channel.divisor == 10 - assert channel.multiplier == 1 + assert channel.ac_power_divisor == 10 + assert channel.ac_power_multiplier == 1 assert hass.states.get(entity_id).state == "3.0" # update power multiplier await send_attributes_report(hass, cluster, {0: 1, 1291: 20, 0x0402: 6, 10: 1000}) - assert channel.divisor == 10 - assert channel.multiplier == 6 + assert channel.ac_power_divisor == 10 + assert channel.ac_power_multiplier == 6 assert hass.states.get(entity_id).state == "12.0" await send_attributes_report(hass, cluster, {0: 1, 1291: 30, 0x0604: 20, 10: 1000}) - assert channel.divisor == 10 - assert channel.multiplier == 20 + assert channel.ac_power_divisor == 10 + assert channel.ac_power_multiplier == 20 assert hass.states.get(entity_id).state == "60.0" @pytest.mark.parametrize( "cluster_id, unsupported_attributes, entity_ids, missing_entity_ids", ( + ( + homeautomation.ElectricalMeasurement.cluster_id, + {"rms_voltage", "rms_current"}, + {"electrical_measurement"}, + { + "electrical_measurement_rms_voltage", + "electrical_measurement_rms_current", + }, + ), + ( + homeautomation.ElectricalMeasurement.cluster_id, + {"rms_current"}, + {"electrical_measurement_rms_voltage", "electrical_measurement"}, + {"electrical_measurement_rms_current"}, + ), + ( + homeautomation.ElectricalMeasurement.cluster_id, + set(), + { + "electrical_measurement_rms_voltage", + "electrical_measurement", + "electrical_measurement_rms_current", + }, + set(), + ), ( smartenergy.Metering.cluster_id, { @@ -650,3 +773,101 @@ async def test_se_summation_uom( await zha_device_joined(zigpy_device) assert_state(hass, entity_id, expected_state, expected_uom) + + +@pytest.mark.parametrize( + "raw_measurement_type, expected_type", + ( + (1, "ACTIVE_MEASUREMENT"), + (8, "PHASE_A_MEASUREMENT"), + (9, "ACTIVE_MEASUREMENT, PHASE_A_MEASUREMENT"), + ( + 15, + "ACTIVE_MEASUREMENT, REACTIVE_MEASUREMENT, APPARENT_MEASUREMENT, PHASE_A_MEASUREMENT", + ), + ), +) +async def test_elec_measurement_sensor_type( + hass, + elec_measurement_zigpy_dev, + raw_measurement_type, + expected_type, + zha_device_joined, +): + """Test zha electrical measurement sensor type.""" + + entity_id = ENTITY_ID_PREFIX.format("electrical_measurement") + zigpy_dev = elec_measurement_zigpy_dev + zigpy_dev.endpoints[1].electrical_measurement.PLUGGED_ATTR_READS[ + "measurement_type" + ] = raw_measurement_type + + await zha_device_joined(zigpy_dev) + + state = hass.states.get(entity_id) + assert state is not None + assert state.attributes["measurement_type"] == expected_type + + +@pytest.mark.parametrize( + "supported_attributes", + ( + set(), + { + "active_power", + "active_power_max", + "rms_current", + "rms_current_max", + "rms_voltage", + "rms_voltage_max", + }, + { + "active_power", + }, + { + "active_power", + "active_power_max", + }, + { + "rms_current", + "rms_current_max", + }, + { + "rms_voltage", + "rms_voltage_max", + }, + ), +) +async def test_elec_measurement_skip_unsupported_attribute( + hass, + elec_measurement_zha_dev, + supported_attributes, +): + """Test zha electrical measurement skipping update of unsupported attributes.""" + + entity_id = ENTITY_ID_PREFIX.format("electrical_measurement") + zha_dev = elec_measurement_zha_dev + + cluster = zha_dev.device.endpoints[1].electrical_measurement + + all_attrs = { + "active_power", + "active_power_max", + "rms_current", + "rms_current_max", + "rms_voltage", + "rms_voltage_max", + } + for attr in all_attrs - supported_attributes: + cluster.add_unsupported_attribute(attr) + cluster.read_attributes.reset_mock() + + await async_update_entity(hass, entity_id) + await hass.async_block_till_done() + assert cluster.read_attributes.call_count == math.ceil( + len(supported_attributes) / ZHA_CHANNEL_READS_PER_REQ + ) + read_attrs = { + a for call in cluster.read_attributes.call_args_list for a in call[0][0] + } + assert read_attrs == supported_attributes diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index 531e9649ec3..6276aa12068 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -117,6 +117,8 @@ DEVICES = [ }, DEV_SIG_ENTITIES: [ "sensor.centralite_3210_l_77665544_electrical_measurement", + "sensor.centralite_3210_l_77665544_electrical_measurement_rms_current", + "sensor.centralite_3210_l_77665544_electrical_measurement_rms_voltage", "sensor.centralite_3210_l_77665544_smartenergy_metering", "sensor.centralite_3210_l_77665544_smartenergy_metering_summation_delivered", "switch.centralite_3210_l_77665544_on_off", @@ -142,6 +144,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_electrical_measurement_rms_current", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_electrical_measurement_rms_voltage", + }, }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], SIG_MANUFACTURER: "CentraLite", @@ -1436,6 +1448,8 @@ DEVICES = [ "sensor.lumi_lumi_plug_maus01_77665544_analog_input", "sensor.lumi_lumi_plug_maus01_77665544_analog_input_2", "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement", + "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_rms_current", + "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_rms_voltage", "switch.lumi_lumi_plug_maus01_77665544_on_off", ], DEV_SIG_ENT_MAP: { @@ -1459,6 +1473,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_rms_current", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_rms_voltage", + }, ("binary_sensor", "00:11:22:33:44:55:66:77-100-15"): { DEV_SIG_CHANNELS: ["binary_input"], DEV_SIG_ENT_MAP_CLASS: "BinaryInput", @@ -1493,6 +1517,8 @@ DEVICES = [ "light.lumi_lumi_relay_c2acn01_77665544_on_off", "light.lumi_lumi_relay_c2acn01_77665544_on_off_2", "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement", + "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_rms_current", + "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_rms_voltage", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { @@ -1505,6 +1531,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_rms_current", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_rms_voltage", + }, ("light", "00:11:22:33:44:55:66:77-2"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", @@ -2566,6 +2602,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "light.osram_lightify_rt_tunable_white_77665544_level_light_color_on_off", "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement", + "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_rms_current", + "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_rms_voltage", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-3"): { @@ -2578,6 +2616,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-3-2820-rms_current"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_rms_current", + }, + ("sensor", "00:11:22:33:44:55:66:77-3-2820-rms_voltage"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_rms_voltage", + }, }, DEV_SIG_EVT_CHANNELS: ["3:0x0019"], SIG_MANUFACTURER: "OSRAM", @@ -2598,6 +2646,8 @@ DEVICES = [ }, DEV_SIG_ENTITIES: [ "sensor.osram_plug_01_77665544_electrical_measurement", + "sensor.osram_plug_01_77665544_electrical_measurement_rms_current", + "sensor.osram_plug_01_77665544_electrical_measurement_rms_voltage", "switch.osram_plug_01_77665544_on_off", ], DEV_SIG_ENT_MAP: { @@ -2611,6 +2661,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-3-2820-rms_current"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_electrical_measurement_rms_current", + }, + ("sensor", "00:11:22:33:44:55:66:77-3-2820-rms_voltage"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_electrical_measurement_rms_voltage", + }, }, DEV_SIG_EVT_CHANNELS: ["3:0x0019"], SIG_MANUFACTURER: "OSRAM", @@ -2870,6 +2930,8 @@ DEVICES = [ }, DEV_SIG_ENTITIES: [ "sensor.securifi_ltd_unk_model_77665544_electrical_measurement", + "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_rms_current", + "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_rms_voltage", "switch.securifi_ltd_unk_model_77665544_on_off", ], DEV_SIG_ENT_MAP: { @@ -2883,6 +2945,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_rms_current", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_rms_voltage", + }, }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0019"], SIG_MANUFACTURER: "Securifi Ltd.", @@ -2948,6 +3020,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "light.sercomm_corp_sz_esw01_77665544_on_off", "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement", + "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_rms_current", + "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_rms_voltage", "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering", "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering_summation_delivered", ], @@ -2972,6 +3046,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_rms_current", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_rms_voltage", + }, }, DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006"], SIG_MANUFACTURER: "Sercomm Corp.", @@ -3035,6 +3119,8 @@ DEVICES = [ }, DEV_SIG_ENTITIES: [ "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement", + "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_rms_current", + "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_rms_voltage", "switch.sinope_technologies_rm3250zb_77665544_on_off", ], DEV_SIG_ENT_MAP: { @@ -3048,6 +3134,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_rms_current", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_rms_voltage", + }, }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], SIG_MANUFACTURER: "Sinope Technologies", @@ -3075,6 +3171,8 @@ DEVICES = [ DEV_SIG_ENTITIES: [ "climate.sinope_technologies_th1123zb_77665544_thermostat", "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement", + "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_rms_current", + "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_rms_voltage", "sensor.sinope_technologies_th1123zb_77665544_temperature", ], DEV_SIG_ENT_MAP: { @@ -3093,6 +3191,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_rms_current", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_rms_voltage", + }, }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], SIG_MANUFACTURER: "Sinope Technologies", @@ -3120,6 +3228,8 @@ DEVICES = [ }, DEV_SIG_ENTITIES: [ "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement", + "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_rms_current", + "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_rms_voltage", "sensor.sinope_technologies_th1124zb_77665544_temperature", "climate.sinope_technologies_th1124zb_77665544_thermostat", ], @@ -3139,6 +3249,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_rms_current", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_rms_voltage", + }, }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], SIG_MANUFACTURER: "Sinope Technologies", @@ -3159,6 +3279,8 @@ DEVICES = [ }, DEV_SIG_ENTITIES: [ "sensor.smartthings_outletv4_77665544_electrical_measurement", + "sensor.smartthings_outletv4_77665544_electrical_measurement_rms_current", + "sensor.smartthings_outletv4_77665544_electrical_measurement_rms_voltage", "switch.smartthings_outletv4_77665544_on_off", ], DEV_SIG_ENT_MAP: { @@ -3172,6 +3294,16 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_electrical_measurement", }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_electrical_measurement_rms_current", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { + DEV_SIG_CHANNELS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_electrical_measurement_rms_voltage", + }, ("binary_sensor", "00:11:22:33:44:55:66:77-1-15"): { DEV_SIG_CHANNELS: ["binary_input"], DEV_SIG_ENT_MAP_CLASS: "BinaryInput", From 1aeab65f5664347207d9aa3439f5b5d640a723cf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 2 Oct 2021 21:08:31 -1000 Subject: [PATCH 0057/1038] Fix yeelight state when controlled outside of Home Assistant (#56964) --- homeassistant/components/yeelight/__init__.py | 24 +- homeassistant/components/yeelight/light.py | 69 +++-- tests/components/yeelight/test_init.py | 5 +- tests/components/yeelight/test_light.py | 259 ++++++++++-------- 4 files changed, 201 insertions(+), 156 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index a4ff947191e..e7f7b06f58f 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -36,6 +36,9 @@ from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) +STATE_CHANGE_TIME = 0.25 # seconds + + DOMAIN = "yeelight" DATA_YEELIGHT = DOMAIN DATA_UPDATED = "yeelight_{}_data_updated" @@ -546,6 +549,17 @@ class YeelightScanner: self._async_stop_scan() +def update_needs_bg_power_workaround(data): + """Check if a push update needs the bg_power workaround. + + Some devices will push the incorrect state for bg_power. + + To work around this any time we are pushed an update + with bg_power, we force poll state which will be correct. + """ + return "bg_power" in data + + class YeelightDevice: """Represents single Yeelight device.""" @@ -692,12 +706,18 @@ class YeelightDevice: await self._async_update_properties() async_dispatcher_send(self._hass, DATA_UPDATED.format(self._host)) + async def _async_forced_update(self, _now): + """Call a forced update.""" + await self.async_update(True) + @callback def async_update_callback(self, data): """Update push from device.""" was_available = self._available self._available = data.get(KEY_CONNECTED, True) - if self._did_first_update and not was_available and self._available: + if update_needs_bg_power_workaround(data) or ( + self._did_first_update and not was_available and self._available + ): # On reconnect the properties may be out of sync # # We need to make sure the DEVICE_INITIALIZED dispatcher is setup @@ -708,7 +728,7 @@ class YeelightDevice: # to be called when async_setup_entry reaches the end of the # function # - asyncio.create_task(self.async_update(True)) + async_call_later(self._hass, STATE_CHANGE_TIME, self._async_forced_update) async_dispatcher_send(self._hass, DATA_UPDATED.format(self._host)) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 3f5bb29bab7..69dde0e75b6 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -1,7 +1,6 @@ """Light platform support for yeelight.""" from __future__ import annotations -import asyncio import logging import math @@ -210,9 +209,6 @@ SERVICE_SCHEMA_SET_AUTO_DELAY_OFF_SCENE = { } -STATE_CHANGE_TIME = 0.25 # seconds - - @callback def _transitions_config_parser(transitions): """Parse transitions config into initialized objects.""" @@ -252,13 +248,15 @@ def _async_cmd(func): # A network error happened, the bulb is likely offline now self.device.async_mark_unavailable() self.async_write_ha_state() + exc_message = str(ex) or type(ex) raise HomeAssistantError( - f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {ex}" + f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {exc_message}" ) from ex except BULB_EXCEPTIONS as ex: # The bulb likely responded but had an error + exc_message = str(ex) or type(ex) raise HomeAssistantError( - f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {ex}" + f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {exc_message}" ) from ex return _async_wrap @@ -762,11 +760,6 @@ class YeelightGenericLight(YeelightEntity, LightEntity): if self.config[CONF_SAVE_ON_CHANGE] and (brightness or colortemp or rgb): await self.async_set_default() - # Some devices (mainly nightlights) will not send back the on state so we need to force a refresh - await asyncio.sleep(STATE_CHANGE_TIME) - if not self.is_on: - await self.device.async_update(True) - @_async_cmd async def _async_turn_off(self, duration) -> None: """Turn off with a given transition duration wrapped with _async_cmd.""" @@ -782,10 +775,6 @@ class YeelightGenericLight(YeelightEntity, LightEntity): duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s await self._async_turn_off(duration) - # Some devices will not send back the off state so we need to force a refresh - await asyncio.sleep(STATE_CHANGE_TIME) - if self.is_on: - await self.device.async_update(True) @_async_cmd async def async_set_mode(self, mode: str): @@ -850,10 +839,8 @@ class YeelightNightLightSupport: return PowerMode.NORMAL -class YeelightColorLightWithoutNightlightSwitch( - YeelightColorLightSupport, YeelightGenericLight -): - """Representation of a Color Yeelight light.""" +class YeelightWithoutNightlightSwitchMixIn: + """A mix-in for yeelights without a nightlight switch.""" @property def _brightness_property(self): @@ -861,9 +848,25 @@ class YeelightColorLightWithoutNightlightSwitch( # want to "current_brightness" since it will check # "bg_power" and main light could still be on if self.device.is_nightlight_enabled: - return "current_brightness" + return "nl_br" return super()._brightness_property + @property + def color_temp(self) -> int: + """Return the color temperature.""" + if self.device.is_nightlight_enabled: + # Enabling the nightlight locks the colortemp to max + return self._max_mireds + return super().color_temp + + +class YeelightColorLightWithoutNightlightSwitch( + YeelightColorLightSupport, + YeelightWithoutNightlightSwitchMixIn, + YeelightGenericLight, +): + """Representation of a Color Yeelight light.""" + class YeelightColorLightWithNightlightSwitch( YeelightNightLightSupport, YeelightColorLightSupport, YeelightGenericLight @@ -880,19 +883,12 @@ class YeelightColorLightWithNightlightSwitch( class YeelightWhiteTempWithoutNightlightSwitch( - YeelightWhiteTempLightSupport, YeelightGenericLight + YeelightWhiteTempLightSupport, + YeelightWithoutNightlightSwitchMixIn, + YeelightGenericLight, ): """White temp light, when nightlight switch is not set to light.""" - @property - def _brightness_property(self): - # 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 - if self.device.is_nightlight_enabled: - return "current_brightness" - return super()._brightness_property - class YeelightWithNightLight( YeelightNightLightSupport, YeelightWhiteTempLightSupport, YeelightGenericLight @@ -911,6 +907,9 @@ class YeelightWithNightLight( class YeelightNightLightMode(YeelightGenericLight): """Representation of a Yeelight when in nightlight mode.""" + _attr_color_mode = COLOR_MODE_BRIGHTNESS + _attr_supported_color_modes = {COLOR_MODE_BRIGHTNESS} + @property def unique_id(self) -> str: """Return a unique ID.""" @@ -941,8 +940,9 @@ class YeelightNightLightMode(YeelightGenericLight): return PowerMode.MOONLIGHT @property - def _predefined_effects(self): - return YEELIGHT_TEMP_ONLY_EFFECT_LIST + def supported_features(self): + """Flag no supported features.""" + return 0 class YeelightNightLightModeWithAmbientSupport(YeelightNightLightMode): @@ -962,11 +962,6 @@ class YeelightNightLightModeWithoutBrightnessControl(YeelightNightLightMode): _attr_color_mode = COLOR_MODE_ONOFF _attr_supported_color_modes = {COLOR_MODE_ONOFF} - @property - def supported_features(self): - """Flag no supported features.""" - return 0 - class YeelightWithAmbientWithoutNightlight(YeelightWhiteTempWithoutNightlightSwitch): """Representation of a Yeelight which has ambilight support. diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index cee798308c4..aed2025ab5d 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -13,6 +13,7 @@ from homeassistant.components.yeelight import ( DATA_DEVICE, DOMAIN, NIGHTLIGHT_SWITCH_TYPE_LIGHT, + STATE_CHANGE_TIME, ) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( @@ -458,6 +459,8 @@ async def test_connection_dropped_resyncs_properties(hass: HomeAssistant): await hass.async_block_till_done() assert len(mocked_bulb.async_get_properties.mock_calls) == 1 mocked_bulb._async_callback({KEY_CONNECTED: True}) - await hass.async_block_till_done() + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=STATE_CHANGE_TIME) + ) await hass.async_block_till_done() assert len(mocked_bulb.async_get_properties.mock_calls) == 2 diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index f4cae17a30c..fd6e12f2635 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -545,25 +545,27 @@ async def test_update_errors(hass: HomeAssistant, caplog): # Timeout usually means the bulb is overloaded with commands # but will still respond eventually. - mocked_bulb.async_get_properties = AsyncMock(side_effect=asyncio.TimeoutError) - await hass.services.async_call( - "light", - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: ENTITY_LIGHT}, - blocking=True, - ) + mocked_bulb.async_turn_off = AsyncMock(side_effect=asyncio.TimeoutError) + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "light", + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT}, + blocking=True, + ) assert hass.states.get(ENTITY_LIGHT).state == STATE_ON # socket.error usually means the bulb dropped the connection # or lost wifi, then came back online and forced the existing # connection closed with a TCP RST - mocked_bulb.async_get_properties = AsyncMock(side_effect=socket.error) - await hass.services.async_call( - "light", - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: ENTITY_LIGHT}, - blocking=True, - ) + mocked_bulb.async_turn_off = AsyncMock(side_effect=socket.error) + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + "light", + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT}, + blocking=True, + ) assert hass.states.get(ENTITY_LIGHT).state == STATE_UNAVAILABLE @@ -572,6 +574,7 @@ async def test_state_already_set_avoid_ratelimit(hass: HomeAssistant): mocked_bulb = _mocked_bulb() properties = {**PROPERTIES} properties.pop("active_mode") + properties.pop("nl_br") properties["color_mode"] = "3" # HSV mocked_bulb.last_properties = properties mocked_bulb.bulb_type = BulbType.Color @@ -579,7 +582,9 @@ async def test_state_already_set_avoid_ratelimit(hass: HomeAssistant): domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: False} ) config_entry.add_to_hass(hass) - with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): + with _patch_discovery(), _patch_discovery_interval(), patch( + f"{MODULE}.AsyncBulb", return_value=mocked_bulb + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() # We use asyncio.create_task now to avoid @@ -623,7 +628,7 @@ async def test_state_already_set_avoid_ratelimit(hass: HomeAssistant): SERVICE_TURN_ON, { ATTR_ENTITY_ID: ENTITY_LIGHT, - ATTR_BRIGHTNESS_PCT: PROPERTIES["current_brightness"], + ATTR_BRIGHTNESS_PCT: PROPERTIES["bright"], }, blocking=True, ) @@ -696,9 +701,10 @@ async def test_device_types(hass: HomeAssistant, caplog): bulb_type, model, target_properties, - nightlight_properties=None, + nightlight_entity_properties=None, name=UNIQUE_FRIENDLY_NAME, entity_id=ENTITY_LIGHT, + nightlight_mode_properties=None, ): config_entry = MockConfigEntry( domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: False} @@ -708,6 +714,9 @@ async def test_device_types(hass: HomeAssistant, caplog): mocked_bulb.bulb_type = bulb_type model_specs = _MODEL_SPECS.get(model) type(mocked_bulb).get_model_specs = MagicMock(return_value=model_specs) + original_nightlight_brightness = mocked_bulb.last_properties["nl_br"] + + mocked_bulb.last_properties["nl_br"] = "0" await _async_setup(config_entry) state = hass.states.get(entity_id) @@ -715,41 +724,58 @@ async def test_device_types(hass: HomeAssistant, caplog): assert state.state == "on" target_properties["friendly_name"] = name target_properties["flowing"] = False - target_properties["night_light"] = True + target_properties["night_light"] = False target_properties["music_mode"] = False assert dict(state.attributes) == target_properties - await hass.config_entries.async_unload(config_entry.entry_id) await config_entry.async_remove(hass) registry = er.async_get(hass) registry.async_clear_config_entry(config_entry.entry_id) + mocked_bulb.last_properties["nl_br"] = original_nightlight_brightness - # nightlight - if nightlight_properties is None: - return - config_entry = MockConfigEntry( - domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: True} - ) - config_entry.add_to_hass(hass) - await _async_setup(config_entry) + # nightlight as a setting of the main entity + if nightlight_mode_properties is not None: + mocked_bulb.last_properties["active_mode"] = True + config_entry.add_to_hass(hass) + await _async_setup(config_entry) + state = hass.states.get(entity_id) + assert state.state == "on" + nightlight_mode_properties["friendly_name"] = name + nightlight_mode_properties["flowing"] = False + nightlight_mode_properties["night_light"] = True + nightlight_mode_properties["music_mode"] = False + assert dict(state.attributes) == nightlight_mode_properties - assert hass.states.get(entity_id).state == "off" - state = hass.states.get(f"{entity_id}_nightlight") - assert state.state == "on" - nightlight_properties["friendly_name"] = f"{name} Nightlight" - nightlight_properties["icon"] = "mdi:weather-night" - nightlight_properties["flowing"] = False - nightlight_properties["night_light"] = True - nightlight_properties["music_mode"] = False - assert dict(state.attributes) == nightlight_properties + await hass.config_entries.async_unload(config_entry.entry_id) + await config_entry.async_remove(hass) + registry.async_clear_config_entry(config_entry.entry_id) + await hass.async_block_till_done() + mocked_bulb.last_properties.pop("active_mode") - await hass.config_entries.async_unload(config_entry.entry_id) - await config_entry.async_remove(hass) - registry.async_clear_config_entry(config_entry.entry_id) - await hass.async_block_till_done() + # nightlight as a separate entity + if nightlight_entity_properties is not None: + config_entry = MockConfigEntry( + domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: True} + ) + config_entry.add_to_hass(hass) + await _async_setup(config_entry) + + assert hass.states.get(entity_id).state == "off" + state = hass.states.get(f"{entity_id}_nightlight") + assert state.state == "on" + nightlight_entity_properties["friendly_name"] = f"{name} Nightlight" + nightlight_entity_properties["icon"] = "mdi:weather-night" + nightlight_entity_properties["flowing"] = False + nightlight_entity_properties["night_light"] = True + nightlight_entity_properties["music_mode"] = False + assert dict(state.attributes) == nightlight_entity_properties + + await hass.config_entries.async_unload(config_entry.entry_id) + await config_entry.async_remove(hass) + registry.async_clear_config_entry(config_entry.entry_id) + await hass.async_block_till_done() bright = round(255 * int(PROPERTIES["bright"]) / 100) - current_brightness = round(255 * int(PROPERTIES["current_brightness"]) / 100) ct = color_temperature_kelvin_to_mired(int(PROPERTIES["ct"])) hue = int(PROPERTIES["hue"]) sat = int(PROPERTIES["sat"]) @@ -806,7 +832,7 @@ async def test_device_types(hass: HomeAssistant, caplog): "max_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["min"] ), - "brightness": current_brightness, + "brightness": bright, "color_temp": ct, "color_mode": "color_temp", "supported_color_modes": ["color_temp", "hs", "rgb"], @@ -814,11 +840,30 @@ async def test_device_types(hass: HomeAssistant, caplog): "rgb_color": (255, 205, 166), "xy_color": (0.421, 0.364), }, - { + nightlight_entity_properties={ "supported_features": 0, "color_mode": "onoff", "supported_color_modes": ["onoff"], }, + nightlight_mode_properties={ + "effect_list": YEELIGHT_COLOR_EFFECT_LIST, + "supported_features": SUPPORT_YEELIGHT, + "hs_color": (28.401, 100.0), + "rgb_color": (255, 120, 0), + "xy_color": (0.621, 0.367), + "min_mireds": color_temperature_kelvin_to_mired( + model_specs["color_temp"]["max"] + ), + "max_mireds": color_temperature_kelvin_to_mired( + model_specs["color_temp"]["min"] + ), + "brightness": nl_br, + "color_mode": "color_temp", + "supported_color_modes": ["color_temp", "hs", "rgb"], + "color_temp": color_temperature_kelvin_to_mired( + model_specs["color_temp"]["min"] + ), + }, ) # Color - color mode HS @@ -836,14 +881,14 @@ async def test_device_types(hass: HomeAssistant, caplog): "max_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["min"] ), - "brightness": current_brightness, + "brightness": bright, "hs_color": hs_color, "rgb_color": color_hs_to_RGB(*hs_color), "xy_color": color_hs_to_xy(*hs_color), "color_mode": "hs", "supported_color_modes": ["color_temp", "hs", "rgb"], }, - { + nightlight_entity_properties={ "supported_features": 0, "color_mode": "onoff", "supported_color_modes": ["onoff"], @@ -865,14 +910,14 @@ async def test_device_types(hass: HomeAssistant, caplog): "max_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["min"] ), - "brightness": current_brightness, + "brightness": bright, "hs_color": color_RGB_to_hs(*rgb_color), "rgb_color": rgb_color, "xy_color": color_RGB_to_xy(*rgb_color), "color_mode": "rgb", "supported_color_modes": ["color_temp", "hs", "rgb"], }, - { + nightlight_entity_properties={ "supported_features": 0, "color_mode": "onoff", "supported_color_modes": ["onoff"], @@ -895,11 +940,11 @@ async def test_device_types(hass: HomeAssistant, caplog): "max_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["min"] ), - "brightness": current_brightness, + "brightness": bright, "color_mode": "hs", "supported_color_modes": ["color_temp", "hs", "rgb"], }, - { + nightlight_entity_properties={ "supported_features": 0, "color_mode": "onoff", "supported_color_modes": ["onoff"], @@ -922,11 +967,11 @@ async def test_device_types(hass: HomeAssistant, caplog): "max_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["min"] ), - "brightness": current_brightness, + "brightness": bright, "color_mode": "rgb", "supported_color_modes": ["color_temp", "hs", "rgb"], }, - { + nightlight_entity_properties={ "supported_features": 0, "color_mode": "onoff", "supported_color_modes": ["onoff"], @@ -973,7 +1018,7 @@ async def test_device_types(hass: HomeAssistant, caplog): "max_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["min"] ), - "brightness": current_brightness, + "brightness": bright, "color_temp": ct, "color_mode": "color_temp", "supported_color_modes": ["color_temp"], @@ -981,13 +1026,31 @@ async def test_device_types(hass: HomeAssistant, caplog): "rgb_color": (255, 205, 166), "xy_color": (0.421, 0.364), }, - { - "effect_list": YEELIGHT_TEMP_ONLY_EFFECT_LIST, - "supported_features": SUPPORT_YEELIGHT, + nightlight_entity_properties={ + "supported_features": 0, "brightness": nl_br, "color_mode": "brightness", "supported_color_modes": ["brightness"], }, + nightlight_mode_properties={ + "effect_list": YEELIGHT_TEMP_ONLY_EFFECT_LIST, + "supported_features": SUPPORT_YEELIGHT, + "min_mireds": color_temperature_kelvin_to_mired( + model_specs["color_temp"]["max"] + ), + "max_mireds": color_temperature_kelvin_to_mired( + model_specs["color_temp"]["min"] + ), + "brightness": nl_br, + "color_temp": color_temperature_kelvin_to_mired( + model_specs["color_temp"]["min"] + ), + "color_mode": "color_temp", + "supported_color_modes": ["color_temp"], + "hs_color": (28.391, 65.659), + "rgb_color": (255, 166, 87), + "xy_color": (0.526, 0.387), + }, ) # WhiteTempMood @@ -1009,7 +1072,7 @@ async def test_device_types(hass: HomeAssistant, caplog): "max_mireds": color_temperature_kelvin_to_mired( model_specs["color_temp"]["min"] ), - "brightness": current_brightness, + "brightness": bright, "color_temp": ct, "color_mode": "color_temp", "supported_color_modes": ["color_temp"], @@ -1017,13 +1080,34 @@ async def test_device_types(hass: HomeAssistant, caplog): "rgb_color": (255, 205, 166), "xy_color": (0.421, 0.364), }, - { - "effect_list": YEELIGHT_TEMP_ONLY_EFFECT_LIST, - "supported_features": SUPPORT_YEELIGHT, + nightlight_entity_properties={ + "supported_features": 0, "brightness": nl_br, "color_mode": "brightness", "supported_color_modes": ["brightness"], }, + nightlight_mode_properties={ + "friendly_name": NAME, + "effect_list": YEELIGHT_TEMP_ONLY_EFFECT_LIST, + "flowing": False, + "night_light": True, + "supported_features": SUPPORT_YEELIGHT, + "min_mireds": color_temperature_kelvin_to_mired( + model_specs["color_temp"]["max"] + ), + "max_mireds": color_temperature_kelvin_to_mired( + model_specs["color_temp"]["min"] + ), + "brightness": nl_br, + "color_temp": color_temperature_kelvin_to_mired( + model_specs["color_temp"]["min"] + ), + "color_mode": "color_temp", + "supported_color_modes": ["color_temp"], + "hs_color": (28.391, 65.659), + "rgb_color": (255, 166, 87), + "xy_color": (0.526, 0.387), + }, ) # Background light - color mode CT mocked_bulb.last_properties["bg_lmode"] = "2" # CT @@ -1261,62 +1345,6 @@ async def test_effects(hass: HomeAssistant): await _async_test_effect("not_existed", called=False) -async def test_state_fails_to_update_triggers_update(hass: HomeAssistant): - """Ensure we call async_get_properties if the turn on/off fails to update the state.""" - mocked_bulb = _mocked_bulb() - properties = {**PROPERTIES} - properties.pop("active_mode") - properties["color_mode"] = "3" # HSV - mocked_bulb.last_properties = properties - mocked_bulb.bulb_type = BulbType.Color - config_entry = MockConfigEntry( - domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: False} - ) - config_entry.add_to_hass(hass) - with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - # We use asyncio.create_task now to avoid - # blocking starting so we need to block again - await hass.async_block_till_done() - - mocked_bulb.last_properties["power"] = "off" - await hass.services.async_call( - "light", - SERVICE_TURN_ON, - { - ATTR_ENTITY_ID: ENTITY_LIGHT, - }, - blocking=True, - ) - assert len(mocked_bulb.async_turn_on.mock_calls) == 1 - assert len(mocked_bulb.async_get_properties.mock_calls) == 2 - - mocked_bulb.last_properties["power"] = "on" - await hass.services.async_call( - "light", - SERVICE_TURN_OFF, - { - ATTR_ENTITY_ID: ENTITY_LIGHT, - }, - blocking=True, - ) - assert len(mocked_bulb.async_turn_off.mock_calls) == 1 - assert len(mocked_bulb.async_get_properties.mock_calls) == 3 - - # But if the state is correct no calls - await hass.services.async_call( - "light", - SERVICE_TURN_ON, - { - ATTR_ENTITY_ID: ENTITY_LIGHT, - }, - blocking=True, - ) - assert len(mocked_bulb.async_turn_on.mock_calls) == 1 - assert len(mocked_bulb.async_get_properties.mock_calls) == 3 - - async def test_ambilight_with_nightlight_disabled(hass: HomeAssistant): """Test that main light on ambilights with the nightlight disabled shows the correct brightness.""" mocked_bulb = _mocked_bulb() @@ -1325,7 +1353,6 @@ async def test_ambilight_with_nightlight_disabled(hass: HomeAssistant): capabilities["model"] = "ceiling10" properties["color_mode"] = "3" # HSV properties["bg_power"] = "off" - properties["current_brightness"] = 0 properties["bg_lmode"] = "2" # CT mocked_bulb.last_properties = properties mocked_bulb.bulb_type = BulbType.WhiteTempMood From f3c76fb859a7d76aa8bbef1ba81a3783cc00b69d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 3 Oct 2021 09:13:12 +0200 Subject: [PATCH 0058/1038] Add strict typing to Tractive integration (#56948) * Strict typing * Add few missing types * Run hassfest * Fix mypy errors * Use List instead of list --- .strict-typing | 1 + homeassistant/components/tractive/__init__.py | 51 ++++++++------ .../components/tractive/binary_sensor.py | 50 +++++++------- .../components/tractive/config_flow.py | 8 +-- homeassistant/components/tractive/const.py | 27 ++++---- .../components/tractive/device_tracker.py | 65 ++++++++---------- homeassistant/components/tractive/entity.py | 7 +- homeassistant/components/tractive/sensor.py | 66 +++++++++++-------- homeassistant/components/tractive/switch.py | 14 ++-- mypy.ini | 11 ++++ 10 files changed, 166 insertions(+), 134 deletions(-) diff --git a/.strict-typing b/.strict-typing index 8e957faabf0..18dc1be41eb 100644 --- a/.strict-typing +++ b/.strict-typing @@ -112,6 +112,7 @@ homeassistant.components.tautulli.* homeassistant.components.tcp.* homeassistant.components.tile.* homeassistant.components.tplink.* +homeassistant.components.tractive.* homeassistant.components.tradfri.* homeassistant.components.tts.* homeassistant.components.upcloud.* diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index be612ef5cc7..be9f2b317d2 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from dataclasses import dataclass import logging +from typing import Any, Final, List, cast import aiotractive @@ -15,7 +16,7 @@ from homeassistant.const import ( CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -36,10 +37,10 @@ from .const import ( TRACKER_POSITION_UPDATED, ) -PLATFORMS = ["binary_sensor", "device_tracker", "sensor", "switch"] +PLATFORMS: Final = ["binary_sensor", "device_tracker", "sensor", "switch"] -_LOGGER = logging.getLogger(__name__) +_LOGGER: Final = logging.getLogger(__name__) @dataclass @@ -92,7 +93,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) - async def cancel_listen_task(_): + async def cancel_listen_task(_: Event) -> None: await tractive.unsubscribe() entry.async_on_unload( @@ -102,13 +103,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def _generate_trackables(client, trackable): +async def _generate_trackables( + client: aiotractive.Tractive, + trackable: aiotractive.trackable_object.TrackableObject, +) -> Trackables | None: """Generate trackables.""" trackable = await trackable.details() # Check that the pet has tracker linked. if not trackable["device_id"]: - return + return None tracker = client.tracker(trackable["device_id"]) @@ -132,37 +136,44 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class TractiveClient: """A Tractive client.""" - def __init__(self, hass, client, user_id): + def __init__( + self, hass: HomeAssistant, client: aiotractive.Tractive, user_id: str + ) -> None: """Initialize the client.""" self._hass = hass self._client = client self._user_id = user_id - self._listen_task = None + self._listen_task: asyncio.Task | None = None @property - def user_id(self): + def user_id(self) -> str: """Return user id.""" return self._user_id - async def trackable_objects(self): + async def trackable_objects( + self, + ) -> list[aiotractive.trackable_object.TrackableObject]: """Get list of trackable objects.""" - return await self._client.trackable_objects() + return cast( + List[aiotractive.trackable_object.TrackableObject], + await self._client.trackable_objects(), + ) - def tracker(self, tracker_id): + def tracker(self, tracker_id: str) -> aiotractive.tracker.Tracker: """Get tracker by id.""" return self._client.tracker(tracker_id) - def subscribe(self): + def subscribe(self) -> None: """Start event listener coroutine.""" self._listen_task = asyncio.create_task(self._listen()) - async def unsubscribe(self): + async def unsubscribe(self) -> None: """Stop event listener coroutine.""" if self._listen_task: self._listen_task.cancel() await self._client.close() - async def _listen(self): + async def _listen(self) -> None: server_was_unavailable = False while True: try: @@ -191,7 +202,7 @@ class TractiveClient: server_was_unavailable = True continue - def _send_hardware_update(self, event): + def _send_hardware_update(self, event: dict[str, Any]) -> None: # Sometimes hardware event doesn't contain complete data. payload = { ATTR_BATTERY_LEVEL: event["hardware"]["battery_level"], @@ -204,7 +215,7 @@ class TractiveClient: TRACKER_HARDWARE_STATUS_UPDATED, event["tracker_id"], payload ) - def _send_activity_update(self, event): + def _send_activity_update(self, event: dict[str, Any]) -> None: payload = { ATTR_MINUTES_ACTIVE: event["progress"]["achieved_minutes"], ATTR_DAILY_GOAL: event["progress"]["goal_minutes"], @@ -213,7 +224,7 @@ class TractiveClient: TRACKER_ACTIVITY_STATUS_UPDATED, event["pet_id"], payload ) - def _send_position_update(self, event): + def _send_position_update(self, event: dict[str, Any]) -> None: payload = { "latitude": event["position"]["latlong"][0], "longitude": event["position"]["latlong"][1], @@ -223,7 +234,9 @@ class TractiveClient: TRACKER_POSITION_UPDATED, event["tracker_id"], payload ) - def _dispatch_tracker_event(self, event_name, tracker_id, payload): + def _dispatch_tracker_event( + self, event_name: str, tracker_id: str, payload: dict[str, Any] + ) -> None: async_dispatcher_send( self._hass, f"{event_name}-{tracker_id}", diff --git a/homeassistant/components/tractive/binary_sensor.py b/homeassistant/components/tractive/binary_sensor.py index fd3a00c377d..d9b41c83940 100644 --- a/homeassistant/components/tractive/binary_sensor.py +++ b/homeassistant/components/tractive/binary_sensor.py @@ -1,15 +1,20 @@ """Support for Tractive binary sensors.""" from __future__ import annotations +from typing import Any, Final + from homeassistant.components.binary_sensor import ( DEVICE_CLASS_BATTERY_CHARGING, BinarySensorEntity, BinarySensorEntityDescription, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_BATTERY_CHARGING -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import Trackables from .const import ( CLIENT, DOMAIN, @@ -19,34 +24,36 @@ from .const import ( ) from .entity import TractiveEntity -TRACKERS_WITH_BUILTIN_BATTERY = ("TRNJA4", "TRAXL1") +TRACKERS_WITH_BUILTIN_BATTERY: Final = ("TRNJA4", "TRAXL1") class TractiveBinarySensor(TractiveEntity, BinarySensorEntity): """Tractive sensor.""" - def __init__(self, user_id, trackable, tracker_details, unique_id, description): + def __init__( + self, user_id: str, item: Trackables, description: BinarySensorEntityDescription + ) -> None: """Initialize sensor entity.""" - super().__init__(user_id, trackable, tracker_details) + super().__init__(user_id, item.trackable, item.tracker_details) - self._attr_name = f"{trackable['details']['name']} {description.name}" - self._attr_unique_id = unique_id + self._attr_name = f"{item.trackable['details']['name']} {description.name}" + self._attr_unique_id = f"{item.trackable['_id']}_{description.key}" self.entity_description = description @callback - def handle_server_unavailable(self): + def handle_server_unavailable(self) -> None: """Handle server unavailable.""" self._attr_available = False self.async_write_ha_state() @callback - def handle_hardware_status_update(self, event): + def handle_hardware_status_update(self, event: dict[str, Any]) -> None: """Handle hardware status update.""" self._attr_is_on = event[self.entity_description.key] self._attr_available = True 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( @@ -66,31 +73,24 @@ class TractiveBinarySensor(TractiveEntity, BinarySensorEntity): ) -SENSOR_TYPE = BinarySensorEntityDescription( +SENSOR_TYPE: Final = BinarySensorEntityDescription( key=ATTR_BATTERY_CHARGING, name="Battery Charging", device_class=DEVICE_CLASS_BATTERY_CHARGING, ) -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up Tractive device trackers.""" client = hass.data[DOMAIN][entry.entry_id][CLIENT] trackables = hass.data[DOMAIN][entry.entry_id][TRACKABLES] - entities = [] - - for item in trackables: - if item.tracker_details["model_number"] not in TRACKERS_WITH_BUILTIN_BATTERY: - continue - entities.append( - TractiveBinarySensor( - client.user_id, - item.trackable, - item.tracker_details, - f"{item.trackable['_id']}_{SENSOR_TYPE.key}", - SENSOR_TYPE, - ) - ) + entities = [ + TractiveBinarySensor(client.user_id, item, SENSOR_TYPE) + for item in trackables + if item.tracker_details["model_number"] in TRACKERS_WITH_BUILTIN_BATTERY + ] async_add_entities(entities) diff --git a/homeassistant/components/tractive/config_flow.py b/homeassistant/components/tractive/config_flow.py index 4b1fc241110..f3b36ae3d03 100644 --- a/homeassistant/components/tractive/config_flow.py +++ b/homeassistant/components/tractive/config_flow.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any +from typing import Any, Final import aiotractive import voluptuous as vol @@ -15,9 +15,9 @@ from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN -_LOGGER = logging.getLogger(__name__) +_LOGGER: Final = logging.getLogger(__name__) -USER_DATA_SCHEMA = vol.Schema( +USER_DATA_SCHEMA: Final = vol.Schema( {vol.Required(CONF_EMAIL): str, vol.Required(CONF_PASSWORD): str} ) @@ -74,7 +74,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( - self, user_input: dict[str, Any] = None + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Dialog that informs the user that reauth is required.""" diff --git a/homeassistant/components/tractive/const.py b/homeassistant/components/tractive/const.py index 6a61024cd51..f0ee6b7813e 100644 --- a/homeassistant/components/tractive/const.py +++ b/homeassistant/components/tractive/const.py @@ -1,22 +1,23 @@ """Constants for the tractive integration.""" from datetime import timedelta +from typing import Final -DOMAIN = "tractive" +DOMAIN: Final = "tractive" -RECONNECT_INTERVAL = timedelta(seconds=10) +RECONNECT_INTERVAL: Final = timedelta(seconds=10) -ATTR_DAILY_GOAL = "daily_goal" -ATTR_BUZZER = "buzzer" -ATTR_LED = "led" -ATTR_LIVE_TRACKING = "live_tracking" -ATTR_MINUTES_ACTIVE = "minutes_active" +ATTR_DAILY_GOAL: Final = "daily_goal" +ATTR_BUZZER: Final = "buzzer" +ATTR_LED: Final = "led" +ATTR_LIVE_TRACKING: Final = "live_tracking" +ATTR_MINUTES_ACTIVE: Final = "minutes_active" -CLIENT = "client" -TRACKABLES = "trackables" +CLIENT: Final = "client" +TRACKABLES: Final = "trackables" -TRACKER_HARDWARE_STATUS_UPDATED = f"{DOMAIN}_tracker_hardware_status_updated" -TRACKER_POSITION_UPDATED = f"{DOMAIN}_tracker_position_updated" -TRACKER_ACTIVITY_STATUS_UPDATED = f"{DOMAIN}_tracker_activity_updated" +TRACKER_HARDWARE_STATUS_UPDATED: Final = f"{DOMAIN}_tracker_hardware_status_updated" +TRACKER_POSITION_UPDATED: Final = f"{DOMAIN}_tracker_position_updated" +TRACKER_ACTIVITY_STATUS_UPDATED: Final = f"{DOMAIN}_tracker_activity_updated" -SERVER_UNAVAILABLE = f"{DOMAIN}_server_unavailable" +SERVER_UNAVAILABLE: Final = f"{DOMAIN}_server_unavailable" diff --git a/homeassistant/components/tractive/device_tracker.py b/homeassistant/components/tractive/device_tracker.py index 1e35e41fc8a..a4109eee71c 100644 --- a/homeassistant/components/tractive/device_tracker.py +++ b/homeassistant/components/tractive/device_tracker.py @@ -1,12 +1,16 @@ """Support for Tractive device trackers.""" +from __future__ import annotations -import logging +from typing import Any from homeassistant.components.device_tracker import SOURCE_TYPE_GPS from homeassistant.components.device_tracker.config_entry import TrackerEntity -from homeassistant.core import callback +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 . import Trackables from .const import ( CLIENT, DOMAIN, @@ -17,26 +21,15 @@ from .const import ( ) from .entity import TractiveEntity -_LOGGER = logging.getLogger(__name__) - -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up Tractive device trackers.""" client = hass.data[DOMAIN][entry.entry_id][CLIENT] trackables = hass.data[DOMAIN][entry.entry_id][TRACKABLES] - entities = [] - - for item in trackables: - entities.append( - TractiveDeviceTracker( - client.user_id, - item.trackable, - item.tracker_details, - item.hw_info, - item.pos_report, - ) - ) + entities = [TractiveDeviceTracker(client.user_id, item) for item in trackables] async_add_entities(entities) @@ -46,51 +39,51 @@ class TractiveDeviceTracker(TractiveEntity, TrackerEntity): _attr_icon = "mdi:paw" - def __init__(self, user_id, trackable, tracker_details, hw_info, pos_report): + def __init__(self, user_id: str, item: Trackables) -> None: """Initialize tracker entity.""" - super().__init__(user_id, trackable, tracker_details) + super().__init__(user_id, item.trackable, item.tracker_details) - self._battery_level = hw_info["battery_level"] - self._latitude = pos_report["latlong"][0] - self._longitude = pos_report["latlong"][1] - self._accuracy = pos_report["pos_uncertainty"] + self._battery_level: int = item.hw_info["battery_level"] + self._latitude: float = item.pos_report["latlong"][0] + self._longitude: float = item.pos_report["latlong"][1] + self._accuracy: int = item.pos_report["pos_uncertainty"] - self._attr_name = f"{self._tracker_id} {trackable['details']['name']}" - self._attr_unique_id = trackable["_id"] + self._attr_name = f"{self._tracker_id} {item.trackable['details']['name']}" + self._attr_unique_id = item.trackable["_id"] @property - def source_type(self): + def source_type(self) -> str: """Return the source type, eg gps or router, of the device.""" return SOURCE_TYPE_GPS @property - def latitude(self): + def latitude(self) -> float: """Return latitude value of the device.""" return self._latitude @property - def longitude(self): + def longitude(self) -> float: """Return longitude value of the device.""" return self._longitude @property - def location_accuracy(self): + def location_accuracy(self) -> int: """Return the gps accuracy of the device.""" return self._accuracy @property - def battery_level(self): + def battery_level(self) -> int: """Return the battery level of the device.""" return self._battery_level @callback - def _handle_hardware_status_update(self, event): + def _handle_hardware_status_update(self, event: dict[str, Any]) -> None: self._battery_level = event["battery_level"] self._attr_available = True self.async_write_ha_state() @callback - def _handle_position_update(self, event): + def _handle_position_update(self, event: dict[str, Any]) -> None: self._latitude = event["latitude"] self._longitude = event["longitude"] self._accuracy = event["accuracy"] @@ -98,15 +91,11 @@ class TractiveDeviceTracker(TractiveEntity, TrackerEntity): self.async_write_ha_state() @callback - def _handle_server_unavailable(self): - self._latitude = None - self._longitude = None - self._accuracy = None - self._battery_level = None + def _handle_server_unavailable(self) -> None: self._attr_available = False 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( diff --git a/homeassistant/components/tractive/entity.py b/homeassistant/components/tractive/entity.py index 4ddc7f7aa35..ffc84fc9788 100644 --- a/homeassistant/components/tractive/entity.py +++ b/homeassistant/components/tractive/entity.py @@ -1,4 +1,7 @@ """A entity class for Tractive integration.""" +from __future__ import annotations + +from typing import Any from homeassistant.helpers.entity import Entity @@ -8,7 +11,9 @@ from .const import DOMAIN class TractiveEntity(Entity): """Tractive entity class.""" - def __init__(self, user_id, trackable, tracker_details): + def __init__( + self, user_id: str, trackable: dict[str, Any], tracker_details: dict[str, Any] + ) -> None: """Initialize tracker entity.""" self._attr_device_info = { "identifiers": {(DOMAIN, tracker_details["_id"])}, diff --git a/homeassistant/components/tractive/sensor.py b/homeassistant/components/tractive/sensor.py index 9fd8ee6ac5f..b7025b3555b 100644 --- a/homeassistant/components/tractive/sensor.py +++ b/homeassistant/components/tractive/sensor.py @@ -2,17 +2,21 @@ from __future__ import annotations from dataclasses import dataclass +from typing import Any, Final from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_BATTERY_LEVEL, DEVICE_CLASS_BATTERY, PERCENTAGE, TIME_MINUTES, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import Trackables from .const import ( ATTR_DAILY_GOAL, ATTR_MINUTES_ACTIVE, @@ -27,25 +31,37 @@ from .entity import TractiveEntity @dataclass -class TractiveSensorEntityDescription(SensorEntityDescription): - """Class describing Tractive sensor entities.""" +class TractiveRequiredKeysMixin: + """Mixin for required keys.""" - entity_class: type[TractiveSensor] | None = None + entity_class: type[TractiveSensor] + + +@dataclass +class TractiveSensorEntityDescription( + SensorEntityDescription, TractiveRequiredKeysMixin +): + """Class describing Tractive sensor entities.""" class TractiveSensor(TractiveEntity, SensorEntity): """Tractive sensor.""" - def __init__(self, user_id, trackable, tracker_details, unique_id, description): + def __init__( + self, + user_id: str, + item: Trackables, + description: TractiveSensorEntityDescription, + ) -> None: """Initialize sensor entity.""" - super().__init__(user_id, trackable, tracker_details) + super().__init__(user_id, item.trackable, item.tracker_details) - self._attr_name = f"{trackable['details']['name']} {description.name}" - self._attr_unique_id = unique_id + self._attr_name = f"{item.trackable['details']['name']} {description.name}" + self._attr_unique_id = f"{item.trackable['_id']}_{description.key}" self.entity_description = description @callback - def handle_server_unavailable(self): + def handle_server_unavailable(self) -> None: """Handle server unavailable.""" self._attr_available = False self.async_write_ha_state() @@ -55,13 +71,13 @@ class TractiveHardwareSensor(TractiveSensor): """Tractive hardware sensor.""" @callback - def handle_hardware_status_update(self, event): + def handle_hardware_status_update(self, event: dict[str, Any]) -> None: """Handle hardware status update.""" self._attr_native_value = event[self.entity_description.key] self._attr_available = True 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( @@ -85,13 +101,13 @@ class TractiveActivitySensor(TractiveSensor): """Tractive active sensor.""" @callback - def handle_activity_status_update(self, event): + def handle_activity_status_update(self, event: dict[str, Any]) -> None: """Handle activity status update.""" self._attr_native_value = event[self.entity_description.key] self._attr_available = True 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( @@ -111,7 +127,7 @@ class TractiveActivitySensor(TractiveSensor): ) -SENSOR_TYPES = ( +SENSOR_TYPES: Final[tuple[TractiveSensorEntityDescription, ...]] = ( TractiveSensorEntityDescription( key=ATTR_BATTERY_LEVEL, name="Battery Level", @@ -136,23 +152,17 @@ SENSOR_TYPES = ( ) -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up Tractive device trackers.""" client = hass.data[DOMAIN][entry.entry_id][CLIENT] trackables = hass.data[DOMAIN][entry.entry_id][TRACKABLES] - entities = [] - - for item in trackables: - for description in SENSOR_TYPES: - entities.append( - description.entity_class( - client.user_id, - item.trackable, - item.tracker_details, - f"{item.trackable['_id']}_{description.key}", - description, - ) - ) + entities = [ + description.entity_class(client.user_id, item, description) + for description in SENSOR_TYPES + for item in trackables + ] async_add_entities(entities) diff --git a/homeassistant/components/tractive/switch.py b/homeassistant/components/tractive/switch.py index d58e38a7cc9..6bc5ecb7b1b 100644 --- a/homeassistant/components/tractive/switch.py +++ b/homeassistant/components/tractive/switch.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass import logging -from typing import Any, Literal +from typing import Any, Final, Literal, cast from aiotractive.exceptions import TractiveError @@ -26,7 +26,7 @@ from .const import ( ) from .entity import TractiveEntity -_LOGGER = logging.getLogger(__name__) +_LOGGER: Final = logging.getLogger(__name__) @dataclass @@ -43,7 +43,7 @@ class TractiveSwitchEntityDescription( """Class describing Tractive switch entities.""" -SWITCH_TYPES: tuple[TractiveSwitchEntityDescription, ...] = ( +SWITCH_TYPES: Final[tuple[TractiveSwitchEntityDescription, ...]] = ( TractiveSwitchEntityDescription( key=ATTR_BUZZER, name="Tracker Buzzer", @@ -162,12 +162,14 @@ class TractiveSwitch(TractiveEntity, SwitchEntity): async def async_set_buzzer(self, active: bool) -> dict[str, Any]: """Set the buzzer on/off.""" - return await self._tracker.set_buzzer_active(active) + return cast(dict[str, Any], await self._tracker.set_buzzer_active(active)) async def async_set_led(self, active: bool) -> dict[str, Any]: """Set the LED on/off.""" - return await self._tracker.set_led_active(active) + return cast(dict[str, Any], await self._tracker.set_led_active(active)) async def async_set_live_tracking(self, active: bool) -> dict[str, Any]: """Set the live tracking on/off.""" - return await self._tracker.set_live_tracking_active(active) + return cast( + dict[str, Any], await self._tracker.set_live_tracking_active(active) + ) diff --git a/mypy.ini b/mypy.ini index 8f9e49702fc..4e91295b597 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1243,6 +1243,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.tractive.*] +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.tradfri.*] check_untyped_defs = true disallow_incomplete_defs = true From ddc99afba93d2c1387788f0ea0c6784aeac755ca Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sun, 3 Oct 2021 13:07:17 +0300 Subject: [PATCH 0059/1038] Bump aioshelly to 1.0.2 (#56980) --- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index ca092295473..09a046ee78d 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", - "requirements": ["aioshelly==1.0.1"], + "requirements": ["aioshelly==1.0.2"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 411f78865a2..eb2a4c5d623 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -243,7 +243,7 @@ aiopylgtv==0.4.0 aiorecollect==1.0.8 # homeassistant.components.shelly -aioshelly==1.0.1 +aioshelly==1.0.2 # homeassistant.components.switcher_kis aioswitcher==2.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5d5d6f132d7..4e306ed0293 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -167,7 +167,7 @@ aiopylgtv==0.4.0 aiorecollect==1.0.8 # homeassistant.components.shelly -aioshelly==1.0.1 +aioshelly==1.0.2 # homeassistant.components.switcher_kis aioswitcher==2.0.6 From f8b6fba3ebeccc623c29e7e1a3ac8bfa8d455c1c Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 3 Oct 2021 12:41:22 +0200 Subject: [PATCH 0060/1038] Bump gios library to 2.1.0 (#56984) --- homeassistant/components/gios/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json index 3e7bf9aceca..0e7227797d2 100644 --- a/homeassistant/components/gios/manifest.json +++ b/homeassistant/components/gios/manifest.json @@ -3,7 +3,7 @@ "name": "GIO\u015a", "documentation": "https://www.home-assistant.io/integrations/gios", "codeowners": ["@bieniu"], - "requirements": ["gios==2.0.0"], + "requirements": ["gios==2.1.0"], "config_flow": true, "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index eb2a4c5d623..ae84aea2b92 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -708,7 +708,7 @@ georss_qld_bushfire_alert_client==0.5 getmac==0.8.2 # homeassistant.components.gios -gios==2.0.0 +gios==2.1.0 # homeassistant.components.gitter gitterpy==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4e306ed0293..c1ff83d1697 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -425,7 +425,7 @@ georss_qld_bushfire_alert_client==0.5 getmac==0.8.2 # homeassistant.components.gios -gios==2.0.0 +gios==2.1.0 # homeassistant.components.glances glances_api==0.2.0 From 2fdef0e1440db7d1fd17a4832b9186a92321a444 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sun, 3 Oct 2021 13:28:52 +0200 Subject: [PATCH 0061/1038] Update surepetcare test (#56871) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- tests/components/surepetcare/test_lock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/surepetcare/test_lock.py b/tests/components/surepetcare/test_lock.py index a2c4ebad0b3..560f12b9d04 100644 --- a/tests/components/surepetcare/test_lock.py +++ b/tests/components/surepetcare/test_lock.py @@ -100,7 +100,7 @@ async def test_unlock_failing(hass, surepetcare) -> None: assert await async_setup_component(hass, DOMAIN, MOCK_CONFIG) await hass.async_block_till_done() - entity_id = list(EXPECTED_ENTITY_IDS.keys())[0] + entity_id = list(EXPECTED_ENTITY_IDS)[0] await hass.services.async_call( "lock", "lock", {"entity_id": entity_id}, blocking=True From 64d4e8537fc8c996f7325dbbab9cdf4ee9309055 Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Mon, 4 Oct 2021 02:09:30 +1100 Subject: [PATCH 0062/1038] Disable discovery for dlna_dmr until it is more selective (#56950) --- .../components/dlna_dmr/manifest.json | 20 ------------------- homeassistant/generated/ssdp.py | 20 ------------------- 2 files changed, 40 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 002228e28b3..8ea4ab48e27 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -5,26 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", "requirements": ["async-upnp-client==0.22.4"], "dependencies": ["network", "ssdp"], - "ssdp": [ - { - "st": "urn:schemas-upnp-org:device:MediaRenderer:1" - }, - { - "st": "urn:schemas-upnp-org:device:MediaRenderer:2" - }, - { - "st": "urn:schemas-upnp-org:device:MediaRenderer:3" - }, - { - "nt": "urn:schemas-upnp-org:device:MediaRenderer:1" - }, - { - "nt": "urn:schemas-upnp-org:device:MediaRenderer:2" - }, - { - "nt": "urn:schemas-upnp-org:device:MediaRenderer:3" - } - ], "codeowners": ["@StevenLooman", "@chishm"], "iot_class": "local_push" } diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 6d15477bf02..cb255e5acfe 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -83,26 +83,6 @@ SSDP = { "manufacturer": "DIRECTV" } ], - "dlna_dmr": [ - { - "st": "urn:schemas-upnp-org:device:MediaRenderer:1" - }, - { - "st": "urn:schemas-upnp-org:device:MediaRenderer:2" - }, - { - "st": "urn:schemas-upnp-org:device:MediaRenderer:3" - }, - { - "nt": "urn:schemas-upnp-org:device:MediaRenderer:1" - }, - { - "nt": "urn:schemas-upnp-org:device:MediaRenderer:2" - }, - { - "nt": "urn:schemas-upnp-org:device:MediaRenderer:3" - } - ], "fritz": [ { "st": "urn:schemas-upnp-org:device:fritzbox:1" From 41d2f03b2f70f6d7f2ed24a963b0af392408cb6f Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sun, 3 Oct 2021 19:28:41 +0200 Subject: [PATCH 0063/1038] Bump async-upnp-client to 0.22.5 (#56989) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 8ea4ab48e27..53bee3d8519 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Renderer", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.22.4"], + "requirements": ["async-upnp-client==0.22.5"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman", "@chishm"], "iot_class": "local_push" diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 3a6531fcacb..3e99a77e8bb 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,7 +2,7 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", - "requirements": ["async-upnp-client==0.22.4"], + "requirements": ["async-upnp-client==0.22.5"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 6ab3896cfdb..9a1875777a6 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -3,7 +3,7 @@ "name": "UPnP/IGD", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": ["async-upnp-client==0.22.4"], + "requirements": ["async-upnp-client==0.22.5"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman","@ehendrix23"], "ssdp": [ diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index ca6fe09fe53..cc40f07ce46 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,7 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.7.5", "async-upnp-client==0.22.4"], + "requirements": ["yeelight==0.7.5", "async-upnp-client==0.22.5"], "codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e7f0cb6a5c3..d69ae6d5e43 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.4 aiohttp==3.7.4.post0 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.22.4 +async-upnp-client==0.22.5 async_timeout==3.0.1 attrs==21.2.0 awesomeversion==21.8.1 diff --git a/requirements_all.txt b/requirements_all.txt index ae84aea2b92..6001d15e345 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -330,7 +330,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.22.4 +async-upnp-client==0.22.5 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c1ff83d1697..24193191e9f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -224,7 +224,7 @@ arcam-fmj==0.7.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.22.4 +async-upnp-client==0.22.5 # homeassistant.components.aurora auroranoaa==0.0.2 From 641f0babce6fcd11ef6cc2c039461c6e54240106 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sun, 3 Oct 2021 19:29:01 +0200 Subject: [PATCH 0064/1038] Fix upnp invalid key in ssdp discovery_info (#56986) --- homeassistant/components/upnp/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 9352ae0a5ff..d1c2c4b3c0f 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -273,7 +273,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): title = _friendly_name_from_discovery(discovery) data = { - CONFIG_ENTRY_UDN: discovery["_udn"], + CONFIG_ENTRY_UDN: discovery[ssdp.ATTR_UPNP_UDN], CONFIG_ENTRY_ST: discovery[ssdp.ATTR_SSDP_ST], CONFIG_ENTRY_HOSTNAME: discovery["_host"], } From 0d91167cdd8680783d6baeb1fc02dff2d3d9aecd Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Sun, 3 Oct 2021 19:37:38 +0200 Subject: [PATCH 0065/1038] Update pyhomematic to 0.1.75 (#56995) --- homeassistant/components/homematic/const.py | 8 ++++++++ homeassistant/components/homematic/manifest.json | 2 +- homeassistant/components/homematic/sensor.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematic/const.py b/homeassistant/components/homematic/const.py index 0880d168375..03b3f55e505 100644 --- a/homeassistant/components/homematic/const.py +++ b/homeassistant/components/homematic/const.py @@ -59,6 +59,7 @@ HM_DEVICE_TYPES = { "IPMultiIO", "IPWSwitch", "IOSwitchWireless", + "IPSwitchRssiDevice", "IPWIODevice", "IPSwitchBattery", "IPMultiIOPCB", @@ -145,9 +146,11 @@ HM_DEVICE_TYPES = { "ShutterContact", "Smoke", "SmokeV2", + "SmokeV2Team", "Motion", "MotionV2", "MotionIP", + "MotionIPContactSabotage", "RemoteMotion", "WeatherSensor", "TiltSensor", @@ -174,6 +177,7 @@ HM_DEVICE_TYPES = { "IPRainSensor", "IPLanRouter", "IPMultiIOPCB", + "IPLockDLD", ], DISCOVER_COVER: [ "Blind", @@ -221,6 +225,10 @@ HM_ATTRIBUTE_SUPPORT = { "OPERATING_VOLTAGE": ["voltage", {}], "WORKING": ["working", {0: "No", 1: "Yes"}], "STATE_UNCERTAIN": ["state_uncertain", {}], + "SENDERID": ["last_senderid", {}], + "SENDERADDRESS": ["last_senderaddress", {}], + "ERROR_ALARM_TEST": ["error_alarm_test", {0: "No", 1: "Yes"}], + "ERROR_SMOKE_CHAMBER": ["error_smoke_chamber", {0: "No", 1: "Yes"}], } HM_PRESS_EVENTS = [ diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json index f500ef54b56..34015426d78 100644 --- a/homeassistant/components/homematic/manifest.json +++ b/homeassistant/components/homematic/manifest.json @@ -2,7 +2,7 @@ "domain": "homematic", "name": "Homematic", "documentation": "https://www.home-assistant.io/integrations/homematic", - "requirements": ["pyhomematic==0.1.74"], + "requirements": ["pyhomematic==0.1.75"], "codeowners": ["@pvizeli", "@danielperna84"], "iot_class": "local_push" } diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index 18690ac3553..84bb7b4d5a3 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -42,6 +42,7 @@ HM_STATE_HA_CAST = { 2: "allsens_armed", 3: "alarm_blocked", }, + "IPLockDLD": {0: None, 1: "locked", 2: "unlocked"}, } HM_UNIT_HA_CAST = { diff --git a/requirements_all.txt b/requirements_all.txt index 6001d15e345..57cd02b3a26 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1526,7 +1526,7 @@ pyhik==0.2.8 pyhiveapi==0.4.2 # homeassistant.components.homematic -pyhomematic==0.1.74 +pyhomematic==0.1.75 # homeassistant.components.homeworks pyhomeworks==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 24193191e9f..76b3946dadb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -893,7 +893,7 @@ pyheos==0.7.2 pyhiveapi==0.4.2 # homeassistant.components.homematic -pyhomematic==0.1.74 +pyhomematic==0.1.75 # homeassistant.components.ialarm pyialarm==1.9.0 From 57851e9623587b8da98ab0d87d4dd033765118ba Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Sun, 3 Oct 2021 14:06:29 -0400 Subject: [PATCH 0066/1038] Support connecting to ElkM1 over TLS 1.2 (#56887) --- homeassistant/components/elkm1/config_flow.py | 11 +++-- tests/components/elkm1/test_config_flow.py | 47 +++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py index 919aad3d012..f8cfdbe9851 100644 --- a/homeassistant/components/elkm1/config_flow.py +++ b/homeassistant/components/elkm1/config_flow.py @@ -25,12 +25,17 @@ from .const import CONF_AUTO_CONFIGURE, DOMAIN _LOGGER = logging.getLogger(__name__) -PROTOCOL_MAP = {"secure": "elks://", "non-secure": "elk://", "serial": "serial://"} +PROTOCOL_MAP = { + "secure": "elks://", + "TLS 1.2": "elksv1_2://", + "non-secure": "elk://", + "serial": "serial://", +} DATA_SCHEMA = vol.Schema( { vol.Required(CONF_PROTOCOL, default="secure"): vol.In( - ["secure", "non-secure", "serial"] + ["secure", "TLS 1.2", "non-secure", "serial"] ), vol.Required(CONF_ADDRESS): str, vol.Optional(CONF_USERNAME, default=""): str, @@ -55,7 +60,7 @@ async def validate_input(data): prefix = data[CONF_PREFIX] url = _make_url_from_data(data) - requires_password = url.startswith("elks://") + requires_password = url.startswith("elks://") or url.startswith("elksv1_2") if requires_password and (not userid or not password): raise InvalidAuth diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index a84ff0351d5..d0498496bf2 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -70,6 +70,53 @@ async def test_form_user_with_secure_elk(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_form_user_with_tls_elk(hass): + """Test we can setup a secure elk.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with patch( + "homeassistant.components.elkm1.config_flow.elkm1.Elk", + return_value=mocked_elk, + ), patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "protocol": "TLS 1.2", + "address": "1.2.3.4", + "username": "test-username", + "password": "test-password", + "temperature_unit": "°F", + "prefix": "", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "ElkM1" + assert result2["data"] == { + "auto_configure": True, + "host": "elksv1_2://1.2.3.4", + "password": "test-password", + "prefix": "", + "temperature_unit": "°F", + "username": "test-username", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_form_user_with_non_secure_elk(hass): """Test we can setup a non-secure elk.""" await setup.async_setup_component(hass, "persistent_notification", {}) From 4c51d0d2cf655518abb3604632c3cfd247e93a1d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Oct 2021 09:28:39 -1000 Subject: [PATCH 0067/1038] Round tplink energy sensors to prevent insignificant updates (#56999) - These sensors wobble quite a bit and the precision did not have sensible limits which generated a massive amount of data in the database which was not very useful --- homeassistant/components/tplink/sensor.py | 10 ++++++++-- tests/components/tplink/test_sensor.py | 20 ++++++++++---------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/tplink/sensor.py b/homeassistant/components/tplink/sensor.py index 0afcf96dba5..9bd4a056d33 100644 --- a/homeassistant/components/tplink/sensor.py +++ b/homeassistant/components/tplink/sensor.py @@ -44,6 +44,7 @@ class TPLinkSensorEntityDescription(SensorEntityDescription): """Describes TPLink sensor entity.""" emeter_attr: str | None = None + precision: int | None = None ENERGY_SENSORS: tuple[TPLinkSensorEntityDescription, ...] = ( @@ -54,6 +55,7 @@ ENERGY_SENSORS: tuple[TPLinkSensorEntityDescription, ...] = ( state_class=STATE_CLASS_MEASUREMENT, name="Current Consumption", emeter_attr="power", + precision=1, ), TPLinkSensorEntityDescription( key=ATTR_TOTAL_ENERGY_KWH, @@ -62,6 +64,7 @@ ENERGY_SENSORS: tuple[TPLinkSensorEntityDescription, ...] = ( state_class=STATE_CLASS_TOTAL_INCREASING, name="Total Consumption", emeter_attr="total", + precision=3, ), TPLinkSensorEntityDescription( key=ATTR_TODAY_ENERGY_KWH, @@ -69,6 +72,7 @@ ENERGY_SENSORS: tuple[TPLinkSensorEntityDescription, ...] = ( device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, name="Today's Consumption", + precision=3, ), TPLinkSensorEntityDescription( key=ATTR_VOLTAGE, @@ -77,6 +81,7 @@ ENERGY_SENSORS: tuple[TPLinkSensorEntityDescription, ...] = ( state_class=STATE_CLASS_MEASUREMENT, name="Voltage", emeter_attr="voltage", + precision=1, ), TPLinkSensorEntityDescription( key=ATTR_CURRENT_A, @@ -85,6 +90,7 @@ ENERGY_SENSORS: tuple[TPLinkSensorEntityDescription, ...] = ( state_class=STATE_CLASS_MEASUREMENT, name="Current", emeter_attr="current", + precision=2, ), ) @@ -97,11 +103,11 @@ def async_emeter_from_device( val = getattr(device.emeter_realtime, attr) if val is None: return None - return cast(float, val) + return round(cast(float, val), description.precision) # ATTR_TODAY_ENERGY_KWH if (emeter_today := device.emeter_today) is not None: - return cast(float, emeter_today) + return round(cast(float, emeter_today), description.precision) # today's consumption not available, when device was off all the day # bulb's do not report this information, so filter it out return None if device.is_bulb else 0.0 diff --git a/tests/components/tplink/test_sensor.py b/tests/components/tplink/test_sensor.py index 839588d2756..5413e036d96 100644 --- a/tests/components/tplink/test_sensor.py +++ b/tests/components/tplink/test_sensor.py @@ -34,14 +34,14 @@ async def test_color_light_with_an_emeter(hass: HomeAssistant) -> None: voltage=None, current=5, ) - bulb.emeter_today = 5000 + bulb.emeter_today = 5000.0036 with _patch_discovery(device=bulb), _patch_single_discovery(device=bulb): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() await hass.async_block_till_done() expected = { - "sensor.my_bulb_today_s_consumption": 5000, + "sensor.my_bulb_today_s_consumption": 5000.004, "sensor.my_bulb_current": 5, } entity_id = "light.my_bulb" @@ -69,10 +69,10 @@ async def test_plug_with_an_emeter(hass: HomeAssistant) -> None: plug.color_temp = None plug.has_emeter = True plug.emeter_realtime = Mock( - power=100, - total=30, - voltage=121, - current=5, + power=100.06, + total=30.0049, + voltage=121.19, + current=5.035, ) plug.emeter_today = None with _patch_discovery(device=plug), _patch_single_discovery(device=plug): @@ -81,11 +81,11 @@ async def test_plug_with_an_emeter(hass: HomeAssistant) -> None: await hass.async_block_till_done() expected = { - "sensor.my_plug_current_consumption": 100, - "sensor.my_plug_total_consumption": 30, + "sensor.my_plug_current_consumption": 100.1, + "sensor.my_plug_total_consumption": 30.005, "sensor.my_plug_today_s_consumption": 0.0, - "sensor.my_plug_voltage": 121, - "sensor.my_plug_current": 5, + "sensor.my_plug_voltage": 121.2, + "sensor.my_plug_current": 5.04, } entity_id = "switch.my_plug" state = hass.states.get(entity_id) From 1488019cd94d999b06812509c0bc02c3d2a85fc4 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 3 Oct 2021 21:56:50 +0200 Subject: [PATCH 0068/1038] Bump nettigo_air_monitor library to version 1.1.0 (#56952) --- homeassistant/components/nam/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nam/manifest.json b/homeassistant/components/nam/manifest.json index a1401c485de..74555941351 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.0.0"], + "requirements": ["nettigo-air-monitor==1.1.0"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 57cd02b3a26..935e10a662c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1053,7 +1053,7 @@ netdisco==3.0.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==1.0.0 +nettigo-air-monitor==1.1.0 # homeassistant.components.neurio_energy neurio==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 76b3946dadb..b5250070275 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -621,7 +621,7 @@ netdisco==3.0.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==1.0.0 +nettigo-air-monitor==1.1.0 # homeassistant.components.nexia nexia==0.9.11 From 946a265c9e6742b006513969958129aac7aa3a3c Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 3 Oct 2021 22:12:59 +0200 Subject: [PATCH 0069/1038] Use Final type only when needed in Tractive (#57000) --- homeassistant/components/tractive/__init__.py | 6 ++--- .../components/tractive/binary_sensor.py | 6 ++--- .../components/tractive/config_flow.py | 6 ++--- homeassistant/components/tractive/const.py | 27 +++++++++---------- homeassistant/components/tractive/sensor.py | 4 +-- homeassistant/components/tractive/switch.py | 6 ++--- 6 files changed, 27 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index be9f2b317d2..1d51ab66585 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from dataclasses import dataclass import logging -from typing import Any, Final, List, cast +from typing import Any, List, cast import aiotractive @@ -37,10 +37,10 @@ from .const import ( TRACKER_POSITION_UPDATED, ) -PLATFORMS: Final = ["binary_sensor", "device_tracker", "sensor", "switch"] +PLATFORMS = ["binary_sensor", "device_tracker", "sensor", "switch"] -_LOGGER: Final = logging.getLogger(__name__) +_LOGGER = logging.getLogger(__name__) @dataclass diff --git a/homeassistant/components/tractive/binary_sensor.py b/homeassistant/components/tractive/binary_sensor.py index d9b41c83940..1686fb0af9b 100644 --- a/homeassistant/components/tractive/binary_sensor.py +++ b/homeassistant/components/tractive/binary_sensor.py @@ -1,7 +1,7 @@ """Support for Tractive binary sensors.""" from __future__ import annotations -from typing import Any, Final +from typing import Any from homeassistant.components.binary_sensor import ( DEVICE_CLASS_BATTERY_CHARGING, @@ -24,7 +24,7 @@ from .const import ( ) from .entity import TractiveEntity -TRACKERS_WITH_BUILTIN_BATTERY: Final = ("TRNJA4", "TRAXL1") +TRACKERS_WITH_BUILTIN_BATTERY = ("TRNJA4", "TRAXL1") class TractiveBinarySensor(TractiveEntity, BinarySensorEntity): @@ -73,7 +73,7 @@ class TractiveBinarySensor(TractiveEntity, BinarySensorEntity): ) -SENSOR_TYPE: Final = BinarySensorEntityDescription( +SENSOR_TYPE = BinarySensorEntityDescription( key=ATTR_BATTERY_CHARGING, name="Battery Charging", device_class=DEVICE_CLASS_BATTERY_CHARGING, diff --git a/homeassistant/components/tractive/config_flow.py b/homeassistant/components/tractive/config_flow.py index f3b36ae3d03..7ba6602a520 100644 --- a/homeassistant/components/tractive/config_flow.py +++ b/homeassistant/components/tractive/config_flow.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any, Final +from typing import Any import aiotractive import voluptuous as vol @@ -15,9 +15,9 @@ from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN -_LOGGER: Final = logging.getLogger(__name__) +_LOGGER = logging.getLogger(__name__) -USER_DATA_SCHEMA: Final = vol.Schema( +USER_DATA_SCHEMA = vol.Schema( {vol.Required(CONF_EMAIL): str, vol.Required(CONF_PASSWORD): str} ) diff --git a/homeassistant/components/tractive/const.py b/homeassistant/components/tractive/const.py index f0ee6b7813e..6a61024cd51 100644 --- a/homeassistant/components/tractive/const.py +++ b/homeassistant/components/tractive/const.py @@ -1,23 +1,22 @@ """Constants for the tractive integration.""" from datetime import timedelta -from typing import Final -DOMAIN: Final = "tractive" +DOMAIN = "tractive" -RECONNECT_INTERVAL: Final = timedelta(seconds=10) +RECONNECT_INTERVAL = timedelta(seconds=10) -ATTR_DAILY_GOAL: Final = "daily_goal" -ATTR_BUZZER: Final = "buzzer" -ATTR_LED: Final = "led" -ATTR_LIVE_TRACKING: Final = "live_tracking" -ATTR_MINUTES_ACTIVE: Final = "minutes_active" +ATTR_DAILY_GOAL = "daily_goal" +ATTR_BUZZER = "buzzer" +ATTR_LED = "led" +ATTR_LIVE_TRACKING = "live_tracking" +ATTR_MINUTES_ACTIVE = "minutes_active" -CLIENT: Final = "client" -TRACKABLES: Final = "trackables" +CLIENT = "client" +TRACKABLES = "trackables" -TRACKER_HARDWARE_STATUS_UPDATED: Final = f"{DOMAIN}_tracker_hardware_status_updated" -TRACKER_POSITION_UPDATED: Final = f"{DOMAIN}_tracker_position_updated" -TRACKER_ACTIVITY_STATUS_UPDATED: Final = f"{DOMAIN}_tracker_activity_updated" +TRACKER_HARDWARE_STATUS_UPDATED = f"{DOMAIN}_tracker_hardware_status_updated" +TRACKER_POSITION_UPDATED = f"{DOMAIN}_tracker_position_updated" +TRACKER_ACTIVITY_STATUS_UPDATED = f"{DOMAIN}_tracker_activity_updated" -SERVER_UNAVAILABLE: Final = f"{DOMAIN}_server_unavailable" +SERVER_UNAVAILABLE = f"{DOMAIN}_server_unavailable" diff --git a/homeassistant/components/tractive/sensor.py b/homeassistant/components/tractive/sensor.py index b7025b3555b..f81bcc6f869 100644 --- a/homeassistant/components/tractive/sensor.py +++ b/homeassistant/components/tractive/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any, Final +from typing import Any from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry @@ -127,7 +127,7 @@ class TractiveActivitySensor(TractiveSensor): ) -SENSOR_TYPES: Final[tuple[TractiveSensorEntityDescription, ...]] = ( +SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( TractiveSensorEntityDescription( key=ATTR_BATTERY_LEVEL, name="Battery Level", diff --git a/homeassistant/components/tractive/switch.py b/homeassistant/components/tractive/switch.py index 6bc5ecb7b1b..e31b380e794 100644 --- a/homeassistant/components/tractive/switch.py +++ b/homeassistant/components/tractive/switch.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass import logging -from typing import Any, Final, Literal, cast +from typing import Any, Literal, cast from aiotractive.exceptions import TractiveError @@ -26,7 +26,7 @@ from .const import ( ) from .entity import TractiveEntity -_LOGGER: Final = logging.getLogger(__name__) +_LOGGER = logging.getLogger(__name__) @dataclass @@ -43,7 +43,7 @@ class TractiveSwitchEntityDescription( """Class describing Tractive switch entities.""" -SWITCH_TYPES: Final[tuple[TractiveSwitchEntityDescription, ...]] = ( +SWITCH_TYPES: tuple[TractiveSwitchEntityDescription, ...] = ( TractiveSwitchEntityDescription( key=ATTR_BUZZER, name="Tracker Buzzer", From 1d643d6da7a6c280fc014b2e1923fc30869f94f5 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 3 Oct 2021 22:14:28 +0200 Subject: [PATCH 0070/1038] Minor improvements to deCONZ light platform (#56953) Use library constnats for flash and effect Use attr_effect_list to specify supported effects Use isinstance to identify if it is light or group --- homeassistant/components/deconz/light.py | 46 +++++++++++------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 2202bdbe58f..1a3cca6df05 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -2,7 +2,14 @@ from __future__ import annotations -from pydeconz.light import Light +from pydeconz.group import DeconzGroup as Group +from pydeconz.light import ( + ALERT_LONG, + ALERT_SHORT, + EFFECT_COLOR_LOOP, + EFFECT_NONE, + Light, +) from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -35,6 +42,8 @@ from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry DECONZ_GROUP = "is_deconz_group" +EFFECT_TO_DECONZ = {EFFECT_COLORLOOP: EFFECT_COLOR_LOOP, "None": EFFECT_NONE} +FLASH_TO_DECONZ = {FLASH_SHORT: ALERT_SHORT, FLASH_LONG: ALERT_LONG} async def async_setup_entry(hass, config_entry, async_add_entities): @@ -126,6 +135,7 @@ class DeconzBaseLight(DeconzDevice, LightEntity): if device.effect is not None: self._attr_supported_features |= SUPPORT_EFFECT + self._attr_effect_list = [EFFECT_COLORLOOP] @property def color_mode(self) -> str: @@ -147,11 +157,6 @@ class DeconzBaseLight(DeconzDevice, LightEntity): """Return the brightness of this light between 0..255.""" return self._device.brightness - @property - def effect_list(self): - """Return the list of supported effects.""" - return [EFFECT_COLORLOOP] - @property def color_temp(self): """Return the CT color value.""" @@ -197,19 +202,12 @@ class DeconzBaseLight(DeconzDevice, LightEntity): elif "IKEA" in self._device.manufacturer: data["transition_time"] = 0 - if ATTR_FLASH in kwargs: - if kwargs[ATTR_FLASH] == FLASH_SHORT: - data["alert"] = "select" - del data["on"] - elif kwargs[ATTR_FLASH] == FLASH_LONG: - data["alert"] = "lselect" - del data["on"] + if (alert := FLASH_TO_DECONZ.get(kwargs.get(ATTR_FLASH))) is not None: + data["alert"] = alert + del data["on"] - if ATTR_EFFECT in kwargs: - if kwargs[ATTR_EFFECT] == EFFECT_COLORLOOP: - data["effect"] = "colorloop" - else: - data["effect"] = "none" + if (effect := EFFECT_TO_DECONZ.get(kwargs.get(ATTR_EFFECT))) is not None: + data["effect"] = effect await self._device.set_state(**data) @@ -224,20 +222,16 @@ class DeconzBaseLight(DeconzDevice, LightEntity): data["brightness"] = 0 data["transition_time"] = int(kwargs[ATTR_TRANSITION] * 10) - if ATTR_FLASH in kwargs: - if kwargs[ATTR_FLASH] == FLASH_SHORT: - data["alert"] = "select" - del data["on"] - elif kwargs[ATTR_FLASH] == FLASH_LONG: - data["alert"] = "lselect" - del data["on"] + if (alert := FLASH_TO_DECONZ.get(kwargs.get(ATTR_FLASH))) is not None: + data["alert"] = alert + del data["on"] await self._device.set_state(**data) @property def extra_state_attributes(self): """Return the device state attributes.""" - return {DECONZ_GROUP: self._device.type == "LightGroup"} + return {DECONZ_GROUP: isinstance(self._device, Group)} class DeconzLight(DeconzBaseLight): From c46e8cfbc1891ac698c7099b6812eeea9314ff59 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 4 Oct 2021 00:09:58 +0200 Subject: [PATCH 0071/1038] Bump pyatmo to v6.1.0 (#57014) --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index f51f1a22f48..f162abbaad5 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", "requirements": [ - "pyatmo==6.0.0" + "pyatmo==6.1.0" ], "after_dependencies": [ "cloud", diff --git a/requirements_all.txt b/requirements_all.txt index 935e10a662c..93f681896de 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1360,7 +1360,7 @@ pyarlo==0.2.4 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==6.0.0 +pyatmo==6.1.0 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b5250070275..e24f5f54687 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -802,7 +802,7 @@ pyarlo==0.2.4 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==6.0.0 +pyatmo==6.1.0 # homeassistant.components.apple_tv pyatv==0.8.2 From 2b464b00ddf16d109ab09f245ab581e1175156a7 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 3 Oct 2021 18:24:23 -0400 Subject: [PATCH 0072/1038] Bump zwave-js-server-python to 0.31.2 (#57007) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 1a9091005e2..e80549d815d 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.31.1"], + "requirements": ["zwave-js-server-python==0.31.2"], "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 93f681896de..2d635a0045a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2504,4 +2504,4 @@ zigpy==0.38.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.31.1 +zwave-js-server-python==0.31.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e24f5f54687..c8377b2b948 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1433,4 +1433,4 @@ zigpy-znp==0.5.4 zigpy==0.38.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.31.1 +zwave-js-server-python==0.31.2 From 9d671eff27e3c8a2f81b57b634983ebb06bf8eff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Oct 2021 13:07:25 -1000 Subject: [PATCH 0073/1038] Bump yeelight to 0.7.6 (#57009) - Fixes compat with Lamp15 model - May improvment Monob model drops seen in #56646 Changes: https://gitlab.com/stavros/python-yeelight/-/commit/0b94e5214e3375f20defa386067ef6cb058c872c --- homeassistant/components/yeelight/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index cc40f07ce46..561606f5509 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,7 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.7.5", "async-upnp-client==0.22.5"], + "requirements": ["yeelight==0.7.6", "async-upnp-client==0.22.5"], "codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"], "config_flow": true, "dependencies": ["network"], diff --git a/requirements_all.txt b/requirements_all.txt index 2d635a0045a..b5dc17c5c97 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2459,7 +2459,7 @@ yalesmartalarmclient==0.3.4 yalexs==1.1.13 # homeassistant.components.yeelight -yeelight==0.7.5 +yeelight==0.7.6 # homeassistant.components.yeelightsunflower yeelightsunflower==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c8377b2b948..c2057ac9cd6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1406,7 +1406,7 @@ yalesmartalarmclient==0.3.4 yalexs==1.1.13 # homeassistant.components.yeelight -yeelight==0.7.5 +yeelight==0.7.6 # homeassistant.components.youless youless-api==0.13 From 397644329234ba6e60d9ee149cbef32d12cb367d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 4 Oct 2021 01:42:25 +0200 Subject: [PATCH 0074/1038] Use EntityDescription - homekit_controller (#56945) --- .../components/homekit_controller/number.py | 49 ++-- .../components/homekit_controller/sensor.py | 236 +++++++++--------- 2 files changed, 136 insertions(+), 149 deletions(-) diff --git a/homeassistant/components/homekit_controller/number.py b/homeassistant/components/homekit_controller/number.py index 79130bfcef7..beb0ffa7fa5 100644 --- a/homeassistant/components/homekit_controller/number.py +++ b/homeassistant/components/homekit_controller/number.py @@ -4,22 +4,26 @@ Support for Homekit number ranges. These are mostly used where a HomeKit accessory exposes additional non-standard characteristics that don't map to a Home Assistant feature. """ +from __future__ import annotations + from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes -from homeassistant.components.number import NumberEntity +from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.core import callback from . import KNOWN_DEVICES, CharacteristicEntity -NUMBER_ENTITIES = { - CharacteristicsTypes.Vendor.VOCOLINC_HUMIDIFIER_SPRAY_LEVEL: { - "name": "Spray Quantity", - "icon": "mdi:water", - }, - CharacteristicsTypes.Vendor.EVE_DEGREE_ELEVATION: { - "name": "Elevation", - "icon": "mdi:elevation-rise", - }, +NUMBER_ENTITIES: dict[str, NumberEntityDescription] = { + CharacteristicsTypes.Vendor.VOCOLINC_HUMIDIFIER_SPRAY_LEVEL: NumberEntityDescription( + key=CharacteristicsTypes.Vendor.VOCOLINC_HUMIDIFIER_SPRAY_LEVEL, + name="Spray Quantity", + icon="mdi:water", + ), + CharacteristicsTypes.Vendor.EVE_DEGREE_ELEVATION: NumberEntityDescription( + key=CharacteristicsTypes.Vendor.EVE_DEGREE_ELEVATION, + name="Elevation", + icon="mdi:elevation-rise", + ), } @@ -30,11 +34,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def async_add_characteristic(char: Characteristic): - kwargs = NUMBER_ENTITIES.get(char.type) - if not kwargs: + if not (description := NUMBER_ENTITIES.get(char.type)): return False info = {"aid": char.service.accessory.aid, "iid": char.service.iid} - async_add_entities([HomeKitNumber(conn, info, char, **kwargs)], True) + async_add_entities([HomeKitNumber(conn, info, char, description)], True) return True conn.add_char_factory(async_add_characteristic) @@ -48,32 +51,16 @@ class HomeKitNumber(CharacteristicEntity, NumberEntity): conn, info, char, - device_class=None, - icon=None, - name=None, - **kwargs, + description: NumberEntityDescription, ): """Initialise a HomeKit number control.""" - self._device_class = device_class - self._icon = icon - self._name = name - + self.entity_description = description super().__init__(conn, info, char) def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" return [self._char.type] - @property - def device_class(self): - """Return type of sensor.""" - return self._device_class - - @property - def icon(self): - """Return the sensor icon.""" - return self._icon - @property def min_value(self) -> float: """Return the minimum value.""" diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index aaddadd2c53..bc0ed0455fa 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -1,8 +1,17 @@ """Support for Homekit sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes from aiohomekit.model.services import ServicesTypes -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, @@ -32,92 +41,112 @@ from . import KNOWN_DEVICES, CharacteristicEntity, HomeKitEntity CO2_ICON = "mdi:molecule-co2" -SIMPLE_SENSOR = { - CharacteristicsTypes.Vendor.EVE_ENERGY_WATT: { - "name": "Real Time Energy", - "device_class": DEVICE_CLASS_POWER, - "state_class": STATE_CLASS_MEASUREMENT, - "unit": POWER_WATT, - }, - CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY: { - "name": "Real Time Energy", - "device_class": DEVICE_CLASS_POWER, - "state_class": STATE_CLASS_MEASUREMENT, - "unit": POWER_WATT, - }, - CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY_2: { - "name": "Real Time Energy", - "device_class": DEVICE_CLASS_POWER, - "state_class": STATE_CLASS_MEASUREMENT, - "unit": POWER_WATT, - }, - CharacteristicsTypes.Vendor.EVE_DEGREE_AIR_PRESSURE: { - "name": "Air Pressure", - "device_class": DEVICE_CLASS_PRESSURE, - "state_class": STATE_CLASS_MEASUREMENT, - "unit": PRESSURE_HPA, - }, - CharacteristicsTypes.TEMPERATURE_CURRENT: { - "name": "Current Temperature", - "device_class": DEVICE_CLASS_TEMPERATURE, - "state_class": STATE_CLASS_MEASUREMENT, - "unit": TEMP_CELSIUS, +@dataclass +class HomeKitSensorEntityDescription(SensorEntityDescription): + """Describes Homekit sensor.""" + + probe: Callable[[Characteristic], bool] | None = None + + +SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { + CharacteristicsTypes.Vendor.EVE_ENERGY_WATT: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.Vendor.EVE_ENERGY_WATT, + name="Real Time Energy", + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=POWER_WATT, + ), + CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY, + name="Real Time Energy", + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=POWER_WATT, + ), + CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY_2: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY_2, + name="Real Time Energy", + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=POWER_WATT, + ), + CharacteristicsTypes.Vendor.EVE_DEGREE_AIR_PRESSURE: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.Vendor.EVE_DEGREE_AIR_PRESSURE, + name="Air Pressure", + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=PRESSURE_HPA, + ), + CharacteristicsTypes.TEMPERATURE_CURRENT: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.TEMPERATURE_CURRENT, + name="Current Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=TEMP_CELSIUS, # This sensor is only for temperature characteristics that are not part # of a temperature sensor service. - "probe": lambda char: char.service.type + probe=lambda char: char.service.type != ServicesTypes.get_uuid(ServicesTypes.TEMPERATURE_SENSOR), - }, - CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT: { - "name": "Current Humidity", - "device_class": DEVICE_CLASS_HUMIDITY, - "state_class": STATE_CLASS_MEASUREMENT, - "unit": PERCENTAGE, + ), + CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT, + name="Current Humidity", + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, # This sensor is only for humidity characteristics that are not part # of a humidity sensor service. - "probe": lambda char: char.service.type + probe=lambda char: char.service.type != ServicesTypes.get_uuid(ServicesTypes.HUMIDITY_SENSOR), - }, - CharacteristicsTypes.AIR_QUALITY: { - "name": "Air Quality", - "device_class": DEVICE_CLASS_AQI, - "state_class": STATE_CLASS_MEASUREMENT, - }, - CharacteristicsTypes.DENSITY_PM25: { - "name": "PM2.5 Density", - "device_class": DEVICE_CLASS_PM25, - "state_class": STATE_CLASS_MEASUREMENT, - "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - }, - CharacteristicsTypes.DENSITY_PM10: { - "name": "PM10 Density", - "device_class": DEVICE_CLASS_PM10, - "state_class": STATE_CLASS_MEASUREMENT, - "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - }, - CharacteristicsTypes.DENSITY_OZONE: { - "name": "Ozone Density", - "device_class": DEVICE_CLASS_OZONE, - "state_class": STATE_CLASS_MEASUREMENT, - "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - }, - CharacteristicsTypes.DENSITY_NO2: { - "name": "Nitrogen Dioxide Density", - "device_class": DEVICE_CLASS_NITROGEN_DIOXIDE, - "state_class": STATE_CLASS_MEASUREMENT, - "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - }, - CharacteristicsTypes.DENSITY_SO2: { - "name": "Sulphur Dioxide Density", - "device_class": DEVICE_CLASS_SULPHUR_DIOXIDE, - "state_class": STATE_CLASS_MEASUREMENT, - "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - }, - CharacteristicsTypes.DENSITY_VOC: { - "name": "Volatile Organic Compound Density", - "device_class": DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, - "state_class": STATE_CLASS_MEASUREMENT, - "unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - }, + ), + CharacteristicsTypes.AIR_QUALITY: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.AIR_QUALITY, + name="Air Quality", + device_class=DEVICE_CLASS_AQI, + state_class=STATE_CLASS_MEASUREMENT, + ), + CharacteristicsTypes.DENSITY_PM25: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.DENSITY_PM25, + name="PM2.5 Density", + device_class=DEVICE_CLASS_PM25, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ), + CharacteristicsTypes.DENSITY_PM10: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.DENSITY_PM10, + name="PM10 Density", + device_class=DEVICE_CLASS_PM10, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ), + CharacteristicsTypes.DENSITY_OZONE: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.DENSITY_OZONE, + name="Ozone Density", + device_class=DEVICE_CLASS_OZONE, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ), + CharacteristicsTypes.DENSITY_NO2: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.DENSITY_NO2, + name="Nitrogen Dioxide Density", + device_class=DEVICE_CLASS_NITROGEN_DIOXIDE, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ), + CharacteristicsTypes.DENSITY_SO2: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.DENSITY_SO2, + name="Sulphur Dioxide Density", + device_class=DEVICE_CLASS_SULPHUR_DIOXIDE, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ), + CharacteristicsTypes.DENSITY_VOC: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.DENSITY_VOC, + name="Volatile Organic Compound Density", + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ), } # For legacy reasons, "built-in" characteristic types are in their short form @@ -285,55 +314,27 @@ class SimpleSensor(CharacteristicEntity, SensorEntity): be multiple entities per HomeKit service (this was not previously the case). """ + entity_description: HomeKitSensorEntityDescription + def __init__( self, conn, info, char, - device_class=None, - state_class=None, - unit=None, - icon=None, - name=None, - **kwargs, + description: HomeKitSensorEntityDescription, ): """Initialise a secondary HomeKit characteristic sensor.""" - self._device_class = device_class - self._state_class = state_class - self._unit = unit - self._icon = icon - self._name = name - + self.entity_description = description super().__init__(conn, info, char) def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" return [self._char.type] - @property - def device_class(self): - """Return type of sensor.""" - return self._device_class - - @property - def state_class(self): - """Return type of state.""" - return self._state_class - - @property - def native_unit_of_measurement(self): - """Return units for the sensor.""" - return self._unit - - @property - def icon(self): - """Return the sensor icon.""" - return self._icon - @property def name(self) -> str: """Return the name of the device if any.""" - return f"{super().name} - {self._name}" + return f"{super().name} - {self.entity_description.name}" @property def native_value(self): @@ -368,13 +369,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def async_add_characteristic(char: Characteristic): - kwargs = SIMPLE_SENSOR.get(char.type) - if not kwargs: + if not (description := SIMPLE_SENSOR.get(char.type)): return False - if "probe" in kwargs and not kwargs["probe"](char): + if description.probe and not description.probe(char): return False info = {"aid": char.service.accessory.aid, "iid": char.service.iid} - async_add_entities([SimpleSensor(conn, info, char, **kwargs)], True) + async_add_entities([SimpleSensor(conn, info, char, description)], True) return True From 1747578be5f2bf450e0884982094b38962a17fc8 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 4 Oct 2021 01:55:07 +0200 Subject: [PATCH 0075/1038] Fix camera tests (#57020) --- tests/components/netatmo/common.py | 11 +++++++++++ tests/components/netatmo/conftest.py | 3 ++- tests/components/netatmo/test_camera.py | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/components/netatmo/common.py b/tests/components/netatmo/common.py index 5ba989e2504..f2c03ac7de1 100644 --- a/tests/components/netatmo/common.py +++ b/tests/components/netatmo/common.py @@ -71,6 +71,17 @@ async def fake_post_request(*args, **kwargs): ) +async def fake_get_image(*args, **kwargs): + """Return fake data.""" + if "url" not in kwargs: + return "{}" + + endpoint = kwargs["url"].split("/")[-1] + + if endpoint in "snapshot_720.jpg": + return b"test stream image bytes" + + async def fake_post_request_no_data(*args, **kwargs): """Fake error during requesting backend data.""" return "{}" diff --git a/tests/components/netatmo/conftest.py b/tests/components/netatmo/conftest.py index d443802a41d..4d6bbb752f3 100644 --- a/tests/components/netatmo/conftest.py +++ b/tests/components/netatmo/conftest.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, patch import pytest -from .common import ALL_SCOPES, fake_post_request +from .common import ALL_SCOPES, fake_get_image, fake_post_request from tests.common import MockConfigEntry @@ -60,6 +60,7 @@ def netatmo_auth(): "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" ) as mock_auth: mock_auth.return_value.async_post_request.side_effect = fake_post_request + mock_auth.return_value.async_get_image.side_effect = fake_get_image mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() yield diff --git a/tests/components/netatmo/test_camera.py b/tests/components/netatmo/test_camera.py index c8132331bf3..45c8dc48b22 100644 --- a/tests/components/netatmo/test_camera.py +++ b/tests/components/netatmo/test_camera.py @@ -478,6 +478,7 @@ async def test_camera_image_raises_exception(hass, config_entry, requests_mock): "homeassistant.components.webhook.async_generate_url" ): mock_auth.return_value.async_post_request.side_effect = fake_post + mock_auth.return_value.async_get_image.side_effect = fake_post mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() From a4530d2bfcb51f7139525fdb4a499b2d4f681870 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 4 Oct 2021 00:11:57 +0000 Subject: [PATCH 0076/1038] [ci skip] Translation update --- .../components/flux_led/translations/ca.json | 36 +++++++++++++++++++ .../components/flux_led/translations/et.json | 36 +++++++++++++++++++ .../components/flux_led/translations/hu.json | 36 +++++++++++++++++++ .../components/flux_led/translations/it.json | 36 +++++++++++++++++++ .../components/flux_led/translations/ru.json | 36 +++++++++++++++++++ .../components/tuya/translations/hu.json | 14 ++++++++ .../components/zwave_js/translations/hu.json | 8 +++++ 7 files changed, 202 insertions(+) create mode 100644 homeassistant/components/flux_led/translations/ca.json create mode 100644 homeassistant/components/flux_led/translations/et.json create mode 100644 homeassistant/components/flux_led/translations/hu.json create mode 100644 homeassistant/components/flux_led/translations/it.json create mode 100644 homeassistant/components/flux_led/translations/ru.json diff --git a/homeassistant/components/flux_led/translations/ca.json b/homeassistant/components/flux_led/translations/ca.json new file mode 100644 index 00000000000..25314edc1b8 --- /dev/null +++ b/homeassistant/components/flux_led/translations/ca.json @@ -0,0 +1,36 @@ +{ + "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" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "flow_title": "{model} {id} ({ipaddr})", + "step": { + "discovery_confirm": { + "description": "Vols configurar {model} {id} ({ipaddr})?" + }, + "user": { + "data": { + "host": "Amfitri\u00f3" + }, + "description": "Si deixes l'amfitri\u00f3 buit, s'utilitzar\u00e0 el descobriment per cercar dispositius." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "custom_effect_colors": "Efecte personalitzat: llista d'1 a 16 colors [R,G,B]. Exemple: [255,0,255],[60,128,0]", + "custom_effect_speed_pct": "Efecte personalitzat: velocitat en percentatges de l'efecte de canvi de color.", + "custom_effect_transition": "Efecte personalitzat: tipus de transici\u00f3 entre colors.", + "mode": "Mode de brillantor escollit." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/et.json b/homeassistant/components/flux_led/translations/et.json new file mode 100644 index 00000000000..0c2e1f444cb --- /dev/null +++ b/homeassistant/components/flux_led/translations/et.json @@ -0,0 +1,36 @@ +{ + "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" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "flow_title": "{model} {id} ({ipaddr})", + "step": { + "discovery_confirm": { + "description": "Kas seadistada {model} {id} ({ipaddr})?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Kui j\u00e4tad hosti t\u00fchjaks kasutatakse seadmete leidmiseks avastamist." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "custom_effect_colors": "Kohandatud efekt: Loetelu 1 kuni 16 [R,G,B] v\u00e4rvist. N\u00e4ide: [255,0,255],[60,128,0]", + "custom_effect_speed_pct": "Kohandatud efekt: v\u00e4rvide vahetamise efekti kiirus protsentides.", + "custom_effect_transition": "Kohandatud efekt: v\u00e4rvide vahelise \u00fclemineku t\u00fc\u00fcp.", + "mode": "Valitud heleduse re\u017eiim." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/hu.json b/homeassistant/components/flux_led/translations/hu.json new file mode 100644 index 00000000000..3cfef2c9eb6 --- /dev/null +++ b/homeassistant/components/flux_led/translations/hu.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "flow_title": "{model} {id} ({ipaddr})", + "step": { + "discovery_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani a(z) {model} {id} ({ipaddr}) webhelyet?" + }, + "user": { + "data": { + "host": "C\u00edm" + }, + "description": "Ha nem ad meg c\u00edmet, akkor az eszk\u00f6z\u00f6k keres\u00e9se a felder\u00edt\u00e9ssel t\u00f6rt\u00e9nik." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "custom_effect_colors": "Egy\u00e9ni effektus: 1-16 [R,G,B] sz\u00edn list\u00e1ja. P\u00e9lda: [255,0,255], [60,128,0]", + "custom_effect_speed_pct": "Egy\u00e9ni effektus: A sz\u00edneket v\u00e1lt\u00f3 hat\u00e1s sz\u00e1zal\u00e9kos ar\u00e1nya.", + "custom_effect_transition": "Egy\u00e9ni hat\u00e1s: A sz\u00ednek k\u00f6z\u00f6tti \u00e1tmenet t\u00edpusa.", + "mode": "A v\u00e1lasztott f\u00e9nyer\u0151 m\u00f3d." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/it.json b/homeassistant/components/flux_led/translations/it.json new file mode 100644 index 00000000000..91654fb3542 --- /dev/null +++ b/homeassistant/components/flux_led/translations/it.json @@ -0,0 +1,36 @@ +{ + "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" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "flow_title": "{model} {id} ({ipaddr})", + "step": { + "discovery_confirm": { + "description": "Vuoi configurare {model} {id} ({ipaddr})?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Se lasci vuoto l'host, il rilevamento verr\u00e0 utilizzato per trovare i dispositivi." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "custom_effect_colors": "Effetto personalizzato: Lista da 1 a 16 colori [R,G,B]. Esempio: [255,0,255],[60,128,0]", + "custom_effect_speed_pct": "Effetto personalizzato: Velocit\u00e0 in percentuale per l'effetto che cambia colore.", + "custom_effect_transition": "Effetto personalizzato: Tipo di transizione tra i colori.", + "mode": "La modalit\u00e0 di luminosit\u00e0 scelta." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/ru.json b/homeassistant/components/flux_led/translations/ru.json new file mode 100644 index 00000000000..e0f7a73baab --- /dev/null +++ b/homeassistant/components/flux_led/translations/ru.json @@ -0,0 +1,36 @@ +{ + "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." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "flow_title": "{model} {id} ({ipaddr})", + "step": { + "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} {id} ({ipaddr})?" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u0415\u0441\u043b\u0438 \u043d\u0435 \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430, \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0431\u0443\u0434\u0443\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "custom_effect_colors": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u044d\u0444\u0444\u0435\u043a\u0442: \u0441\u043f\u0438\u0441\u043e\u043a \u043e\u0442 1 \u0434\u043e 16 [R,G,B] \u0446\u0432\u0435\u0442\u043e\u0432. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: [255,0,255],[60,128,0]", + "custom_effect_speed_pct": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u044d\u0444\u0444\u0435\u043a\u0442: \u0441\u043a\u043e\u0440\u043e\u0441\u0442\u044c \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0446\u0432\u0435\u0442\u043e\u0432 (\u0432 \u043f\u0440\u043e\u0446\u0435\u043d\u0442\u0430\u0445).", + "custom_effect_transition": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u044d\u0444\u0444\u0435\u043a\u0442: \u0442\u0438\u043f \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0430 \u043c\u0435\u0436\u0434\u0443 \u0446\u0432\u0435\u0442\u0430\u043c\u0438.", + "mode": "\u0412\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c \u044f\u0440\u043a\u043e\u0441\u0442\u0438." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/hu.json b/homeassistant/components/tuya/translations/hu.json index b90d2a2ff81..d721a8cd133 100644 --- a/homeassistant/components/tuya/translations/hu.json +++ b/homeassistant/components/tuya/translations/hu.json @@ -10,11 +10,25 @@ }, "flow_title": "Tuya konfigur\u00e1ci\u00f3", "step": { + "login": { + "data": { + "access_id": "Hozz\u00e1f\u00e9r\u00e9si azonos\u00edt\u00f3", + "access_secret": "Hozz\u00e1f\u00e9r\u00e9si token", + "country_code": "Orsz\u00e1g k\u00f3d", + "endpoint": "El\u00e9rhet\u0151s\u00e9gi z\u00f3na", + "password": "Jelsz\u00f3", + "tuya_app_type": "Mobil app", + "username": "Fi\u00f3k" + }, + "description": "Adja meg Tuya hiteles\u00edt\u0151 adatait.", + "title": "Tuya" + }, "user": { "data": { "country_code": "A fi\u00f3k orsz\u00e1gk\u00f3dja (pl. 1 USA, 36 Magyarorsz\u00e1g, vagy 86 K\u00edna)", "password": "Jelsz\u00f3", "platform": "Az alkalmaz\u00e1s, ahol a fi\u00f3k regisztr\u00e1lt", + "tuya_project_type": "Tuya felh\u0151 projekt t\u00edpusa", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, "description": "Adja meg Tuya hiteles\u00edt\u0151 adatait.", diff --git a/homeassistant/components/zwave_js/translations/hu.json b/homeassistant/components/zwave_js/translations/hu.json index bf541fce26a..715881fb329 100644 --- a/homeassistant/components/zwave_js/translations/hu.json +++ b/homeassistant/components/zwave_js/translations/hu.json @@ -27,6 +27,10 @@ "configure_addon": { "data": { "network_key": "H\u00e1l\u00f3zati kulcs", + "s0_legacy_key": "S0 kulcs (r\u00e9gi)", + "s2_access_control_key": "S2 Hozz\u00e1f\u00e9r\u00e9s kulcs", + "s2_authenticated_key": "S2 hiteles\u00edtett kulcs", + "s2_unauthenticated_key": "S2 nem hiteles\u00edtett kulcs", "usb_path": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" }, "title": "Adja meg a Z-Wave JS kieg\u00e9sz\u00edt\u0151 konfigur\u00e1ci\u00f3j\u00e1t" @@ -109,6 +113,10 @@ "emulate_hardware": "Hardver emul\u00e1ci\u00f3", "log_level": "Napl\u00f3szint", "network_key": "H\u00e1l\u00f3zati kulcs", + "s0_legacy_key": "S0 kulcs (r\u00e9gi)", + "s2_access_control_key": "S2 hozz\u00e1f\u00e9r\u00e9si ", + "s2_authenticated_key": "S2 hiteles\u00edtett kulcs", + "s2_unauthenticated_key": "S2 nem hiteles\u00edtett kulcs", "usb_path": "USB eszk\u00f6z \u00fatvonala" }, "title": "Adja meg a Z-Wave JS kieg\u00e9sz\u00edt\u0151 konfigur\u00e1ci\u00f3j\u00e1t" From 19d54399c25f9cf32bed86754e1ff68aa0a954d1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Oct 2021 14:40:27 -1000 Subject: [PATCH 0077/1038] Add DHCP support for TPLink KP400 (#57023) --- homeassistant/components/tplink/manifest.json | 4 ++++ homeassistant/generated/dhcp.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index a24d95bbc75..0c45ca84ac6 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -9,6 +9,10 @@ "quality_scale": "platinum", "iot_class": "local_polling", "dhcp": [ + { + "hostname": "k[lp]*", + "macaddress": "403F8C*" + }, { "hostname": "ep*", "macaddress": "E848B8*" diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 9036832c2f1..3a0d42d88ea 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -279,6 +279,11 @@ DHCP = [ "hostname": "eneco-*", "macaddress": "74C63B*" }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "403F8C*" + }, { "domain": "tplink", "hostname": "ep*", From 33541ab28716aae87871db35499b9eb671e7a0de Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 3 Oct 2021 23:13:08 -0500 Subject: [PATCH 0078/1038] Shorten album titles when browsing artist (#57027) --- homeassistant/components/plex/helpers.py | 5 ++- tests/components/plex/test_browse_media.py | 45 +++++++++++++++++++--- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/plex/helpers.py b/homeassistant/components/plex/helpers.py index be873614ba6..c534eca1f27 100644 --- a/homeassistant/components/plex/helpers.py +++ b/homeassistant/components/plex/helpers.py @@ -5,7 +5,10 @@ def pretty_title(media, short_name=False): """Return a formatted title for the given media item.""" year = None if media.type == "album": - title = f"{media.parentTitle} - {media.title}" + if short_name: + title = media.title + else: + title = f"{media.parentTitle} - {media.title}" elif media.type == "episode": title = f"{media.seasonEpisode.upper()} - {media.title}" if not short_name: diff --git a/tests/components/plex/test_browse_media.py b/tests/components/plex/test_browse_media.py index be4869839d2..d4ea73f6a97 100644 --- a/tests/components/plex/test_browse_media.py +++ b/tests/components/plex/test_browse_media.py @@ -46,6 +46,18 @@ class MockPlexEpisode: type = "episode" +class MockPlexArtist: + """Mock a plexapi Artist instance.""" + + ratingKey = 300 + title = "Artist" + type = "artist" + + def __iter__(self): + """Iterate over albums.""" + yield MockPlexAlbum() + + class MockPlexAlbum: """Mock a plexapi Album instance.""" @@ -53,7 +65,7 @@ class MockPlexAlbum: parentTitle = "Artist" title = "Album" type = "album" - year = 2001 + year = 2019 def __iter__(self): """Iterate over tracks.""" @@ -290,11 +302,13 @@ async def test_browse_media( assert result[ATTR_MEDIA_CONTENT_TYPE] == "library" assert result["title"] == "Music" - # Browse into a Plex album + # Browse into a Plex artist msg_id += 1 - mock_album = MockPlexAlbum() + mock_artist = MockPlexArtist() + mock_album = next(iter(MockPlexArtist())) + mock_track = next(iter(MockPlexAlbum())) with patch.object( - mock_plex_server, "fetch_item", return_value=mock_album + mock_plex_server, "fetch_item", return_value=mock_artist ) as mock_fetch: await websocket_client.send_json( { @@ -312,14 +326,35 @@ async def test_browse_media( msg = await websocket_client.receive_json() assert mock_fetch.called + assert msg["success"] + result = msg["result"] + result_id = int(result[ATTR_MEDIA_CONTENT_ID]) + assert result[ATTR_MEDIA_CONTENT_TYPE] == "artist" + assert result["title"] == mock_artist.title + assert result["children"][0]["title"] == f"{mock_album.title} ({mock_album.year})" + + # Browse into a Plex album + msg_id += 1 + await websocket_client.send_json( + { + "id": msg_id, + "type": "media_player/browse_media", + "entity_id": media_players[0], + ATTR_MEDIA_CONTENT_TYPE: result["children"][-1][ATTR_MEDIA_CONTENT_TYPE], + ATTR_MEDIA_CONTENT_ID: str(result["children"][-1][ATTR_MEDIA_CONTENT_ID]), + } + ) + msg = await websocket_client.receive_json() + assert msg["success"] result = msg["result"] result_id = int(result[ATTR_MEDIA_CONTENT_ID]) assert result[ATTR_MEDIA_CONTENT_TYPE] == "album" assert ( result["title"] - == f"{mock_album.parentTitle} - {mock_album.title} ({mock_album.year})" + == f"{mock_artist.title} - {mock_album.title} ({mock_album.year})" ) + assert result["children"][0]["title"] == f"{mock_track.index}. {mock_track.title}" # Browse into a non-existent TV season unknown_key = 99999999999999 From c72a34dbec2f4c6d9113ed92e7403cd1f00e45a1 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Mon, 4 Oct 2021 05:14:45 +0100 Subject: [PATCH 0079/1038] Use pycarwings2.12 for Nissan Leaf integration (#56996) --- homeassistant/components/nissan_leaf/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nissan_leaf/manifest.json b/homeassistant/components/nissan_leaf/manifest.json index 55cd28d59fa..89e55cb69d9 100644 --- a/homeassistant/components/nissan_leaf/manifest.json +++ b/homeassistant/components/nissan_leaf/manifest.json @@ -2,7 +2,7 @@ "domain": "nissan_leaf", "name": "Nissan Leaf", "documentation": "https://www.home-assistant.io/integrations/nissan_leaf", - "requirements": ["pycarwings2==2.11"], + "requirements": ["pycarwings2==2.12"], "codeowners": ["@filcole"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index b5dc17c5c97..e5e137da8b2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1381,7 +1381,7 @@ pyblackbird==0.5 pybotvac==0.0.22 # homeassistant.components.nissan_leaf -pycarwings2==2.11 +pycarwings2==2.12 # homeassistant.components.cloudflare pycfdns==1.2.1 From 378cfab501c70a6f8566aef6828031da8520bd9a Mon Sep 17 00:00:00 2001 From: Oncleben31 Date: Mon, 4 Oct 2021 06:15:41 +0200 Subject: [PATCH 0080/1038] Meteofrance fix #56975 (#57016) --- .../components/meteo_france/sensor.py | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 9f24cf02a2c..1a5b3c4a33a 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -44,18 +44,23 @@ async def async_setup_entry( MeteoFranceSensor(coordinator_forecast, description) for description in SENSOR_TYPES ] - entities.extend( - [ - MeteoFranceRainSensor(coordinator_rain, description) - for description in SENSOR_TYPES_RAIN - ] - ) - entities.extend( - [ - MeteoFranceAlertSensor(coordinator_alert, description) - for description in SENSOR_TYPES_ALERT - ] - ) + # Add rain forecast entity only if location support this feature + if coordinator_rain: + entities.extend( + [ + MeteoFranceRainSensor(coordinator_rain, description) + for description in SENSOR_TYPES_RAIN + ] + ) + # Add weather alert entity only if location support this feature + if coordinator_alert: + entities.extend( + [ + MeteoFranceAlertSensor(coordinator_alert, description) + for description in SENSOR_TYPES_ALERT + ] + ) + # Add weather probability entities only if location support this feature if coordinator_forecast.data.probability_forecast: entities.extend( [ From 255ffe801bfa2a31f0088e36a0b7f712dbdb46a1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 3 Oct 2021 21:46:06 -0700 Subject: [PATCH 0081/1038] Fix tractive flaky test (#57026) --- tests/components/tractive/test_config_flow.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/components/tractive/test_config_flow.py b/tests/components/tractive/test_config_flow.py index 7ccfdc63a34..115df39175c 100644 --- a/tests/components/tractive/test_config_flow.py +++ b/tests/components/tractive/test_config_flow.py @@ -121,7 +121,10 @@ async def test_reauthentication(hass): assert result["errors"] == {} assert result["step_id"] == "reauth_confirm" - with patch("aiotractive.api.API.user_id", return_value="USERID"): + with patch("aiotractive.api.API.user_id", return_value="USERID"), patch( + "homeassistant.components.tractive.async_setup_entry", + return_value=True, + ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], USER_INPUT, @@ -130,6 +133,7 @@ async def test_reauthentication(hass): assert result2["type"] == "abort" assert result2["reason"] == "reauth_successful" + assert len(mock_setup_entry.mock_calls) == 1 async def test_reauthentication_failure(hass): From 79b10c43d86392c3ab32c2c409c548d00e89f32c Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Mon, 4 Oct 2021 05:59:36 +0100 Subject: [PATCH 0082/1038] Ignore utility_meter restore state if state is invalid (#57010) Co-authored-by: Paulus Schoutsen --- .../components/utility_meter/sensor.py | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 96bf12fdd4d..50185461030 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -1,5 +1,6 @@ """Utility meter from sensors providing raw data.""" from datetime import date, datetime, timedelta +import decimal from decimal import Decimal, DecimalException import logging @@ -323,19 +324,29 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): state = await self.async_get_last_state() if state: - self._state = Decimal(state.state) - self._unit_of_measurement = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - self._last_period = ( - float(state.attributes.get(ATTR_LAST_PERIOD)) - if state.attributes.get(ATTR_LAST_PERIOD) - else 0 - ) - self._last_reset = dt_util.as_utc( - dt_util.parse_datetime(state.attributes.get(ATTR_LAST_RESET)) - ) - if state.attributes.get(ATTR_STATUS) == COLLECTING: - # Fake cancellation function to init the meter in similar state - self._collecting = lambda: None + try: + self._state = Decimal(state.state) + except decimal.InvalidOperation: + _LOGGER.error( + "Could not restore state <%s>. Resetting utility_meter.%s", + state.state, + self.name, + ) + else: + self._unit_of_measurement = state.attributes.get( + ATTR_UNIT_OF_MEASUREMENT + ) + self._last_period = ( + float(state.attributes.get(ATTR_LAST_PERIOD)) + if state.attributes.get(ATTR_LAST_PERIOD) + else 0 + ) + self._last_reset = dt_util.as_utc( + dt_util.parse_datetime(state.attributes.get(ATTR_LAST_RESET)) + ) + if state.attributes.get(ATTR_STATUS) == COLLECTING: + # Fake cancellation function to init the meter in similar state + self._collecting = lambda: None @callback def async_source_tracking(event): From 2d374d65b65b27f3678a7d829031cc9407a4ecc6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 3 Oct 2021 22:02:30 -0700 Subject: [PATCH 0083/1038] Mark auth voluptuous schema fields as required (#57003) --- homeassistant/auth/mfa_modules/insecure_example.py | 4 ++-- homeassistant/auth/mfa_modules/notify.py | 2 +- homeassistant/auth/mfa_modules/totp.py | 2 +- homeassistant/auth/providers/command_line.py | 14 ++++++++------ homeassistant/auth/providers/homeassistant.py | 14 ++++++++------ homeassistant/auth/providers/insecure_example.py | 14 ++++++++------ .../auth/providers/legacy_api_password.py | 4 +++- homeassistant/auth/providers/trusted_networks.py | 4 +++- tests/components/auth/test_mfa_setup_flow.py | 2 +- 9 files changed, 35 insertions(+), 25 deletions(-) diff --git a/homeassistant/auth/mfa_modules/insecure_example.py b/homeassistant/auth/mfa_modules/insecure_example.py index 1d40339417b..a50b762b121 100644 --- a/homeassistant/auth/mfa_modules/insecure_example.py +++ b/homeassistant/auth/mfa_modules/insecure_example.py @@ -38,12 +38,12 @@ class InsecureExampleModule(MultiFactorAuthModule): @property def input_schema(self) -> vol.Schema: """Validate login flow input data.""" - return vol.Schema({"pin": str}) + return vol.Schema({vol.Required("pin"): str}) @property def setup_schema(self) -> vol.Schema: """Validate async_setup_user input data.""" - return vol.Schema({"pin": str}) + return vol.Schema({vol.Required("pin"): str}) async def async_setup_flow(self, user_id: str) -> SetupFlow: """Return a data entry flow handler for setup module. diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index 7d5cf0b0641..ec5d5b7cd03 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -110,7 +110,7 @@ class NotifyAuthModule(MultiFactorAuthModule): @property def input_schema(self) -> vol.Schema: """Validate login flow input data.""" - return vol.Schema({INPUT_FIELD_CODE: str}) + return vol.Schema({vol.Required(INPUT_FIELD_CODE): str}) async def _async_load(self) -> None: """Load stored data.""" diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index 5ff2c01c755..0ff7e1147b1 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -84,7 +84,7 @@ class TotpAuthModule(MultiFactorAuthModule): @property def input_schema(self) -> vol.Schema: """Validate login flow input data.""" - return vol.Schema({INPUT_FIELD_CODE: str}) + return vol.Schema({vol.Required(INPUT_FIELD_CODE): str}) async def _async_load(self) -> None: """Load stored data.""" diff --git a/homeassistant/auth/providers/command_line.py b/homeassistant/auth/providers/command_line.py index 6d1a1627fd5..81a6b6d78e5 100644 --- a/homeassistant/auth/providers/command_line.py +++ b/homeassistant/auth/providers/command_line.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -import collections from collections.abc import Mapping import logging import os @@ -148,10 +147,13 @@ class CommandLineLoginFlow(LoginFlow): user_input.pop("password") return await self.async_finish(user_input) - schema: dict[str, type] = collections.OrderedDict() - schema["username"] = str - schema["password"] = str - return self.async_show_form( - step_id="init", data_schema=vol.Schema(schema), errors=errors + step_id="init", + data_schema=vol.Schema( + { + vol.Required("username"): str, + vol.Required("password"): str, + } + ), + errors=errors, ) diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index 6ac9fac03e5..1ffed6f87fd 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio import base64 -from collections import OrderedDict from collections.abc import Mapping import logging from typing import Any, cast @@ -335,10 +334,13 @@ class HassLoginFlow(LoginFlow): user_input.pop("password") return await self.async_finish(user_input) - schema: dict[str, type] = OrderedDict() - schema["username"] = str - schema["password"] = str - return self.async_show_form( - step_id="init", data_schema=vol.Schema(schema), errors=errors + step_id="init", + data_schema=vol.Schema( + { + vol.Required("username"): str, + vol.Required("password"): str, + } + ), + errors=errors, ) diff --git a/homeassistant/auth/providers/insecure_example.py b/homeassistant/auth/providers/insecure_example.py index fb390b65b0d..9ad6da27ce3 100644 --- a/homeassistant/auth/providers/insecure_example.py +++ b/homeassistant/auth/providers/insecure_example.py @@ -1,7 +1,6 @@ """Example auth provider.""" from __future__ import annotations -from collections import OrderedDict from collections.abc import Mapping import hmac from typing import Any, cast @@ -117,10 +116,13 @@ class ExampleLoginFlow(LoginFlow): user_input.pop("password") return await self.async_finish(user_input) - schema: dict[str, type] = OrderedDict() - schema["username"] = str - schema["password"] = str - return self.async_show_form( - step_id="init", data_schema=vol.Schema(schema), errors=errors + step_id="init", + data_schema=vol.Schema( + { + vol.Required("username"): str, + vol.Required("password"): str, + } + ), + errors=errors, ) diff --git a/homeassistant/auth/providers/legacy_api_password.py b/homeassistant/auth/providers/legacy_api_password.py index af24506210b..2cb113b8b8c 100644 --- a/homeassistant/auth/providers/legacy_api_password.py +++ b/homeassistant/auth/providers/legacy_api_password.py @@ -102,5 +102,7 @@ class LegacyLoginFlow(LoginFlow): return await self.async_finish({}) return self.async_show_form( - step_id="init", data_schema=vol.Schema({"password": str}), errors=errors + step_id="init", + data_schema=vol.Schema({vol.Required("password"): str}), + errors=errors, ) diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py index a9ee6a48335..0f2b287a227 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -244,5 +244,7 @@ class TrustedNetworksLoginFlow(LoginFlow): return self.async_show_form( step_id="init", - data_schema=vol.Schema({"user": vol.In(self._available_users)}), + data_schema=vol.Schema( + {vol.Required("user"): vol.In(self._available_users)} + ), ) diff --git a/tests/components/auth/test_mfa_setup_flow.py b/tests/components/auth/test_mfa_setup_flow.py index 3569d7d5233..edf45742dbd 100644 --- a/tests/components/auth/test_mfa_setup_flow.py +++ b/tests/components/auth/test_mfa_setup_flow.py @@ -67,7 +67,7 @@ async def test_ws_setup_depose_mfa(hass, hass_ws_client): assert flow["type"] == data_entry_flow.RESULT_TYPE_FORM assert flow["handler"] == "example_module" assert flow["step_id"] == "init" - assert flow["data_schema"][0] == {"type": "string", "name": "pin"} + assert flow["data_schema"][0] == {"type": "string", "name": "pin", "required": True} await client.send_json( { From 7446e388ed141640d139ecb4f4f287530c996c62 Mon Sep 17 00:00:00 2001 From: Oliver Ou Date: Mon, 4 Oct 2021 16:45:37 +0800 Subject: [PATCH 0084/1038] Fix Tuya v2 login issue (#56973) * fix login issue * fix:login error * update COUNTRY_CODE_CHINA line location * added one blank line * feat:added line #L88 was not covered by tests * ci build errors Co-authored-by: erchuan --- homeassistant/components/tuya/config_flow.py | 8 ++- tests/components/tuya/test_config_flow.py | 76 +++++++++++++++----- 2 files changed, 66 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py index 9761b1b6c96..357910b5388 100644 --- a/homeassistant/components/tuya/config_flow.py +++ b/homeassistant/components/tuya/config_flow.py @@ -26,6 +26,8 @@ from .const import ( RESULT_SINGLE_INSTANCE = "single_instance_allowed" RESULT_AUTH_FAILED = "invalid_auth" TUYA_ENDPOINT_BASE = "https://openapi.tuyacn.com" +TUYA_ENDPOINT_OTHER = "https://openapi.tuyaus.com" +COUNTRY_CODE_CHINA = ["86", "+86", "China"] _LOGGER = logging.getLogger(__name__) @@ -82,7 +84,11 @@ class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if project_type == ProjectType.INDUSTY_SOLUTIONS: response = api.login(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) else: - api.endpoint = TUYA_ENDPOINT_BASE + if user_input[CONF_COUNTRY_CODE] in COUNTRY_CODE_CHINA: + api.endpoint = TUYA_ENDPOINT_BASE + else: + api.endpoint = TUYA_ENDPOINT_OTHER + response = api.login( user_input[CONF_USERNAME], user_input[CONF_PASSWORD], diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index a15cfcc0fdf..b01745ee8db 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -24,7 +24,8 @@ MOCK_ACCESS_ID = "myAccessId" MOCK_ACCESS_SECRET = "myAccessSecret" MOCK_USERNAME = "myUsername" MOCK_PASSWORD = "myPassword" -MOCK_COUNTRY_CODE = "1" +MOCK_COUNTRY_CODE_BASE = "86" +MOCK_COUNTRY_CODE_OTHER = "1" MOCK_APP_TYPE = "smartlife" MOCK_ENDPOINT = "https://openapi-ueaz.tuyaus.com" @@ -35,15 +36,6 @@ TUYA_INDUSTRY_PROJECT_DATA = { CONF_PROJECT_TYPE: MOCK_INDUSTRY_PROJECT_TYPE, } -TUYA_INPUT_SMART_HOME_DATA = { - CONF_ACCESS_ID: MOCK_ACCESS_ID, - CONF_ACCESS_SECRET: MOCK_ACCESS_SECRET, - CONF_USERNAME: MOCK_USERNAME, - CONF_PASSWORD: MOCK_PASSWORD, - CONF_COUNTRY_CODE: MOCK_COUNTRY_CODE, - CONF_APP_TYPE: MOCK_APP_TYPE, -} - TUYA_INPUT_INDUSTRY_DATA = { CONF_ENDPOINT: MOCK_ENDPOINT, CONF_ACCESS_ID: MOCK_ACCESS_ID, @@ -52,15 +44,23 @@ TUYA_INPUT_INDUSTRY_DATA = { CONF_PASSWORD: MOCK_PASSWORD, } -TUYA_IMPORT_SMART_HOME_DATA = { +TUYA_IMPORT_SMART_HOME_DATA_BASE = { CONF_ACCESS_ID: MOCK_ACCESS_ID, CONF_ACCESS_SECRET: MOCK_ACCESS_SECRET, CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: MOCK_PASSWORD, - CONF_COUNTRY_CODE: MOCK_COUNTRY_CODE, + CONF_COUNTRY_CODE: MOCK_COUNTRY_CODE_BASE, CONF_APP_TYPE: MOCK_APP_TYPE, } +TUYA_IMPORT_SMART_HOME_DATA_OTHER = { + CONF_ACCESS_ID: MOCK_ACCESS_ID, + CONF_ACCESS_SECRET: MOCK_ACCESS_SECRET, + CONF_USERNAME: MOCK_USERNAME, + CONF_PASSWORD: MOCK_PASSWORD, + CONF_COUNTRY_CODE: MOCK_COUNTRY_CODE_OTHER, + CONF_APP_TYPE: MOCK_APP_TYPE, +} TUYA_IMPORT_INDUSTRY_DATA = { CONF_PROJECT_TYPE: MOCK_SMART_HOME_PROJECT_TYPE, @@ -118,8 +118,8 @@ async def test_industry_user(hass, tuya): assert not result["result"].unique_id -async def test_smart_home_user(hass, tuya): - """Test smart home user config.""" +async def test_smart_home_user_base(hass, tuya): + """Test smart home user config base.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -137,7 +137,7 @@ async def test_smart_home_user(hass, tuya): tuya().login = MagicMock(return_value={"success": False, "errorCode": 1024}) result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=TUYA_IMPORT_SMART_HOME_DATA + result["flow_id"], user_input=TUYA_IMPORT_SMART_HOME_DATA_BASE ) await hass.async_block_till_done() @@ -145,7 +145,7 @@ async def test_smart_home_user(hass, tuya): tuya().login = MagicMock(return_value={"success": True, "errorCode": 1024}) result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=TUYA_IMPORT_SMART_HOME_DATA + result["flow_id"], user_input=TUYA_IMPORT_SMART_HOME_DATA_BASE ) await hass.async_block_till_done() @@ -155,7 +155,49 @@ async def test_smart_home_user(hass, tuya): assert result["data"][CONF_ACCESS_SECRET] == MOCK_ACCESS_SECRET assert result["data"][CONF_USERNAME] == MOCK_USERNAME assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD - assert result["data"][CONF_COUNTRY_CODE] == MOCK_COUNTRY_CODE + assert result["data"][CONF_COUNTRY_CODE] == MOCK_COUNTRY_CODE_BASE + assert result["data"][CONF_APP_TYPE] == MOCK_APP_TYPE + assert not result["result"].unique_id + + +async def test_smart_home_user_other(hass, tuya): + """Test smart home user config other.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=TUYA_SMART_HOME_PROJECT_DATA + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "login" + + tuya().login = MagicMock(return_value={"success": False, "errorCode": 1024}) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=TUYA_IMPORT_SMART_HOME_DATA_OTHER + ) + await hass.async_block_till_done() + + assert result["errors"]["base"] == RESULT_AUTH_FAILED + + tuya().login = MagicMock(return_value={"success": True, "errorCode": 1024}) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=TUYA_IMPORT_SMART_HOME_DATA_OTHER + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == MOCK_USERNAME + assert result["data"][CONF_ACCESS_ID] == MOCK_ACCESS_ID + assert result["data"][CONF_ACCESS_SECRET] == MOCK_ACCESS_SECRET + assert result["data"][CONF_USERNAME] == MOCK_USERNAME + assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD + assert result["data"][CONF_COUNTRY_CODE] == MOCK_COUNTRY_CODE_OTHER assert result["data"][CONF_APP_TYPE] == MOCK_APP_TYPE assert not result["result"].unique_id From da63a962737d6411f231cf71867c24ca46e9e125 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 4 Oct 2021 13:17:42 +0200 Subject: [PATCH 0085/1038] ESPHome fix zeroconf add_listener issue (#57031) --- homeassistant/components/esphome/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index ed23aa7ec75..ee258317357 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -503,16 +503,14 @@ class ReconnectLogic(RecordUpdateListener): """ async with self._zc_lock: if not self._zc_listening: - await self._hass.async_add_executor_job( - self._zc.add_listener, self, None - ) + self._zc.async_add_listener(self, None) self._zc_listening = True async def _stop_zc_listen(self) -> None: """Stop listening for zeroconf updates.""" async with self._zc_lock: if self._zc_listening: - await self._hass.async_add_executor_job(self._zc.remove_listener, self) + self._zc.async_remove_listener(self) self._zc_listening = False @callback From bf9f55c376ad33a053b542d4f7bad64a6959931f Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 4 Oct 2021 13:23:11 +0200 Subject: [PATCH 0086/1038] Bump aioesphomeapi from 9.1.2 to 9.1.4 (#57036) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 28371e89d8e..33801431994 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==9.1.2"], + "requirements": ["aioesphomeapi==9.1.4"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], diff --git a/requirements_all.txt b/requirements_all.txt index e5e137da8b2..ba3aa664fe9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -164,7 +164,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==9.1.2 +aioesphomeapi==9.1.4 # homeassistant.components.flo aioflo==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c2057ac9cd6..88cc5b38602 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -109,7 +109,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==9.1.2 +aioesphomeapi==9.1.4 # homeassistant.components.flo aioflo==0.4.1 From dc6f6b7f680123f05a7c7eb355579ac09f7bb78c Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Mon, 4 Oct 2021 13:31:40 +0200 Subject: [PATCH 0087/1038] Remove Essent integration (#56991) --- .coveragerc | 1 - CODEOWNERS | 1 - homeassistant/components/essent/__init__.py | 1 - homeassistant/components/essent/manifest.json | 8 -- homeassistant/components/essent/sensor.py | 130 ------------------ requirements_all.txt | 3 - 6 files changed, 144 deletions(-) delete mode 100644 homeassistant/components/essent/__init__.py delete mode 100644 homeassistant/components/essent/manifest.json delete mode 100644 homeassistant/components/essent/sensor.py diff --git a/.coveragerc b/.coveragerc index 1a716ef4e5c..8a53086d840 100644 --- a/.coveragerc +++ b/.coveragerc @@ -290,7 +290,6 @@ omit = homeassistant/components/esphome/select.py homeassistant/components/esphome/sensor.py homeassistant/components/esphome/switch.py - homeassistant/components/essent/sensor.py homeassistant/components/etherscan/sensor.py homeassistant/components/eufy/* homeassistant/components/everlights/light.py diff --git a/CODEOWNERS b/CODEOWNERS index 58225f4e36a..64b87195d8f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -157,7 +157,6 @@ homeassistant/components/epson/* @pszafer homeassistant/components/epsonworkforce/* @ThaStealth homeassistant/components/eq3btsmart/* @rytilahti homeassistant/components/esphome/* @OttoWinter @jesserockz -homeassistant/components/essent/* @TheLastProject homeassistant/components/evohome/* @zxdavb homeassistant/components/ezviz/* @RenierM26 @baqs homeassistant/components/faa_delays/* @ntilley905 diff --git a/homeassistant/components/essent/__init__.py b/homeassistant/components/essent/__init__.py deleted file mode 100644 index 42e867c6d21..00000000000 --- a/homeassistant/components/essent/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The Essent component.""" diff --git a/homeassistant/components/essent/manifest.json b/homeassistant/components/essent/manifest.json deleted file mode 100644 index d136cae43a9..00000000000 --- a/homeassistant/components/essent/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "essent", - "name": "Essent", - "documentation": "https://www.home-assistant.io/integrations/essent", - "requirements": ["PyEssent==0.14"], - "codeowners": ["@TheLastProject"], - "iot_class": "cloud_polling" -} diff --git a/homeassistant/components/essent/sensor.py b/homeassistant/components/essent/sensor.py deleted file mode 100644 index 42a4c1c399b..00000000000 --- a/homeassistant/components/essent/sensor.py +++ /dev/null @@ -1,130 +0,0 @@ -"""Support for Essent API.""" -from __future__ import annotations - -from datetime import timedelta - -from pyessent import PyEssent -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, ENERGY_KILO_WATT_HOUR -import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle - -SCAN_INTERVAL = timedelta(hours=1) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} -) - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the Essent platform.""" - username = config[CONF_USERNAME] - password = config[CONF_PASSWORD] - - essent = EssentBase(username, password) - meters = [] - for meter in essent.retrieve_meters(): - data = essent.retrieve_meter_data(meter) - for tariff in data["values"]["LVR"]: - meters.append( - EssentMeter( - essent, - meter, - data["type"], - tariff, - data["values"]["LVR"][tariff]["unit"], - ) - ) - - if not meters: - hass.components.persistent_notification.create( - "Couldn't find any meter readings. " - "Please ensure Verbruiks Manager is enabled in Mijn Essent " - "and at least one reading has been logged to Meterstanden.", - title="Essent", - notification_id="essent_notification", - ) - return - - add_devices(meters, True) - - -class EssentBase: - """Essent Base.""" - - def __init__(self, username, password): - """Initialize the Essent API.""" - self._username = username - self._password = password - self._meter_data = {} - - self.update() - - def retrieve_meters(self): - """Retrieve the list of meters.""" - return self._meter_data.keys() - - def retrieve_meter_data(self, meter): - """Retrieve the data for this meter.""" - return self._meter_data[meter] - - @Throttle(timedelta(minutes=30)) - def update(self): - """Retrieve the latest meter data from Essent.""" - essent = PyEssent(self._username, self._password) - eans = set(essent.get_EANs()) - for possible_meter in eans: - meter_data = essent.read_meter(possible_meter, only_last_meter_reading=True) - if meter_data: - self._meter_data[possible_meter] = meter_data - - -class EssentMeter(SensorEntity): - """Representation of Essent measurements.""" - - def __init__(self, essent_base, meter, meter_type, tariff, unit): - """Initialize the sensor.""" - self._state = None - self._essent_base = essent_base - self._meter = meter - self._type = meter_type - self._tariff = tariff - self._unit = unit - - @property - def unique_id(self) -> str | None: - """Return a unique ID.""" - return f"{self._meter}-{self._type}-{self._tariff}" - - @property - def name(self): - """Return the name of the sensor.""" - return f"Essent {self._type} ({self._tariff})" - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement.""" - if self._unit.lower() == "kwh": - return ENERGY_KILO_WATT_HOUR - - return self._unit - - def update(self): - """Fetch the energy usage.""" - # Ensure our data isn't too old - self._essent_base.update() - - # Retrieve our meter - data = self._essent_base.retrieve_meter_data(self._meter) - - # Set our value - self._state = next( - iter(data["values"]["LVR"][self._tariff]["records"].values()) - ) diff --git a/requirements_all.txt b/requirements_all.txt index ba3aa664fe9..24812d8e25c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -22,9 +22,6 @@ Mastodon.py==1.5.1 # homeassistant.components.orangepi_gpio OPi.GPIO==0.4.0 -# homeassistant.components.essent -PyEssent==0.14 - # homeassistant.components.flick_electric PyFlick==0.0.2 From 80a225ca98606f0713160874067891c435e6b392 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Oct 2021 13:33:29 +0200 Subject: [PATCH 0088/1038] Prevent opening of sockets in kira tests (#57038) --- tests/components/kira/test_init.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/tests/components/kira/test_init.py b/tests/components/kira/test_init.py index 93f3ee4d82a..3ca7a72a60a 100644 --- a/tests/components/kira/test_init.py +++ b/tests/components/kira/test_init.py @@ -1,9 +1,9 @@ -"""The tests for Home Assistant ffmpeg.""" +"""The tests for Kira.""" import os import shutil import tempfile -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest @@ -33,13 +33,8 @@ KIRA_CODES = """ @pytest.fixture(autouse=True) def setup_comp(): """Set up things to be run when tests are started.""" - _base_mock = MagicMock() - pykira = _base_mock.pykira - pykira.__file__ = "test" - _module_patcher = patch.dict("sys.modules", {"pykira": pykira}) - _module_patcher.start() - yield - _module_patcher.stop() + with patch("homeassistant.components.kira.pykira.KiraReceiver"): + yield @pytest.fixture(scope="module") From e0ab4ee84257baed77013f30e4e0b1eb96fe91a4 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 4 Oct 2021 13:35:31 +0200 Subject: [PATCH 0089/1038] Use NamedTuple for homekit valve type + service info (#56944) --- .../components/homekit/type_sensors.py | 46 +++++++++++++------ .../components/homekit/type_switches.py | 25 +++++++--- 2 files changed, 49 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index bcef7564fa3..c309e42a0f0 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -1,5 +1,8 @@ """Class to hold all sensor accessories.""" +from __future__ import annotations + import logging +from typing import Callable, NamedTuple from pyhap.const import CATEGORY_SENSOR @@ -60,18 +63,31 @@ from .util import convert_to_float, density_to_air_quality, temperature_to_homek _LOGGER = logging.getLogger(__name__) -BINARY_SENSOR_SERVICE_MAP = { - DEVICE_CLASS_CO: (SERV_CARBON_MONOXIDE_SENSOR, CHAR_CARBON_MONOXIDE_DETECTED, int), - DEVICE_CLASS_CO2: (SERV_CARBON_DIOXIDE_SENSOR, CHAR_CARBON_DIOXIDE_DETECTED, int), - DEVICE_CLASS_DOOR: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int), - DEVICE_CLASS_GARAGE_DOOR: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int), - DEVICE_CLASS_GAS: (SERV_CARBON_MONOXIDE_SENSOR, CHAR_CARBON_MONOXIDE_DETECTED, int), - DEVICE_CLASS_MOISTURE: (SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED, int), - DEVICE_CLASS_MOTION: (SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED, bool), - DEVICE_CLASS_OCCUPANCY: (SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED, int), - DEVICE_CLASS_OPENING: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int), - DEVICE_CLASS_SMOKE: (SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED, int), - DEVICE_CLASS_WINDOW: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int), + +class SI(NamedTuple): + """Service info.""" + + service: str + char: str + format: Callable[[bool], int | bool] + + +BINARY_SENSOR_SERVICE_MAP: dict[str, SI] = { + DEVICE_CLASS_CO: SI( + SERV_CARBON_MONOXIDE_SENSOR, CHAR_CARBON_MONOXIDE_DETECTED, int + ), + DEVICE_CLASS_CO2: SI(SERV_CARBON_DIOXIDE_SENSOR, CHAR_CARBON_DIOXIDE_DETECTED, int), + DEVICE_CLASS_DOOR: SI(SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int), + DEVICE_CLASS_GARAGE_DOOR: SI(SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int), + DEVICE_CLASS_GAS: SI( + SERV_CARBON_MONOXIDE_SENSOR, CHAR_CARBON_MONOXIDE_DETECTED, int + ), + DEVICE_CLASS_MOISTURE: SI(SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED, int), + DEVICE_CLASS_MOTION: SI(SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED, bool), + DEVICE_CLASS_OCCUPANCY: SI(SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED, int), + DEVICE_CLASS_OPENING: SI(SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int), + DEVICE_CLASS_SMOKE: SI(SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED, int), + DEVICE_CLASS_WINDOW: SI(SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, int), } @@ -276,11 +292,11 @@ class BinarySensor(HomeAccessory): else BINARY_SENSOR_SERVICE_MAP[DEVICE_CLASS_OCCUPANCY] ) - self.format = service_char[2] - service = self.add_preload_service(service_char[0]) + self.format = service_char.format + service = self.add_preload_service(service_char.service) initial_value = False if self.format is bool else 0 self.char_detected = service.configure_char( - service_char[1], value=initial_value + service_char.char, value=initial_value ) # Set the state so it is in sync on initial # GET to avoid an event storm after homekit startup diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 4e76b0369fe..9b9ff1f4df2 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -1,5 +1,8 @@ """Class to hold all switch accessories.""" +from __future__ import annotations + import logging +from typing import NamedTuple from pyhap.const import ( CATEGORY_FAUCET, @@ -50,11 +53,19 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -VALVE_TYPE = { - TYPE_FAUCET: (CATEGORY_FAUCET, 3), - TYPE_SHOWER: (CATEGORY_SHOWER_HEAD, 2), - TYPE_SPRINKLER: (CATEGORY_SPRINKLER, 1), - TYPE_VALVE: (CATEGORY_FAUCET, 0), + +class ValveInfo(NamedTuple): + """Category and type information for valve.""" + + category: int + valve_type: int + + +VALVE_TYPE: dict[str, ValveInfo] = { + TYPE_FAUCET: ValveInfo(CATEGORY_FAUCET, 3), + TYPE_SHOWER: ValveInfo(CATEGORY_SHOWER_HEAD, 2), + TYPE_SPRINKLER: ValveInfo(CATEGORY_SPRINKLER, 1), + TYPE_VALVE: ValveInfo(CATEGORY_FAUCET, 0), } @@ -199,7 +210,7 @@ class Valve(HomeAccessory): super().__init__(*args) state = self.hass.states.get(self.entity_id) valve_type = self.config[CONF_TYPE] - self.category = VALVE_TYPE[valve_type][0] + self.category = VALVE_TYPE[valve_type].category serv_valve = self.add_preload_service(SERV_VALVE) self.char_active = serv_valve.configure_char( @@ -207,7 +218,7 @@ class Valve(HomeAccessory): ) self.char_in_use = serv_valve.configure_char(CHAR_IN_USE, value=False) self.char_valve_type = serv_valve.configure_char( - CHAR_VALVE_TYPE, value=VALVE_TYPE[valve_type][1] + CHAR_VALVE_TYPE, value=VALVE_TYPE[valve_type].valve_type ) # Set the state so it is in sync on initial # GET to avoid an event storm after homekit startup From 099428fa737960c23d47aad75f2337ca4cb66fb3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Oct 2021 13:37:05 +0200 Subject: [PATCH 0090/1038] Use hass_client_no_auth test fixture in additional tests (#57037) --- tests/components/konnected/test_init.py | 12 ++++++------ tests/components/lyric/test_config_flow.py | 8 ++++---- tests/components/mailgun/test_init.py | 4 ++-- tests/components/media_player/test_init.py | 12 ++++++------ tests/components/motioneye/test_web_hooks.py | 16 ++++++++-------- tests/components/mqtt/test_camera.py | 4 ++-- 6 files changed, 28 insertions(+), 28 deletions(-) diff --git a/tests/components/konnected/test_init.py b/tests/components/konnected/test_init.py index 91d6633cf1d..9be44a5d6da 100644 --- a/tests/components/konnected/test_init.py +++ b/tests/components/konnected/test_init.py @@ -402,7 +402,7 @@ async def test_unload_entry(hass, mock_panel): assert hass.data[konnected.DOMAIN]["devices"] == {} -async def test_api(hass, aiohttp_client, mock_panel): +async def test_api(hass, hass_client_no_auth, mock_panel): """Test callback view.""" await async_setup_component(hass, "http", {"http": {}}) @@ -470,7 +470,7 @@ async def test_api(hass, aiohttp_client, mock_panel): is True ) - client = await aiohttp_client(hass.http.app) + client = await hass_client_no_auth() # Test the get endpoint for switch status polling resp = await client.get("/api/konnected") @@ -569,7 +569,7 @@ async def test_api(hass, aiohttp_client, mock_panel): assert result == {"message": "ok"} -async def test_state_updates_zone(hass, aiohttp_client, mock_panel): +async def test_state_updates_zone(hass, hass_client_no_auth, mock_panel): """Test callback view.""" await async_process_ha_core_config( hass, @@ -642,7 +642,7 @@ async def test_state_updates_zone(hass, aiohttp_client, mock_panel): is True ) - client = await aiohttp_client(hass.http.app) + client = await hass_client_no_auth() # Test updating a binary sensor resp = await client.post( @@ -720,7 +720,7 @@ async def test_state_updates_zone(hass, aiohttp_client, mock_panel): assert hass.states.get("sensor.temper_temperature").state == "42.0" -async def test_state_updates_pin(hass, aiohttp_client, mock_panel): +async def test_state_updates_pin(hass, hass_client_no_auth, mock_panel): """Test callback view.""" await async_process_ha_core_config( hass, @@ -797,7 +797,7 @@ async def test_state_updates_pin(hass, aiohttp_client, mock_panel): is True ) - client = await aiohttp_client(hass.http.app) + client = await hass_client_no_auth() # Test updating a binary sensor resp = await client.post( diff --git a/tests/components/lyric/test_config_flow.py b/tests/components/lyric/test_config_flow.py index c8197e9d678..23f2a42e449 100644 --- a/tests/components/lyric/test_config_flow.py +++ b/tests/components/lyric/test_config_flow.py @@ -44,7 +44,7 @@ async def test_abort_if_no_configuration(hass): async def test_full_flow( - hass, aiohttp_client, aioclient_mock, current_request_with_host + hass, hass_client_no_auth, aioclient_mock, current_request_with_host ): """Check full flow.""" assert await setup.async_setup_component( @@ -77,7 +77,7 @@ async def test_full_flow( f"&state={state}" ) - client = await aiohttp_client(hass.http.app) + client = await hass_client_no_auth() resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") assert resp.status == 200 assert resp.headers["content-type"] == "text/html; charset=utf-8" @@ -136,7 +136,7 @@ async def test_abort_if_authorization_timeout( async def test_reauthentication_flow( - hass, aiohttp_client, aioclient_mock, current_request_with_host + hass, hass_client_no_auth, aioclient_mock, current_request_with_host ): """Test reauthentication flow.""" await setup.async_setup_component( @@ -176,7 +176,7 @@ async def test_reauthentication_flow( "redirect_uri": "https://example.com/auth/external/callback", }, ) - client = await aiohttp_client(hass.http.app) + client = await hass_client_no_auth() await client.get(f"/auth/external/callback?code=abcd&state={state}") aioclient_mock.post( diff --git a/tests/components/mailgun/test_init.py b/tests/components/mailgun/test_init.py index bf0df205c93..c6eeb9f99ad 100644 --- a/tests/components/mailgun/test_init.py +++ b/tests/components/mailgun/test_init.py @@ -15,10 +15,10 @@ API_KEY = "abc123" @pytest.fixture -async def http_client(hass, aiohttp_client): +async def http_client(hass, hass_client_no_auth): """Initialize a Home Assistant Server for testing this module.""" await async_setup_component(hass, webhook.DOMAIN, {}) - return await aiohttp_client(hass.http.app) + return await hass_client_no_auth() @pytest.fixture diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index 98b54ace01f..60c5425c6f6 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -40,7 +40,7 @@ async def test_get_image(hass, hass_ws_client, caplog): assert "media_player_thumbnail is deprecated" in caplog.text -async def test_get_image_http(hass, aiohttp_client): +async def test_get_image_http(hass, hass_client_no_auth): """Test get image via http command.""" await async_setup_component( hass, "media_player", {"media_player": {"platform": "demo"}} @@ -50,7 +50,7 @@ async def test_get_image_http(hass, aiohttp_client): state = hass.states.get("media_player.bedroom") assert "entity_picture_local" not in state.attributes - client = await aiohttp_client(hass.http.app) + client = await hass_client_no_auth() with patch( "homeassistant.components.media_player.MediaPlayerEntity." @@ -63,7 +63,7 @@ async def test_get_image_http(hass, aiohttp_client): assert content == b"image" -async def test_get_image_http_remote(hass, aiohttp_client): +async def test_get_image_http_remote(hass, hass_client_no_auth): """Test get image url via http command.""" with patch( "homeassistant.components.media_player.MediaPlayerEntity." @@ -78,7 +78,7 @@ async def test_get_image_http_remote(hass, aiohttp_client): state = hass.states.get("media_player.bedroom") assert "entity_picture_local" in state.attributes - client = await aiohttp_client(hass.http.app) + client = await hass_client_no_auth() with patch( "homeassistant.components.media_player.MediaPlayerEntity." @@ -91,7 +91,7 @@ async def test_get_image_http_remote(hass, aiohttp_client): assert content == b"image" -async def test_get_async_get_browse_image(hass, aiohttp_client, hass_ws_client): +async def test_get_async_get_browse_image(hass, hass_client_no_auth, hass_ws_client): """Test get browse image.""" await async_setup_component( hass, "media_player", {"media_player": {"platform": "demo"}} @@ -104,7 +104,7 @@ async def test_get_async_get_browse_image(hass, aiohttp_client, hass_ws_client): player = entity_comp.get_entity("media_player.bedroom") assert player - client = await aiohttp_client(hass.http.app) + client = await hass_client_no_auth() with patch( "homeassistant.components.media_player.MediaPlayerEntity." diff --git a/tests/components/motioneye/test_web_hooks.py b/tests/components/motioneye/test_web_hooks.py index f20ef5101e9..ac84bd405cd 100644 --- a/tests/components/motioneye/test_web_hooks.py +++ b/tests/components/motioneye/test_web_hooks.py @@ -282,7 +282,7 @@ async def test_setup_camera_with_no_home_assistant_urls( assert entity_state -async def test_good_query(hass: HomeAssistant, aiohttp_client: Any) -> None: +async def test_good_query(hass: HomeAssistant, hass_client_no_auth: Any) -> None: """Test good callbacks.""" await async_setup_component(hass, "http", {"http": {}}) @@ -300,7 +300,7 @@ async def test_good_query(hass: HomeAssistant, aiohttp_client: Any) -> None: "two": "2", ATTR_DEVICE_ID: device.id, } - client = await aiohttp_client(hass.http.app) + client = await hass_client_no_auth() for event in (EVENT_MOTION_DETECTED, EVENT_FILE_STORED): events = async_capture_events(hass, f"{DOMAIN}.{event}") @@ -325,13 +325,13 @@ async def test_good_query(hass: HomeAssistant, aiohttp_client: Any) -> None: async def test_bad_query_missing_parameters( - hass: HomeAssistant, aiohttp_client: Any + hass: HomeAssistant, hass_client_no_auth: Any ) -> None: """Test a query with missing parameters.""" await async_setup_component(hass, "http", {"http": {}}) config_entry = await setup_mock_motioneye_config_entry(hass) - client = await aiohttp_client(hass.http.app) + client = await hass_client_no_auth() resp = await client.post( URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]), json={} @@ -340,13 +340,13 @@ async def test_bad_query_missing_parameters( async def test_bad_query_no_such_device( - hass: HomeAssistant, aiohttp_client: Any + hass: HomeAssistant, hass_client_no_auth: Any ) -> None: """Test a correct query with incorrect device.""" await async_setup_component(hass, "http", {"http": {}}) config_entry = await setup_mock_motioneye_config_entry(hass) - client = await aiohttp_client(hass.http.app) + client = await hass_client_no_auth() resp = await client.post( URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]), @@ -359,13 +359,13 @@ async def test_bad_query_no_such_device( async def test_bad_query_cannot_decode( - hass: HomeAssistant, aiohttp_client: Any + hass: HomeAssistant, hass_client_no_auth: Any ) -> None: """Test a correct query with incorrect device.""" await async_setup_component(hass, "http", {"http": {}}) config_entry = await setup_mock_motioneye_config_entry(hass) - client = await aiohttp_client(hass.http.app) + client = await hass_client_no_auth() motion_events = async_capture_events(hass, f"{DOMAIN}.{EVENT_MOTION_DETECTED}") storage_events = async_capture_events(hass, f"{DOMAIN}.{EVENT_FILE_STORED}") diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index a73a63d3439..ed5fa9e3927 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -40,7 +40,7 @@ DEFAULT_CONFIG = { } -async def test_run_camera_setup(hass, aiohttp_client, mqtt_mock): +async def test_run_camera_setup(hass, hass_client_no_auth, mqtt_mock): """Test that it fetches the given payload.""" topic = "test/camera" await async_setup_component( @@ -54,7 +54,7 @@ async def test_run_camera_setup(hass, aiohttp_client, mqtt_mock): async_fire_mqtt_message(hass, topic, "beer") - client = await aiohttp_client(hass.http.app) + client = await hass_client_no_auth() resp = await client.get(url) assert resp.status == 200 body = await resp.text() From 96681ab3a9260b9b134433cdd0e447d7198894b9 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 4 Oct 2021 13:38:32 +0200 Subject: [PATCH 0091/1038] Use NamedTuple for darksky condition picture (#56942) --- homeassistant/components/darksky/sensor.py | 73 ++++++++++++++++------ 1 file changed, 54 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index e73d9b2e1be..beb277d76fa 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -1,6 +1,9 @@ """Support for Dark Sky weather service.""" +from __future__ import annotations + from datetime import timedelta import logging +from typing import NamedTuple import forecastio from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout @@ -384,23 +387,55 @@ SENSOR_TYPES = { "alerts": ["Alerts", None, None, None, None, None, "mdi:alert-circle-outline", []], } -CONDITION_PICTURES = { - "clear-day": ["/static/images/darksky/weather-sunny.svg", "mdi:weather-sunny"], - "clear-night": ["/static/images/darksky/weather-night.svg", "mdi:weather-night"], - "rain": ["/static/images/darksky/weather-pouring.svg", "mdi:weather-pouring"], - "snow": ["/static/images/darksky/weather-snowy.svg", "mdi:weather-snowy"], - "sleet": ["/static/images/darksky/weather-hail.svg", "mdi:weather-snowy-rainy"], - "wind": ["/static/images/darksky/weather-windy.svg", "mdi:weather-windy"], - "fog": ["/static/images/darksky/weather-fog.svg", "mdi:weather-fog"], - "cloudy": ["/static/images/darksky/weather-cloudy.svg", "mdi:weather-cloudy"], - "partly-cloudy-day": [ - "/static/images/darksky/weather-partlycloudy.svg", - "mdi:weather-partly-cloudy", - ], - "partly-cloudy-night": [ - "/static/images/darksky/weather-cloudy.svg", - "mdi:weather-night-partly-cloudy", - ], + +class ConditionPicture(NamedTuple): + """Entity picture and icon for condition.""" + + entity_picture: str + icon: str + + +CONDITION_PICTURES: dict[str, ConditionPicture] = { + "clear-day": ConditionPicture( + entity_picture="/static/images/darksky/weather-sunny.svg", + icon="mdi:weather-sunny", + ), + "clear-night": ConditionPicture( + entity_picture="/static/images/darksky/weather-night.svg", + icon="mdi:weather-night", + ), + "rain": ConditionPicture( + entity_picture="/static/images/darksky/weather-pouring.svg", + icon="mdi:weather-pouring", + ), + "snow": ConditionPicture( + entity_picture="/static/images/darksky/weather-snowy.svg", + icon="mdi:weather-snowy", + ), + "sleet": ConditionPicture( + entity_picture="/static/images/darksky/weather-hail.svg", + icon="mdi:weather-snowy-rainy", + ), + "wind": ConditionPicture( + entity_picture="/static/images/darksky/weather-windy.svg", + icon="mdi:weather-windy", + ), + "fog": ConditionPicture( + entity_picture="/static/images/darksky/weather-fog.svg", + icon="mdi:weather-fog", + ), + "cloudy": ConditionPicture( + entity_picture="/static/images/darksky/weather-cloudy.svg", + icon="mdi:weather-cloudy", + ), + "partly-cloudy-day": ConditionPicture( + entity_picture="/static/images/darksky/weather-partlycloudy.svg", + icon="mdi:weather-partly-cloudy", + ), + "partly-cloudy-night": ConditionPicture( + entity_picture="/static/images/darksky/weather-cloudy.svg", + icon="mdi:weather-night-partly-cloudy", + ), } # Language Supported Codes @@ -595,7 +630,7 @@ class DarkSkySensor(SensorEntity): return None if self._icon in CONDITION_PICTURES: - return CONDITION_PICTURES[self._icon][0] + return CONDITION_PICTURES[self._icon].entity_picture return None @@ -610,7 +645,7 @@ class DarkSkySensor(SensorEntity): def icon(self): """Icon to use in the frontend, if any.""" if "summary" in self.type and self._icon in CONDITION_PICTURES: - return CONDITION_PICTURES[self._icon][1] + return CONDITION_PICTURES[self._icon].icon return SENSOR_TYPES[self.type][6] From bdf6c79062ec7cf04f06510c5ccc90a8fdc79f89 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 4 Oct 2021 14:17:43 +0200 Subject: [PATCH 0092/1038] Upgrade coverage to 6.0 (#57041) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 8f593777d82..22ffaa60a0f 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==5.5 +coverage==6.0 jsonpickle==1.4.1 mock-open==1.4.0 mypy==0.910 From 70f4bdf63ed6668823bd5769b6793402cec47ed3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Oct 2021 15:09:42 +0200 Subject: [PATCH 0093/1038] Prevent opening of sockets in watttime tests (#57040) --- tests/components/watttime/test_config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/watttime/test_config_flow.py b/tests/components/watttime/test_config_flow.py index a3d2867eb2d..efee2429d59 100644 --- a/tests/components/watttime/test_config_flow.py +++ b/tests/components/watttime/test_config_flow.py @@ -81,7 +81,7 @@ async def test_duplicate_error(hass: HomeAssistant, client_login): assert result["reason"] == "already_configured" -async def test_show_form_coordinates(hass: HomeAssistant) -> None: +async def test_show_form_coordinates(hass: HomeAssistant, client_login) -> None: """Test showing the form to input custom latitude/longitude.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} From 582788026ad9b6c09894452b8fb5106d117c8c50 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Oct 2021 15:33:13 +0200 Subject: [PATCH 0094/1038] Mock out network.util.async_get_source_ip in tests (#57039) --- tests/components/local_ip/test_config_flow.py | 4 ++-- tests/components/local_ip/test_init.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/local_ip/test_config_flow.py b/tests/components/local_ip/test_config_flow.py index e3a9ecd9ef8..4804ab83aca 100644 --- a/tests/components/local_ip/test_config_flow.py +++ b/tests/components/local_ip/test_config_flow.py @@ -6,7 +6,7 @@ from homeassistant.config_entries import SOURCE_USER from tests.common import MockConfigEntry -async def test_config_flow(hass): +async def test_config_flow(hass, mock_get_source_ip): """Test we can finish a config flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -21,7 +21,7 @@ async def test_config_flow(hass): assert state -async def test_already_setup(hass): +async def test_already_setup(hass, mock_get_source_ip): """Test we abort if already setup.""" MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/local_ip/test_init.py b/tests/components/local_ip/test_init.py index be1e689ca16..a3fba49b969 100644 --- a/tests/components/local_ip/test_init.py +++ b/tests/components/local_ip/test_init.py @@ -6,7 +6,7 @@ from homeassistant.components.zeroconf import MDNS_TARGET_IP from tests.common import MockConfigEntry -async def test_basic_setup(hass): +async def test_basic_setup(hass, mock_get_source_ip): """Test component setup creates entry from config.""" entry = MockConfigEntry(domain=DOMAIN, data={}) entry.add_to_hass(hass) From 771740c5f967adb7331c0512a006fe0b6d3e5e4d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 4 Oct 2021 16:12:22 +0200 Subject: [PATCH 0095/1038] Fix multiline lambda formatting - homekit_controller (#57046) --- .../components/homekit_controller/sensor.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index bc0ed0455fa..765acfe74b6 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -85,8 +85,10 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { native_unit_of_measurement=TEMP_CELSIUS, # This sensor is only for temperature characteristics that are not part # of a temperature sensor service. - probe=lambda char: char.service.type - != ServicesTypes.get_uuid(ServicesTypes.TEMPERATURE_SENSOR), + probe=( + lambda char: char.service.type + != ServicesTypes.get_uuid(ServicesTypes.TEMPERATURE_SENSOR) + ), ), CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT: HomeKitSensorEntityDescription( key=CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT, @@ -96,8 +98,10 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { native_unit_of_measurement=PERCENTAGE, # This sensor is only for humidity characteristics that are not part # of a humidity sensor service. - probe=lambda char: char.service.type - != ServicesTypes.get_uuid(ServicesTypes.HUMIDITY_SENSOR), + probe=( + lambda char: char.service.type + != ServicesTypes.get_uuid(ServicesTypes.HUMIDITY_SENSOR) + ), ), CharacteristicsTypes.AIR_QUALITY: HomeKitSensorEntityDescription( key=CharacteristicsTypes.AIR_QUALITY, From 12c32ac806ef78966566e02521e2e3d75bbd959a Mon Sep 17 00:00:00 2001 From: Chris Browet Date: Mon, 4 Oct 2021 17:10:41 +0200 Subject: [PATCH 0096/1038] Universal media player: consider unknown as inactive child state (#57029) --- homeassistant/components/universal/media_player.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index 2e3e6892c1c..d658a44a117 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -87,6 +87,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import EVENT_HOMEASSISTANT_START, callback from homeassistant.exceptions import TemplateError @@ -101,7 +102,7 @@ CONF_ATTRS = "attributes" CONF_CHILDREN = "children" CONF_COMMANDS = "commands" -OFF_STATES = [STATE_IDLE, STATE_OFF, STATE_UNAVAILABLE] +OFF_STATES = [STATE_IDLE, STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN] ATTRS_SCHEMA = cv.schema_with_slug_keys(cv.string) CMD_SCHEMA = cv.schema_with_slug_keys(cv.SERVICE_SCHEMA) From 8567aa9e1332bc89fb7b8922f63f0ce89865d977 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Oct 2021 17:21:40 +0200 Subject: [PATCH 0097/1038] Evict purged states from recorder's old_state cache (#56877) Co-authored-by: J. Nick Koston --- homeassistant/components/recorder/purge.py | 51 ++++++++++---- tests/components/recorder/test_purge.py | 80 ++++++++++++---------- 2 files changed, 81 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index bc91f7ce67e..2b84a439871 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -38,7 +38,8 @@ def purge_old_data( event_ids = _select_event_ids_to_purge(session, purge_before) state_ids = _select_state_ids_to_purge(session, purge_before, event_ids) if state_ids: - _purge_state_ids(session, state_ids) + _purge_state_ids(instance, session, state_ids) + if event_ids: _purge_event_ids(session, event_ids) # If states or events purging isn't processing the purge_before yet, @@ -68,10 +69,10 @@ def _select_event_ids_to_purge(session: Session, purge_before: datetime) -> list def _select_state_ids_to_purge( session: Session, purge_before: datetime, event_ids: list[int] -) -> list[int]: +) -> set[int]: """Return a list of state ids to purge.""" if not event_ids: - return [] + return set() states = ( session.query(States.state_id) .filter(States.last_updated < purge_before) @@ -79,10 +80,10 @@ def _select_state_ids_to_purge( .all() ) _LOGGER.debug("Selected %s state ids to remove", len(states)) - return [state.state_id for state in states] + return {state.state_id for state in states} -def _purge_state_ids(session: Session, state_ids: list[int]) -> None: +def _purge_state_ids(instance: Recorder, session: Session, state_ids: set[int]) -> None: """Disconnect states and delete by state id.""" # Update old_state_id to NULL before deleting to ensure @@ -103,6 +104,26 @@ def _purge_state_ids(session: Session, state_ids: list[int]) -> None: ) _LOGGER.debug("Deleted %s states", deleted_rows) + # Evict eny entries in the old_states cache referring to a purged state + _evict_purged_states_from_old_states_cache(instance, state_ids) + + +def _evict_purged_states_from_old_states_cache( + instance: Recorder, purged_state_ids: set[int] +) -> None: + """Evict purged states from the old states cache.""" + # Make a map from old_state_id to entity_id + old_states = instance._old_states # pylint: disable=protected-access + old_state_reversed = { + old_state.state_id: entity_id + for entity_id, old_state in old_states.items() + if old_state.state_id + } + + # Evict any purged state from the old states cache + for purged_state_id in purged_state_ids.intersection(old_state_reversed): + old_states.pop(old_state_reversed[purged_state_id], None) + def _purge_event_ids(session: Session, event_ids: list[int]) -> None: """Delete by event id.""" @@ -139,7 +160,7 @@ def _purge_filtered_data(instance: Recorder, session: Session) -> bool: if not instance.entity_filter(entity_id) ] if len(excluded_entity_ids) > 0: - _purge_filtered_states(session, excluded_entity_ids) + _purge_filtered_states(instance, session, excluded_entity_ids) return False # Check if excluded event_types are in database @@ -149,13 +170,15 @@ def _purge_filtered_data(instance: Recorder, session: Session) -> bool: if event_type in instance.exclude_t ] if len(excluded_event_types) > 0: - _purge_filtered_events(session, excluded_event_types) + _purge_filtered_events(instance, session, excluded_event_types) return False return True -def _purge_filtered_states(session: Session, excluded_entity_ids: list[str]) -> None: +def _purge_filtered_states( + instance: Recorder, session: Session, excluded_entity_ids: list[str] +) -> None: """Remove filtered states and linked events.""" state_ids: list[int] event_ids: list[int | None] @@ -171,11 +194,13 @@ def _purge_filtered_states(session: Session, excluded_entity_ids: list[str]) -> _LOGGER.debug( "Selected %s state_ids to remove that should be filtered", len(state_ids) ) - _purge_state_ids(session, state_ids) + _purge_state_ids(instance, session, set(state_ids)) _purge_event_ids(session, event_ids) # type: ignore # type of event_ids already narrowed to 'list[int]' -def _purge_filtered_events(session: Session, excluded_event_types: list[str]) -> None: +def _purge_filtered_events( + instance: Recorder, session: Session, excluded_event_types: list[str] +) -> None: """Remove filtered events and linked states.""" events: list[Events] = ( session.query(Events.event_id) @@ -190,8 +215,8 @@ def _purge_filtered_events(session: Session, excluded_event_types: list[str]) -> states: list[States] = ( session.query(States.state_id).filter(States.event_id.in_(event_ids)).all() ) - state_ids: list[int] = [state.state_id for state in states] - _purge_state_ids(session, state_ids) + state_ids: set[int] = {state.state_id for state in states} + _purge_state_ids(instance, session, state_ids) _purge_event_ids(session, event_ids) @@ -207,7 +232,7 @@ def purge_entity_data(instance: Recorder, entity_filter: Callable[[str], bool]) _LOGGER.debug("Purging entity data for %s", selected_entity_ids) if len(selected_entity_ids) > 0: # Purge a max of MAX_ROWS_TO_PURGE, based on the oldest states or events record - _purge_filtered_states(session, selected_entity_ids) + _purge_filtered_states(instance, session, selected_entity_ids) _LOGGER.debug("Purging entity data hasn't fully completed yet") return False diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 40ad71096c1..0e66beecd87 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -44,6 +44,7 @@ async def test_purge_old_states( events = session.query(Events).filter(Events.event_type == "state_changed") assert events.count() == 6 + assert "test.recorder2" in instance._old_states purge_before = dt_util.utcnow() - timedelta(days=4) @@ -51,6 +52,7 @@ async def test_purge_old_states( finished = purge_old_data(instance, purge_before, repack=False) assert not finished assert states.count() == 2 + assert "test.recorder2" in instance._old_states states_after_purge = session.query(States) assert states_after_purge[1].old_state_id == states_after_purge[0].state_id @@ -59,6 +61,28 @@ async def test_purge_old_states( finished = purge_old_data(instance, purge_before, repack=False) assert finished assert states.count() == 2 + assert "test.recorder2" in instance._old_states + + # run purge_old_data again + purge_before = dt_util.utcnow() + finished = purge_old_data(instance, purge_before, repack=False) + assert not finished + assert states.count() == 0 + assert "test.recorder2" not in instance._old_states + + # Add some more states + await _add_test_states(hass, instance) + + # make sure we start with 6 states + with session_scope(hass=hass) as session: + states = session.query(States) + assert states.count() == 6 + assert states[0].old_state_id is None + assert states[-1].old_state_id == states[-2].state_id + + events = session.query(Events).filter(Events.event_type == "state_changed") + assert events.count() == 6 + assert "test.recorder2" in instance._old_states async def test_purge_old_states_encouters_database_corruption( @@ -872,45 +896,27 @@ async def _add_test_states(hass: HomeAssistant, instance: recorder.Recorder): eleven_days_ago = utcnow - timedelta(days=11) attributes = {"test_attr": 5, "test_attr_10": "nice"} - await hass.async_block_till_done() - await async_wait_recording_done(hass, instance) + async def set_state(entity_id, state, **kwargs): + """Set the state.""" + hass.states.async_set(entity_id, state, **kwargs) + await hass.async_block_till_done() + await async_wait_recording_done(hass, instance) - with recorder.session_scope(hass=hass) as session: - old_state_id = None - for event_id in range(6): - if event_id < 2: - timestamp = eleven_days_ago - state = "autopurgeme" - elif event_id < 4: - timestamp = five_days_ago - state = "purgeme" - else: - timestamp = utcnow - state = "dontpurgeme" + for event_id in range(6): + if event_id < 2: + timestamp = eleven_days_ago + state = f"autopurgeme_{event_id}" + elif event_id < 4: + timestamp = five_days_ago + state = f"purgeme_{event_id}" + else: + timestamp = utcnow + state = f"dontpurgeme_{event_id}" - event = Events( - event_type="state_changed", - event_data="{}", - origin="LOCAL", - created=timestamp, - time_fired=timestamp, - ) - session.add(event) - session.flush() - state = States( - entity_id="test.recorder2", - domain="sensor", - state=state, - attributes=json.dumps(attributes), - last_changed=timestamp, - last_updated=timestamp, - created=timestamp, - event_id=event.event_id, - old_state_id=old_state_id, - ) - session.add(state) - session.flush() - old_state_id = state.state_id + with patch( + "homeassistant.components.recorder.dt_util.utcnow", return_value=timestamp + ): + await set_state("test.recorder2", state, attributes=attributes) async def _add_test_events(hass: HomeAssistant, instance: recorder.Recorder): From 745298408a807b129ed197e22bec99bd026a616e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 4 Oct 2021 17:27:24 +0200 Subject: [PATCH 0098/1038] Rewrite tuya config flow (#57043) Co-authored-by: Martin Hjelmare Co-authored-by: Franck Nijhof --- homeassistant/components/tuya/config_flow.py | 186 ++++++++--------- homeassistant/components/tuya/const.py | 28 ++- homeassistant/components/tuya/strings.json | 25 +-- .../components/tuya/translations/en.json | 73 +------ tests/components/tuya/test_config_flow.py | 195 +++++------------- 5 files changed, 168 insertions(+), 339 deletions(-) diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py index 357910b5388..1b439d49007 100644 --- a/homeassistant/components/tuya/config_flow.py +++ b/homeassistant/components/tuya/config_flow.py @@ -1,10 +1,12 @@ -#!/usr/bin/env python3 """Config flow for Tuya.""" +from __future__ import annotations import logging +from typing import Any from tuya_iot import ProjectType, TuyaOpenAPI import voluptuous as vol +from voluptuous.schema_builder import UNDEFINED from homeassistant import config_entries @@ -16,131 +18,123 @@ from .const import ( CONF_ENDPOINT, CONF_PASSWORD, CONF_PROJECT_TYPE, + CONF_REGION, CONF_USERNAME, DOMAIN, - TUYA_APP_TYPE, - TUYA_ENDPOINT, - TUYA_PROJECT_TYPE, + SMARTLIFE_APP, + TUYA_REGIONS, + TUYA_RESPONSE_CODE, + TUYA_RESPONSE_MSG, + TUYA_RESPONSE_PLATFROM_URL, + TUYA_RESPONSE_RESULT, + TUYA_RESPONSE_SUCCESS, + TUYA_SMART_APP, ) -RESULT_SINGLE_INSTANCE = "single_instance_allowed" -RESULT_AUTH_FAILED = "invalid_auth" -TUYA_ENDPOINT_BASE = "https://openapi.tuyacn.com" -TUYA_ENDPOINT_OTHER = "https://openapi.tuyaus.com" -COUNTRY_CODE_CHINA = ["86", "+86", "China"] - _LOGGER = logging.getLogger(__name__) -# Project Type -DATA_SCHEMA_PROJECT_TYPE = vol.Schema( - {vol.Required(CONF_PROJECT_TYPE, default=0): vol.In(TUYA_PROJECT_TYPE)} -) - -# INDUSTY_SOLUTIONS Schema -DATA_SCHEMA_INDUSTRY_SOLUTIONS = vol.Schema( - { - vol.Required(CONF_ENDPOINT): vol.In(TUYA_ENDPOINT), - vol.Required(CONF_ACCESS_ID): str, - vol.Required(CONF_ACCESS_SECRET): str, - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, - } -) - -# SMART_HOME Schema -DATA_SCHEMA_SMART_HOME = vol.Schema( - { - vol.Required(CONF_ACCESS_ID): str, - vol.Required(CONF_ACCESS_SECRET): str, - vol.Required(CONF_APP_TYPE): vol.In(TUYA_APP_TYPE), - vol.Required(CONF_COUNTRY_CODE): str, - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, - } -) - class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Tuya Config Flow.""" - def __init__(self) -> None: - """Init tuya config flow.""" - super().__init__() - self.conf_project_type = None - @staticmethod - def _try_login(user_input): - project_type = ProjectType(user_input[CONF_PROJECT_TYPE]) - api = TuyaOpenAPI( - user_input[CONF_ENDPOINT] - if project_type == ProjectType.INDUSTY_SOLUTIONS - else "", - user_input[CONF_ACCESS_ID], - user_input[CONF_ACCESS_SECRET], - project_type, - ) - api.set_dev_channel("hass") + def _try_login(user_input: dict[str, Any]) -> tuple[dict[Any, Any], dict[str, Any]]: + """Try login.""" + response = {} - if project_type == ProjectType.INDUSTY_SOLUTIONS: - response = api.login(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) - else: - if user_input[CONF_COUNTRY_CODE] in COUNTRY_CODE_CHINA: - api.endpoint = TUYA_ENDPOINT_BASE + data = { + CONF_ENDPOINT: TUYA_REGIONS[user_input[CONF_REGION]], + CONF_PROJECT_TYPE: ProjectType.INDUSTY_SOLUTIONS, + CONF_ACCESS_ID: user_input[CONF_ACCESS_ID], + CONF_ACCESS_SECRET: user_input[CONF_ACCESS_SECRET], + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_COUNTRY_CODE: user_input[CONF_REGION], + } + + for app_type in ("", TUYA_SMART_APP, SMARTLIFE_APP): + data[CONF_APP_TYPE] = app_type + if data[CONF_APP_TYPE] == "": + data[CONF_PROJECT_TYPE] = ProjectType.INDUSTY_SOLUTIONS else: - api.endpoint = TUYA_ENDPOINT_OTHER + data[CONF_PROJECT_TYPE] = ProjectType.SMART_HOME + + api = TuyaOpenAPI( + endpoint=data[CONF_ENDPOINT], + access_id=data[CONF_ACCESS_ID], + access_secret=data[CONF_ACCESS_SECRET], + project_type=data[CONF_PROJECT_TYPE], + ) + api.set_dev_channel("hass") response = api.login( - user_input[CONF_USERNAME], - user_input[CONF_PASSWORD], - user_input[CONF_COUNTRY_CODE], - user_input[CONF_APP_TYPE], + username=data[CONF_USERNAME], + password=data[CONF_PASSWORD], + country_code=data[CONF_COUNTRY_CODE], + schema=data[CONF_APP_TYPE], ) - if response.get("success", False) and isinstance( - api.token_info.platform_url, str - ): - api.endpoint = api.token_info.platform_url - user_input[CONF_ENDPOINT] = api.token_info.platform_url - _LOGGER.debug("TuyaConfigFlow._try_login finish, response:, %s", response) - return response + _LOGGER.debug("Response %s", response) + + if response.get(TUYA_RESPONSE_SUCCESS, False): + break + + return response, data async def async_step_user(self, user_input=None): """Step user.""" - if user_input is None: - return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA_PROJECT_TYPE - ) - - self.conf_project_type = user_input[CONF_PROJECT_TYPE] - - return await self.async_step_login() - - async def async_step_login(self, user_input=None): - """Step login.""" errors = {} - if user_input is not None: - assert self.conf_project_type is not None - user_input[CONF_PROJECT_TYPE] = self.conf_project_type + placeholders = {} - response = await self.hass.async_add_executor_job( + if user_input is not None: + response, data = await self.hass.async_add_executor_job( self._try_login, user_input ) - if response.get("success", False): - _LOGGER.debug("TuyaConfigFlow.async_step_user login success") + if response.get(TUYA_RESPONSE_SUCCESS, False): + if endpoint := response.get(TUYA_RESPONSE_RESULT, {}).get( + TUYA_RESPONSE_PLATFROM_URL + ): + data[CONF_ENDPOINT] = endpoint + + data[CONF_PROJECT_TYPE] = data[CONF_PROJECT_TYPE].value + return self.async_create_entry( title=user_input[CONF_USERNAME], - data=user_input, + data=data, ) - errors["base"] = RESULT_AUTH_FAILED + errors["base"] = "login_error" + placeholders = { + TUYA_RESPONSE_CODE: response.get(TUYA_RESPONSE_CODE), + TUYA_RESPONSE_MSG: response.get(TUYA_RESPONSE_MSG), + } - if ProjectType(self.conf_project_type) == ProjectType.SMART_HOME: - return self.async_show_form( - step_id="login", data_schema=DATA_SCHEMA_SMART_HOME, errors=errors - ) + def _schema_default(key: str) -> str | UNDEFINED: + if not user_input: + return UNDEFINED + return user_input[key] return self.async_show_form( - step_id="login", - data_schema=DATA_SCHEMA_INDUSTRY_SOLUTIONS, + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_REGION, default=_schema_default(CONF_REGION) + ): vol.In(TUYA_REGIONS.keys()), + vol.Required( + CONF_ACCESS_ID, default=_schema_default(CONF_ACCESS_ID) + ): str, + vol.Required( + CONF_ACCESS_SECRET, default=_schema_default(CONF_ACCESS_SECRET) + ): str, + vol.Required( + CONF_USERNAME, default=_schema_default(CONF_USERNAME) + ): str, + vol.Required( + CONF_PASSWORD, default=_schema_default(CONF_PASSWORD) + ): str, + } + ), errors=errors, + description_placeholders=placeholders, ) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index e259dd9190b..f86180226ee 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -9,6 +9,7 @@ CONF_ACCESS_ID = "access_id" CONF_ACCESS_SECRET = "access_secret" CONF_USERNAME = "username" CONF_PASSWORD = "password" +CONF_REGION = "region" CONF_COUNTRY_CODE = "country_code" CONF_APP_TYPE = "tuya_app_type" @@ -19,19 +20,24 @@ TUYA_MQTT_LISTENER = "tuya_mqtt_listener" TUYA_HA_TUYA_MAP = "tuya_ha_tuya_map" TUYA_HA_DEVICES = "tuya_ha_devices" +TUYA_RESPONSE_CODE = "code" +TUYA_RESPONSE_RESULT = "result" +TUYA_RESPONSE_MSG = "msg" +TUYA_RESPONSE_SUCCESS = "success" +TUYA_RESPONSE_PLATFROM_URL = "platform_url" + TUYA_HA_SIGNAL_UPDATE_ENTITY = "tuya_entry_update" -TUYA_ENDPOINT = { - "https://openapi.tuyaus.com": "America", - "https://openapi.tuyacn.com": "China", - "https://openapi.tuyaeu.com": "Europe", - "https://openapi.tuyain.com": "India", - "https://openapi-ueaz.tuyaus.com": "EasternAmerica", - "https://openapi-weaz.tuyaeu.com": "WesternEurope", +TUYA_SMART_APP = "tuyaSmart" +SMARTLIFE_APP = "smartlife" + +TUYA_REGIONS = { + "America": "https://openapi.tuyaus.com", + "China": "https://openapi.tuyacn.com", + "Eastern America": "https://openapi-ueaz.tuyaus.com", + "Europe": "https://openapi.tuyaeu.com", + "India": "https://openapi.tuyain.com", + "Western Europe": "https://openapi-weaz.tuyaeu.com", } -TUYA_PROJECT_TYPE = {1: "Custom Development", 0: "Smart Home PaaS"} - -TUYA_APP_TYPE = {"tuyaSmart": "TuyaSmart", "smartlife": "Smart Life"} - PLATFORMS = ["climate", "fan", "light", "scene", "switch"] diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index 91ca045e1f5..044c068ac9c 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -1,29 +1,20 @@ { "config": { - "flow_title": "Tuya configuration", "step": { - "user":{ - "title":"Tuya Integration", - "data":{ - "tuya_project_type": "Tuya cloud project type" - } - }, - "login": { - "title": "Tuya", - "description": "Enter your Tuya credential", + "user": { + "description": "Enter your Tuya credentials", "data": { - "endpoint": "Availability Zone", - "access_id": "Access ID", - "access_secret": "Access Secret", - "tuya_app_type": "Mobile App", - "country_code": "Country Code", + "region": "Region", + "access_id": "Tuya IoT Access ID", + "access_secret": "Tuya IoT Access Secret", "username": "Account", - "password": "Password" + "password": "[%key:common::config_flow::data::password%]" } } }, "error": { - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "login_error": "Login error ({code}): {msg}" } } } \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/en.json b/homeassistant/components/tuya/translations/en.json index c7aaee977ee..a3630e66406 100644 --- a/homeassistant/components/tuya/translations/en.json +++ b/homeassistant/components/tuya/translations/en.json @@ -1,78 +1,19 @@ { "config": { - "abort": { - "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", - "single_instance_allowed": "Already configured. Only a single configuration possible." - }, "error": { - "invalid_auth": "Invalid authentication" + "invalid_auth": "Invalid authentication", + "login_error": "Login error ({code}): {msg}" }, - "flow_title": "Tuya configuration", "step": { - "login": { - "data": { - "access_id": "Access ID", - "access_secret": "Access Secret", - "country_code": "Country Code", - "endpoint": "Availability Zone", - "password": "Password", - "tuya_app_type": "Mobile App", - "username": "Account" - }, - "description": "Enter your Tuya credential", - "title": "Tuya" - }, "user": { "data": { - "country_code": "Your account country code (e.g., 1 for USA or 86 for China)", + "access_id": "Tuya IoT Access ID", + "access_secret": "Tuya IoT Access Secret", "password": "Password", - "platform": "The app where your account is registered", - "tuya_project_type": "Tuya cloud project type", - "username": "Username" + "region": "Region", + "username": "Account" }, - "description": "Enter your Tuya credentials.", - "title": "Tuya Integration" - } - } - }, - "options": { - "abort": { - "cannot_connect": "Failed to connect" - }, - "error": { - "dev_multi_type": "Multiple selected devices to configure must be of the same type", - "dev_not_config": "Device type not configurable", - "dev_not_found": "Device not found" - }, - "step": { - "device": { - "data": { - "brightness_range_mode": "Brightness range used by device", - "curr_temp_divider": "Current Temperature value divider (0 = use default)", - "max_kelvin": "Max color temperature supported in kelvin", - "max_temp": "Max target temperature (use min and max = 0 for default)", - "min_kelvin": "Min color temperature supported in kelvin", - "min_temp": "Min target temperature (use min and max = 0 for default)", - "set_temp_divided": "Use divided Temperature value for set temperature command", - "support_color": "Force color support", - "temp_divider": "Temperature values divider (0 = use default)", - "temp_step_override": "Target Temperature step", - "tuya_max_coltemp": "Max color temperature reported by device", - "unit_of_measurement": "Temperature unit used by device" - }, - "description": "Configure options to adjust displayed information for {device_type} device `{device_name}`", - "title": "Configure Tuya Device" - }, - "init": { - "data": { - "discovery_interval": "Discovery device polling interval in seconds", - "list_devices": "Select the devices to configure or leave empty to save configuration", - "query_device": "Select device that will use query method for faster status update", - "query_interval": "Query device polling interval in seconds" - }, - "description": "Do not set pollings interval values too low or the calls will fail generating error message in the log", - "title": "Configure Tuya Options" + "description": "Enter your Tuya credentials" } } } diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index b01745ee8db..04fb8ebe009 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -1,93 +1,84 @@ """Tests for the Tuya config flow.""" -from unittest.mock import MagicMock, Mock, patch +from __future__ import annotations + +from typing import Any +from unittest.mock import MagicMock, patch import pytest from homeassistant import config_entries, data_entry_flow -from homeassistant.components.tuya.config_flow import RESULT_AUTH_FAILED from homeassistant.components.tuya.const import ( CONF_ACCESS_ID, CONF_ACCESS_SECRET, CONF_APP_TYPE, - CONF_COUNTRY_CODE, CONF_ENDPOINT, CONF_PASSWORD, CONF_PROJECT_TYPE, + CONF_REGION, CONF_USERNAME, DOMAIN, + SMARTLIFE_APP, + TUYA_REGIONS, + TUYA_SMART_APP, ) +from homeassistant.core import HomeAssistant MOCK_SMART_HOME_PROJECT_TYPE = 0 MOCK_INDUSTRY_PROJECT_TYPE = 1 +MOCK_REGION = "Europe" MOCK_ACCESS_ID = "myAccessId" MOCK_ACCESS_SECRET = "myAccessSecret" MOCK_USERNAME = "myUsername" MOCK_PASSWORD = "myPassword" -MOCK_COUNTRY_CODE_BASE = "86" -MOCK_COUNTRY_CODE_OTHER = "1" -MOCK_APP_TYPE = "smartlife" MOCK_ENDPOINT = "https://openapi-ueaz.tuyaus.com" -TUYA_SMART_HOME_PROJECT_DATA = { - CONF_PROJECT_TYPE: MOCK_SMART_HOME_PROJECT_TYPE, -} -TUYA_INDUSTRY_PROJECT_DATA = { - CONF_PROJECT_TYPE: MOCK_INDUSTRY_PROJECT_TYPE, -} - -TUYA_INPUT_INDUSTRY_DATA = { - CONF_ENDPOINT: MOCK_ENDPOINT, +TUYA_INPUT_DATA = { + CONF_REGION: MOCK_REGION, CONF_ACCESS_ID: MOCK_ACCESS_ID, CONF_ACCESS_SECRET: MOCK_ACCESS_SECRET, CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: MOCK_PASSWORD, } -TUYA_IMPORT_SMART_HOME_DATA_BASE = { - CONF_ACCESS_ID: MOCK_ACCESS_ID, - CONF_ACCESS_SECRET: MOCK_ACCESS_SECRET, - CONF_USERNAME: MOCK_USERNAME, - CONF_PASSWORD: MOCK_PASSWORD, - CONF_COUNTRY_CODE: MOCK_COUNTRY_CODE_BASE, - CONF_APP_TYPE: MOCK_APP_TYPE, -} - -TUYA_IMPORT_SMART_HOME_DATA_OTHER = { - CONF_ACCESS_ID: MOCK_ACCESS_ID, - CONF_ACCESS_SECRET: MOCK_ACCESS_SECRET, - CONF_USERNAME: MOCK_USERNAME, - CONF_PASSWORD: MOCK_PASSWORD, - CONF_COUNTRY_CODE: MOCK_COUNTRY_CODE_OTHER, - CONF_APP_TYPE: MOCK_APP_TYPE, -} - -TUYA_IMPORT_INDUSTRY_DATA = { - CONF_PROJECT_TYPE: MOCK_SMART_HOME_PROJECT_TYPE, - CONF_ENDPOINT: MOCK_ENDPOINT, - CONF_ACCESS_ID: MOCK_ACCESS_ID, - CONF_ACCESS_SECRET: MOCK_ACCESS_SECRET, - CONF_USERNAME: MOCK_USERNAME, - CONF_PASSWORD: MOCK_PASSWORD, +RESPONSE_SUCCESS = { + "success": True, + "code": 1024, + "result": {"platform_url": MOCK_ENDPOINT}, } +RESPONSE_ERROR = {"success": False, "code": 123, "msg": "Error"} @pytest.fixture(name="tuya") -def tuya_fixture() -> Mock: +def tuya_fixture() -> MagicMock: """Patch libraries.""" with patch("homeassistant.components.tuya.config_flow.TuyaOpenAPI") as tuya: yield tuya @pytest.fixture(name="tuya_setup", autouse=True) -def tuya_setup_fixture(): +def tuya_setup_fixture() -> None: """Mock tuya entry setup.""" with patch("homeassistant.components.tuya.async_setup_entry", return_value=True): yield -async def test_industry_user(hass, tuya): - """Test industry user config.""" +@pytest.mark.parametrize( + "app_type,side_effects, project_type", + [ + ("", [RESPONSE_SUCCESS], 1), + (TUYA_SMART_APP, [RESPONSE_ERROR, RESPONSE_SUCCESS], 0), + (SMARTLIFE_APP, [RESPONSE_ERROR, RESPONSE_ERROR, RESPONSE_SUCCESS], 0), + ], +) +async def test_user_flow( + hass: HomeAssistant, + tuya: MagicMock, + app_type: str, + side_effects: list[dict[str, Any]], + project_type: int, +): + """Test user flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -95,17 +86,9 @@ async def test_industry_user(hass, tuya): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" + tuya().login = MagicMock(side_effect=side_effects) result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=TUYA_INDUSTRY_PROJECT_DATA - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "login" - - tuya().login = MagicMock(return_value={"success": True, "errorCode": 1024}) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=TUYA_INPUT_INDUSTRY_DATA + result["flow_id"], user_input=TUYA_INPUT_DATA ) await hass.async_block_till_done() @@ -115,90 +98,10 @@ async def test_industry_user(hass, tuya): assert result["data"][CONF_ACCESS_SECRET] == MOCK_ACCESS_SECRET assert result["data"][CONF_USERNAME] == MOCK_USERNAME assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD - assert not result["result"].unique_id - - -async def test_smart_home_user_base(hass, tuya): - """Test smart home user config base.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=TUYA_SMART_HOME_PROJECT_DATA - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "login" - - tuya().login = MagicMock(return_value={"success": False, "errorCode": 1024}) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=TUYA_IMPORT_SMART_HOME_DATA_BASE - ) - await hass.async_block_till_done() - - assert result["errors"]["base"] == RESULT_AUTH_FAILED - - tuya().login = MagicMock(return_value={"success": True, "errorCode": 1024}) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=TUYA_IMPORT_SMART_HOME_DATA_BASE - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == MOCK_USERNAME - assert result["data"][CONF_ACCESS_ID] == MOCK_ACCESS_ID - assert result["data"][CONF_ACCESS_SECRET] == MOCK_ACCESS_SECRET - assert result["data"][CONF_USERNAME] == MOCK_USERNAME - assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD - assert result["data"][CONF_COUNTRY_CODE] == MOCK_COUNTRY_CODE_BASE - assert result["data"][CONF_APP_TYPE] == MOCK_APP_TYPE - assert not result["result"].unique_id - - -async def test_smart_home_user_other(hass, tuya): - """Test smart home user config other.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=TUYA_SMART_HOME_PROJECT_DATA - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "login" - - tuya().login = MagicMock(return_value={"success": False, "errorCode": 1024}) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=TUYA_IMPORT_SMART_HOME_DATA_OTHER - ) - await hass.async_block_till_done() - - assert result["errors"]["base"] == RESULT_AUTH_FAILED - - tuya().login = MagicMock(return_value={"success": True, "errorCode": 1024}) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=TUYA_IMPORT_SMART_HOME_DATA_OTHER - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == MOCK_USERNAME - assert result["data"][CONF_ACCESS_ID] == MOCK_ACCESS_ID - assert result["data"][CONF_ACCESS_SECRET] == MOCK_ACCESS_SECRET - assert result["data"][CONF_USERNAME] == MOCK_USERNAME - assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD - assert result["data"][CONF_COUNTRY_CODE] == MOCK_COUNTRY_CODE_OTHER - assert result["data"][CONF_APP_TYPE] == MOCK_APP_TYPE + assert result["data"][CONF_ENDPOINT] == MOCK_ENDPOINT + assert result["data"][CONF_ENDPOINT] != TUYA_REGIONS[TUYA_INPUT_DATA[CONF_REGION]] + assert result["data"][CONF_APP_TYPE] == app_type + assert result["data"][CONF_PROJECT_TYPE] == project_type assert not result["result"].unique_id @@ -212,18 +115,12 @@ async def test_error_on_invalid_credentials(hass, tuya): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" + tuya().login = MagicMock(return_value=RESPONSE_ERROR) result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=TUYA_INDUSTRY_PROJECT_DATA + result["flow_id"], user_input=TUYA_INPUT_DATA ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "login" - - tuya().login = MagicMock(return_value={"success": False, "errorCode": 1024}) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=TUYA_INPUT_INDUSTRY_DATA - ) - await hass.async_block_till_done() - - assert result["errors"]["base"] == RESULT_AUTH_FAILED + assert result["errors"]["base"] == "login_error" + assert result["description_placeholders"]["code"] == RESPONSE_ERROR["code"] + assert result["description_placeholders"]["msg"] == RESPONSE_ERROR["msg"] From eb9b9c57a46e3c001d6e4d369085792f7958bb78 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 4 Oct 2021 08:38:24 -0700 Subject: [PATCH 0099/1038] [ci skip] Translation update --- .../components/adguard/translations/hu.json | 2 +- .../components/almond/translations/hu.json | 2 +- .../components/ambee/translations/hu.json | 2 +- .../components/broadlink/translations/hu.json | 2 +- .../components/brother/translations/hu.json | 2 +- .../components/cast/translations/hu.json | 2 +- .../components/deconz/translations/hu.json | 4 +- .../components/esphome/translations/hu.json | 2 +- .../components/flux_led/translations/nl.json | 36 ++++++++++ .../components/flux_led/translations/no.json | 36 ++++++++++ .../flux_led/translations/zh-Hant.json | 36 ++++++++++ .../homekit_controller/translations/hu.json | 2 +- .../components/hue/translations/hu.json | 2 +- .../components/kodi/translations/hu.json | 2 +- .../mobile_app/translations/hu.json | 2 +- .../modern_forms/translations/hu.json | 2 +- .../components/motioneye/translations/hu.json | 2 +- .../components/mqtt/translations/hu.json | 2 +- .../components/roon/translations/hu.json | 2 +- .../components/samsungtv/translations/hu.json | 4 +- .../components/smappee/translations/hu.json | 2 +- .../components/tuya/translations/en.json | 65 ++++++++++++++++++- .../components/tuya/translations/nl.json | 1 + .../components/tuya/translations/no.json | 16 ++++- .../components/vera/translations/hu.json | 4 +- .../components/vizio/translations/hu.json | 2 +- .../components/volumio/translations/hu.json | 2 +- .../components/wled/translations/hu.json | 4 +- .../components/zwave_js/translations/nl.json | 8 +++ .../components/zwave_js/translations/no.json | 8 +++ 30 files changed, 230 insertions(+), 28 deletions(-) create mode 100644 homeassistant/components/flux_led/translations/nl.json create mode 100644 homeassistant/components/flux_led/translations/no.json create mode 100644 homeassistant/components/flux_led/translations/zh-Hant.json diff --git a/homeassistant/components/adguard/translations/hu.json b/homeassistant/components/adguard/translations/hu.json index 3939de8aea5..b04d67fbb89 100644 --- a/homeassistant/components/adguard/translations/hu.json +++ b/homeassistant/components/adguard/translations/hu.json @@ -9,7 +9,7 @@ }, "step": { "hassio_confirm": { - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani Home Assistant-ot AdGuard Home-hoz val\u00f3 csatlakoz\u00e1shoz, {addon} kieg\u00e9sz\u00edt\u0151 \u00e1ltal?", + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani Home Assistantot AdGuard Home-hoz val\u00f3 csatlakoz\u00e1shoz, {addon} kieg\u00e9sz\u00edt\u0151 \u00e1ltal?", "title": "Az AdGuard Home a Home Assistant kieg\u00e9sz\u00edt\u0151 seg\u00edts\u00e9g\u00e9vel" }, "user": { diff --git a/homeassistant/components/almond/translations/hu.json b/homeassistant/components/almond/translations/hu.json index 27932696561..d75290b4fd1 100644 --- a/homeassistant/components/almond/translations/hu.json +++ b/homeassistant/components/almond/translations/hu.json @@ -8,7 +8,7 @@ }, "step": { "hassio_confirm": { - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani Home Assistant-ot az Almondhoz val\u00f3 csatlakoz\u00e1shoz, {addon} kieg\u00e9sz\u00edt\u0151 \u00e1ltal?", + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani Home Assistantot Almondhoz val\u00f3 csatlakoz\u00e1shoz, {addon} kieg\u00e9sz\u00edt\u0151 \u00e1ltal?", "title": "Almond - Home Assistant kieg\u00e9sz\u00edt\u0151 \u00e1ltal" }, "pick_implementation": { diff --git a/homeassistant/components/ambee/translations/hu.json b/homeassistant/components/ambee/translations/hu.json index 299d97914bc..6cb59bba925 100644 --- a/homeassistant/components/ambee/translations/hu.json +++ b/homeassistant/components/ambee/translations/hu.json @@ -21,7 +21,7 @@ "longitude": "Hossz\u00fas\u00e1g", "name": "N\u00e9v" }, - "description": "\u00c1ll\u00edtsa be Ambee-t Home Assistant-tal val\u00f3 integr\u00e1ci\u00f3hoz." + "description": "Integr\u00e1lja \u00f6ssze Ambeet Home Assistanttal." } } } diff --git a/homeassistant/components/broadlink/translations/hu.json b/homeassistant/components/broadlink/translations/hu.json index 3d792f43597..d3a59a03cea 100644 --- a/homeassistant/components/broadlink/translations/hu.json +++ b/homeassistant/components/broadlink/translations/hu.json @@ -32,7 +32,7 @@ "data": { "unlock": "Igen, csin\u00e1ld." }, - "description": "{name} ({model} a {host} c\u00edmen) z\u00e1rolva van. Ez hiteles\u00edt\u00e9si probl\u00e9m\u00e1khoz vezethet Home Assistant-ban. Szeretn\u00e9 feloldani?", + "description": "{name} ({model} a {host} c\u00edmen) z\u00e1rolva van. Ez hiteles\u00edt\u00e9si probl\u00e9m\u00e1khoz vezethet Home Assistantban. Szeretn\u00e9 feloldani?", "title": "Az eszk\u00f6z felold\u00e1sa (opcion\u00e1lis)" }, "user": { diff --git a/homeassistant/components/brother/translations/hu.json b/homeassistant/components/brother/translations/hu.json index 9d733e4cda6..f0218dc2647 100644 --- a/homeassistant/components/brother/translations/hu.json +++ b/homeassistant/components/brother/translations/hu.json @@ -22,7 +22,7 @@ "data": { "type": "A nyomtat\u00f3 t\u00edpusa" }, - "description": "Hozz\u00e1 szeretn\u00e9 adni a {model} Brother nyomtat\u00f3t, amelynek sorsz\u00e1ma: `{serial_number}`, Home Assistant-hoz?", + "description": "Hozz\u00e1 szeretn\u00e9 adni a {model} Brother nyomtat\u00f3t, amelynek sorsz\u00e1ma: `{serial_number}`, Home Assistanthoz?", "title": "Felfedezett Brother nyomtat\u00f3" } } diff --git a/homeassistant/components/cast/translations/hu.json b/homeassistant/components/cast/translations/hu.json index 2d74d3183c8..a4c8da3242e 100644 --- a/homeassistant/components/cast/translations/hu.json +++ b/homeassistant/components/cast/translations/hu.json @@ -29,7 +29,7 @@ "ignore_cec": "A CEC figyelmen k\u00edv\u00fcl hagy\u00e1sa", "uuid": "Enged\u00e9lyezett UUID-k" }, - "description": "Enged\u00e9lyezett UUID - vessz\u0151vel elv\u00e1lasztott lista a Cast-eszk\u00f6z\u00f6k UUID-j\u00e9b\u0151l, amelyeket hozz\u00e1 lehet adni Home Assistant-hoz. Csak akkor haszn\u00e1lja, ha nem akarja hozz\u00e1adni az \u00f6sszes rendelkez\u00e9sre \u00e1ll\u00f3 cast eszk\u00f6zt.\nCEC figyelmen k\u00edv\u00fcl hagy\u00e1sa - vessz\u0151vel elv\u00e1lasztott Chromecast-lista, amelynek figyelmen k\u00edv\u00fcl kell hagynia a CEC-adatokat az akt\u00edv bemenet meghat\u00e1roz\u00e1s\u00e1hoz. Ezt tov\u00e1bb\u00edtjuk a pychromecast.IGNORE_CEC c\u00edmre.", + "description": "Enged\u00e9lyezett UUID - vessz\u0151vel elv\u00e1lasztott lista a Cast-eszk\u00f6z\u00f6k UUID-j\u00e9b\u0151l, amelyeket hozz\u00e1 lehet adni Home Assistanthoz. Csak akkor haszn\u00e1lja, ha nem akarja hozz\u00e1adni az \u00f6sszes rendelkez\u00e9sre \u00e1ll\u00f3 cast eszk\u00f6zt.\nCEC figyelmen k\u00edv\u00fcl hagy\u00e1sa - vessz\u0151vel elv\u00e1lasztott Chromecast-lista, amelynek figyelmen k\u00edv\u00fcl kell hagynia a CEC-adatokat az akt\u00edv bemenet meghat\u00e1roz\u00e1s\u00e1hoz. Ezt tov\u00e1bb\u00edtjuk a pychromecast.IGNORE_CEC c\u00edmre.", "title": "Speci\u00e1lis Google Cast-konfigur\u00e1ci\u00f3" }, "basic_options": { diff --git a/homeassistant/components/deconz/translations/hu.json b/homeassistant/components/deconz/translations/hu.json index 664f3768a22..a78a6ef1961 100644 --- a/homeassistant/components/deconz/translations/hu.json +++ b/homeassistant/components/deconz/translations/hu.json @@ -14,11 +14,11 @@ "flow_title": "{host}", "step": { "hassio_confirm": { - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani Home Assistant-ot, hogy csatlakozzon {addon} \u00e1ltal biztos\u00edtott deCONZ \u00e1tj\u00e1r\u00f3hoz?", + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani Home Assistantot, hogy csatlakozzon {addon} \u00e1ltal biztos\u00edtott deCONZ \u00e1tj\u00e1r\u00f3hoz?", "title": "deCONZ Zigbee \u00e1tj\u00e1r\u00f3 Home Assistant kieg\u00e9sz\u00edt\u0151 \u00e1ltal" }, "link": { - "description": "Enged\u00e9lyezze fel a deCONZ \u00e1tj\u00e1r\u00f3ban a Home Assistant-hoz val\u00f3 regisztr\u00e1l\u00e1st.\n\n1. V\u00e1lassza ki a deCONZ rendszer be\u00e1ll\u00edt\u00e1sait\n2. Nyomja meg az \"Authenticate app\" gombot", + "description": "Enged\u00e9lyezze fel a deCONZ \u00e1tj\u00e1r\u00f3ban a Home Assistanthoz val\u00f3 regisztr\u00e1l\u00e1st.\n\n1. V\u00e1lassza ki a deCONZ rendszer be\u00e1ll\u00edt\u00e1sait\n2. Nyomja meg az \"Authenticate app\" gombot", "title": "Kapcsol\u00f3d\u00e1s a deCONZ-hoz" }, "manual_input": { diff --git a/homeassistant/components/esphome/translations/hu.json b/homeassistant/components/esphome/translations/hu.json index d7ac503d83c..e65577f055e 100644 --- a/homeassistant/components/esphome/translations/hu.json +++ b/homeassistant/components/esphome/translations/hu.json @@ -20,7 +20,7 @@ "description": "K\u00e9rem, adja meg a konfigur\u00e1ci\u00f3ban {name} n\u00e9vhez be\u00e1ll\u00edtott jelsz\u00f3t." }, "discovery_confirm": { - "description": "Szeretn\u00e9 hozz\u00e1adni `{name}` ESPHome csom\u00f3pontot Home Assistant-hoz?", + "description": "Szeretn\u00e9 hozz\u00e1adni `{name}` ESPHome csom\u00f3pontot Home Assistanthoz?", "title": "Felfedezett ESPHome csom\u00f3pont" }, "encryption_key": { diff --git a/homeassistant/components/flux_led/translations/nl.json b/homeassistant/components/flux_led/translations/nl.json new file mode 100644 index 00000000000..fd9e04bd475 --- /dev/null +++ b/homeassistant/components/flux_led/translations/nl.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang", + "no_devices_found": "Geen apparaten gevonden op het netwerk" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "flow_title": "{model} {id} ({ipaddr})", + "step": { + "discovery_confirm": { + "description": "Wilt u {model} {id} ( {ipaddr} ) instellen?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Als u de host leeg laat, wordt detectie gebruikt om apparaten te vinden." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "custom_effect_colors": "Aangepast effect: Lijst van 1 tot 16 [R,G,B] kleuren. Voorbeeld: [255,0,255],[60,128,0]", + "custom_effect_speed_pct": "Aangepast effect: snelheid in procenten voor het effect dat van kleur verandert.", + "custom_effect_transition": "Aangepast effect: Type overgang tussen de kleuren.", + "mode": "De gekozen helderheidsstand." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/no.json b/homeassistant/components/flux_led/translations/no.json new file mode 100644 index 00000000000..ec105c1ac14 --- /dev/null +++ b/homeassistant/components/flux_led/translations/no.json @@ -0,0 +1,36 @@ +{ + "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" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "flow_title": "{model} {id} ( {ipaddr} )", + "step": { + "discovery_confirm": { + "description": "Vil du konfigurere {model} {id} ( {ipaddr} )?" + }, + "user": { + "data": { + "host": "Vert" + }, + "description": "Hvis du lar verten st\u00e5 tom, brukes automatisk oppdagelse til \u00e5 finne enheter" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "custom_effect_colors": "Egendefinert effekt: Liste med farger fra 1 til 16 [R,G,B]. Eksempel: [255,0,255],[60,128,0]", + "custom_effect_speed_pct": "Egendefinert effekt: Hastighet i prosent for effekten som bytter farger.", + "custom_effect_transition": "Egendefinert effekt: Overgangstype mellom fargene.", + "mode": "Den valgte lysstyrkemodusen." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/zh-Hant.json b/homeassistant/components/flux_led/translations/zh-Hant.json new file mode 100644 index 00000000000..4e14b58ff18 --- /dev/null +++ b/homeassistant/components/flux_led/translations/zh-Hant.json @@ -0,0 +1,36 @@ +{ + "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" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "flow_title": "{model} {id} ({ipaddr})", + "step": { + "discovery_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {model} {id} ({ipaddr})\uff1f" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + }, + "description": "\u5047\u5982\u4e3b\u6a5f\u7aef\u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u63a2\u7d22\u6240\u6709\u53ef\u7528\u88dd\u7f6e\u3002" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "custom_effect_colors": "\u81ea\u8a02\u7279\u6548\uff1a1 \u5230 16 \u7a2e [R,G,B] \u984f\u8272\u3002\u4f8b\u5982\uff1a[255,0,255]\u3001[60,128,0]", + "custom_effect_speed_pct": "\u81ea\u8a02\u7279\u6548\uff1a\u984f\u8272\u5207\u63db\u7684\u901f\u5ea6\u767e\u5206\u6bd4\u3002", + "custom_effect_transition": "\u81ea\u8a02\u7279\u6548\uff1a\u984f\u8272\u9593\u7684\u8f49\u63db\u985e\u578b\u3002", + "mode": "\u9078\u64c7\u4eae\u5ea6\u6a21\u5f0f\u3002" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/hu.json b/homeassistant/components/homekit_controller/translations/hu.json index aef97c7b3ba..7703925ae67 100644 --- a/homeassistant/components/homekit_controller/translations/hu.json +++ b/homeassistant/components/homekit_controller/translations/hu.json @@ -6,7 +6,7 @@ "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", "already_paired": "Ez a tartoz\u00e9k m\u00e1r p\u00e1ros\u00edtva van egy m\u00e1sik eszk\u00f6zzel. \u00c1ll\u00edtsa alaphelyzetbe a tartoz\u00e9kot, majd pr\u00f3b\u00e1lkozzon \u00fajra.", "ignored_model": "A HomeKit t\u00e1mogat\u00e1sa e modelln\u00e9l blokkolva van, mivel a szolg\u00e1ltat\u00e1shoz teljes nat\u00edv integr\u00e1ci\u00f3 \u00e9rhet\u0151 el.", - "invalid_config_entry": "Ez az eszk\u00f6z k\u00e9szen \u00e1ll a p\u00e1ros\u00edt\u00e1sra, de m\u00e1r van egy \u00fctk\u00f6z\u0151 konfigur\u00e1ci\u00f3s bejegyz\u00e9s Home Assistant-ben, amelyet el\u0151sz\u00f6r el kell t\u00e1vol\u00edtani.", + "invalid_config_entry": "Ez az eszk\u00f6z k\u00e9szen \u00e1ll a p\u00e1ros\u00edt\u00e1sra, de m\u00e1r van egy \u00fctk\u00f6z\u0151 konfigur\u00e1ci\u00f3s bejegyz\u00e9s Home Assistantban, amelyet el\u0151sz\u00f6r el kell t\u00e1vol\u00edtani.", "invalid_properties": "Az eszk\u00f6z \u00e1ltal bejelentett \u00e9rv\u00e9nytelen tulajdons\u00e1gok.", "no_devices": "Nem tal\u00e1lhat\u00f3 nem p\u00e1ros\u00edtott eszk\u00f6z" }, diff --git a/homeassistant/components/hue/translations/hu.json b/homeassistant/components/hue/translations/hu.json index 2f04c53163f..a114fc2c890 100644 --- a/homeassistant/components/hue/translations/hu.json +++ b/homeassistant/components/hue/translations/hu.json @@ -22,7 +22,7 @@ "title": "V\u00e1lasszon Hue bridge-t" }, "link": { - "description": "Nyomja meg a gombot a bridge-en a Philips Hue Home Assistant-ben val\u00f3 regisztr\u00e1l\u00e1s\u00e1hoz.\n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)", + "description": "Nyomja meg a gombot a bridge-en a Philips Hue Home Assistantban val\u00f3 regisztr\u00e1l\u00e1s\u00e1hoz.\n\n![Gomb helye](/static/images/config_philips_hue.jpg)", "title": "Kapcsol\u00f3d\u00e1s a hubhoz" }, "manual": { diff --git a/homeassistant/components/kodi/translations/hu.json b/homeassistant/components/kodi/translations/hu.json index 017d33010ac..e561bd5d6a4 100644 --- a/homeassistant/components/kodi/translations/hu.json +++ b/homeassistant/components/kodi/translations/hu.json @@ -22,7 +22,7 @@ "description": "Adja meg a Kodi felhaszn\u00e1l\u00f3nevet \u00e9s jelsz\u00f3t. Ezek megtal\u00e1lhat\u00f3k a Rendszer/Be\u00e1ll\u00edt\u00e1sok/H\u00e1l\u00f3zat/Szolg\u00e1ltat\u00e1sok r\u00e9szben." }, "discovery_confirm": { - "description": "Szeretn\u00e9 hozz\u00e1adni a Kodi (`{name}`)-t Home Assistant-hoz?", + "description": "Szeretn\u00e9 hozz\u00e1adni a Kodi (`{name}`)-t Home Assistanthoz?", "title": "Felfedezett Kodi" }, "user": { diff --git a/homeassistant/components/mobile_app/translations/hu.json b/homeassistant/components/mobile_app/translations/hu.json index 1dda8ce7223..a92f84958be 100644 --- a/homeassistant/components/mobile_app/translations/hu.json +++ b/homeassistant/components/mobile_app/translations/hu.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "install_app": "Nyissa meg a mobil alkalmaz\u00e1st Home Assistant-tal val\u00f3 integr\u00e1ci\u00f3hoz. A kompatibilis alkalmaz\u00e1sok list\u00e1j\u00e1nak megtekint\u00e9s\u00e9hez ellen\u0151rizze [a le\u00edr\u00e1st]({apps_url})." + "install_app": "Nyissa meg a mobil alkalmaz\u00e1st Home Assistanttal val\u00f3 integr\u00e1ci\u00f3hoz. A kompatibilis alkalmaz\u00e1sok list\u00e1j\u00e1nak megtekint\u00e9s\u00e9hez ellen\u0151rizze [a le\u00edr\u00e1st]({apps_url})." }, "step": { "confirm": { diff --git a/homeassistant/components/modern_forms/translations/hu.json b/homeassistant/components/modern_forms/translations/hu.json index 5bea7c3054e..49f5da5339f 100644 --- a/homeassistant/components/modern_forms/translations/hu.json +++ b/homeassistant/components/modern_forms/translations/hu.json @@ -16,7 +16,7 @@ "data": { "host": "C\u00edm" }, - "description": "\u00c1ll\u00edtsa be Modern Forms-t, hogy integr\u00e1l\u00f3djon Home Assistant-ba." + "description": "Integr\u00e1lja \u00f6ssze Modern Formst Home Assistanttal." }, "zeroconf_confirm": { "description": "Hozz\u00e1 szeretn\u00e9 adni `{name}`nev\u0171 Modern Forms rajong\u00f3t Home Assistanthoz?", diff --git a/homeassistant/components/motioneye/translations/hu.json b/homeassistant/components/motioneye/translations/hu.json index c381d3954d4..0acc46509a4 100644 --- a/homeassistant/components/motioneye/translations/hu.json +++ b/homeassistant/components/motioneye/translations/hu.json @@ -12,7 +12,7 @@ }, "step": { "hassio_confirm": { - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani Home Assistant-ot, hogy csatlakozzon {addon} kieg\u00e9sz\u00edt\u0151 \u00e1ltal biztos\u00edtott motionEye szolg\u00e1ltat\u00e1shoz?", + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani Home Assistantot motionEyehez val\u00f3 csatlakoz\u00e1shoz, {addon} kieg\u00e9sz\u00edt\u0151 \u00e1ltal?", "title": "motionEye a Home Assistant kieg\u00e9sz\u00edt\u0151 seg\u00edts\u00e9g\u00e9vel" }, "user": { diff --git a/homeassistant/components/mqtt/translations/hu.json b/homeassistant/components/mqtt/translations/hu.json index 471982756eb..9da3c6d9666 100644 --- a/homeassistant/components/mqtt/translations/hu.json +++ b/homeassistant/components/mqtt/translations/hu.json @@ -22,7 +22,7 @@ "data": { "discovery": "Felfedez\u00e9s enged\u00e9lyez\u00e9se" }, - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani Home Assistant-ot MQTT br\u00f3kerhez val\u00f3 csatlakoz\u00e1shoz, {addon} kieg\u00e9sz\u00edt\u0151 \u00e1ltal?", + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani Home Assistantot MQTT br\u00f3kerhez val\u00f3 csatlakoz\u00e1shoz, {addon} kieg\u00e9sz\u00edt\u0151 \u00e1ltal?", "title": "MQTT Br\u00f3ker - Home Assistant kieg\u00e9sz\u00edt\u0151 \u00e1ltal" } } diff --git a/homeassistant/components/roon/translations/hu.json b/homeassistant/components/roon/translations/hu.json index 09bad262c45..7d2b63f0f4b 100644 --- a/homeassistant/components/roon/translations/hu.json +++ b/homeassistant/components/roon/translations/hu.json @@ -9,7 +9,7 @@ }, "step": { "link": { - "description": "Enged\u00e9lyeznie kell az Home Assistant-ot a Roonban. Miut\u00e1n r\u00e1kattintott a K\u00fcld\u00e9s gombra, nyissa meg a Roon Core alkalmaz\u00e1st, nyissa meg a Be\u00e1ll\u00edt\u00e1sokat, \u00e9s enged\u00e9lyezze a Home Assistant funkci\u00f3t a B\u0151v\u00edtm\u00e9nyek lapon.", + "description": "Enged\u00e9lyeznie kell az Home Assistantot a Roonban. Miut\u00e1n r\u00e1kattintott a K\u00fcld\u00e9s gombra, nyissa meg a Roon Core alkalmaz\u00e1st, nyissa meg a Be\u00e1ll\u00edt\u00e1sokat, \u00e9s enged\u00e9lyezze a Home Assistant funkci\u00f3t a B\u0151v\u00edtm\u00e9nyek lapon.", "title": "Enged\u00e9lyezze a Home Assistant alkalmaz\u00e1st Roon-ban" }, "user": { diff --git a/homeassistant/components/samsungtv/translations/hu.json b/homeassistant/components/samsungtv/translations/hu.json index 93c0b2bee6d..efdf5f4810b 100644 --- a/homeassistant/components/samsungtv/translations/hu.json +++ b/homeassistant/components/samsungtv/translations/hu.json @@ -17,7 +17,7 @@ "flow_title": "{device}", "step": { "confirm": { - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani {device} k\u00e9sz\u00fcl\u00e9k\u00e9t? Ha kor\u00e1bban m\u00e9g sosem csatlakoztatta Home Assistant-hoz, akkor meg kell jelennie egy felugr\u00f3 ablaknak a TV k\u00e9perny\u0151j\u00e9n, ami j\u00f3v\u00e1hagy\u00e1sra v\u00e1r.", + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani {device} k\u00e9sz\u00fcl\u00e9k\u00e9t? Ha kor\u00e1bban m\u00e9g sosem csatlakoztatta Home Assistanthoz, akkor meg kell jelennie egy felugr\u00f3 ablaknak a TV k\u00e9perny\u0151j\u00e9n, ami j\u00f3v\u00e1hagy\u00e1sra v\u00e1r.", "title": "Samsung TV" }, "reauth_confirm": { @@ -28,7 +28,7 @@ "host": "C\u00edm", "name": "N\u00e9v" }, - "description": "\u00cdrd be a Samsung TV adatait. Ha m\u00e9g soha nem csatlakozott Home Assistant-hez, akkor meg kell jelennie egy felugr\u00f3 ablaknak a TV k\u00e9perny\u0151j\u00e9n, ahol hiteles\u00edt\u00e9st k\u00e9r." + "description": "\u00cdrja be a Samsung TV adatait. Ha m\u00e9g soha nem csatlakozott Home Assistanthoz, akkor meg kell jelennie egy felugr\u00f3 ablaknak a TV k\u00e9perny\u0151j\u00e9n, ahol meg kell adni az enged\u00e9lyt." } } } diff --git a/homeassistant/components/smappee/translations/hu.json b/homeassistant/components/smappee/translations/hu.json index 5b4a83a74b0..9c3d90ac43e 100644 --- a/homeassistant/components/smappee/translations/hu.json +++ b/homeassistant/components/smappee/translations/hu.json @@ -15,7 +15,7 @@ "data": { "environment": "K\u00f6rnyezet" }, - "description": "\u00c1ll\u00edtsa be a Smappee k\u00e9sz\u00fcl\u00e9ket az HomeAssistant-al val\u00f3 integr\u00e1ci\u00f3hoz." + "description": "Integr\u00e1lja \u00f6ssze Smappee k\u00e9sz\u00fcl\u00e9ket HomeAssistanttal." }, "local": { "data": { diff --git a/homeassistant/components/tuya/translations/en.json b/homeassistant/components/tuya/translations/en.json index a3630e66406..4b4b9a6d1dd 100644 --- a/homeassistant/components/tuya/translations/en.json +++ b/homeassistant/components/tuya/translations/en.json @@ -1,19 +1,82 @@ { "config": { + "abort": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, "error": { "invalid_auth": "Invalid authentication", "login_error": "Login error ({code}): {msg}" }, + "flow_title": "Tuya configuration", "step": { + "login": { + "data": { + "access_id": "Access ID", + "access_secret": "Access Secret", + "country_code": "Country Code", + "endpoint": "Availability Zone", + "password": "Password", + "tuya_app_type": "Mobile App", + "username": "Account" + }, + "description": "Enter your Tuya credential", + "title": "Tuya" + }, "user": { "data": { "access_id": "Tuya IoT Access ID", "access_secret": "Tuya IoT Access Secret", + "country_code": "Your account country code (e.g., 1 for USA or 86 for China)", "password": "Password", + "platform": "The app where your account is registered", "region": "Region", + "tuya_project_type": "Tuya cloud project type", "username": "Account" }, - "description": "Enter your Tuya credentials" + "description": "Enter your Tuya credentials", + "title": "Tuya Integration" + } + } + }, + "options": { + "abort": { + "cannot_connect": "Failed to connect" + }, + "error": { + "dev_multi_type": "Multiple selected devices to configure must be of the same type", + "dev_not_config": "Device type not configurable", + "dev_not_found": "Device not found" + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "Brightness range used by device", + "curr_temp_divider": "Current Temperature value divider (0 = use default)", + "max_kelvin": "Max color temperature supported in kelvin", + "max_temp": "Max target temperature (use min and max = 0 for default)", + "min_kelvin": "Min color temperature supported in kelvin", + "min_temp": "Min target temperature (use min and max = 0 for default)", + "set_temp_divided": "Use divided Temperature value for set temperature command", + "support_color": "Force color support", + "temp_divider": "Temperature values divider (0 = use default)", + "temp_step_override": "Target Temperature step", + "tuya_max_coltemp": "Max color temperature reported by device", + "unit_of_measurement": "Temperature unit used by device" + }, + "description": "Configure options to adjust displayed information for {device_type} device `{device_name}`", + "title": "Configure Tuya Device" + }, + "init": { + "data": { + "discovery_interval": "Discovery device polling interval in seconds", + "list_devices": "Select the devices to configure or leave empty to save configuration", + "query_device": "Select device that will use query method for faster status update", + "query_interval": "Query device polling interval in seconds" + }, + "description": "Do not set pollings interval values too low or the calls will fail generating error message in the log", + "title": "Configure Tuya Options" } } } diff --git a/homeassistant/components/tuya/translations/nl.json b/homeassistant/components/tuya/translations/nl.json index be405db1a08..22a63800fd6 100644 --- a/homeassistant/components/tuya/translations/nl.json +++ b/homeassistant/components/tuya/translations/nl.json @@ -13,6 +13,7 @@ "login": { "data": { "access_id": "Toegangs-ID", + "access_secret": "Access Secret", "country_code": "Landcode", "endpoint": "Beschikbaarheidszone", "password": "Wachtwoord", diff --git a/homeassistant/components/tuya/translations/no.json b/homeassistant/components/tuya/translations/no.json index eedf24be696..b5fe4bc1851 100644 --- a/homeassistant/components/tuya/translations/no.json +++ b/homeassistant/components/tuya/translations/no.json @@ -10,15 +10,29 @@ }, "flow_title": "Tuya konfigurasjon", "step": { + "login": { + "data": { + "access_id": "Tilgangs -ID", + "access_secret": "Tilgangshemmelighet", + "country_code": "Landskode", + "endpoint": "Tilgjengelighetssone", + "password": "Passord", + "tuya_app_type": "Mobilapp", + "username": "Konto" + }, + "description": "Skriv inn Tuya-legitimasjonen din", + "title": "Tuya" + }, "user": { "data": { "country_code": "Din landskode for kontoen din (f.eks. 1 for USA eller 86 for Kina)", "password": "Passord", "platform": "Appen der kontoen din er registrert", + "tuya_project_type": "Tuya -skyprosjekttype", "username": "Brukernavn" }, "description": "Angi Tuya-legitimasjonen din.", - "title": "" + "title": "Tuya Integrasjon" } } }, diff --git a/homeassistant/components/vera/translations/hu.json b/homeassistant/components/vera/translations/hu.json index 4e9639b0258..d1d4910c97a 100644 --- a/homeassistant/components/vera/translations/hu.json +++ b/homeassistant/components/vera/translations/hu.json @@ -6,7 +6,7 @@ "step": { "user": { "data": { - "exclude": "Vera eszk\u00f6zazonos\u00edt\u00f3k kiz\u00e1r\u00e1sa Home Assistant-b\u00f3l.", + "exclude": "Vera eszk\u00f6zazonos\u00edt\u00f3k kiz\u00e1r\u00e1sa Home Assistantb\u00f3l.", "lights": "A Vera kapcsol\u00f3eszk\u00f6z-azonos\u00edt\u00f3k f\u00e9nyk\u00e9nt kezelhet\u0151k a Home Assistant alkalmaz\u00e1sban.", "vera_controller_url": "Vez\u00e9rl\u0151 URL" }, @@ -19,7 +19,7 @@ "step": { "init": { "data": { - "exclude": "Vera eszk\u00f6zazonos\u00edt\u00f3k kiz\u00e1r\u00e1sa Home Assistant-b\u00f3l.", + "exclude": "Vera eszk\u00f6zazonos\u00edt\u00f3k kiz\u00e1r\u00e1sa Home Assistantb\u00f3l.", "lights": "A Vera kapcsol\u00f3eszk\u00f6z-azonos\u00edt\u00f3k f\u00e9nyk\u00e9nt kezelhet\u0151k a Home Assistant alkalmaz\u00e1sban." }, "description": "Az opcion\u00e1lis param\u00e9terekr\u0151l a vera dokument\u00e1ci\u00f3j\u00e1ban olvashat: https://www.home-assistant.io/integrations/vera/. Megjegyz\u00e9s: Az itt v\u00e9grehajtott v\u00e1ltoztat\u00e1sokhoz \u00fajra kell ind\u00edtani a h\u00e1zi asszisztens szervert. Az \u00e9rt\u00e9kek t\u00f6rl\u00e9s\u00e9hez adjon meg egy sz\u00f3k\u00f6zt.", diff --git a/homeassistant/components/vizio/translations/hu.json b/homeassistant/components/vizio/translations/hu.json index 3708bfbc379..bb619e359c0 100644 --- a/homeassistant/components/vizio/translations/hu.json +++ b/homeassistant/components/vizio/translations/hu.json @@ -19,7 +19,7 @@ "title": "V\u00e9gezze el a p\u00e1ros\u00edt\u00e1si folyamatot" }, "pairing_complete": { - "description": "A VIZIO SmartCast Eszk\u00f6z mostant\u00f3l csatlakozik Home Assistant-hoz.", + "description": "A VIZIO SmartCast Eszk\u00f6z mostant\u00f3l csatlakozik Home Assistanthoz.", "title": "P\u00e1ros\u00edt\u00e1s k\u00e9sz" }, "pairing_complete_import": { diff --git a/homeassistant/components/volumio/translations/hu.json b/homeassistant/components/volumio/translations/hu.json index 209a892af3d..b504275e03f 100644 --- a/homeassistant/components/volumio/translations/hu.json +++ b/homeassistant/components/volumio/translations/hu.json @@ -10,7 +10,7 @@ }, "step": { "discovery_confirm": { - "description": "Szeretn\u00e9 hozz\u00e1adni a Volumio (`{name}`)-t a Home Assistant-hoz?", + "description": "Szeretn\u00e9 hozz\u00e1adni a Volumio (`{name}`)-t a Home Assistanthoz?", "title": "Felfedezett Volumio" }, "user": { diff --git a/homeassistant/components/wled/translations/hu.json b/homeassistant/components/wled/translations/hu.json index 2e0ac08d3cb..1fa29cfee48 100644 --- a/homeassistant/components/wled/translations/hu.json +++ b/homeassistant/components/wled/translations/hu.json @@ -13,10 +13,10 @@ "data": { "host": "C\u00edm" }, - "description": "\u00c1ll\u00edtsa be a WLED-et Home Assistant-ba val\u00f3 integr\u00e1l\u00e1shoz." + "description": "\u00c1ll\u00edtsa be a WLED-et Home Assistantba val\u00f3 integr\u00e1l\u00e1shoz." }, "zeroconf_confirm": { - "description": "Szeretn\u00e9 hozz\u00e1adni a(z) `{name}` WLED-et Home Assistant-hoz?", + "description": "Szeretn\u00e9 hozz\u00e1adni a(z) `{name}` WLED-et Home Assistanthoz?", "title": "Felfedezett WLED eszk\u00f6z" } } diff --git a/homeassistant/components/zwave_js/translations/nl.json b/homeassistant/components/zwave_js/translations/nl.json index b7a3a68fe6b..76718aa5346 100644 --- a/homeassistant/components/zwave_js/translations/nl.json +++ b/homeassistant/components/zwave_js/translations/nl.json @@ -27,6 +27,10 @@ "configure_addon": { "data": { "network_key": "Netwerksleutel", + "s0_legacy_key": "S0 Sleutel (Legacy)", + "s2_access_control_key": "S2 Toegangscontrolesleutel", + "s2_authenticated_key": "S2 geverifieerde sleutel", + "s2_unauthenticated_key": "S2 niet-geverifieerde sleutel", "usb_path": "USB-apparaatpad" }, "title": "Voer de Z-Wave JS add-on configuratie in" @@ -109,6 +113,10 @@ "emulate_hardware": "Emulate Hardware", "log_level": "Log level", "network_key": "Netwerksleutel", + "s0_legacy_key": "S0 Sleutel (Legacy)", + "s2_access_control_key": "S2 Toegangscontrolesleutel", + "s2_authenticated_key": "S2 geverifieerde sleutel", + "s2_unauthenticated_key": "S2 niet-geverifieerde sleutel", "usb_path": "USB-apparaatpad" }, "title": "Voer de configuratie van de Z-Wave JS-add-on in" diff --git a/homeassistant/components/zwave_js/translations/no.json b/homeassistant/components/zwave_js/translations/no.json index 9ddf12a3b85..f08f5bd07cb 100644 --- a/homeassistant/components/zwave_js/translations/no.json +++ b/homeassistant/components/zwave_js/translations/no.json @@ -27,6 +27,10 @@ "configure_addon": { "data": { "network_key": "Nettverksn\u00f8kkel", + "s0_legacy_key": "S0-n\u00f8kkel (eldre)", + "s2_access_control_key": "N\u00f8kkel for S2-tilgangskontroll", + "s2_authenticated_key": "S2 Autentisert n\u00f8kkel", + "s2_unauthenticated_key": "S2 Uautentisert n\u00f8kkel", "usb_path": "USB enhetsbane" }, "title": "Angi konfigurasjon for Z-Wave JS-tillegg" @@ -109,6 +113,10 @@ "emulate_hardware": "Emuler maskinvare", "log_level": "Loggniv\u00e5", "network_key": "Nettverksn\u00f8kkel", + "s0_legacy_key": "S0-n\u00f8kkel (eldre)", + "s2_access_control_key": "N\u00f8kkel for S2-tilgangskontroll", + "s2_authenticated_key": "S2 Autentisert n\u00f8kkel", + "s2_unauthenticated_key": "S2 Uautentisert n\u00f8kkel", "usb_path": "USB enhetsbane" }, "title": "Angi konfigurasjon for Z-Wave JS-tillegg" From 2f9943fe7a63889a0eeba4693e76cd01eaac2f78 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 4 Oct 2021 17:41:51 +0200 Subject: [PATCH 0100/1038] Use NamedTuple for repetier API methods (#56941) --- homeassistant/components/repetier/__init__.py | 90 ++++++++++--------- 1 file changed, 50 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/repetier/__init__.py b/homeassistant/components/repetier/__init__.py index 6a47f7bbdf5..a71038ffcb8 100644 --- a/homeassistant/components/repetier/__init__.py +++ b/homeassistant/components/repetier/__init__.py @@ -37,28 +37,38 @@ UPDATE_SIGNAL = "repetier_update_signal" TEMP_DATA = {"tempset": "temp_set", "tempread": "state", "output": "output"} -API_PRINTER_METHODS = { - "bed_temperature": { - "offline": {"heatedbeds": None, "state": "off"}, - "state": {"heatedbeds": "temp_data"}, - "temp_data": TEMP_DATA, - "attribute": "heatedbeds", - }, - "extruder_temperature": { - "offline": {"extruder": None, "state": "off"}, - "state": {"extruder": "temp_data"}, - "temp_data": TEMP_DATA, - "attribute": "extruder", - }, - "chamber_temperature": { - "offline": {"heatedchambers": None, "state": "off"}, - "state": {"heatedchambers": "temp_data"}, - "temp_data": TEMP_DATA, - "attribute": "heatedchambers", - }, - "current_state": { - "offline": {"state": None}, - "state": { +@dataclass +class APIMethods: + """API methods for properties.""" + + offline: dict[str, str | None] + state: dict[str, str] + temp_data: dict[str, str] | None = None + attribute: str | None = None + + +API_PRINTER_METHODS: dict[str, APIMethods] = { + "bed_temperature": APIMethods( + offline={"heatedbeds": None, "state": "off"}, + state={"heatedbeds": "temp_data"}, + temp_data=TEMP_DATA, + attribute="heatedbeds", + ), + "extruder_temperature": APIMethods( + offline={"extruder": None, "state": "off"}, + state={"extruder": "temp_data"}, + temp_data=TEMP_DATA, + attribute="extruder", + ), + "chamber_temperature": APIMethods( + offline={"heatedchambers": None, "state": "off"}, + state={"heatedchambers": "temp_data"}, + temp_data=TEMP_DATA, + attribute="heatedchambers", + ), + "current_state": APIMethods( + offline={"state": None}, + state={ "state": "state", "activeextruder": "active_extruder", "hasxhome": "x_homed", @@ -67,10 +77,10 @@ API_PRINTER_METHODS = { "firmware": "firmware", "firmwareurl": "firmware_url", }, - }, - "current_job": { - "offline": {"job": None, "state": "off"}, - "state": { + ), + "current_job": APIMethods( + offline={"job": None, "state": "off"}, + state={ "done": "state", "job": "job_name", "jobid": "job_id", @@ -84,25 +94,25 @@ API_PRINTER_METHODS = { "y": "y", "z": "z", }, - }, - "job_end": { - "offline": {"job": None, "state": "off", "start": None, "printtime": None}, - "state": { + ), + "job_end": APIMethods( + offline={"job": None, "state": "off", "start": None, "printtime": None}, + state={ "job": "job_name", "start": "start", "printtime": "print_time", "printedtimecomp": "from_start", }, - }, - "job_start": { - "offline": { + ), + "job_start": APIMethods( + offline={ "job": None, "state": "off", "start": None, "printedtimecomp": None, }, - "state": {"job": "job_name", "start": "start", "printedtimecomp": "from_start"}, - }, + state={"job": "job_name", "start": "start", "printedtimecomp": "from_start"}, + ), } @@ -251,17 +261,17 @@ class PrinterAPI: """Get data from the state cache.""" printer = self.printers[printer_id] methods = API_PRINTER_METHODS[sensor_type] - for prop, offline in methods["offline"].items(): + for prop, offline in methods.offline.items(): state = getattr(printer, prop) if state == offline: # if state matches offline, sensor is offline return None data = {} - for prop, attr in methods["state"].items(): + for prop, attr in methods.state.items(): prop_data = getattr(printer, prop) if attr == "temp_data": - temp_methods = methods["temp_data"] + temp_methods = methods.temp_data or {} for temp_prop, temp_attr in temp_methods.items(): data[temp_attr] = getattr(prop_data[temp_id], temp_prop) else: @@ -290,8 +300,8 @@ class PrinterAPI: continue methods = API_PRINTER_METHODS[sensor_type] - if "temp_data" in methods["state"].values(): - prop_data = getattr(printer, methods["attribute"]) + if "temp_data" in methods.state.values(): + prop_data = getattr(printer, methods.attribute or "") if prop_data is None: continue for idx, _ in enumerate(prop_data): From 69875cbd11ecd064861cac955a60e63579b39537 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Oct 2021 18:47:44 +0200 Subject: [PATCH 0101/1038] Improve sensor statistics validation (#56892) --- homeassistant/components/sensor/recorder.py | 59 ++- .../components/recorder/test_websocket_api.py | 202 +------- tests/components/sensor/test_recorder.py | 437 +++++++++++++++++- 3 files changed, 486 insertions(+), 212 deletions(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index fd6cf5e0f2f..c485622af80 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -637,35 +637,70 @@ def validate_statistics( """Validate statistics.""" validation_result = defaultdict(list) - sensor_states = _get_sensor_states(hass) + sensor_states = hass.states.all(DOMAIN) + metadatas = statistics.get_metadata(hass, [i.entity_id for i in sensor_states]) for state in sensor_states: entity_id = state.entity_id device_class = state.attributes.get(ATTR_DEVICE_CLASS) + state_class = state.attributes.get(ATTR_STATE_CLASS) state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - if device_class not in UNIT_CONVERSIONS: - metadata = statistics.get_metadata(hass, (entity_id,)) - if not metadata: - continue - metadata_unit = metadata[entity_id][1]["unit_of_measurement"] - if state_unit != metadata_unit: + if metadata := metadatas.get(entity_id): + if not is_entity_recorded(hass, state.entity_id): + # Sensor was previously recorded, but no longer is validation_result[entity_id].append( statistics.ValidationIssue( - "units_changed", + "entity_not_recorded", + {"statistic_id": entity_id}, + ) + ) + + if state_class not in STATE_CLASSES: + # Sensor no longer has a valid state class + validation_result[entity_id].append( + statistics.ValidationIssue( + "unsupported_state_class", + {"statistic_id": entity_id, "state_class": state_class}, + ) + ) + + metadata_unit = metadata[1]["unit_of_measurement"] + if device_class not in UNIT_CONVERSIONS: + if state_unit != metadata_unit: + # The unit has changed + validation_result[entity_id].append( + statistics.ValidationIssue( + "units_changed", + { + "statistic_id": entity_id, + "state_unit": state_unit, + "metadata_unit": metadata_unit, + }, + ) + ) + elif metadata_unit != DEVICE_CLASS_UNITS[device_class]: + # The unit in metadata is not supported for this device class + validation_result[entity_id].append( + statistics.ValidationIssue( + "unsupported_unit_metadata", { "statistic_id": entity_id, - "state_unit": state_unit, + "device_class": device_class, "metadata_unit": metadata_unit, + "supported_unit": DEVICE_CLASS_UNITS[device_class], }, ) ) - continue - if state_unit not in UNIT_CONVERSIONS[device_class]: + if ( + device_class in UNIT_CONVERSIONS + and state_unit not in UNIT_CONVERSIONS[device_class] + ): + # The unit in the state is not supported for this device class validation_result[entity_id].append( statistics.ValidationIssue( - "unsupported_unit", + "unsupported_unit_state", { "statistic_id": entity_id, "device_class": device_class, diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 4f54a43ca6e..e60659aaab2 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -6,34 +6,19 @@ import pytest from pytest import approx from homeassistant.components.recorder.const import DATA_INSTANCE -from homeassistant.components.recorder.models import StatisticsMeta -from homeassistant.components.recorder.util import session_scope from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM +from homeassistant.util.unit_system import METRIC_SYSTEM from .common import trigger_db_commit from tests.common import init_recorder_component -BATTERY_SENSOR_ATTRIBUTES = { - "device_class": "battery", - "state_class": "measurement", - "unit_of_measurement": "%", -} POWER_SENSOR_ATTRIBUTES = { "device_class": "power", "state_class": "measurement", "unit_of_measurement": "kW", } -NONE_SENSOR_ATTRIBUTES = { - "state_class": "measurement", -} -PRESSURE_SENSOR_ATTRIBUTES = { - "device_class": "pressure", - "state_class": "measurement", - "unit_of_measurement": "hPa", -} TEMPERATURE_SENSOR_ATTRIBUTES = { "device_class": "temperature", "state_class": "measurement", @@ -41,21 +26,8 @@ TEMPERATURE_SENSOR_ATTRIBUTES = { } -@pytest.mark.parametrize( - "units, attributes, 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"), - ], -) -async def test_validate_statistics_supported_device_class( - hass, hass_ws_client, units, attributes, unit -): - """Test list_statistic_ids.""" +async def test_validate_statistics(hass, hass_ws_client): + """Test validate_statistics can be called.""" id = 1 def next_id(): @@ -71,177 +43,9 @@ async def test_validate_statistics_supported_device_class( assert response["success"] assert response["result"] == expected_result - now = dt_util.utcnow() - - hass.config.units = units - await hass.async_add_executor_job(init_recorder_component, hass) - await async_setup_component(hass, "sensor", {}) - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) - client = await hass_ws_client() - # No statistics, no state - empty response - await assert_validation_result(client, {}) - - # No statistics, valid state - empty response - hass.states.async_set( - "sensor.test", 10, attributes={**attributes, **{"unit_of_measurement": unit}} - ) - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) - await assert_validation_result(client, {}) - - # No statistics, invalid state - expect error - hass.states.async_set( - "sensor.test", 11, attributes={**attributes, **{"unit_of_measurement": "dogs"}} - ) - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) - expected = { - "sensor.test": [ - { - "data": { - "device_class": attributes["device_class"], - "state_unit": "dogs", - "statistic_id": "sensor.test", - }, - "type": "unsupported_unit", - } - ], - } - await assert_validation_result(client, expected) - - # Statistics has run, invalid state - expect error - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) - hass.data[DATA_INSTANCE].do_adhoc_statistics(start=now) - hass.states.async_set( - "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "dogs"}} - ) - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) - await assert_validation_result(client, expected) - - # Valid state - empty response - hass.states.async_set( - "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": unit}} - ) - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) - await assert_validation_result(client, {}) - - # Valid state, statistic runs again - empty response - hass.data[DATA_INSTANCE].do_adhoc_statistics(start=now) - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) - await assert_validation_result(client, {}) - - # Remove the state - empty response - hass.states.async_remove("sensor.test") - await assert_validation_result(client, {}) - - -@pytest.mark.parametrize( - "attributes", - [BATTERY_SENSOR_ATTRIBUTES, NONE_SENSOR_ATTRIBUTES], -) -async def test_validate_statistics_unsupported_device_class( - hass, hass_ws_client, attributes -): - """Test list_statistic_ids.""" - id = 1 - - def next_id(): - nonlocal id - id += 1 - return id - - async def assert_validation_result(client, expected_result): - await client.send_json( - {"id": next_id(), "type": "recorder/validate_statistics"} - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == expected_result - - async def assert_statistic_ids(expected_result): - with session_scope(hass=hass) as session: - db_states = list(session.query(StatisticsMeta)) - assert len(db_states) == len(expected_result) - for i in range(len(db_states)): - assert db_states[i].statistic_id == expected_result[i]["statistic_id"] - assert ( - db_states[i].unit_of_measurement - == expected_result[i]["unit_of_measurement"] - ) - - now = dt_util.utcnow() - await hass.async_add_executor_job(init_recorder_component, hass) - await async_setup_component(hass, "sensor", {}) - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) client = await hass_ws_client() - rec = hass.data[DATA_INSTANCE] - - # No statistics, no state - empty response - await assert_validation_result(client, {}) - - # No statistics, original unit - empty response - hass.states.async_set("sensor.test", 10, attributes=attributes) - await assert_validation_result(client, {}) - - # No statistics, changed unit - empty response - hass.states.async_set( - "sensor.test", 11, attributes={**attributes, **{"unit_of_measurement": "dogs"}} - ) - await assert_validation_result(client, {}) - - # Run statistics, no statistics will be generated because of conflicting units - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) - rec.do_adhoc_statistics(start=now) - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) - await assert_statistic_ids([]) - - # No statistics, changed unit - empty response - hass.states.async_set( - "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "dogs"}} - ) - await assert_validation_result(client, {}) - - # Run statistics one hour later, only the "dogs" state will be considered - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) - rec.do_adhoc_statistics(start=now + timedelta(hours=1)) - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) - await assert_statistic_ids( - [{"statistic_id": "sensor.test", "unit_of_measurement": "dogs"}] - ) - await assert_validation_result(client, {}) - - # Change back to original unit - expect error - hass.states.async_set("sensor.test", 13, attributes=attributes) - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) - expected = { - "sensor.test": [ - { - "data": { - "metadata_unit": "dogs", - "state_unit": attributes.get("unit_of_measurement"), - "statistic_id": "sensor.test", - }, - "type": "units_changed", - } - ], - } - await assert_validation_result(client, expected) - - # Changed unit - empty response - hass.states.async_set( - "sensor.test", 14, attributes={**attributes, **{"unit_of_measurement": "dogs"}} - ) - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) - await assert_validation_result(client, {}) - - # Valid state, statistic runs again - empty response - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) - hass.data[DATA_INSTANCE].do_adhoc_statistics(start=now) - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) - await assert_validation_result(client, {}) - - # Remove the state - empty response - hass.states.async_remove("sensor.test") await assert_validation_result(client, {}) diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index f13dc5084bb..9ae4b467da5 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -11,24 +11,37 @@ from pytest import approx from homeassistant import loader from homeassistant.components.recorder import history from homeassistant.components.recorder.const import DATA_INSTANCE -from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat +from homeassistant.components.recorder.models import ( + StatisticsMeta, + process_timestamp_to_utc_isoformat, +) from homeassistant.components.recorder.statistics import ( get_metadata, list_statistic_ids, statistics_during_period, ) +from homeassistant.components.recorder.util import session_scope from homeassistant.const import STATE_UNAVAILABLE from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM +from tests.common import async_setup_component, init_recorder_component from tests.components.recorder.common import wait_recording_done +BATTERY_SENSOR_ATTRIBUTES = { + "device_class": "battery", + "state_class": "measurement", + "unit_of_measurement": "%", +} ENERGY_SENSOR_ATTRIBUTES = { "device_class": "energy", "state_class": "measurement", "unit_of_measurement": "kWh", } +NONE_SENSOR_ATTRIBUTES = { + "state_class": "measurement", +} POWER_SENSOR_ATTRIBUTES = { "device_class": "power", "state_class": "measurement", @@ -2080,6 +2093,428 @@ def record_states(hass, zero, entity_id, attributes, seq=None): return four, states +@pytest.mark.parametrize( + "units, attributes, 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"), + ], +) +async def test_validate_statistics_supported_device_class( + hass, hass_ws_client, units, attributes, unit +): + """Test validate_statistics.""" + id = 1 + + def next_id(): + nonlocal id + id += 1 + return id + + async def assert_validation_result(client, expected_result): + await client.send_json( + {"id": next_id(), "type": "recorder/validate_statistics"} + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == expected_result + + now = dt_util.utcnow() + + hass.config.units = units + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "sensor", {}) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + client = await hass_ws_client() + + # No statistics, no state - empty response + await assert_validation_result(client, {}) + + # No statistics, valid state - empty response + hass.states.async_set( + "sensor.test", 10, attributes={**attributes, **{"unit_of_measurement": unit}} + ) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + await assert_validation_result(client, {}) + + # No statistics, invalid state - expect error + hass.states.async_set( + "sensor.test", 11, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + ) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + expected = { + "sensor.test": [ + { + "data": { + "device_class": attributes["device_class"], + "state_unit": "dogs", + "statistic_id": "sensor.test", + }, + "type": "unsupported_unit_state", + } + ], + } + await assert_validation_result(client, expected) + + # Statistics has run, invalid state - expect error + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + hass.data[DATA_INSTANCE].do_adhoc_statistics(start=now) + hass.states.async_set( + "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + ) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + await assert_validation_result(client, expected) + + # Valid state - empty response + hass.states.async_set( + "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": unit}} + ) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + await assert_validation_result(client, {}) + + # Valid state, statistic runs again - empty response + hass.data[DATA_INSTANCE].do_adhoc_statistics(start=now) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + await assert_validation_result(client, {}) + + # Remove the state - empty response + hass.states.async_remove("sensor.test") + await assert_validation_result(client, {}) + + +@pytest.mark.parametrize( + "units, attributes, unit", + [ + (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"), + ], +) +async def test_validate_statistics_supported_device_class_2( + hass, hass_ws_client, units, attributes, unit +): + """Test validate_statistics.""" + id = 1 + + def next_id(): + nonlocal id + id += 1 + return id + + async def assert_validation_result(client, expected_result): + await client.send_json( + {"id": next_id(), "type": "recorder/validate_statistics"} + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == expected_result + + now = dt_util.utcnow() + + hass.config.units = units + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "sensor", {}) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + client = await hass_ws_client() + + # No statistics, no state - empty response + await assert_validation_result(client, {}) + + # No statistics, valid state - empty response + initial_attributes = {"state_class": "measurement"} + hass.states.async_set("sensor.test", 10, attributes=initial_attributes) + await hass.async_block_till_done() + await assert_validation_result(client, {}) + + # Statistics has run, device class set - expect error + hass.data[DATA_INSTANCE].do_adhoc_statistics(start=now) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + hass.states.async_set("sensor.test", 12, attributes=attributes) + await hass.async_block_till_done() + expected = { + "sensor.test": [ + { + "data": { + "device_class": attributes["device_class"], + "metadata_unit": None, + "statistic_id": "sensor.test", + "supported_unit": unit, + }, + "type": "unsupported_unit_metadata", + } + ], + } + await assert_validation_result(client, expected) + + # Invalid state too, expect double errors + hass.states.async_set( + "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + ) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + expected = { + "sensor.test": [ + { + "data": { + "device_class": attributes["device_class"], + "metadata_unit": None, + "statistic_id": "sensor.test", + "supported_unit": unit, + }, + "type": "unsupported_unit_metadata", + }, + { + "data": { + "device_class": attributes["device_class"], + "state_unit": "dogs", + "statistic_id": "sensor.test", + }, + "type": "unsupported_unit_state", + }, + ], + } + await assert_validation_result(client, expected) + + +@pytest.mark.parametrize( + "units, attributes, unit", + [ + (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"), + ], +) +async def test_validate_statistics_unsupported_state_class( + hass, hass_ws_client, units, attributes, unit +): + """Test validate_statistics.""" + id = 1 + + def next_id(): + nonlocal id + id += 1 + return id + + async def assert_validation_result(client, expected_result): + await client.send_json( + {"id": next_id(), "type": "recorder/validate_statistics"} + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == expected_result + + now = dt_util.utcnow() + + hass.config.units = units + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "sensor", {}) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + client = await hass_ws_client() + + # No statistics, no state - empty response + await assert_validation_result(client, {}) + + # No statistics, valid state - empty response + hass.states.async_set("sensor.test", 10, attributes=attributes) + await hass.async_block_till_done() + await assert_validation_result(client, {}) + + # Statistics has run, empty response + hass.data[DATA_INSTANCE].do_adhoc_statistics(start=now) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + await assert_validation_result(client, {}) + + # State update with invalid state class, expect error + _attributes = dict(attributes) + _attributes.pop("state_class") + hass.states.async_set("sensor.test", 12, attributes=_attributes) + await hass.async_block_till_done() + expected = { + "sensor.test": [ + { + "data": { + "state_class": None, + "statistic_id": "sensor.test", + }, + "type": "unsupported_state_class", + } + ], + } + await assert_validation_result(client, expected) + + +@pytest.mark.parametrize( + "units, attributes, unit", + [ + (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"), + ], +) +async def test_validate_statistics_sensor_not_recorded( + hass, hass_ws_client, units, attributes, unit +): + """Test validate_statistics.""" + id = 1 + + def next_id(): + nonlocal id + id += 1 + return id + + async def assert_validation_result(client, expected_result): + await client.send_json( + {"id": next_id(), "type": "recorder/validate_statistics"} + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == expected_result + + now = dt_util.utcnow() + + hass.config.units = units + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "sensor", {}) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + client = await hass_ws_client() + + # No statistics, no state - empty response + await assert_validation_result(client, {}) + + # No statistics, valid state - empty response + hass.states.async_set("sensor.test", 10, attributes=attributes) + await hass.async_block_till_done() + await assert_validation_result(client, {}) + + # Statistics has run, empty response + hass.data[DATA_INSTANCE].do_adhoc_statistics(start=now) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + await assert_validation_result(client, {}) + + # Sensor no longer recorded, expect error + expected = { + "sensor.test": [ + { + "data": {"statistic_id": "sensor.test"}, + "type": "entity_not_recorded", + } + ], + } + with patch( + "homeassistant.components.sensor.recorder.is_entity_recorded", + return_value=False, + ): + await assert_validation_result(client, expected) + + +@pytest.mark.parametrize( + "attributes", + [BATTERY_SENSOR_ATTRIBUTES, NONE_SENSOR_ATTRIBUTES], +) +async def test_validate_statistics_unsupported_device_class( + hass, hass_ws_client, attributes +): + """Test validate_statistics.""" + id = 1 + + def next_id(): + nonlocal id + id += 1 + return id + + async def assert_validation_result(client, expected_result): + await client.send_json( + {"id": next_id(), "type": "recorder/validate_statistics"} + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == expected_result + + async def assert_statistic_ids(expected_result): + with session_scope(hass=hass) as session: + db_states = list(session.query(StatisticsMeta)) + assert len(db_states) == len(expected_result) + for i in range(len(db_states)): + assert db_states[i].statistic_id == expected_result[i]["statistic_id"] + assert ( + db_states[i].unit_of_measurement + == expected_result[i]["unit_of_measurement"] + ) + + now = dt_util.utcnow() + + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "sensor", {}) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + client = await hass_ws_client() + rec = hass.data[DATA_INSTANCE] + + # No statistics, no state - empty response + await assert_validation_result(client, {}) + + # No statistics, original unit - empty response + hass.states.async_set("sensor.test", 10, attributes=attributes) + await assert_validation_result(client, {}) + + # No statistics, changed unit - empty response + hass.states.async_set( + "sensor.test", 11, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + ) + await assert_validation_result(client, {}) + + # Run statistics, no statistics will be generated because of conflicting units + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + rec.do_adhoc_statistics(start=now) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + await assert_statistic_ids([]) + + # No statistics, changed unit - empty response + hass.states.async_set( + "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + ) + await assert_validation_result(client, {}) + + # Run statistics one hour later, only the "dogs" state will be considered + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + rec.do_adhoc_statistics(start=now + timedelta(hours=1)) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + await assert_statistic_ids( + [{"statistic_id": "sensor.test", "unit_of_measurement": "dogs"}] + ) + await assert_validation_result(client, {}) + + # Change back to original unit - expect error + hass.states.async_set("sensor.test", 13, attributes=attributes) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + expected = { + "sensor.test": [ + { + "data": { + "metadata_unit": "dogs", + "state_unit": attributes.get("unit_of_measurement"), + "statistic_id": "sensor.test", + }, + "type": "units_changed", + } + ], + } + await assert_validation_result(client, expected) + + # Changed unit - empty response + hass.states.async_set( + "sensor.test", 14, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + ) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + await assert_validation_result(client, {}) + + # Valid state, statistic runs again - empty response + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + hass.data[DATA_INSTANCE].do_adhoc_statistics(start=now) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + await assert_validation_result(client, {}) + + # Remove the state - empty response + hass.states.async_remove("sensor.test") + await assert_validation_result(client, {}) + + def record_meter_states(hass, zero, entity_id, _attributes, seq): """Record some test states. From 723596076d0e41d925208a68a74a1797b5776155 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 4 Oct 2021 12:57:58 -0400 Subject: [PATCH 0102/1038] Add ZHA HVAC Action sensor (#57021) * WIP * Refactor multi-entity matching Eliminate the notion on primary channel. * Cleanup climate tests * Refactor multi-entity match Remove the "primary channel" in multiple entity matches * Cleanup * Add HVAC Action sensor * Add a "stop_on_match" option for multi entities matches Nominally working HVAC state sensors * Add id_suffix for HVAC action sensor * Fix Zen HVAC action sensor * Pylint --- homeassistant/components/zha/climate.py | 17 ++- .../components/zha/core/discovery.py | 47 ++++--- .../components/zha/core/registries.py | 53 +++++--- homeassistant/components/zha/sensor.py | 125 ++++++++++++++++++ tests/components/zha/test_climate.py | 38 ++++++ tests/components/zha/test_registries.py | 29 +--- tests/components/zha/zha_devices_list.py | 18 +++ 7 files changed, 266 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index 3aa11d85516..e734c9cb415 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -60,8 +60,6 @@ from .core.const import ( from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity -DEPENDENCIES = ["zha"] - ATTR_SYS_MODE = "system_mode" ATTR_RUNNING_MODE = "running_mode" ATTR_SETPT_CHANGE_SRC = "setpoint_change_source" @@ -76,6 +74,7 @@ ATTR_UNOCCP_COOL_SETPT = "unoccupied_cooling_setpoint" STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) +MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, DOMAIN) RUNNING_MODE = {0x00: HVAC_MODE_OFF, 0x03: HVAC_MODE_COOL, 0x04: HVAC_MODE_HEAT} @@ -164,16 +163,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) -@STRICT_MATCH(channel_names=CHANNEL_THERMOSTAT, aux_channels=CHANNEL_FAN) +@MULTI_MATCH(channel_names=CHANNEL_THERMOSTAT, aux_channels=CHANNEL_FAN) class Thermostat(ZhaEntity, ClimateEntity): """Representation of a ZHA Thermostat device.""" DEFAULT_MAX_TEMP = 35 DEFAULT_MIN_TEMP = 7 - _domain = DOMAIN - value_attribute = 0x0000 - def __init__(self, unique_id, zha_device, channels, **kwargs): """Initialize ZHA Thermostat instance.""" super().__init__(unique_id, zha_device, channels, **kwargs) @@ -519,9 +515,10 @@ class Thermostat(ZhaEntity, ClimateEntity): return await handler(enable) -@STRICT_MATCH( +@MULTI_MATCH( channel_names={CHANNEL_THERMOSTAT, "sinope_manufacturer_specific"}, manufacturers="Sinope Technologies", + stop_on_match=True, ) class SinopeTechnologiesThermostat(Thermostat): """Sinope Technologies Thermostat.""" @@ -570,10 +567,11 @@ class SinopeTechnologiesThermostat(Thermostat): return res -@STRICT_MATCH( +@MULTI_MATCH( channel_names=CHANNEL_THERMOSTAT, aux_channels=CHANNEL_FAN, manufacturers="Zen Within", + stop_on_match=True, ) class ZenWithinThermostat(Thermostat): """Zen Within Thermostat implementation.""" @@ -599,11 +597,12 @@ class ZenWithinThermostat(Thermostat): return CURRENT_HVAC_OFF -@STRICT_MATCH( +@MULTI_MATCH( channel_names=CHANNEL_THERMOSTAT, aux_channels=CHANNEL_FAN, manufacturers="Centralite", models="3157100", + stop_on_match=True, ) class CentralitePearl(ZenWithinThermostat): """Centralite Pearl Thermostat implementation.""" diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 4d70c7aea96..43a0186d88b 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -63,8 +63,8 @@ class ProbeEndpoint: def discover_entities(self, channel_pool: zha_typing.ChannelPoolType) -> None: """Process an endpoint on a zigpy device.""" self.discover_by_device_type(channel_pool) - self.discover_by_cluster_id(channel_pool) self.discover_multi_entities(channel_pool) + self.discover_by_cluster_id(channel_pool) @callback def discover_by_device_type(self, channel_pool: zha_typing.ChannelPoolType) -> None: @@ -166,25 +166,42 @@ class ProbeEndpoint: def discover_multi_entities(channel_pool: zha_typing.ChannelPoolType) -> None: """Process an endpoint on and discover multiple entities.""" + ep_profile_id = channel_pool.endpoint.profile_id + ep_device_type = channel_pool.endpoint.device_type + cmpt_by_dev_type = zha_regs.DEVICE_CLASS[ep_profile_id].get(ep_device_type) remaining_channels = channel_pool.unclaimed_channels() - for channel in remaining_channels: - unique_id = f"{channel_pool.unique_id}-{channel.cluster.cluster_id}" - matches, claimed = zha_regs.ZHA_ENTITIES.get_multi_entity( - channel_pool.manufacturer, - channel_pool.model, - channel, - remaining_channels, - ) - if not claimed: - continue + matches, claimed = zha_regs.ZHA_ENTITIES.get_multi_entity( + channel_pool.manufacturer, channel_pool.model, remaining_channels + ) - channel_pool.claim_channels(claimed) - for component, ent_classes_list in matches.items(): - for entity_class in ent_classes_list: + channel_pool.claim_channels(claimed) + for component, ent_n_chan_list in matches.items(): + for entity_and_channel in ent_n_chan_list: + _LOGGER.debug( + "'%s' component -> '%s' using %s", + component, + entity_and_channel.entity_class.__name__, + [ch.name for ch in entity_and_channel.claimed_channel], + ) + for component, ent_n_chan_list in matches.items(): + for entity_and_channel in ent_n_chan_list: + if component == cmpt_by_dev_type: + # for well known device types, like thermostats we'll take only 1st class channel_pool.async_new_entity( - component, entity_class, unique_id, claimed + component, + entity_and_channel.entity_class, + channel_pool.unique_id, + entity_and_channel.claimed_channel, ) + break + first_ch = entity_and_channel.claimed_channel[0] + channel_pool.async_new_entity( + component, + entity_and_channel.entity_class, + f"{channel_pool.unique_id}-{first_ch.cluster.cluster_id}", + entity_and_channel.claimed_channel, + ) def initialize(self, hass: HomeAssistant) -> None: """Update device overrides config.""" diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 2bf324e3007..1e41c313836 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -3,7 +3,9 @@ from __future__ import annotations import collections from collections.abc import Callable -from typing import Dict +import dataclasses +import logging +from typing import Dict, List import attr from zigpy import zcl @@ -27,6 +29,7 @@ from . import channels as zha_channels # noqa: F401 pylint: disable=unused-impo from .decorators import CALLABLE_T, DictRegistry, SetRegistry from .typing import ChannelType +_LOGGER = logging.getLogger(__name__) GROUP_ENTITY_DOMAINS = [LIGHT, SWITCH, FAN] PHILLIPS_REMOTE_CLUSTER = 0xFC00 @@ -157,6 +160,8 @@ class MatchRule: aux_channels: Callable | set[str] | str = attr.ib( factory=frozenset, converter=set_or_callable ) + # for multi entities, stop further processing on a match for a component + stop_on_match: bool = attr.ib(default=False) @property def weight(self) -> int: @@ -234,8 +239,16 @@ class MatchRule: return matches -RegistryDictType = Dict[str, Dict[MatchRule, CALLABLE_T]] +@dataclasses.dataclass +class EntityClassAndChannels: + """Container for entity class and corresponding channels.""" + entity_class: CALLABLE_T + claimed_channel: list[ChannelType] + + +RegistryDictType = Dict[str, Dict[MatchRule, CALLABLE_T]] +MultiRegistryDictType = Dict[str, Dict[MatchRule, List[CALLABLE_T]]] GroupRegistryDictType = Dict[str, CALLABLE_T] @@ -245,7 +258,7 @@ class ZHAEntityRegistry: def __init__(self): """Initialize Registry instance.""" self._strict_registry: RegistryDictType = collections.defaultdict(dict) - self._multi_entity_registry: RegistryDictType = collections.defaultdict( + self._multi_entity_registry: MultiRegistryDictType = collections.defaultdict( lambda: collections.defaultdict(list) ) self._group_registry: GroupRegistryDictType = {} @@ -271,22 +284,26 @@ class ZHAEntityRegistry: self, manufacturer: str, model: str, - primary_channel: ChannelType, - aux_channels: list[ChannelType], + channels: list[ChannelType], components: set | None = None, - ) -> tuple[dict[str, list[CALLABLE_T]], list[ChannelType]]: + ) -> tuple[dict[str, list[EntityClassAndChannels]], list[ChannelType]]: """Match ZHA Channels to potentially multiple ZHA Entity classes.""" - result: dict[str, list[CALLABLE_T]] = collections.defaultdict(list) - claimed: set[ChannelType] = set() + result: dict[str, list[EntityClassAndChannels]] = collections.defaultdict(list) + all_claimed: set[ChannelType] = set() for component in components or self._multi_entity_registry: matches = self._multi_entity_registry[component] - for match in sorted(matches, key=lambda x: x.weight, reverse=True): - if match.strict_matched(manufacturer, model, [primary_channel]): - claimed |= set(match.claim_channels(aux_channels)) - ent_classes = self._multi_entity_registry[component][match] - result[component].extend(ent_classes) + sorted_matches = sorted(matches, key=lambda x: x.weight, reverse=True) + for match in sorted_matches: + if match.strict_matched(manufacturer, model, channels): + claimed = match.claim_channels(channels) + for ent_class in self._multi_entity_registry[component][match]: + ent_n_channels = EntityClassAndChannels(ent_class, claimed) + result[component].append(ent_n_channels) + all_claimed |= set(claimed) + if match.stop_on_match: + break - return result, list(claimed) + return result, list(all_claimed) def get_group_entity(self, component: str) -> CALLABLE_T: """Match a ZHA group to a ZHA Entity class.""" @@ -325,11 +342,17 @@ class ZHAEntityRegistry: manufacturers: Callable | set[str] | str = None, models: Callable | set[str] | str = None, aux_channels: Callable | set[str] | str = None, + stop_on_match: bool = False, ) -> Callable[[CALLABLE_T], CALLABLE_T]: """Decorate a loose match rule.""" rule = MatchRule( - channel_names, generic_ids, manufacturers, models, aux_channels + channel_names, + generic_ids, + manufacturers, + models, + aux_channels, + stop_on_match, ) def decorator(zha_entity: CALLABLE_T) -> CALLABLE_T: diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index b2cc414ad5f..18df552986d 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -5,6 +5,13 @@ import functools import numbers from typing import Any +from homeassistant.components.climate.const import ( + CURRENT_HVAC_COOL, + CURRENT_HVAC_FAN, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, +) from homeassistant.components.sensor import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_CO, @@ -57,6 +64,7 @@ from .core.const import ( CHANNEL_PRESSURE, CHANNEL_SMARTENERGY_METERING, CHANNEL_TEMPERATURE, + CHANNEL_THERMOSTAT, DATA_ZHA, DATA_ZHA_DISPATCHERS, SIGNAL_ADD_ENTITIES, @@ -482,3 +490,120 @@ class FormaldehydeConcentration(Sensor): _decimals = 0 _multiplier = 1e6 _unit = CONCENTRATION_PARTS_PER_MILLION + + +@MULTI_MATCH(channel_names=CHANNEL_THERMOSTAT) +class ThermostatHVACAction(Sensor, id_suffix="hvac_action"): + """Thermostat HVAC action sensor.""" + + @classmethod + def create_entity( + cls, + unique_id: str, + zha_device: ZhaDeviceType, + channels: list[ChannelType], + **kwargs, + ) -> ZhaEntity | None: + """Entity Factory. + + Return entity if it is a supported configuration, otherwise return None + """ + + return cls(unique_id, zha_device, channels, **kwargs) + + @property + def native_value(self) -> str | None: + """Return the current HVAC action.""" + if ( + self._channel.pi_heating_demand is None + and self._channel.pi_cooling_demand is None + ): + return self._rm_rs_action + return self._pi_demand_action + + @property + def _rm_rs_action(self) -> str | None: + """Return the current HVAC action based on running mode and running state.""" + + running_mode = self._channel.running_mode + if running_mode == self._channel.RunningMode.Heat: + return CURRENT_HVAC_HEAT + if running_mode == self._channel.RunningMode.Cool: + return CURRENT_HVAC_COOL + + running_state = self._channel.running_state + if running_state and running_state & ( + self._channel.RunningState.Fan_State_On + | self._channel.RunningState.Fan_2nd_Stage_On + | self._channel.RunningState.Fan_3rd_Stage_On + ): + return CURRENT_HVAC_FAN + if ( + self._channel.system_mode != self._channel.SystemMode.Off + and running_mode == self._channel.SystemMode.Off + ): + return CURRENT_HVAC_IDLE + return CURRENT_HVAC_OFF + + @property + def _pi_demand_action(self) -> str | None: + """Return the current HVAC action based on pi_demands.""" + + heating_demand = self._channel.pi_heating_demand + if heating_demand is not None and heating_demand > 0: + return CURRENT_HVAC_HEAT + cooling_demand = self._channel.pi_cooling_demand + if cooling_demand is not None and cooling_demand > 0: + return CURRENT_HVAC_COOL + + if self._channel.system_mode != self._channel.SystemMode.Off: + return CURRENT_HVAC_IDLE + return CURRENT_HVAC_OFF + + @callback + def async_set_state(self, *args, **kwargs) -> None: + """Handle state update from channel.""" + self.async_write_ha_state() + + +@MULTI_MATCH( + channel_names=CHANNEL_THERMOSTAT, + manufacturers="Zen Within", + stop_on_match=True, +) +class ZenHVACAction(ThermostatHVACAction): + """Zen Within Thermostat HVAC Action.""" + + @property + def _rm_rs_action(self) -> str | None: + """Return the current HVAC action based on running mode and running state.""" + + running_state = self._channel.running_state + if running_state is None: + return None + + rs_heat = ( + self._channel.RunningState.Heat_State_On + | self._channel.RunningState.Heat_2nd_Stage_On + ) + if running_state & rs_heat: + return CURRENT_HVAC_HEAT + + rs_cool = ( + self._channel.RunningState.Cool_State_On + | self._channel.RunningState.Cool_2nd_Stage_On + ) + if running_state & rs_cool: + return CURRENT_HVAC_COOL + + running_state = self._channel.running_state + if running_state and running_state & ( + self._channel.RunningState.Fan_State_On + | self._channel.RunningState.Fan_2nd_Stage_On + | self._channel.RunningState.Fan_3rd_Stage_On + ): + return CURRENT_HVAC_FAN + + if self._channel.system_mode != self._channel.SystemMode.Off: + return CURRENT_HVAC_IDLE + return CURRENT_HVAC_OFF diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py index e452d90d60f..1784813250f 100644 --- a/tests/components/zha/test_climate.py +++ b/tests/components/zha/test_climate.py @@ -45,6 +45,7 @@ from homeassistant.components.climate.const import ( SERVICE_SET_PRESET_MODE, SERVICE_SET_TEMPERATURE, ) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.zha.climate import ( DOMAIN, HVAC_MODE_2_SYSTEM, @@ -174,6 +175,7 @@ def device_climate_mock(hass, zigpy_device_mock, zha_device_joined): plugged_attrs = {**ZCL_ATTR_PLUG, **plug} zigpy_device = zigpy_device_mock(clusters, manufacturer=manuf, quirk=quirk) + zigpy_device.node_desc.mac_capability_flags |= 0b_0000_0100 zigpy_device.endpoints[1].thermostat.PLUGGED_ATTR_READS = plugged_attrs zha_device = await zha_device_joined(zigpy_device) await async_enable_traffic(hass, [zha_device]) @@ -257,45 +259,60 @@ async def test_climate_hvac_action_running_state(hass, device_climate): thrm_cluster = device_climate.device.endpoints[1].thermostat entity_id = await find_entity_id(DOMAIN, device_climate, hass) + sensor_entity_id = await find_entity_id(SENSOR_DOMAIN, device_climate, hass) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_OFF await send_attributes_report( hass, thrm_cluster, {0x001E: Thermostat.RunningMode.Off} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_OFF await send_attributes_report( hass, thrm_cluster, {0x001C: Thermostat.SystemMode.Auto} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_IDLE await send_attributes_report( hass, thrm_cluster, {0x001E: Thermostat.RunningMode.Cool} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_COOL await send_attributes_report( hass, thrm_cluster, {0x001E: Thermostat.RunningMode.Heat} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_HEAT await send_attributes_report( hass, thrm_cluster, {0x001E: Thermostat.RunningMode.Off} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_IDLE await send_attributes_report( hass, thrm_cluster, {0x0029: Thermostat.RunningState.Fan_State_On} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_FAN + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_FAN async def test_climate_hvac_action_running_state_zen(hass, device_climate_zen): @@ -303,63 +320,84 @@ async def test_climate_hvac_action_running_state_zen(hass, device_climate_zen): thrm_cluster = device_climate_zen.device.endpoints[1].thermostat entity_id = await find_entity_id(DOMAIN, device_climate_zen, hass) + sensor_entity_id = await find_entity_id(SENSOR_DOMAIN, device_climate_zen, hass) state = hass.states.get(entity_id) assert ATTR_HVAC_ACTION not in state.attributes + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == "unknown" await send_attributes_report( hass, thrm_cluster, {0x0029: Thermostat.RunningState.Cool_2nd_Stage_On} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_COOL await send_attributes_report( hass, thrm_cluster, {0x0029: Thermostat.RunningState.Fan_State_On} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_FAN + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_FAN await send_attributes_report( hass, thrm_cluster, {0x0029: Thermostat.RunningState.Heat_2nd_Stage_On} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_HEAT await send_attributes_report( hass, thrm_cluster, {0x0029: Thermostat.RunningState.Fan_2nd_Stage_On} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_FAN + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_FAN await send_attributes_report( hass, thrm_cluster, {0x0029: Thermostat.RunningState.Cool_State_On} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_COOL await send_attributes_report( hass, thrm_cluster, {0x0029: Thermostat.RunningState.Fan_3rd_Stage_On} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_FAN + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_FAN await send_attributes_report( hass, thrm_cluster, {0x0029: Thermostat.RunningState.Heat_State_On} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_HEAT await send_attributes_report( hass, thrm_cluster, {0x0029: Thermostat.RunningState.Idle} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_OFF await send_attributes_report( hass, thrm_cluster, {0x001C: Thermostat.SystemMode.Heat} ) state = hass.states.get(entity_id) assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + hvac_sensor_state = hass.states.get(sensor_entity_id) + assert hvac_sensor_state.state == CURRENT_HVAC_IDLE async def test_climate_hvac_action_pi_demand(hass, device_climate): diff --git a/tests/components/zha/test_registries.py b/tests/components/zha/test_registries.py index d202c7256dd..01b2a074187 100644 --- a/tests/components/zha/test_registries.py +++ b/tests/components/zha/test_registries.py @@ -332,27 +332,13 @@ def test_multi_sensor_match(channel, entity_registry): ch_illuminati = channel("illuminance", 0x0401) match, claimed = entity_registry.get_multi_entity( - "manufacturer", - "model", - primary_channel=ch_illuminati, - aux_channels=[ch_se, ch_illuminati], - ) - - assert s.binary_sensor not in match - assert s.component not in match - assert set(claimed) == set() - - match, claimed = entity_registry.get_multi_entity( - "manufacturer", - "model", - primary_channel=ch_se, - aux_channels=[ch_se, ch_illuminati], + "manufacturer", "model", channels=[ch_se, ch_illuminati] ) assert s.binary_sensor in match assert s.component not in match assert set(claimed) == {ch_se} - assert {cls.__name__ for cls in match[s.binary_sensor]} == { + assert {cls.entity_class.__name__ for cls in match[s.binary_sensor]} == { SmartEnergySensor2.__name__ } @@ -371,17 +357,16 @@ def test_multi_sensor_match(channel, entity_registry): pass match, claimed = entity_registry.get_multi_entity( - "manufacturer", - "model", - primary_channel=ch_se, - aux_channels={ch_se, ch_illuminati}, + "manufacturer", "model", channels={ch_se, ch_illuminati} ) assert s.binary_sensor in match assert s.component in match assert set(claimed) == {ch_se, ch_illuminati} - assert {cls.__name__ for cls in match[s.binary_sensor]} == { + assert {cls.entity_class.__name__ for cls in match[s.binary_sensor]} == { SmartEnergySensor2.__name__, SmartEnergySensor3.__name__, } - assert {cls.__name__ for cls in match[s.component]} == {SmartEnergySensor1.__name__} + assert {cls.entity_class.__name__ for cls in match[s.component]} == { + SmartEnergySensor1.__name__ + } diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index 6276aa12068..e85f4c270d5 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -3174,6 +3174,7 @@ DEVICES = [ "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_rms_current", "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_rms_voltage", "sensor.sinope_technologies_th1123zb_77665544_temperature", + "sensor.sinope_technologies_th1123zb_77665544_thermostat_hvac_action", ], DEV_SIG_ENT_MAP: { ("climate", "00:11:22:33:44:55:66:77-1"): { @@ -3201,6 +3202,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_rms_voltage", }, + ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { + DEV_SIG_CHANNELS: ["thermostat"], + DEV_SIG_ENT_MAP_CLASS: "ThermostatHVACAction", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_thermostat_hvac_action", + }, }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], SIG_MANUFACTURER: "Sinope Technologies", @@ -3231,6 +3237,7 @@ DEVICES = [ "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_rms_current", "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_rms_voltage", "sensor.sinope_technologies_th1124zb_77665544_temperature", + "sensor.sinope_technologies_th1124zb_77665544_thermostat_hvac_action", "climate.sinope_technologies_th1124zb_77665544_thermostat", ], DEV_SIG_ENT_MAP: { @@ -3239,6 +3246,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "Thermostat", DEV_SIG_ENT_MAP_ID: "climate.sinope_technologies_th1124zb_77665544_thermostat", }, + ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { + DEV_SIG_CHANNELS: ["thermostat"], + DEV_SIG_ENT_MAP_CLASS: "ThermostatHVACAction", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_thermostat_hvac_action", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", @@ -3454,6 +3466,7 @@ DEVICES = [ }, DEV_SIG_ENTITIES: [ "climate.zen_within_zen_01_77665544_fan_thermostat", + "sensor.zen_within_zen_01_77665544_thermostat_hvac_action", "sensor.zen_within_zen_01_77665544_power", ], DEV_SIG_ENT_MAP: { @@ -3467,6 +3480,11 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "ZenWithinThermostat", DEV_SIG_ENT_MAP_ID: "climate.zen_within_zen_01_77665544_fan_thermostat", }, + ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { + DEV_SIG_CHANNELS: ["thermostat"], + DEV_SIG_ENT_MAP_CLASS: "ZenHVACAction", + DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_77665544_thermostat_hvac_action", + }, }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], SIG_MANUFACTURER: "Zen Within", From d08b65db7da9cb0afafb642e574bbd8a10a3a714 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 4 Oct 2021 07:52:08 -1000 Subject: [PATCH 0103/1038] Update esphome reconnect logic to use newer RecordUpdateListener logic (#57057) --- homeassistant/components/esphome/__init__.py | 54 +++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index ee258317357..de301e0c1bb 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -24,7 +24,7 @@ from aioesphomeapi import ( UserServiceArgType, ) import voluptuous as vol -from zeroconf import DNSPointer, DNSRecord, RecordUpdateListener, Zeroconf +from zeroconf import DNSPointer, RecordUpdate, RecordUpdateListener, Zeroconf from homeassistant import const from homeassistant.components import zeroconf @@ -518,34 +518,40 @@ class ReconnectLogic(RecordUpdateListener): """Stop as an async callback function.""" self._hass.async_create_task(self.stop()) - @callback - def _set_reconnect(self) -> None: - self._reconnect_event.set() + def async_update_records( + self, zc: Zeroconf, now: float, records: list[RecordUpdate] + ) -> None: + """Listen to zeroconf updated mDNS records. - def update_record(self, zc: Zeroconf, now: float, record: DNSRecord) -> None: - """Listen to zeroconf updated mDNS records.""" - if not isinstance(record, DNSPointer): - # We only consider PTR records and match using the alias name - return - if self._entry_data is None or self._entry_data.device_info is None: - # Either the entry was already teared down or we haven't received device info yet + This is a mDNS record from the device and could mean it just woke up. + """ + # Check if already connected, no lock needed for this access and + # bail if either the entry was already teared down or we haven't received device info yet + if ( + self._connected + or self._reconnect_event.is_set() + or self._entry_data is None + or self._entry_data.device_info is None + ): return filter_alias = f"{self._entry_data.device_info.name}._esphomelib._tcp.local." - if record.alias != filter_alias: - return - # This is a mDNS record from the device and could mean it just woke up - # Check if already connected, no lock needed for this access - if self._connected: - return + for record_update in records: + # We only consider PTR records and match using the alias name + if ( + not isinstance(record_update.new, DNSPointer) + or record_update.new.alias != filter_alias + ): + continue - # Tell reconnection logic to retry connection attempt now (even before reconnect timer finishes) - _LOGGER.debug( - "%s: Triggering reconnect because of received mDNS record %s", - self._host, - record, - ) - self._hass.add_job(self._set_reconnect) + # Tell reconnection logic to retry connection attempt now (even before reconnect timer finishes) + _LOGGER.debug( + "%s: Triggering reconnect because of received mDNS record %s", + self._host, + record_update.new, + ) + self._reconnect_event.set() + return async def _async_setup_device_registry( From 77af741099fba3a016fede57d687ae05637c490c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 4 Oct 2021 08:25:09 -1000 Subject: [PATCH 0104/1038] Prevent tplink from opening sockets in tests (#57058) Supports #55516 --- tests/components/tplink/conftest.py | 5 +++++ tests/components/tplink/test_init.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/components/tplink/conftest.py b/tests/components/tplink/conftest.py index 1963b595176..20ce09b9ec8 100644 --- a/tests/components/tplink/conftest.py +++ b/tests/components/tplink/conftest.py @@ -25,3 +25,8 @@ def device_reg_fixture(hass): def entity_reg_fixture(hass): """Return an empty, loaded, registry.""" return mock_registry(hass) + + +@pytest.fixture(autouse=True) +def tplink_mock_get_source_ip(mock_get_source_ip): + """Mock network util's async_get_source_ip.""" diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index c3f7e814ed6..ee57a500a8d 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -21,7 +21,7 @@ async def test_configuring_tplink_causes_discovery(hass): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() - assert len(discover.mock_calls) == 1 + assert discover.mock_calls async def test_config_entry_reload(hass): From 2f960e558f025d0e5f414c4e54058f96ff4e33dd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 4 Oct 2021 08:25:46 -1000 Subject: [PATCH 0105/1038] Prevent dlna_dmr from opening sockets in tests (#57059) Supports #55516 --- tests/components/dlna_dmr/conftest.py | 5 +++++ tests/components/dlna_dmr/test_data.py | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/components/dlna_dmr/conftest.py b/tests/components/dlna_dmr/conftest.py index 521a1c22fa5..60116d949ae 100644 --- a/tests/components/dlna_dmr/conftest.py +++ b/tests/components/dlna_dmr/conftest.py @@ -139,3 +139,8 @@ def async_get_local_ip_mock() -> Iterable[Mock]: ) as func: func.return_value = AddressFamily.AF_INET, LOCAL_IP yield func + + +@pytest.fixture(autouse=True) +def dlna_dmr_mock_get_source_ip(mock_get_source_ip): + """Mock network util's async_get_source_ip.""" diff --git a/tests/components/dlna_dmr/test_data.py b/tests/components/dlna_dmr/test_data.py index b4b9fcc76f2..469cfcece88 100644 --- a/tests/components/dlna_dmr/test_data.py +++ b/tests/components/dlna_dmr/test_data.py @@ -107,7 +107,9 @@ async def test_event_notifier( assert domain_data.stop_listener_remove is None -async def test_cleanup_event_notifiers(hass: HomeAssistant) -> None: +async def test_cleanup_event_notifiers( + hass: HomeAssistant, aiohttp_notify_servers_mock: Mock +) -> None: """Test cleanup function clears all event notifiers.""" domain_data = get_domain_data(hass) await domain_data.async_get_event_notifier(EventListenAddr(None, 0, None), hass) From 08cebb247f17365e86d586581e3734689a465647 Mon Sep 17 00:00:00 2001 From: Tomasz Date: Mon, 4 Oct 2021 22:13:11 +0200 Subject: [PATCH 0106/1038] Activate mypy for rpi_power (#57047) --- .strict-typing | 1 + .../components/rpi_power/__init__.py | 2 +- .../components/rpi_power/binary_sensor.py | 53 ++++++------------- .../components/rpi_power/config_flow.py | 2 +- mypy.ini | 14 +++-- script/hassfest/mypy_config.py | 1 - 6 files changed, 31 insertions(+), 42 deletions(-) diff --git a/.strict-typing b/.strict-typing index 18dc1be41eb..dec244ece15 100644 --- a/.strict-typing +++ b/.strict-typing @@ -91,6 +91,7 @@ homeassistant.components.recorder.statistics homeassistant.components.remote.* homeassistant.components.renault.* homeassistant.components.rituals_perfume_genie.* +homeassistant.components.rpi_power.* homeassistant.components.samsungtv.* homeassistant.components.scene.* homeassistant.components.select.* diff --git a/homeassistant/components/rpi_power/__init__.py b/homeassistant/components/rpi_power/__init__.py index eeb7c4fe181..320043a0260 100644 --- a/homeassistant/components/rpi_power/__init__.py +++ b/homeassistant/components/rpi_power/__init__.py @@ -11,6 +11,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/rpi_power/binary_sensor.py b/homeassistant/components/rpi_power/binary_sensor.py index 79ef36e891a..f223bbf2c91 100644 --- a/homeassistant/components/rpi_power/binary_sensor.py +++ b/homeassistant/components/rpi_power/binary_sensor.py @@ -5,7 +5,7 @@ Minimal Kernel needed is 4.14+ """ import logging -from rpi_bad_power import new_under_voltage +from rpi_bad_power import UnderVoltage, new_under_voltage from homeassistant.components.binary_sensor import ( DEVICE_CLASS_PROBLEM, @@ -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_platform import AddEntitiesCallback _LOGGER = logging.getLogger(__name__) @@ -21,8 +22,10 @@ DESCRIPTION_UNDER_VOLTAGE = "Under-voltage was detected. Consider getting a unin async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities -): + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up rpi_power binary sensor.""" under_voltage = await hass.async_add_executor_job(new_under_voltage) async_add_entities([RaspberryChargerBinarySensor(under_voltage)], True) @@ -31,43 +34,21 @@ async def async_setup_entry( class RaspberryChargerBinarySensor(BinarySensorEntity): """Binary sensor representing the rpi power status.""" - def __init__(self, under_voltage): + _attr_device_class = DEVICE_CLASS_PROBLEM + _attr_icon = "mdi:raspberry-pi" + _attr_name = "RPi Power status" + _attr_unique_id = "rpi_power" # only one sensor possible + + def __init__(self, under_voltage: UnderVoltage) -> None: """Initialize the binary sensor.""" self._under_voltage = under_voltage - self._is_on = None - self._last_is_on = False - def update(self): + def update(self) -> None: """Update the state.""" - self._is_on = self._under_voltage.get() - if self._is_on != self._last_is_on: - if self._is_on: + value = self._under_voltage.get() + if self._attr_is_on != value: + if value: _LOGGER.warning(DESCRIPTION_UNDER_VOLTAGE) else: _LOGGER.info(DESCRIPTION_NORMALIZED) - self._last_is_on = self._is_on - - @property - def unique_id(self): - """Return the unique id of the sensor.""" - return "rpi_power" # only one sensor possible - - @property - def name(self): - """Return the name of the sensor.""" - return "RPi Power status" - - @property - def is_on(self): - """Return if there is a problem detected.""" - return self._is_on - - @property - def icon(self): - """Return the icon of the sensor.""" - return "mdi:raspberry-pi" - - @property - def device_class(self): - """Return the class of this device.""" - return DEVICE_CLASS_PROBLEM + self._attr_is_on = value diff --git a/homeassistant/components/rpi_power/config_flow.py b/homeassistant/components/rpi_power/config_flow.py index 1a038d05fdc..82457d5b296 100644 --- a/homeassistant/components/rpi_power/config_flow.py +++ b/homeassistant/components/rpi_power/config_flow.py @@ -35,7 +35,7 @@ class RPiPowerFlow(DiscoveryFlowHandler, domain=DOMAIN): self, data: dict[str, Any] | None = None ) -> FlowResult: """Handle a flow initialized by onboarding.""" - has_devices = await self._discovery_function(self.hass) + has_devices = await self._discovery_function(self.hass) # type: ignore if not has_devices: return self.async_abort(reason="no_devices_found") diff --git a/mypy.ini b/mypy.ini index 4e91295b597..cc491043c3a 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1012,6 +1012,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.rpi_power.*] +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.samsungtv.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1695,9 +1706,6 @@ ignore_errors = true [mypy-homeassistant.components.ring.*] ignore_errors = true -[mypy-homeassistant.components.rpi_power.*] -ignore_errors = true - [mypy-homeassistant.components.ruckus_unleashed.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 375c55fe84b..d5542692e57 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -106,7 +106,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.profiler.*", "homeassistant.components.rachio.*", "homeassistant.components.ring.*", - "homeassistant.components.rpi_power.*", "homeassistant.components.ruckus_unleashed.*", "homeassistant.components.screenlogic.*", "homeassistant.components.search.*", From c8dc5d15ee3a07c7a72aeb41d5de91fcfe7ecce3 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 4 Oct 2021 23:46:46 +0300 Subject: [PATCH 0107/1038] Fix: Shelly Gen2 - filter unsupported sensors (#57065) --- .../components/shelly/binary_sensor.py | 16 +++++----------- homeassistant/components/shelly/entity.py | 19 +++++++++++++++---- homeassistant/components/shelly/sensor.py | 17 +++++++++++------ 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index 16ffe8b4ee5..46e5468c079 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -122,34 +122,28 @@ REST_SENSORS: Final = { RPC_SENSORS: Final = { "input": RpcAttributeDescription( key="input", + sub_key="state", name="Input", - value=lambda status, _: status["state"], device_class=DEVICE_CLASS_POWER, default_enabled=False, removal_condition=is_rpc_momentary_input, ), "cloud": RpcAttributeDescription( key="cloud", + sub_key="connected", name="Cloud", - value=lambda status, _: status["connected"], device_class=DEVICE_CLASS_CONNECTIVITY, default_enabled=False, ), "fwupdate": RpcAttributeDescription( key="sys", + sub_key="available_updates", name="Firmware Update", device_class=DEVICE_CLASS_UPDATE, - value=lambda status, _: status["available_updates"], default_enabled=False, extra_state_attributes=lambda status: { - "latest_stable_version": status["available_updates"].get( - "stable", - {"version": ""}, - )["version"], - "beta_version": status["available_updates"].get( - "beta", - {"version": ""}, - )["version"], + "latest_stable_version": status.get("stable", {"version": ""})["version"], + "beta_version": status.get("beta", {"version": ""})["version"], }, ), } diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index f12633bd0e3..0fe25884f00 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -166,6 +166,10 @@ async def async_setup_entry_rpc( key_instances = get_rpc_key_instances(wrapper.device.status, description.key) for key in key_instances: + # Filter non-existing sensors + if description.sub_key not in wrapper.device.status[key]: + continue + # Filter and remove entities that according to settings should not create an entity if description.removal_condition and description.removal_condition( wrapper.device.config, key @@ -240,10 +244,11 @@ class RpcAttributeDescription: """Class to describe a RPC sensor.""" key: str + sub_key: str name: str icon: str | None = None unit: str | None = None - value: Callable[[dict, Any], Any] | None = None + value: Callable[[Any, Any], Any] | None = None device_class: str | None = None state_class: str | None = None default_enabled: bool = True @@ -549,6 +554,7 @@ class ShellyRpcAttributeEntity(ShellyRpcEntity, entity.Entity): ) -> None: """Initialize sensor.""" super().__init__(wrapper, key) + self.sub_key = description.sub_key self.attribute = attribute self.description = description @@ -564,8 +570,11 @@ class ShellyRpcAttributeEntity(ShellyRpcEntity, entity.Entity): """Value of sensor.""" if callable(self.description.value): self._last_value = self.description.value( - self.wrapper.device.status[self.key], self._last_value + self.wrapper.device.status[self.key][self.sub_key], self._last_value ) + else: + self._last_value = self.wrapper.device.status[self.key][self.sub_key] + return self._last_value @property @@ -576,7 +585,9 @@ class ShellyRpcAttributeEntity(ShellyRpcEntity, entity.Entity): if not available or not self.description.available: return available - return self.description.available(self.wrapper.device.status[self.key]) + return self.description.available( + self.wrapper.device.status[self.key][self.sub_key] + ) @property def extra_state_attributes(self) -> dict[str, Any] | None: @@ -585,7 +596,7 @@ class ShellyRpcAttributeEntity(ShellyRpcEntity, entity.Entity): return None return self.description.extra_state_attributes( - self.wrapper.device.status[self.key] + self.wrapper.device.status[self.key][self.sub_key] ) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 09e91946cf3..9ee0712aaef 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -242,51 +242,56 @@ REST_SENSORS: Final = { RPC_SENSORS: Final = { "power": RpcAttributeDescription( key="switch", + sub_key="apower", name="Power", unit=POWER_WATT, - value=lambda status, _: round(float(status["apower"]), 1), + value=lambda status, _: round(float(status), 1), device_class=sensor.DEVICE_CLASS_POWER, state_class=sensor.STATE_CLASS_MEASUREMENT, ), "voltage": RpcAttributeDescription( key="switch", + sub_key="voltage", name="Voltage", unit=ELECTRIC_POTENTIAL_VOLT, - value=lambda status, _: round(float(status["voltage"]), 1), + value=lambda status, _: round(float(status), 1), device_class=sensor.DEVICE_CLASS_VOLTAGE, state_class=sensor.STATE_CLASS_MEASUREMENT, default_enabled=False, ), "energy": RpcAttributeDescription( key="switch", + sub_key="aenergy", name="Energy", unit=ENERGY_KILO_WATT_HOUR, - value=lambda status, _: round(status["aenergy"]["total"] / 1000, 2), + value=lambda status, _: round(status["total"] / 1000, 2), device_class=sensor.DEVICE_CLASS_ENERGY, state_class=sensor.STATE_CLASS_TOTAL_INCREASING, ), "temperature": RpcAttributeDescription( key="switch", + sub_key="temperature", name="Temperature", unit=TEMP_CELSIUS, - value=lambda status, _: round(status["temperature"]["tC"], 1), + value=lambda status, _: round(status["tC"], 1), device_class=sensor.DEVICE_CLASS_TEMPERATURE, state_class=sensor.STATE_CLASS_MEASUREMENT, default_enabled=False, ), "rssi": RpcAttributeDescription( key="wifi", + sub_key="rssi", name="RSSI", unit=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - value=lambda status, _: status["rssi"], device_class=sensor.DEVICE_CLASS_SIGNAL_STRENGTH, state_class=sensor.STATE_CLASS_MEASUREMENT, default_enabled=False, ), "uptime": RpcAttributeDescription( key="sys", + sub_key="uptime", name="Uptime", - value=lambda status, last: get_device_uptime(status["uptime"], last), + value=get_device_uptime, device_class=sensor.DEVICE_CLASS_TIMESTAMP, default_enabled=False, ), From d74389184254baabf4b540a4478aece4f1e3bf67 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 4 Oct 2021 15:44:17 -0700 Subject: [PATCH 0108/1038] Update Tuya code owners (#57078) --- CODEOWNERS | 2 +- homeassistant/components/tuya/manifest.json | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 64b87195d8f..606c71f0a1c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -544,7 +544,7 @@ homeassistant/components/trafikverket_train/* @endor-force homeassistant/components/trafikverket_weatherstation/* @endor-force homeassistant/components/transmission/* @engrbm87 @JPHutchins homeassistant/components/tts/* @pvizeli -homeassistant/components/tuya/* @Tuya +homeassistant/components/tuya/* @Tuya @zlinoliver @METISU homeassistant/components/twentemilieu/* @frenck homeassistant/components/twinkly/* @dr1rrb homeassistant/components/ubus/* @noltari diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index 85370bdfcac..0097e7635ec 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -2,12 +2,8 @@ "domain": "tuya", "name": "Tuya", "documentation": "https://github.com/tuya/tuya-home-assistant", - "requirements": [ - "tuya-iot-py-sdk==0.4.1" - ], - "codeowners": [ - "@Tuya" - ], + "requirements": ["tuya-iot-py-sdk==0.4.1"], + "codeowners": ["@Tuya", "@zlinoliver", "@METISU"], "config_flow": true, "iot_class": "cloud_push" -} \ No newline at end of file +} From 1e5d40842604a36f0594efe8b8a210084e4cb208 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 5 Oct 2021 00:12:33 +0000 Subject: [PATCH 0109/1038] [ci skip] Translation update --- .../components/switchbot/translations/it.json | 4 +++- homeassistant/components/tuya/translations/ca.json | 10 +++++++--- homeassistant/components/tuya/translations/et.json | 6 +++++- homeassistant/components/tuya/translations/it.json | 6 +++++- homeassistant/components/tuya/translations/nl.json | 6 +++++- 5 files changed, 25 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/switchbot/translations/it.json b/homeassistant/components/switchbot/translations/it.json index fc8296f6442..9529450232b 100644 --- a/homeassistant/components/switchbot/translations/it.json +++ b/homeassistant/components/switchbot/translations/it.json @@ -8,7 +8,9 @@ "unknown": "Errore imprevisto" }, "error": { - "cannot_connect": "Impossibile connettersi" + "cannot_connect": "Impossibile connettersi", + "one": "Vuoto", + "other": "Vuoti" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/tuya/translations/ca.json b/homeassistant/components/tuya/translations/ca.json index 6759d322484..bff9b7a35b9 100644 --- a/homeassistant/components/tuya/translations/ca.json +++ b/homeassistant/components/tuya/translations/ca.json @@ -6,7 +6,8 @@ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, "error": { - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "login_error": "Error d'inici de sessi\u00f3 ({code}): {msg}" }, "flow_title": "Configuraci\u00f3 de Tuya", "step": { @@ -25,13 +26,16 @@ }, "user": { "data": { + "access_id": "ID d'acc\u00e9s de Tuya IoT", + "access_secret": "Secret d'acc\u00e9s de Tuya IoT", "country_code": "El teu codi de pa\u00eds (per exemple, 1 per l'EUA o 86 per la Xina)", "password": "Contrasenya", "platform": "L'aplicaci\u00f3 on es registra el teu compte", + "region": "Regi\u00f3", "tuya_project_type": "Tipus de projecte al n\u00favol de Tuya", - "username": "Nom d'usuari" + "username": "Compte" }, - "description": "Introdueix les teves credencial de Tuya.", + "description": "Introdueix les teves credencial de Tuya", "title": "Integraci\u00f3 Tuya" } } diff --git a/homeassistant/components/tuya/translations/et.json b/homeassistant/components/tuya/translations/et.json index 45b4e4d2639..96f081621b8 100644 --- a/homeassistant/components/tuya/translations/et.json +++ b/homeassistant/components/tuya/translations/et.json @@ -6,7 +6,8 @@ "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks sidumine." }, "error": { - "invalid_auth": "Tuvastamise viga" + "invalid_auth": "Tuvastamise viga", + "login_error": "Sisenemine nurjus ( {code} ): {msg}" }, "flow_title": "Tuya seaded", "step": { @@ -25,9 +26,12 @@ }, "user": { "data": { + "access_id": "Tuya IoT kasutajatunnus", + "access_secret": "Tuya IoT salas\u00f5na", "country_code": "Konto riigikood (nt 1 USA v\u00f5i 372 Eesti)", "password": "Salas\u00f5na", "platform": "\u00c4pp kus konto registreeriti", + "region": "Piirkond", "tuya_project_type": "Tuya pilveprojekti t\u00fc\u00fcp", "username": "Kasutajanimi" }, diff --git a/homeassistant/components/tuya/translations/it.json b/homeassistant/components/tuya/translations/it.json index 3baed47661c..9f3b7d498e3 100644 --- a/homeassistant/components/tuya/translations/it.json +++ b/homeassistant/components/tuya/translations/it.json @@ -6,7 +6,8 @@ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, "error": { - "invalid_auth": "Autenticazione non valida" + "invalid_auth": "Autenticazione non valida", + "login_error": "Errore di accesso ({code}): {msg}" }, "flow_title": "Configurazione di Tuya", "step": { @@ -25,9 +26,12 @@ }, "user": { "data": { + "access_id": "ID accesso IoT Tuya", + "access_secret": "Secret IoT Tuya", "country_code": "Prefisso internazionale del tuo account (ad es. 1 per gli Stati Uniti o 86 per la Cina)", "password": "Password", "platform": "L'app in cui \u00e8 registrato il tuo account", + "region": "Area geografica", "tuya_project_type": "Tipo di progetto Tuya cloud", "username": "Nome utente" }, diff --git a/homeassistant/components/tuya/translations/nl.json b/homeassistant/components/tuya/translations/nl.json index 22a63800fd6..0ceb0c916cc 100644 --- a/homeassistant/components/tuya/translations/nl.json +++ b/homeassistant/components/tuya/translations/nl.json @@ -6,7 +6,8 @@ "single_instance_allowed": "Al geconfigureerd. Er is maar een configuratie mogelijk." }, "error": { - "invalid_auth": "Ongeldige authenticatie" + "invalid_auth": "Ongeldige authenticatie", + "login_error": "Aanmeldingsfout ({code}): {msg}" }, "flow_title": "Tuya-configuratie", "step": { @@ -25,9 +26,12 @@ }, "user": { "data": { + "access_id": "Tuya IoT-toegangs-ID", + "access_secret": "Tuya IoT Access Secret", "country_code": "De landcode van uw account (bijvoorbeeld 1 voor de VS of 86 voor China)", "password": "Wachtwoord", "platform": "De app waar uw account is geregistreerd", + "region": "Regio", "tuya_project_type": "Tuya cloud project type", "username": "Gebruikersnaam" }, From 91d3d39f6c30f9f6797311973c9b2d972382ea70 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 5 Oct 2021 05:52:17 +0200 Subject: [PATCH 0110/1038] Update frontend to 20211004.0 (#57073) --- 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 d6d38faab27..3b047fbe245 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20211002.0" + "home-assistant-frontend==20211004.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d69ae6d5e43..a496bc80169 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==3.4.8 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211002.0 +home-assistant-frontend==20211004.0 httpx==0.19.0 ifaddr==0.1.7 jinja2==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 24812d8e25c..75e56353958 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -807,7 +807,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211002.0 +home-assistant-frontend==20211004.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 88cc5b38602..507825fedd6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -488,7 +488,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211002.0 +home-assistant-frontend==20211004.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 11ed70b77430d96efc0989987e7f77b2fc115016 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 4 Oct 2021 20:52:40 -0700 Subject: [PATCH 0111/1038] Fix energy gas price validation (#57075) --- homeassistant/components/energy/validate.py | 38 ++++++++++++++++----- tests/components/energy/test_validate.py | 29 ++++++++++++++-- 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py index d03883d046b..24d060b4352 100644 --- a/homeassistant/components/energy/validate.py +++ b/homeassistant/components/energy/validate.py @@ -24,13 +24,21 @@ ENERGY_USAGE_DEVICE_CLASSES = (sensor.DEVICE_CLASS_ENERGY,) ENERGY_USAGE_UNITS = { sensor.DEVICE_CLASS_ENERGY: (ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR) } +ENERGY_PRICE_UNITS = tuple( + f"/{unit}" for units in ENERGY_USAGE_UNITS.values() for unit in units +) ENERGY_UNIT_ERROR = "entity_unexpected_unit_energy" +ENERGY_PRICE_UNIT_ERROR = "entity_unexpected_unit_energy_price" GAS_USAGE_DEVICE_CLASSES = (sensor.DEVICE_CLASS_ENERGY, sensor.DEVICE_CLASS_GAS) GAS_USAGE_UNITS = { sensor.DEVICE_CLASS_ENERGY: (ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR), sensor.DEVICE_CLASS_GAS: (VOLUME_CUBIC_METERS, VOLUME_CUBIC_FEET), } +GAS_PRICE_UNITS = tuple( + f"/{unit}" for units in GAS_USAGE_UNITS.values() for unit in units +) GAS_UNIT_ERROR = "entity_unexpected_unit_gas" +GAS_PRICE_UNIT_ERROR = "entity_unexpected_unit_gas_price" @dataclasses.dataclass @@ -152,7 +160,11 @@ def _async_validate_usage_stat( @callback def _async_validate_price_entity( - hass: HomeAssistant, entity_id: str, result: list[ValidationIssue] + hass: HomeAssistant, + entity_id: str, + result: list[ValidationIssue], + allowed_units: tuple[str, ...], + unit_error: str, ) -> None: """Validate that the price entity is correct.""" state = hass.states.get(entity_id) @@ -176,10 +188,8 @@ def _async_validate_price_entity( unit = state.attributes.get("unit_of_measurement") - if unit is None or not unit.endswith( - (f"/{ENERGY_KILO_WATT_HOUR}", f"/{ENERGY_WATT_HOUR}") - ): - result.append(ValidationIssue("entity_unexpected_unit_price", entity_id, unit)) + if unit is None or not unit.endswith(allowed_units): + result.append(ValidationIssue(unit_error, entity_id, unit)) @callback @@ -274,7 +284,11 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: _async_validate_cost_stat(hass, flow["stat_cost"], source_result) elif flow.get("entity_energy_price") is not None: _async_validate_price_entity( - hass, flow["entity_energy_price"], source_result + hass, + flow["entity_energy_price"], + source_result, + ENERGY_PRICE_UNITS, + ENERGY_PRICE_UNIT_ERROR, ) if ( @@ -303,7 +317,11 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ) elif flow.get("entity_energy_price") is not None: _async_validate_price_entity( - hass, flow["entity_energy_price"], source_result + hass, + flow["entity_energy_price"], + source_result, + ENERGY_PRICE_UNITS, + ENERGY_PRICE_UNIT_ERROR, ) if ( @@ -330,7 +348,11 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: _async_validate_cost_stat(hass, source["stat_cost"], source_result) elif source.get("entity_energy_price") is not None: _async_validate_price_entity( - hass, source["entity_energy_price"], source_result + hass, + source["entity_energy_price"], + source_result, + GAS_PRICE_UNITS, + GAS_PRICE_UNIT_ERROR, ) if ( diff --git a/tests/components/energy/test_validate.py b/tests/components/energy/test_validate.py index 1dd38047209..668f3113fea 100644 --- a/tests/components/energy/test_validate.py +++ b/tests/components/energy/test_validate.py @@ -473,7 +473,7 @@ async def test_validation_grid_price_not_exist(hass, mock_energy_manager): "123", "$/Ws", { - "type": "entity_unexpected_unit_price", + "type": "entity_unexpected_unit_energy_price", "identifier": "sensor.grid_price_1", "value": "$/Ws", }, @@ -551,11 +551,19 @@ async def test_validation_gas(hass, mock_energy_manager, mock_is_entity_recorded { "type": "gas", "stat_energy_from": "sensor.gas_consumption_4", - "stat_cost": "sensor.gas_cost_2", + "entity_energy_from": "sensor.gas_consumption_4", + "entity_energy_price": "sensor.gas_price_1", + }, + { + "type": "gas", + "stat_energy_from": "sensor.gas_consumption_3", + "entity_energy_from": "sensor.gas_consumption_3", + "entity_energy_price": "sensor.gas_price_2", }, ] } ) + await hass.async_block_till_done() hass.states.async_set( "sensor.gas_consumption_1", "10.10", @@ -593,6 +601,16 @@ async def test_validation_gas(hass, mock_energy_manager, mock_is_entity_recorded "10.10", {"unit_of_measurement": "EUR/kWh", "state_class": "total_increasing"}, ) + hass.states.async_set( + "sensor.gas_price_1", + "10.10", + {"unit_of_measurement": "EUR/m³", "state_class": "total_increasing"}, + ) + hass.states.async_set( + "sensor.gas_price_2", + "10.10", + {"unit_of_measurement": "EUR/invalid", "state_class": "total_increasing"}, + ) assert (await validate.async_validate(hass)).as_dict() == { "energy_sources": [ @@ -622,6 +640,13 @@ async def test_validation_gas(hass, mock_energy_manager, mock_is_entity_recorded "value": None, }, ], + [ + { + "type": "entity_unexpected_unit_gas_price", + "identifier": "sensor.gas_price_2", + "value": "EUR/invalid", + }, + ], ], "device_consumption": [], } From 8026a14bc8d10aefad44aa8512320fc1106d6287 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 4 Oct 2021 20:59:23 -0700 Subject: [PATCH 0112/1038] Bump nest 0.3.7 to prepare for WebRTC support (#57089) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 5b078393d1e..c9f99f00eb1 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.3.6"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.3.7"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 75e56353958..f3eb3c897da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -729,7 +729,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.3.6 +google-nest-sdm==0.3.7 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 507825fedd6..a63e5bde972 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -440,7 +440,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.3.6 +google-nest-sdm==0.3.7 # homeassistant.components.google_travel_time googlemaps==2.5.1 From b024d88b3642fe68e482bad9dcae76386d633dc4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 4 Oct 2021 23:58:20 -0700 Subject: [PATCH 0113/1038] Deprecate Python 3.8 (#57079) --- homeassistant/bootstrap.py | 12 +++++++++--- homeassistant/const.py | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index f2b3d5e6ec4..f111ff6a079 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -17,7 +17,10 @@ import yarl from homeassistant import config as conf_util, config_entries, core, loader from homeassistant.components import http -from homeassistant.const import REQUIRED_NEXT_PYTHON_DATE, REQUIRED_NEXT_PYTHON_VER +from homeassistant.const import ( + REQUIRED_NEXT_PYTHON_HA_RELEASE, + REQUIRED_NEXT_PYTHON_VER, +) from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import area_registry, device_registry, entity_registry from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -240,11 +243,14 @@ async def async_from_config_dict( stop = monotonic() _LOGGER.info("Home Assistant initialized in %.2fs", stop - start) - if REQUIRED_NEXT_PYTHON_DATE and sys.version_info[:3] < REQUIRED_NEXT_PYTHON_VER: + if ( + REQUIRED_NEXT_PYTHON_HA_RELEASE + and sys.version_info[:3] < REQUIRED_NEXT_PYTHON_VER + ): msg = ( "Support for the running Python version " f"{'.'.join(str(x) for x in sys.version_info[:3])} is deprecated and will " - f"be removed in the first release after {REQUIRED_NEXT_PYTHON_DATE}. " + f"be removed in Home Assistant {REQUIRED_NEXT_PYTHON_HA_RELEASE}. " "Please upgrade Python to " f"{'.'.join(str(x) for x in REQUIRED_NEXT_PYTHON_VER)} or " "higher." diff --git a/homeassistant/const.py b/homeassistant/const.py index 2f408491d29..483109737ed 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -11,7 +11,7 @@ __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) # Truthy date string triggers showing related deprecation warning messages. REQUIRED_NEXT_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) -REQUIRED_NEXT_PYTHON_DATE: Final = "" +REQUIRED_NEXT_PYTHON_HA_RELEASE: Final = "2022.1" # Format for platform files PLATFORM_FORMAT: Final = "{platform}.{domain}" From 59b1433e5c4227cc8c993cf6074395068c9e2692 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 5 Oct 2021 09:17:45 +0200 Subject: [PATCH 0114/1038] Additional place to use isinstance rather than do a string compare (#57094) --- homeassistant/components/deconz/binary_sensor.py | 4 ++-- homeassistant/components/deconz/sensor.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 33b68f25cab..e0479d9a0c6 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -149,12 +149,12 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorEntity): if self._device.secondary_temperature is not None: attr[ATTR_TEMPERATURE] = self._device.secondary_temperature - if self._device.type in Presence.ZHATYPE: + if isinstance(self._device, Presence): if self._device.dark is not None: attr[ATTR_DARK] = self._device.dark - elif self._device.type in Vibration.ZHATYPE: + elif isinstance(self._device, Vibration): attr[ATTR_ORIENTATION] = self._device.orientation attr[ATTR_TILTANGLE] = self._device.tilt_angle attr[ATTR_VIBRATIONSTRENGTH] = self._device.vibration_strength diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 8b82c2fa7bf..ba8e5c8adec 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -211,13 +211,13 @@ class DeconzSensor(DeconzDevice, SensorEntity): if self._device.secondary_temperature is not None: attr[ATTR_TEMPERATURE] = self._device.secondary_temperature - if self._device.type in Consumption.ZHATYPE: + if isinstance(self._device, Consumption): attr[ATTR_POWER] = self._device.power - elif self._device.type in Daylight.ZHATYPE: + elif isinstance(self._device, Daylight): attr[ATTR_DAYLIGHT] = self._device.daylight - elif self._device.type in LightLevel.ZHATYPE: + elif isinstance(self._device, LightLevel): if self._device.dark is not None: attr[ATTR_DARK] = self._device.dark @@ -225,7 +225,7 @@ class DeconzSensor(DeconzDevice, SensorEntity): if self._device.daylight is not None: attr[ATTR_DAYLIGHT] = self._device.daylight - elif self._device.type in Power.ZHATYPE: + elif isinstance(self._device, Power): attr[ATTR_CURRENT] = self._device.current attr[ATTR_VOLTAGE] = self._device.voltage From f364d53c7b73ade8d8cdc6aea35a51736f8b25a9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 5 Oct 2021 10:22:28 +0200 Subject: [PATCH 0115/1038] Prevent Tuya from accidentally logging credentials in debug mode (#57100) --- homeassistant/components/tuya/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index c59e29ba348..ffaf36ece8e 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -44,9 +44,6 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Async setup hass config entry.""" - - _LOGGER.debug("tuya.__init__.async_setup_entry-->%s", entry.data) - hass.data[DOMAIN] = {entry.entry_id: {TUYA_HA_TUYA_MAP: {}, TUYA_HA_DEVICES: set()}} success = await _init_tuya_sdk(hass, entry) From 3645568dcc993cdbbb2b4e56e4e0a6d7c2b3f719 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 5 Oct 2021 11:03:51 +0200 Subject: [PATCH 0116/1038] Upgrade jinja2 to 3.0.2 (#57095) --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a496bc80169..5e2e891f265 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -18,7 +18,7 @@ hass-nabucasa==0.50.0 home-assistant-frontend==20211004.0 httpx==0.19.0 ifaddr==0.1.7 -jinja2==3.0.1 +jinja2==3.0.2 paho-mqtt==1.5.1 pillow==8.2.0 pip>=8.0.3,<20.3 diff --git a/requirements.txt b/requirements.txt index edb9253b7f7..cd0a920111d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 httpx==0.19.0 -jinja2==3.0.1 +jinja2==3.0.2 PyJWT==2.1.0 cryptography==3.4.8 pip>=8.0.3,<20.3 diff --git a/setup.py b/setup.py index 464aa484fcb..f2f17f67c75 100755 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ REQUIRES = [ "certifi>=2021.5.30", "ciso8601==2.2.0", "httpx==0.19.0", - "jinja2==3.0.1", + "jinja2==3.0.2", "PyJWT==2.1.0", # PyJWT has loose dependency. We want the latest one. "cryptography==3.4.8", From b5916c8310620f1bc56c0330e4097a226fe81ecd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 5 Oct 2021 11:11:33 +0200 Subject: [PATCH 0117/1038] Upgrade sentry-sdk to 1.4.3 (#57096) --- 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 8f6ebb57603..68e6e05405b 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.4.1"], + "requirements": ["sentry-sdk==1.4.3"], "codeowners": ["@dcramer", "@frenck"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index f3eb3c897da..496f1fa18d4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2128,7 +2128,7 @@ sense-hat==2.2.0 sense_energy==0.9.2 # homeassistant.components.sentry -sentry-sdk==1.4.1 +sentry-sdk==1.4.3 # homeassistant.components.sharkiq sharkiqpy==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a63e5bde972..f46af0dcbbd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1218,7 +1218,7 @@ screenlogicpy==0.4.1 sense_energy==0.9.2 # homeassistant.components.sentry -sentry-sdk==1.4.1 +sentry-sdk==1.4.3 # homeassistant.components.sharkiq sharkiqpy==0.1.8 From 653eb3e29d8a719dc9995e2e3a23f1393573f5bc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 5 Oct 2021 11:12:35 +0200 Subject: [PATCH 0118/1038] Upgrade debugpy to 1.5.0 (#57098) --- 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 8d6cab13e62..c8da95a006f 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.4.3"], + "requirements": ["debugpy==1.5.0"], "codeowners": ["@frenck"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 496f1fa18d4..148e2a7a33b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -511,7 +511,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.debugpy -debugpy==1.4.3 +debugpy==1.5.0 # homeassistant.components.decora # decora==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f46af0dcbbd..e7baf9aa00d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -316,7 +316,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.debugpy -debugpy==1.4.3 +debugpy==1.5.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 2059cbacbdce9e3e8bec7f318c14347dbfac2737 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 5 Oct 2021 11:12:55 +0200 Subject: [PATCH 0119/1038] Remove Python shebang line from Tuya integration files (#57103) --- homeassistant/components/tuya/__init__.py | 1 - homeassistant/components/tuya/base.py | 1 - homeassistant/components/tuya/const.py | 1 - homeassistant/components/tuya/switch.py | 1 - 4 files changed, 4 deletions(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index ffaf36ece8e..c356af62509 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 """Support for Tuya Smart devices.""" import itertools diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index 572c452a920..86a508a8180 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 """Tuya Home Assistant Base Device Model.""" from __future__ import annotations diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index f86180226ee..dd18309d128 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 """Constants for the Tuya integration.""" DOMAIN = "tuya" diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index ab34ebbdfc0..5bafbe1b7f6 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 """Support for Tuya switches.""" from __future__ import annotations From f548d1dba7495ddba09e52378b78748d3f8ef00f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 5 Oct 2021 11:14:53 +0200 Subject: [PATCH 0120/1038] Prevent opening of sockets in mqtt tests (#57101) --- tests/components/mqtt/test_config_flow.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index e00e959e606..208541d702c 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -29,7 +29,9 @@ def mock_try_connection(): yield mock_try -async def test_user_connection_works(hass, mock_try_connection, mock_finish_setup): +async def test_user_connection_works( + hass, mock_try_connection, mock_finish_setup, mqtt_client_mock +): """Test we can finish a config flow.""" mock_try_connection.return_value = True @@ -76,7 +78,9 @@ async def test_user_connection_fails(hass, mock_try_connection, mock_finish_setu assert len(mock_finish_setup.mock_calls) == 0 -async def test_manual_config_set(hass, mock_try_connection, mock_finish_setup): +async def test_manual_config_set( + hass, mock_try_connection, mock_finish_setup, mqtt_client_mock +): """Test we ignore entry if manual config available.""" assert await async_setup_component(hass, "mqtt", {"mqtt": {"broker": "bla"}}) await hass.async_block_till_done() @@ -128,7 +132,9 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: assert result.get("reason") == "already_configured" -async def test_hassio_confirm(hass, mock_try_connection, mock_finish_setup): +async def test_hassio_confirm( + hass, mock_try_connection, mock_finish_setup, mqtt_client_mock +): """Test we can finish a config flow.""" mock_try_connection.return_value = True @@ -464,6 +470,9 @@ async def test_option_flow_default_suggested_values( ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + # Make sure all MQTT related jobs are done before ending the test + await hass.async_block_till_done() + async def test_options_user_connection_fails(hass, mock_try_connection): """Test if connection cannot be made.""" From 8b02703585ddb8b5b65a14690ae526530e9ab0b8 Mon Sep 17 00:00:00 2001 From: Boris K Date: Tue, 5 Oct 2021 11:19:09 +0200 Subject: [PATCH 0121/1038] Fix color util links to Philips Hue documentation (#57099) --- homeassistant/util/color.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index ebd3b175905..1cfd6447e8a 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -218,7 +218,7 @@ def color_RGB_to_xy( # Taken from: -# http://www.developers.meethue.com/documentation/color-conversions-rgb-xy +# https://github.com/PhilipsHue/PhilipsHueSDK-iOS-OSX/blob/00187a3/ApplicationDesignNotes/RGB%20to%20xy%20Color%20conversion.md # License: Code is given as is. Use at your own risk and discretion. def color_RGB_to_xy_brightness( iR: int, iG: int, iB: int, Gamut: GamutType | None = None @@ -268,7 +268,7 @@ def color_xy_to_RGB( # Converted to Python from Obj-C, original source from: -# http://www.developers.meethue.com/documentation/color-conversions-rgb-xy +# https://github.com/PhilipsHue/PhilipsHueSDK-iOS-OSX/blob/00187a3/ApplicationDesignNotes/RGB%20to%20xy%20Color%20conversion.md def color_xy_brightness_to_RGB( vX: float, vY: float, ibrightness: int, Gamut: GamutType | None = None ) -> tuple[int, int, int]: From 5b218d7e1c4164e32d41473977459cbaf23adf42 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 5 Oct 2021 11:49:39 +0200 Subject: [PATCH 0122/1038] Bump aioesphomeapi from 9.1.4 to 9.1.5 (#57106) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 33801431994..307227be944 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==9.1.4"], + "requirements": ["aioesphomeapi==9.1.5"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], diff --git a/requirements_all.txt b/requirements_all.txt index 148e2a7a33b..363618c3e99 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -161,7 +161,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==9.1.4 +aioesphomeapi==9.1.5 # homeassistant.components.flo aioflo==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e7baf9aa00d..ab801c5f083 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -109,7 +109,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==9.1.4 +aioesphomeapi==9.1.5 # homeassistant.components.flo aioflo==0.4.1 From 80a04124a20757ff69c4fc3b6d30adfa82470630 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 5 Oct 2021 12:59:51 +0200 Subject: [PATCH 0123/1038] Small code styling tweaks for Tuya (#57102) --- homeassistant/components/tuya/__init__.py | 5 +---- homeassistant/components/tuya/base.py | 6 ++---- homeassistant/components/tuya/climate.py | 8 ++++---- homeassistant/components/tuya/light.py | 3 +-- homeassistant/components/tuya/scene.py | 7 +------ 5 files changed, 9 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index c356af62509..3602f9585af 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -46,10 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN] = {entry.entry_id: {TUYA_HA_TUYA_MAP: {}, TUYA_HA_DEVICES: set()}} success = await _init_tuya_sdk(hass, entry) - if not success: - return False - - return True + return bool(success) async def _init_tuya_sdk(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index 86a508a8180..a1f65227e95 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -24,10 +24,9 @@ class TuyaHaEntity(Entity): @staticmethod def remap(old_value, old_min, old_max, new_min, new_max): """Remap old_value to new_value.""" - new_value = ((old_value - old_min) / (old_max - old_min)) * ( + return ((old_value - old_min) / (old_max - old_min)) * ( new_max - new_min ) + new_min - return new_value @property def should_poll(self) -> bool: @@ -47,13 +46,12 @@ class TuyaHaEntity(Entity): @property def device_info(self): """Return a device description for device registry.""" - _device_info = { + return { "identifiers": {(DOMAIN, f"{self.tuya_device.id}")}, "manufacturer": "Tuya", "name": self.tuya_device.name, "model": self.tuya_device.product_name, } - return _device_info @property def available(self) -> bool: diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index 368a65b8499..810e8ad8aab 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -430,10 +430,10 @@ class TuyaHaClimate(TuyaHaEntity, ClimateEntity): @property def fan_modes(self) -> list[str]: """Return fan modes for select.""" - data = json.loads( - self.tuya_device.function.get(DPCODE_FAN_SPEED_ENUM, {}).values - ).get("range") - return data + fan_speed_device_function = self.tuya_device.function.get(DPCODE_FAN_SPEED_ENUM) + if not fan_speed_device_function: + return [] + return json.loads(fan_speed_device_function.values).get("range", []) @property def swing_mode(self) -> str: diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 180e3a68450..6a119e71ba9 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -299,7 +299,7 @@ class TuyaHaLight(TuyaHaEntity, LightEntity): """Return the color_temp of the light.""" new_range = self._tuya_temp_range() tuya_color_temp = self.tuya_device.status.get(self.dp_code_temp, 0) - ha_color_temp = ( + return ( self.max_mireds - self.remap( tuya_color_temp, @@ -310,7 +310,6 @@ class TuyaHaLight(TuyaHaEntity, LightEntity): ) + self.min_mireds ) - return ha_color_temp @property def min_mireds(self) -> int: diff --git a/homeassistant/components/tuya/scene.py b/homeassistant/components/tuya/scene.py index c6010f9ef87..c90c6798b9b 100644 --- a/homeassistant/components/tuya/scene.py +++ b/homeassistant/components/tuya/scene.py @@ -20,14 +20,9 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up tuya scenes.""" - entities = [] - home_manager = hass.data[DOMAIN][entry.entry_id][TUYA_HOME_MANAGER] scenes = await hass.async_add_executor_job(home_manager.query_scenes) - for scene in scenes: - entities.append(TuyaHAScene(home_manager, scene)) - - async_add_entities(entities) + async_add_entities(TuyaHAScene(home_manager, scene) for scene in scenes) class TuyaHAScene(Scene): From 3c074ab865855bd950f771161c2b02e4fb5d3f2a Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 5 Oct 2021 11:23:44 -0400 Subject: [PATCH 0124/1038] Add additional properties to node_status WS cmd (#56927) * Add node.zwave_plus_version to node_status WS command * Add highest security class to node_status --- homeassistant/components/zwave_js/api.py | 2 ++ tests/components/zwave_js/test_api.py | 2 ++ tests/fixtures/zwave_js/multisensor_6_state.json | 3 ++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 8057b900baa..38d5b99147d 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -342,6 +342,8 @@ async def websocket_node_status( "status": node.status, "is_secure": node.is_secure, "ready": node.ready, + "zwave_plus_version": node.zwave_plus_version, + "highest_security_class": node.highest_security_class, } connection.send_result( msg[ID], diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 44b9acf2db5..f41f5daefb3 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -147,6 +147,8 @@ async def test_node_status(hass, multisensor_6, integration, hass_ws_client): assert result["is_routing"] assert not result["is_secure"] assert result["status"] == 1 + assert result["zwave_plus_version"] == 1 + assert result["highest_security_class"] == SecurityClass.S0_LEGACY # Test getting non-existent node fails await ws_client.send_json( diff --git a/tests/fixtures/zwave_js/multisensor_6_state.json b/tests/fixtures/zwave_js/multisensor_6_state.json index 131a5aa026f..88cdf893d4a 100644 --- a/tests/fixtures/zwave_js/multisensor_6_state.json +++ b/tests/fixtures/zwave_js/multisensor_6_state.json @@ -1824,5 +1824,6 @@ "label": "Z-Wave chip hardware version" } } - ] + ], + "highestSecurityClass": 7 } From 86852df2fc38544c8df0f3c42462a18db595ea5c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 5 Oct 2021 19:21:55 +0200 Subject: [PATCH 0125/1038] Bump tuya-iot-py-sdk to 0.5.0 (#57110) Co-authored-by: Martin Hjelmare --- homeassistant/components/tuya/__init__.py | 26 +++++++++++++------- homeassistant/components/tuya/config_flow.py | 16 ++++++------ homeassistant/components/tuya/const.py | 1 + homeassistant/components/tuya/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tuya/test_config_flow.py | 8 +++--- 7 files changed, 33 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 3602f9585af..df4689268cf 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -4,7 +4,7 @@ import itertools import logging from tuya_iot import ( - ProjectType, + AuthType, TuyaDevice, TuyaDeviceListener, TuyaDeviceManager, @@ -22,6 +22,7 @@ from .const import ( CONF_ACCESS_ID, CONF_ACCESS_SECRET, CONF_APP_TYPE, + CONF_AUTH_TYPE, CONF_COUNTRY_CODE, CONF_ENDPOINT, CONF_PASSWORD, @@ -45,28 +46,35 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Async setup hass config entry.""" hass.data[DOMAIN] = {entry.entry_id: {TUYA_HA_TUYA_MAP: {}, TUYA_HA_DEVICES: set()}} + # Project type has been renamed to auth type in the upstream Tuya IoT SDK. + # This migrates existing config entries to reflect that name change. + if CONF_PROJECT_TYPE in entry.data: + data = {**entry.data, CONF_AUTH_TYPE: entry.data[CONF_PROJECT_TYPE]} + data.pop(CONF_PROJECT_TYPE) + hass.config_entries.async_update_entry(entry, data=data) + success = await _init_tuya_sdk(hass, entry) return bool(success) async def _init_tuya_sdk(hass: HomeAssistant, entry: ConfigEntry) -> bool: - project_type = ProjectType(entry.data[CONF_PROJECT_TYPE]) + auth_type = AuthType(entry.data[CONF_AUTH_TYPE]) api = TuyaOpenAPI( - entry.data[CONF_ENDPOINT], - entry.data[CONF_ACCESS_ID], - entry.data[CONF_ACCESS_SECRET], - project_type, + endpoint=entry.data[CONF_ENDPOINT], + access_id=entry.data[CONF_ACCESS_ID], + access_secret=entry.data[CONF_ACCESS_SECRET], + auth_type=auth_type, ) api.set_dev_channel("hass") - if project_type == ProjectType.INDUSTY_SOLUTIONS: + if auth_type == AuthType.CUSTOM: response = await hass.async_add_executor_job( - api.login, entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD] + api.connect, entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD] ) else: response = await hass.async_add_executor_job( - api.login, + api.connect, entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], entry.data[CONF_COUNTRY_CODE], diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py index 1b439d49007..8fffed3cd9f 100644 --- a/homeassistant/components/tuya/config_flow.py +++ b/homeassistant/components/tuya/config_flow.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging from typing import Any -from tuya_iot import ProjectType, TuyaOpenAPI +from tuya_iot import AuthType, TuyaOpenAPI import voluptuous as vol from voluptuous.schema_builder import UNDEFINED @@ -14,10 +14,10 @@ from .const import ( CONF_ACCESS_ID, CONF_ACCESS_SECRET, CONF_APP_TYPE, + CONF_AUTH_TYPE, CONF_COUNTRY_CODE, CONF_ENDPOINT, CONF_PASSWORD, - CONF_PROJECT_TYPE, CONF_REGION, CONF_USERNAME, DOMAIN, @@ -44,7 +44,7 @@ class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): data = { CONF_ENDPOINT: TUYA_REGIONS[user_input[CONF_REGION]], - CONF_PROJECT_TYPE: ProjectType.INDUSTY_SOLUTIONS, + CONF_AUTH_TYPE: AuthType.CUSTOM, CONF_ACCESS_ID: user_input[CONF_ACCESS_ID], CONF_ACCESS_SECRET: user_input[CONF_ACCESS_SECRET], CONF_USERNAME: user_input[CONF_USERNAME], @@ -55,19 +55,19 @@ class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): for app_type in ("", TUYA_SMART_APP, SMARTLIFE_APP): data[CONF_APP_TYPE] = app_type if data[CONF_APP_TYPE] == "": - data[CONF_PROJECT_TYPE] = ProjectType.INDUSTY_SOLUTIONS + data[CONF_AUTH_TYPE] = AuthType.CUSTOM else: - data[CONF_PROJECT_TYPE] = ProjectType.SMART_HOME + data[CONF_AUTH_TYPE] = AuthType.SMART_HOME api = TuyaOpenAPI( endpoint=data[CONF_ENDPOINT], access_id=data[CONF_ACCESS_ID], access_secret=data[CONF_ACCESS_SECRET], - project_type=data[CONF_PROJECT_TYPE], + auth_type=data[CONF_AUTH_TYPE], ) api.set_dev_channel("hass") - response = api.login( + response = api.connect( username=data[CONF_USERNAME], password=data[CONF_PASSWORD], country_code=data[CONF_COUNTRY_CODE], @@ -97,7 +97,7 @@ class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ): data[CONF_ENDPOINT] = endpoint - data[CONF_PROJECT_TYPE] = data[CONF_PROJECT_TYPE].value + data[CONF_AUTH_TYPE] = data[CONF_AUTH_TYPE].value return self.async_create_entry( title=user_input[CONF_USERNAME], diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index dd18309d128..7c6440d7e48 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -2,6 +2,7 @@ DOMAIN = "tuya" +CONF_AUTH_TYPE = "auth_type" CONF_PROJECT_TYPE = "tuya_project_type" CONF_ENDPOINT = "endpoint" CONF_ACCESS_ID = "access_id" diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index 0097e7635ec..20df33f4573 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -2,7 +2,7 @@ "domain": "tuya", "name": "Tuya", "documentation": "https://github.com/tuya/tuya-home-assistant", - "requirements": ["tuya-iot-py-sdk==0.4.1"], + "requirements": ["tuya-iot-py-sdk==0.5.0"], "codeowners": ["@Tuya", "@zlinoliver", "@METISU"], "config_flow": true, "iot_class": "cloud_push" diff --git a/requirements_all.txt b/requirements_all.txt index 363618c3e99..d15070ddfbb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2329,7 +2329,7 @@ tp-connected==0.0.4 transmissionrpc==0.11 # homeassistant.components.tuya -tuya-iot-py-sdk==0.4.1 +tuya-iot-py-sdk==0.5.0 # homeassistant.components.twentemilieu twentemilieu==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ab801c5f083..5c0fb2fc377 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1318,7 +1318,7 @@ total_connect_client==0.57 transmissionrpc==0.11 # homeassistant.components.tuya -tuya-iot-py-sdk==0.4.1 +tuya-iot-py-sdk==0.5.0 # homeassistant.components.twentemilieu twentemilieu==0.3.0 diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index 04fb8ebe009..745bcfde661 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -11,9 +11,9 @@ from homeassistant.components.tuya.const import ( CONF_ACCESS_ID, CONF_ACCESS_SECRET, CONF_APP_TYPE, + CONF_AUTH_TYPE, CONF_ENDPOINT, CONF_PASSWORD, - CONF_PROJECT_TYPE, CONF_REGION, CONF_USERNAME, DOMAIN, @@ -86,7 +86,7 @@ async def test_user_flow( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" - tuya().login = MagicMock(side_effect=side_effects) + tuya().connect = MagicMock(side_effect=side_effects) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=TUYA_INPUT_DATA ) @@ -101,7 +101,7 @@ async def test_user_flow( assert result["data"][CONF_ENDPOINT] == MOCK_ENDPOINT assert result["data"][CONF_ENDPOINT] != TUYA_REGIONS[TUYA_INPUT_DATA[CONF_REGION]] assert result["data"][CONF_APP_TYPE] == app_type - assert result["data"][CONF_PROJECT_TYPE] == project_type + assert result["data"][CONF_AUTH_TYPE] == project_type assert not result["result"].unique_id @@ -115,7 +115,7 @@ async def test_error_on_invalid_credentials(hass, tuya): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" - tuya().login = MagicMock(return_value=RESPONSE_ERROR) + tuya().connect = MagicMock(return_value=RESPONSE_ERROR) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=TUYA_INPUT_DATA ) From e66064fb3e22911affa16a9b6ade079495b4aac6 Mon Sep 17 00:00:00 2001 From: jrester <31157644+jrester@users.noreply.github.com> Date: Tue, 5 Oct 2021 19:40:37 +0200 Subject: [PATCH 0126/1038] Update tesla_powerwall to 0.3.11 (#57112) --- homeassistant/components/powerwall/binary_sensor.py | 8 ++++++-- homeassistant/components/powerwall/manifest.json | 2 +- homeassistant/components/powerwall/sensor.py | 3 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/powerwall/binary_sensor.py b/homeassistant/components/powerwall/binary_sensor.py index 1cacaa5fc42..1b097b93408 100644 --- a/homeassistant/components/powerwall/binary_sensor.py +++ b/homeassistant/components/powerwall/binary_sensor.py @@ -1,5 +1,5 @@ """Support for powerwall binary sensors.""" -from tesla_powerwall import GridStatus +from tesla_powerwall import GridStatus, MeterType from homeassistant.components.binary_sensor import ( DEVICE_CLASS_BATTERY_CHARGING, @@ -142,4 +142,8 @@ class PowerWallChargingStatusSensor(PowerWallEntity, BinarySensorEntity): def is_on(self): """Powerwall is charging.""" # is_sending_to returns true for values greater than 100 watts - return self.coordinator.data[POWERWALL_API_METERS].battery.is_sending_to() + return ( + self.coordinator.data[POWERWALL_API_METERS] + .get_meter(MeterType.BATTERY) + .is_sending_to() + ) diff --git a/homeassistant/components/powerwall/manifest.json b/homeassistant/components/powerwall/manifest.json index 5cee6c1fd19..802d1fdf5e3 100644 --- a/homeassistant/components/powerwall/manifest.json +++ b/homeassistant/components/powerwall/manifest.json @@ -3,7 +3,7 @@ "name": "Tesla Powerwall", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/powerwall", - "requirements": ["tesla-powerwall==0.3.10"], + "requirements": ["tesla-powerwall==0.3.11"], "codeowners": ["@bdraco", "@jrester"], "dhcp": [ { diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index 940dcad8647..8c45a142206 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -53,7 +53,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): powerwalls_serial_numbers = powerwall_data[POWERWALL_API_SERIAL_NUMBERS] entities = [] - for meter in MeterType: + # coordinator.data[POWERWALL_API_METERS].meters holds all meters that are available + for meter in coordinator.data[POWERWALL_API_METERS].meters: entities.append( PowerWallEnergySensor( meter, diff --git a/requirements_all.txt b/requirements_all.txt index d15070ddfbb..87ba2e555ed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2296,7 +2296,7 @@ temperusb==1.5.3 # tensorflow==2.3.0 # homeassistant.components.powerwall -tesla-powerwall==0.3.10 +tesla-powerwall==0.3.11 # homeassistant.components.tensorflow # tf-models-official==2.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5c0fb2fc377..5c36b71873c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1306,7 +1306,7 @@ systembridge==2.1.0 tellduslive==0.10.11 # homeassistant.components.powerwall -tesla-powerwall==0.3.10 +tesla-powerwall==0.3.11 # homeassistant.components.toon toonapi==0.2.1 From 1a7a4c52f12e764ca67895266f076468172e9f0f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 5 Oct 2021 10:42:45 -0700 Subject: [PATCH 0127/1038] Bump aiohue to 2.6.3 (#57125) --- 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 954ad1f7a7b..6640ffc9fae 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==2.6.2"], + "requirements": ["aiohue==2.6.3"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 87ba2e555ed..fc39a991040 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -186,7 +186,7 @@ aiohomekit==0.6.3 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==2.6.2 +aiohue==2.6.3 # homeassistant.components.imap aioimaplib==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5c36b71873c..020827761db 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -128,7 +128,7 @@ aiohomekit==0.6.3 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==2.6.2 +aiohue==2.6.3 # homeassistant.components.apache_kafka aiokafka==0.6.0 From f76cb129458f94534fe44e44b51c3393cd256270 Mon Sep 17 00:00:00 2001 From: indykoning <15870933+indykoning@users.noreply.github.com> Date: Tue, 5 Oct 2021 21:31:23 +0200 Subject: [PATCH 0128/1038] Fix Growatt login invalid auth response (#57071) --- .../components/growatt_server/config_flow.py | 13 +++++++++++-- homeassistant/components/growatt_server/const.py | 2 ++ homeassistant/components/growatt_server/sensor.py | 7 +++++-- tests/components/growatt_server/test_config_flow.py | 3 ++- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/growatt_server/config_flow.py b/homeassistant/components/growatt_server/config_flow.py index c4a97a81f0a..11f082f1eab 100644 --- a/homeassistant/components/growatt_server/config_flow.py +++ b/homeassistant/components/growatt_server/config_flow.py @@ -6,7 +6,13 @@ from homeassistant import config_entries from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME from homeassistant.core import callback -from .const import CONF_PLANT_ID, DEFAULT_URL, DOMAIN, SERVER_URLS +from .const import ( + CONF_PLANT_ID, + DEFAULT_URL, + DOMAIN, + LOGIN_INVALID_AUTH_CODE, + SERVER_URLS, +) class GrowattServerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -45,7 +51,10 @@ class GrowattServerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.api.login, user_input[CONF_USERNAME], user_input[CONF_PASSWORD] ) - if not login_response["success"] and login_response["errCode"] == "102": + if ( + not login_response["success"] + and login_response["msg"] == LOGIN_INVALID_AUTH_CODE + ): return self._async_show_user_form({"base": "invalid_auth"}) self.user_id = login_response["user"]["id"] diff --git a/homeassistant/components/growatt_server/const.py b/homeassistant/components/growatt_server/const.py index e0297de5eff..5425e26c806 100644 --- a/homeassistant/components/growatt_server/const.py +++ b/homeassistant/components/growatt_server/const.py @@ -16,3 +16,5 @@ DEFAULT_URL = SERVER_URLS[0] DOMAIN = "growatt_server" PLATFORMS = ["sensor"] + +LOGIN_INVALID_AUTH_CODE = "502" diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 9f0fa509105..804d4157543 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -37,7 +37,7 @@ from homeassistant.const import ( ) from homeassistant.util import Throttle, dt -from .const import CONF_PLANT_ID, DEFAULT_PLANT_ID, DEFAULT_URL +from .const import CONF_PLANT_ID, DEFAULT_PLANT_ID, DEFAULT_URL, LOGIN_INVALID_AUTH_CODE _LOGGER = logging.getLogger(__name__) @@ -876,7 +876,10 @@ def get_device_list(api, config): # Log in to api and fetch first plant if no plant id is defined. login_response = api.login(config[CONF_USERNAME], config[CONF_PASSWORD]) - if not login_response["success"] and login_response["errCode"] == "102": + if ( + not login_response["success"] + and login_response["msg"] == LOGIN_INVALID_AUTH_CODE + ): _LOGGER.error("Username, Password or URL may be incorrect!") return user_id = login_response["user"]["id"] diff --git a/tests/components/growatt_server/test_config_flow.py b/tests/components/growatt_server/test_config_flow.py index db46ed36911..ba52e09296c 100644 --- a/tests/components/growatt_server/test_config_flow.py +++ b/tests/components/growatt_server/test_config_flow.py @@ -7,6 +7,7 @@ from homeassistant.components.growatt_server.const import ( CONF_PLANT_ID, DEFAULT_URL, DOMAIN, + LOGIN_INVALID_AUTH_CODE, ) from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME @@ -61,7 +62,7 @@ async def test_incorrect_login(hass): with patch( "growattServer.GrowattApi.login", - return_value={"errCode": "102", "success": False}, + return_value={"msg": LOGIN_INVALID_AUTH_CODE, "success": False}, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_USER_INPUT From 34544da449828a84290402840bac31695cae9044 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 5 Oct 2021 13:32:48 -0700 Subject: [PATCH 0129/1038] Reinstate asking for country in Tuya flow (#57142) --- homeassistant/components/tuya/__init__.py | 17 +- homeassistant/components/tuya/config_flow.py | 37 ++- homeassistant/components/tuya/const.py | 270 +++++++++++++++++- homeassistant/components/tuya/strings.json | 4 +- .../components/tuya/translations/en.json | 67 +---- tests/components/tuya/test_config_flow.py | 17 +- 6 files changed, 312 insertions(+), 100 deletions(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index df4689268cf..28c43d8df46 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -44,7 +44,10 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Async setup hass config entry.""" - hass.data[DOMAIN] = {entry.entry_id: {TUYA_HA_TUYA_MAP: {}, TUYA_HA_DEVICES: set()}} + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + TUYA_HA_TUYA_MAP: {}, + TUYA_HA_DEVICES: set(), + } # Project type has been renamed to auth type in the upstream Tuya IoT SDK. # This migrates existing config entries to reflect that name change. @@ -54,6 +57,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_update_entry(entry, data=data) success = await _init_tuya_sdk(hass, entry) + + if not success: + hass.data[DOMAIN].pop(entry.entry_id) + + if not hass.data[DOMAIN]: + hass.data.pop(DOMAIN) + return bool(success) @@ -143,7 +153,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id][TUYA_MQTT_LISTENER] ) - hass.data.pop(DOMAIN) + hass.data[DOMAIN].pop(entry.entry_id) + + if not hass.data[DOMAIN]: + hass.data.pop(DOMAIN) return unload diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py index 8fffed3cd9f..bcde364ae1b 100644 --- a/homeassistant/components/tuya/config_flow.py +++ b/homeassistant/components/tuya/config_flow.py @@ -6,7 +6,6 @@ from typing import Any from tuya_iot import AuthType, TuyaOpenAPI import voluptuous as vol -from voluptuous.schema_builder import UNDEFINED from homeassistant import config_entries @@ -18,11 +17,10 @@ from .const import ( CONF_COUNTRY_CODE, CONF_ENDPOINT, CONF_PASSWORD, - CONF_REGION, CONF_USERNAME, DOMAIN, SMARTLIFE_APP, - TUYA_REGIONS, + TUYA_COUNTRIES, TUYA_RESPONSE_CODE, TUYA_RESPONSE_MSG, TUYA_RESPONSE_PLATFROM_URL, @@ -42,14 +40,20 @@ class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Try login.""" response = {} + country = [ + country + for country in TUYA_COUNTRIES + if country.name == user_input[CONF_COUNTRY_CODE] + ][0] + data = { - CONF_ENDPOINT: TUYA_REGIONS[user_input[CONF_REGION]], + CONF_ENDPOINT: country.endpoint, CONF_AUTH_TYPE: AuthType.CUSTOM, CONF_ACCESS_ID: user_input[CONF_ACCESS_ID], CONF_ACCESS_SECRET: user_input[CONF_ACCESS_SECRET], CONF_USERNAME: user_input[CONF_USERNAME], CONF_PASSWORD: user_input[CONF_PASSWORD], - CONF_COUNTRY_CODE: user_input[CONF_REGION], + CONF_COUNTRY_CODE: country.country_code, } for app_type in ("", TUYA_SMART_APP, SMARTLIFE_APP): @@ -109,29 +113,32 @@ class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): TUYA_RESPONSE_MSG: response.get(TUYA_RESPONSE_MSG), } - def _schema_default(key: str) -> str | UNDEFINED: - if not user_input: - return UNDEFINED - return user_input[key] + if user_input is None: + user_input = {} return self.async_show_form( step_id="user", data_schema=vol.Schema( { vol.Required( - CONF_REGION, default=_schema_default(CONF_REGION) - ): vol.In(TUYA_REGIONS.keys()), + CONF_COUNTRY_CODE, + default=user_input.get(CONF_COUNTRY_CODE, "United States"), + ): vol.In( + # We don't pass a dict {code:name} because country codes can be duplicate. + [country.name for country in TUYA_COUNTRIES] + ), vol.Required( - CONF_ACCESS_ID, default=_schema_default(CONF_ACCESS_ID) + CONF_ACCESS_ID, default=user_input.get(CONF_ACCESS_ID, "") ): str, vol.Required( - CONF_ACCESS_SECRET, default=_schema_default(CONF_ACCESS_SECRET) + CONF_ACCESS_SECRET, + default=user_input.get(CONF_ACCESS_SECRET, ""), ): str, vol.Required( - CONF_USERNAME, default=_schema_default(CONF_USERNAME) + CONF_USERNAME, default=user_input.get(CONF_USERNAME, "") ): str, vol.Required( - CONF_PASSWORD, default=_schema_default(CONF_PASSWORD) + CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "") ): str, } ), diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 7c6440d7e48..44b66b576e3 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -1,4 +1,5 @@ """Constants for the Tuya integration.""" +from dataclasses import dataclass DOMAIN = "tuya" @@ -9,7 +10,6 @@ CONF_ACCESS_ID = "access_id" CONF_ACCESS_SECRET = "access_secret" CONF_USERNAME = "username" CONF_PASSWORD = "password" -CONF_REGION = "region" CONF_COUNTRY_CODE = "country_code" CONF_APP_TYPE = "tuya_app_type" @@ -31,13 +31,265 @@ TUYA_HA_SIGNAL_UPDATE_ENTITY = "tuya_entry_update" TUYA_SMART_APP = "tuyaSmart" SMARTLIFE_APP = "smartlife" -TUYA_REGIONS = { - "America": "https://openapi.tuyaus.com", - "China": "https://openapi.tuyacn.com", - "Eastern America": "https://openapi-ueaz.tuyaus.com", - "Europe": "https://openapi.tuyaeu.com", - "India": "https://openapi.tuyain.com", - "Western Europe": "https://openapi-weaz.tuyaeu.com", -} +ENDPOINT_AMERICA = "https://openapi.tuyaus.com" +ENDPOINT_CHINA = "https://openapi.tuyacn.com" +ENDPOINT_EASTERN_AMERICA = "https://openapi-ueaz.tuyaus.com" +ENDPOINT_EUROPE = "https://openapi.tuyaeu.com" +ENDPOINT_INDIA = "https://openapi.tuyain.com" +ENDPOINT_WESTERN_EUROPE = "https://openapi-weaz.tuyaeu.com" PLATFORMS = ["climate", "fan", "light", "scene", "switch"] + + +@dataclass +class Country: + """Describe a supported country.""" + + name: str + country_code: str + endpoint: str = ENDPOINT_AMERICA + + +# https://developer.tuya.com/en/docs/iot/oem-app-data-center-distributed?id=Kafi0ku9l07qb#title-4-China%20Data%20Center +TUYA_COUNTRIES = [ + Country("Afghanistan", "93"), + Country("Albania", "355"), + Country("Algeria", "213"), + Country("American Samoa", "1-684"), + Country("Andorra", "376"), + Country("Angola", "244"), + Country("Anguilla", "1-264"), + Country("Antarctica", "672"), + Country("Antigua and Barbuda", "1-268"), + Country("Argentina", "54", ENDPOINT_EUROPE), + Country("Armenia", "374"), + Country("Aruba", "297"), + Country("Australia", "61"), + Country("Austria", "43", ENDPOINT_EUROPE), + Country("Azerbaijan", "994"), + Country("Bahamas", "1-242"), + Country("Bahrain", "973"), + Country("Bangladesh", "880"), + Country("Barbados", "1-246"), + Country("Belarus", "375"), + Country("Belgium", "32", ENDPOINT_EUROPE), + Country("Belize", "501"), + Country("Benin", "229"), + Country("Bermuda", "1-441"), + Country("Bhutan", "975"), + Country("Bolivia", "591"), + Country("Bosnia and Herzegovina", "387"), + Country("Botswana", "267"), + Country("Brazil", "55", ENDPOINT_EUROPE), + Country("British Indian Ocean Territory", "246"), + Country("British Virgin Islands", "1-284"), + Country("Brunei", "673"), + Country("Bulgaria", "359"), + Country("Burkina Faso", "226"), + Country("Burundi", "257"), + Country("Cambodia", "855"), + Country("Cameroon", "237"), + Country("Canada", "1", ENDPOINT_AMERICA), + Country("Cape Verde", "238"), + Country("Cayman Islands", "1-345"), + Country("Central African Republic", "236"), + Country("Chad", "235"), + Country("Chile", "56"), + Country("China", "86", ENDPOINT_CHINA), + Country("Christmas Island", "61"), + Country("Cocos Islands", "61"), + Country("Colombia", "57"), + Country("Comoros", "269"), + Country("Cook Islands", "682"), + Country("Costa Rica", "506"), + Country("Croatia", "385", ENDPOINT_EUROPE), + Country("Cuba", "53"), + Country("Curacao", "599"), + Country("Cyprus", "357", ENDPOINT_EUROPE), + Country("Czech Republic", "420", ENDPOINT_EUROPE), + Country("Democratic Republic of the Congo", "243"), + Country("Denmark", "45", ENDPOINT_EUROPE), + Country("Djibouti", "253"), + Country("Dominica", "1-767"), + Country("Dominican Republic", "1-809"), + Country("East Timor", "670"), + Country("Ecuador", "593"), + Country("Egypt", "20"), + Country("El Salvador", "503"), + Country("Equatorial Guinea", "240"), + Country("Eritrea", "291"), + Country("Estonia", "372", ENDPOINT_EUROPE), + Country("Ethiopia", "251"), + Country("Falkland Islands", "500"), + Country("Faroe Islands", "298"), + Country("Fiji", "679"), + Country("Finland", "358", ENDPOINT_EUROPE), + Country("France", "33", ENDPOINT_EUROPE), + Country("French Polynesia", "689"), + Country("Gabon", "241"), + Country("Gambia", "220"), + Country("Georgia", "995"), + Country("Germany", "49", ENDPOINT_EUROPE), + Country("Ghana", "233"), + Country("Gibraltar", "350"), + Country("Greece", "30", ENDPOINT_EUROPE), + Country("Greenland", "299"), + Country("Grenada", "1-473"), + Country("Guam", "1-671"), + Country("Guatemala", "502"), + Country("Guernsey", "44-1481"), + Country("Guinea", "224"), + Country("Guinea-Bissau", "245"), + Country("Guyana", "592"), + Country("Haiti", "509"), + Country("Honduras", "504"), + Country("Hong Kong", "852"), + Country("Hungary", "36", ENDPOINT_EUROPE), + Country("Iceland", "354", ENDPOINT_EUROPE), + Country("India", "91", ENDPOINT_INDIA), + Country("Indonesia", "62"), + Country("Iran", "98"), + Country("Iraq", "964"), + Country("Ireland", "353", ENDPOINT_EUROPE), + Country("Isle of Man", "44-1624"), + Country("Israel", "972"), + Country("Italy", "39", ENDPOINT_EUROPE), + Country("Ivory Coast", "225"), + Country("Jamaica", "1-876"), + Country("Japan", "81", ENDPOINT_EUROPE), + Country("Jersey", "44-1534"), + Country("Jordan", "962"), + Country("Kazakhstan", "7"), + Country("Kenya", "254"), + Country("Kiribati", "686"), + Country("Kosovo", "383"), + Country("Kuwait", "965"), + Country("Kyrgyzstan", "996"), + Country("Laos", "856"), + Country("Latvia", "371", ENDPOINT_EUROPE), + Country("Lebanon", "961"), + Country("Lesotho", "266"), + Country("Liberia", "231"), + Country("Libya", "218"), + Country("Liechtenstein", "423", ENDPOINT_EUROPE), + Country("Lithuania", "370", ENDPOINT_EUROPE), + Country("Luxembourg", "352", ENDPOINT_EUROPE), + Country("Macau", "853"), + Country("Macedonia", "389"), + Country("Madagascar", "261"), + Country("Malawi", "265"), + Country("Malaysia", "60"), + Country("Maldives", "960"), + Country("Mali", "223"), + Country("Malta", "356", ENDPOINT_EUROPE), + Country("Marshall Islands", "692"), + Country("Mauritania", "222"), + Country("Mauritius", "230"), + Country("Mayotte", "262"), + Country("Mexico", "52"), + Country("Micronesia", "691"), + Country("Moldova", "373"), + Country("Monaco", "377"), + Country("Mongolia", "976"), + Country("Montenegro", "382"), + Country("Montserrat", "1-664"), + Country("Morocco", "212"), + Country("Mozambique", "258"), + Country("Myanmar", "95"), + Country("Namibia", "264"), + Country("Nauru", "674"), + Country("Nepal", "977"), + Country("Netherlands", "31", ENDPOINT_EUROPE), + Country("Netherlands Antilles", "599"), + Country("New Caledonia", "687"), + Country("New Zealand", "64"), + Country("Nicaragua", "505"), + Country("Niger", "227"), + Country("Nigeria", "234"), + Country("Niue", "683"), + Country("North Korea", "850"), + Country("Northern Mariana Islands", "1-670"), + Country("Norway", "47"), + Country("Oman", "968"), + Country("Pakistan", "92"), + Country("Palau", "680"), + Country("Palestine", "970"), + Country("Panama", "507"), + Country("Papua New Guinea", "675"), + Country("Paraguay", "595"), + Country("Peru", "51"), + Country("Philippines", "63"), + Country("Pitcairn", "64"), + Country("Poland", "48", ENDPOINT_EUROPE), + Country("Portugal", "351", ENDPOINT_EUROPE), + Country("Puerto Rico", "1-787, 1-939"), + Country("Qatar", "974"), + Country("Republic of the Congo", "242"), + Country("Reunion", "262"), + Country("Romania", "40", ENDPOINT_EUROPE), + Country("Russia", "7", ENDPOINT_EUROPE), + Country("Rwanda", "250"), + Country("Saint Barthelemy", "590"), + Country("Saint Helena", "290"), + Country("Saint Kitts and Nevis", "1-869"), + Country("Saint Lucia", "1-758"), + Country("Saint Martin", "590"), + Country("Saint Pierre and Miquelon", "508"), + Country("Saint Vincent and the Grenadines", "1-784"), + Country("Samoa", "685"), + Country("San Marino", "378"), + Country("Sao Tome and Principe", "239"), + Country("Saudi Arabia", "966"), + Country("Senegal", "221"), + Country("Serbia", "381"), + Country("Seychelles", "248"), + Country("Sierra Leone", "232"), + Country("Singapore", "65"), + Country("Sint Maarten", "1-721"), + Country("Slovakia", "421", ENDPOINT_EUROPE), + Country("Slovenia", "386", ENDPOINT_EUROPE), + Country("Solomon Islands", "677"), + Country("Somalia", "252"), + Country("South Africa", "27"), + Country("South Korea", "82"), + Country("South Sudan", "211"), + Country("Spain", "34", ENDPOINT_EUROPE), + Country("Sri Lanka", "94"), + Country("Sudan", "249"), + Country("Suriname", "597"), + Country("Svalbard and Jan Mayen", "47", ENDPOINT_EUROPE), + Country("Swaziland", "268"), + Country("Sweden", "46", ENDPOINT_EUROPE), + Country("Switzerland", "41"), + Country("Syria", "963"), + Country("Taiwan", "886"), + Country("Tajikistan", "992"), + Country("Tanzania", "255"), + Country("Thailand", "66"), + Country("Togo", "228"), + Country("Tokelau", "690"), + Country("Tonga", "676"), + Country("Trinidad and Tobago", "1-868"), + Country("Tunisia", "216"), + Country("Turkey", "90"), + Country("Turkmenistan", "993"), + Country("Turks and Caicos Islands", "1-649"), + Country("Tuvalu", "688"), + Country("U.S. Virgin Islands", "1-340"), + Country("Uganda", "256"), + Country("Ukraine", "380"), + Country("United Arab Emirates", "971"), + Country("United Kingdom", "44", ENDPOINT_EUROPE), + Country("United States", "1", ENDPOINT_AMERICA), + Country("Uruguay", "598"), + Country("Uzbekistan", "998"), + Country("Vanuatu", "678"), + Country("Vatican", "379"), + Country("Venezuela", "58"), + Country("Vietnam", "84"), + Country("Wallis and Futuna", "681"), + Country("Western Sahara", "212"), + Country("Yemen", "967"), + Country("Zambia", "260"), + Country("Zimbabwe", "263"), +] diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index 044c068ac9c..0bb59615e6e 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -4,7 +4,7 @@ "user": { "description": "Enter your Tuya credentials", "data": { - "region": "Region", + "country_code": "Country", "access_id": "Tuya IoT Access ID", "access_secret": "Tuya IoT Access Secret", "username": "Account", @@ -17,4 +17,4 @@ "login_error": "Login error ({code}): {msg}" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/tuya/translations/en.json b/homeassistant/components/tuya/translations/en.json index 4b4b9a6d1dd..e69872fd309 100644 --- a/homeassistant/components/tuya/translations/en.json +++ b/homeassistant/components/tuya/translations/en.json @@ -1,82 +1,19 @@ { "config": { - "abort": { - "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", - "single_instance_allowed": "Already configured. Only a single configuration possible." - }, "error": { "invalid_auth": "Invalid authentication", "login_error": "Login error ({code}): {msg}" }, - "flow_title": "Tuya configuration", "step": { - "login": { - "data": { - "access_id": "Access ID", - "access_secret": "Access Secret", - "country_code": "Country Code", - "endpoint": "Availability Zone", - "password": "Password", - "tuya_app_type": "Mobile App", - "username": "Account" - }, - "description": "Enter your Tuya credential", - "title": "Tuya" - }, "user": { "data": { "access_id": "Tuya IoT Access ID", "access_secret": "Tuya IoT Access Secret", - "country_code": "Your account country code (e.g., 1 for USA or 86 for China)", + "country_code": "Country", "password": "Password", - "platform": "The app where your account is registered", - "region": "Region", - "tuya_project_type": "Tuya cloud project type", "username": "Account" }, - "description": "Enter your Tuya credentials", - "title": "Tuya Integration" - } - } - }, - "options": { - "abort": { - "cannot_connect": "Failed to connect" - }, - "error": { - "dev_multi_type": "Multiple selected devices to configure must be of the same type", - "dev_not_config": "Device type not configurable", - "dev_not_found": "Device not found" - }, - "step": { - "device": { - "data": { - "brightness_range_mode": "Brightness range used by device", - "curr_temp_divider": "Current Temperature value divider (0 = use default)", - "max_kelvin": "Max color temperature supported in kelvin", - "max_temp": "Max target temperature (use min and max = 0 for default)", - "min_kelvin": "Min color temperature supported in kelvin", - "min_temp": "Min target temperature (use min and max = 0 for default)", - "set_temp_divided": "Use divided Temperature value for set temperature command", - "support_color": "Force color support", - "temp_divider": "Temperature values divider (0 = use default)", - "temp_step_override": "Target Temperature step", - "tuya_max_coltemp": "Max color temperature reported by device", - "unit_of_measurement": "Temperature unit used by device" - }, - "description": "Configure options to adjust displayed information for {device_type} device `{device_name}`", - "title": "Configure Tuya Device" - }, - "init": { - "data": { - "discovery_interval": "Discovery device polling interval in seconds", - "list_devices": "Select the devices to configure or leave empty to save configuration", - "query_device": "Select device that will use query method for faster status update", - "query_interval": "Query device polling interval in seconds" - }, - "description": "Do not set pollings interval values too low or the calls will fail generating error message in the log", - "title": "Configure Tuya Options" + "description": "Enter your Tuya credentials" } } } diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index 745bcfde661..a5aee459bd7 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -12,13 +12,14 @@ from homeassistant.components.tuya.const import ( CONF_ACCESS_SECRET, CONF_APP_TYPE, CONF_AUTH_TYPE, + CONF_COUNTRY_CODE, CONF_ENDPOINT, CONF_PASSWORD, - CONF_REGION, CONF_USERNAME, DOMAIN, + ENDPOINT_INDIA, SMARTLIFE_APP, - TUYA_REGIONS, + TUYA_COUNTRIES, TUYA_SMART_APP, ) from homeassistant.core import HomeAssistant @@ -26,15 +27,15 @@ from homeassistant.core import HomeAssistant MOCK_SMART_HOME_PROJECT_TYPE = 0 MOCK_INDUSTRY_PROJECT_TYPE = 1 -MOCK_REGION = "Europe" +MOCK_COUNTRY = "India" MOCK_ACCESS_ID = "myAccessId" MOCK_ACCESS_SECRET = "myAccessSecret" MOCK_USERNAME = "myUsername" MOCK_PASSWORD = "myPassword" -MOCK_ENDPOINT = "https://openapi-ueaz.tuyaus.com" +MOCK_ENDPOINT = ENDPOINT_INDIA TUYA_INPUT_DATA = { - CONF_REGION: MOCK_REGION, + CONF_COUNTRY_CODE: MOCK_COUNTRY, CONF_ACCESS_ID: MOCK_ACCESS_ID, CONF_ACCESS_SECRET: MOCK_ACCESS_SECRET, CONF_USERNAME: MOCK_USERNAME, @@ -92,16 +93,18 @@ async def test_user_flow( ) await hass.async_block_till_done() + country = [country for country in TUYA_COUNTRIES if country.name == MOCK_COUNTRY][0] + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == MOCK_USERNAME assert result["data"][CONF_ACCESS_ID] == MOCK_ACCESS_ID assert result["data"][CONF_ACCESS_SECRET] == MOCK_ACCESS_SECRET assert result["data"][CONF_USERNAME] == MOCK_USERNAME assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD - assert result["data"][CONF_ENDPOINT] == MOCK_ENDPOINT - assert result["data"][CONF_ENDPOINT] != TUYA_REGIONS[TUYA_INPUT_DATA[CONF_REGION]] + assert result["data"][CONF_ENDPOINT] == country.endpoint assert result["data"][CONF_APP_TYPE] == app_type assert result["data"][CONF_AUTH_TYPE] == project_type + assert result["data"][CONF_COUNTRY_CODE] == country.country_code assert not result["result"].unique_id From e22407ba16c241446b14dd955558809998870d94 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 5 Oct 2021 16:33:23 -0400 Subject: [PATCH 0130/1038] Bump zwave-js-server-python to 0.31.3 (#57143) --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index e80549d815d..50e0a039488 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.31.2"], + "requirements": ["zwave-js-server-python==0.31.3"], "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 fc39a991040..2da77ab175b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2501,4 +2501,4 @@ zigpy==0.38.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.31.2 +zwave-js-server-python==0.31.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 020827761db..6304ec758c9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1433,4 +1433,4 @@ zigpy-znp==0.5.4 zigpy==0.38.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.31.2 +zwave-js-server-python==0.31.3 From eba7cad33f7de72fe0bb5fe4fdd76897a15534da Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Oct 2021 10:41:56 -1000 Subject: [PATCH 0131/1038] Fix yeelight connection when bulb stops responding to SSDP (#57138) --- homeassistant/components/yeelight/__init__.py | 21 +++--- tests/components/yeelight/test_init.py | 67 +++++++++++-------- 2 files changed, 53 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index e7f7b06f58f..a1dce44893b 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -181,6 +181,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DATA_CUSTOM_EFFECTS: conf.get(CONF_CUSTOM_EFFECTS, {}), DATA_CONFIG_ENTRIES: {}, } + # Make sure the scanner is always started in case we are + # going to retry via ConfigEntryNotReady and the bulb has changed + # ip + scanner = YeelightScanner.async_get(hass) + await scanner.async_setup() # Import manually configured devices for host, device_config in config.get(DOMAIN, {}).get(CONF_DEVICES, {}).items(): @@ -281,11 +286,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: device = await _async_get_device(hass, entry.data[CONF_HOST], entry) except BULB_EXCEPTIONS as ex: - # If CONF_ID is not valid we cannot fallback to discovery - # so we must retry by raising ConfigEntryNotReady - if not entry.data.get(CONF_ID): - raise ConfigEntryNotReady from ex - # Otherwise fall through to discovery + # Always retry later since bulbs can stop responding to SSDP + # sometimes even though they are online. If it has changed + # IP we will update it via discovery to the config flow + raise ConfigEntryNotReady from ex else: # Since device is passed this cannot throw an exception anymore await _async_initialize(hass, entry, entry.data[CONF_HOST], device=device) @@ -298,7 +302,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except BULB_EXCEPTIONS: _LOGGER.exception("Failed to connect to bulb at %s", host) - # discovery scanner = YeelightScanner.async_get(hass) await scanner.async_register_callback(entry.data[CONF_ID], _async_from_discovery) return True @@ -501,7 +504,9 @@ class YeelightScanner: _LOGGER.debug("Discovered via SSDP: %s", response) unique_id = response["id"] host = urlparse(response["location"]).hostname - if unique_id not in self._unique_id_capabilities: + current_entry = self._unique_id_capabilities.get(unique_id) + # Make sure we handle ip changes + if not current_entry or host != urlparse(current_entry["location"]).hostname: _LOGGER.debug("Yeelight discovered with %s", response) self._async_discovered_by_ssdp(response) self._host_capabilities[host] = response @@ -571,7 +576,7 @@ class YeelightDevice: self._bulb_device = bulb self.capabilities = {} self._device_type = None - self._available = False + self._available = True self._initialized = False self._did_first_update = False self._name = None diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index aed2025ab5d..3ad99fa34ac 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -9,8 +9,6 @@ from homeassistant.components.yeelight import ( CONF_MODEL, CONF_NIGHTLIGHT_SWITCH, CONF_NIGHTLIGHT_SWITCH_TYPE, - DATA_CONFIG_ENTRIES, - DATA_DEVICE, DOMAIN, NIGHTLIGHT_SWITCH_TYPE_LIGHT, STATE_CHANGE_TIME, @@ -57,41 +55,41 @@ async def test_ip_changes_fallback_discovery(hass: HomeAssistant): ) config_entry.add_to_hass(hass) - mocked_bulb = _mocked_bulb(True) - mocked_bulb.bulb_type = BulbType.WhiteTempMood - mocked_bulb.async_listen = AsyncMock(side_effect=[BulbException, None, None, None]) + mocked_fail_bulb = _mocked_bulb(cannot_connect=True) + mocked_fail_bulb.bulb_type = BulbType.WhiteTempMood + with patch( + f"{MODULE}.AsyncBulb", return_value=mocked_fail_bulb + ), _patch_discovery(): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.SETUP_RETRY + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=2)) + await hass.async_block_till_done() + + # The discovery should update the ip address + assert config_entry.data[CONF_HOST] == IP_ADDRESS + assert config_entry.state is ConfigEntryState.SETUP_RETRY + mocked_bulb = _mocked_bulb() with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery(): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED binary_sensor_entity_id = ENTITY_BINARY_SENSOR_TEMPLATE.format( f"yeelight_color_{SHORT_ID}" ) - - type(mocked_bulb).async_get_properties = AsyncMock(None) - - await hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][ - DATA_DEVICE - ].async_update() - await hass.async_block_till_done() - await hass.async_block_till_done() - entity_registry = er.async_get(hass) assert entity_registry.async_get(binary_sensor_entity_id) is not None - with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery(): - # The discovery should update the ip address - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=2)) - await hass.async_block_till_done() - assert config_entry.data[CONF_HOST] == IP_ADDRESS - # Make sure we can still reload with the new ip right after we change it with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery(): await hass.config_entries.async_reload(config_entry.entry_id) await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + entity_registry = er.async_get(hass) assert entity_registry.async_get(binary_sensor_entity_id) is not None @@ -328,13 +326,21 @@ async def test_bulb_off_while_adding_in_ha(hass: HomeAssistant): ) config_entry.add_to_hass(hass) - mocked_bulb = _mocked_bulb(True) + mocked_bulb = _mocked_bulb(cannot_connect=True) mocked_bulb.bulb_type = BulbType.WhiteTempMood with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb), _patch_discovery( no_device=True ), _patch_discovery_timeout(), _patch_discovery_interval(): - 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 config_entry.state is ConfigEntryState.SETUP_RETRY + + with patch(f"{MODULE}.AsyncBulb", return_value=_mocked_bulb()), _patch_discovery( + no_device=True + ), _patch_discovery_timeout(), _patch_discovery_interval(): + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=2)) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED @@ -401,7 +407,7 @@ async def test_async_listen_error_has_host_with_id(hass: HomeAssistant): ): await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state is ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_async_listen_error_has_host_without_id(hass: HomeAssistant): @@ -433,9 +439,16 @@ async def test_async_setup_with_missing_id(hass: HomeAssistant): f"{MODULE}.AsyncBulb", return_value=_mocked_bulb(cannot_connect=True) ): await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.SETUP_RETRY + assert config_entry.data[CONF_ID] == ID - assert config_entry.state is ConfigEntryState.LOADED - assert config_entry.data[CONF_ID] == ID + with _patch_discovery(), _patch_discovery_timeout(), _patch_discovery_interval(), patch( + f"{MODULE}.AsyncBulb", return_value=_mocked_bulb() + ): + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=2)) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED async def test_connection_dropped_resyncs_properties(hass: HomeAssistant): From 25253f2b7ad3b82604eea587ba0cd60b52994ff7 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 5 Oct 2021 15:03:39 -0600 Subject: [PATCH 0132/1038] Use current config entry standards for OpenUV (#57137) --- homeassistant/components/openuv/__init__.py | 42 +++++++++---------- .../components/openuv/config_flow.py | 8 ++-- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index d14760d6cb1..5b92dbcc39e 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -50,7 +50,7 @@ TOPIC_UPDATE = f"{DOMAIN}_data_update" PLATFORMS = ["binary_sensor", "sensor"] -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up OpenUV as config entry.""" hass.data.setdefault(DOMAIN, {DATA_CLIENT: {}, DATA_LISTENER: {}}) @@ -59,22 +59,22 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b try: websession = aiohttp_client.async_get_clientsession(hass) openuv = OpenUV( - config_entry, + entry, Client( - config_entry.data[CONF_API_KEY], - config_entry.data.get(CONF_LATITUDE, hass.config.latitude), - config_entry.data.get(CONF_LONGITUDE, hass.config.longitude), - altitude=config_entry.data.get(CONF_ELEVATION, hass.config.elevation), + entry.data[CONF_API_KEY], + entry.data.get(CONF_LATITUDE, hass.config.latitude), + entry.data.get(CONF_LONGITUDE, hass.config.longitude), + altitude=entry.data.get(CONF_ELEVATION, hass.config.elevation), session=websession, ), ) await openuv.async_update() - hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = openuv + hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] = openuv except OpenUvError as err: LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady from err - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) @_verify_domain_control async def update_data(_: ServiceCall) -> None: @@ -107,21 +107,19 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b return True -async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload an OpenUV config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms( - config_entry, PLATFORMS - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) + hass.data[DOMAIN][DATA_CLIENT].pop(entry.entry_id) return unload_ok -async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Migrate the config entry upon new versions.""" - version = config_entry.version - data = {**config_entry.data} + version = entry.version + data = {**entry.data} LOGGER.debug("Migrating from version %s", version) @@ -129,8 +127,8 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> if version == 1: data.pop(CONF_BINARY_SENSORS, None) data.pop(CONF_SENSORS, None) - version = config_entry.version = 2 - hass.config_entries.async_update_entry(config_entry, data=data) + version = entry.version = 2 + hass.config_entries.async_update_entry(entry, data=data) LOGGER.debug("Migration to version %s successful", version) return True @@ -139,16 +137,16 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> class OpenUV: """Define a generic OpenUV object.""" - def __init__(self, config_entry: ConfigEntry, client: Client) -> None: + def __init__(self, entry: ConfigEntry, client: Client) -> None: """Initialize.""" - self._config_entry = config_entry + self._entry = entry self.client = client self.data: dict[str, Any] = {} async def async_update_protection_data(self) -> None: """Update binary sensor (protection window) data.""" - low = self._config_entry.options.get(CONF_FROM_WINDOW, DEFAULT_FROM_WINDOW) - high = self._config_entry.options.get(CONF_TO_WINDOW, DEFAULT_TO_WINDOW) + low = self._entry.options.get(CONF_FROM_WINDOW, DEFAULT_FROM_WINDOW) + high = self._entry.options.get(CONF_TO_WINDOW, DEFAULT_TO_WINDOW) try: resp = await self.client.uv_protection_window(low=low, high=high) diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py index e397bbf7f95..facbc37986e 100644 --- a/homeassistant/components/openuv/config_flow.py +++ b/homeassistant/components/openuv/config_flow.py @@ -90,9 +90,9 @@ class OpenUvFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class OpenUvOptionsFlowHandler(config_entries.OptionsFlow): """Handle a OpenUV options flow.""" - def __init__(self, config_entry: ConfigEntry) -> None: + def __init__(self, entry: ConfigEntry) -> None: """Initialize.""" - self.config_entry = config_entry + self.entry = entry async def async_step_init( self, user_input: dict[str, Any] | None = None @@ -108,7 +108,7 @@ class OpenUvOptionsFlowHandler(config_entries.OptionsFlow): vol.Optional( CONF_FROM_WINDOW, description={ - "suggested_value": self.config_entry.options.get( + "suggested_value": self.entry.options.get( CONF_FROM_WINDOW, DEFAULT_FROM_WINDOW ) }, @@ -116,7 +116,7 @@ class OpenUvOptionsFlowHandler(config_entries.OptionsFlow): vol.Optional( CONF_TO_WINDOW, description={ - "suggested_value": self.config_entry.options.get( + "suggested_value": self.entry.options.get( CONF_FROM_WINDOW, DEFAULT_TO_WINDOW ) }, From 659229e255234341cf4b76a6f7ae57513de94f51 Mon Sep 17 00:00:00 2001 From: Tom Brien Date: Tue, 5 Oct 2021 22:18:17 +0100 Subject: [PATCH 0133/1038] Add support for POLY and RLY in Coinbase (#57144) * Support POLY currency * Support RLY currency --- homeassistant/components/coinbase/const.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/coinbase/const.py b/homeassistant/components/coinbase/const.py index 486da82dfcd..65c2636cd82 100644 --- a/homeassistant/components/coinbase/const.py +++ b/homeassistant/components/coinbase/const.py @@ -192,9 +192,11 @@ WALLETS = { "PHP": "PHP", "PKR": "PKR", "PLN": "PLN", + "POLY": "POLY", "PYG": "PYG", "QAR": "QAR", "QNT": "QNT", + "RLY": "RLY", "REN": "REN", "REP": "REP", "REPV2": "REPV2", @@ -427,8 +429,10 @@ RATES = { "PHP": "PHP", "PKR": "PKR", "PLN": "PLN", + "POLY": "POLY", "PYG": "PYG", "QAR": "QAR", + "RLY": "RLY", "REN": "REN", "REP": "REP", "RON": "RON", From d60f6a9943968a3e50bba19a1edc220998ca1f6f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Oct 2021 13:06:17 -1000 Subject: [PATCH 0134/1038] Add additional devices to flux_led discovery (#57086) --- homeassistant/components/flux_led/manifest.json | 8 ++++++++ homeassistant/generated/dhcp.py | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 279ea05e3d2..c6f06cb20ab 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -18,6 +18,14 @@ { "macaddress": "B4E842*", "hostname": "[ba][lk]*" + }, + { + "macaddress": "2462AB*", + "hostname": "zengge_35*" + }, + { + "macaddress": "C82E47*", + "hostname": "sta*" } ] } diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 3a0d42d88ea..1ab07fc414a 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -86,6 +86,16 @@ DHCP = [ "macaddress": "B4E842*", "hostname": "[ba][lk]*" }, + { + "domain": "flux_led", + "macaddress": "2462AB*", + "hostname": "zengge_35*" + }, + { + "domain": "flux_led", + "macaddress": "C82E47*", + "hostname": "sta*" + }, { "domain": "goalzero", "hostname": "yeti*" From a8b7c521f62327f77e126d8e300cf535f75f8dc2 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 6 Oct 2021 00:12:58 +0000 Subject: [PATCH 0135/1038] [ci skip] Translation update --- .../components/homekit/translations/nl.json | 2 +- .../components/roomba/translations/nl.json | 2 +- .../components/tuya/translations/de.json | 8 ++- .../components/tuya/translations/en.json | 65 ++++++++++++++++++- .../components/tuya/translations/hu.json | 6 +- .../components/tuya/translations/nl.json | 2 +- .../components/tuya/translations/no.json | 10 ++- .../components/tuya/translations/ru.json | 8 ++- .../components/tuya/translations/zh-Hant.json | 10 ++- .../components/yeelight/translations/nl.json | 2 +- 10 files changed, 99 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/homekit/translations/nl.json b/homeassistant/components/homekit/translations/nl.json index 2ab21f66db5..3f65f8e5e7f 100644 --- a/homeassistant/components/homekit/translations/nl.json +++ b/homeassistant/components/homekit/translations/nl.json @@ -24,7 +24,7 @@ "auto_start": "Autostart (deactiveer als je de homekit.start service handmatig aanroept)", "devices": "Apparaten (triggers)" }, - "description": "Deze instellingen hoeven alleen te worden aangepast als HomeKit niet functioneert.", + "description": "Voor elk geselecteerd apparaat worden programmeerbare schakelaars gemaakt. Wanneer een apparaattrigger wordt geactiveerd, kan HomeKit worden geconfigureerd om een automatisering of sc\u00e8ne uit te voeren.", "title": "Geavanceerde configuratie" }, "cameras": { diff --git a/homeassistant/components/roomba/translations/nl.json b/homeassistant/components/roomba/translations/nl.json index 40c821b62db..bbb4fdbe765 100644 --- a/homeassistant/components/roomba/translations/nl.json +++ b/homeassistant/components/roomba/translations/nl.json @@ -34,7 +34,7 @@ "blid": "BLID", "host": "Host" }, - "description": "Er is geen Roomba of Braava ontdekt op uw netwerk. De BLID is het gedeelte van de hostnaam van het apparaat na `iRobot-`. Volg de stappen die worden beschreven in de documentatie op: {auth_help_url}", + "description": "Er is geen Roomba of Braava ontdekt op uw netwerk.", "title": "Handmatig verbinding maken met het apparaat" }, "user": { diff --git a/homeassistant/components/tuya/translations/de.json b/homeassistant/components/tuya/translations/de.json index 57439e1fa76..895c34d60bd 100644 --- a/homeassistant/components/tuya/translations/de.json +++ b/homeassistant/components/tuya/translations/de.json @@ -6,7 +6,8 @@ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "error": { - "invalid_auth": "Ung\u00fcltige Authentifizierung" + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "login_error": "Anmeldefehler ({code}): {msg}" }, "flow_title": "Tuya Konfiguration", "step": { @@ -25,11 +26,14 @@ }, "user": { "data": { + "access_id": "Tuya IoT-Zugriffs-ID", + "access_secret": "Tuya IoT-Zugriffsgeheimnis", "country_code": "L\u00e4ndercode deines Kontos (z. B. 1 f\u00fcr USA oder 86 f\u00fcr China)", "password": "Passwort", "platform": "Die App, in der dein Konto registriert ist", + "region": "Region", "tuya_project_type": "Tuya Cloud Projekttyp", - "username": "Benutzername" + "username": "Konto" }, "description": "Gib deine Tuya-Anmeldeinformationen ein.", "title": "Tuya-Integration" diff --git a/homeassistant/components/tuya/translations/en.json b/homeassistant/components/tuya/translations/en.json index e69872fd309..e928dc37d57 100644 --- a/homeassistant/components/tuya/translations/en.json +++ b/homeassistant/components/tuya/translations/en.json @@ -1,19 +1,82 @@ { "config": { + "abort": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, "error": { "invalid_auth": "Invalid authentication", "login_error": "Login error ({code}): {msg}" }, + "flow_title": "Tuya configuration", "step": { + "login": { + "data": { + "access_id": "Access ID", + "access_secret": "Access Secret", + "country_code": "Country Code", + "endpoint": "Availability Zone", + "password": "Password", + "tuya_app_type": "Mobile App", + "username": "Account" + }, + "description": "Enter your Tuya credential", + "title": "Tuya" + }, "user": { "data": { "access_id": "Tuya IoT Access ID", "access_secret": "Tuya IoT Access Secret", "country_code": "Country", "password": "Password", + "platform": "The app where your account is registered", + "region": "Region", + "tuya_project_type": "Tuya cloud project type", "username": "Account" }, - "description": "Enter your Tuya credentials" + "description": "Enter your Tuya credentials", + "title": "Tuya Integration" + } + } + }, + "options": { + "abort": { + "cannot_connect": "Failed to connect" + }, + "error": { + "dev_multi_type": "Multiple selected devices to configure must be of the same type", + "dev_not_config": "Device type not configurable", + "dev_not_found": "Device not found" + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "Brightness range used by device", + "curr_temp_divider": "Current Temperature value divider (0 = use default)", + "max_kelvin": "Max color temperature supported in kelvin", + "max_temp": "Max target temperature (use min and max = 0 for default)", + "min_kelvin": "Min color temperature supported in kelvin", + "min_temp": "Min target temperature (use min and max = 0 for default)", + "set_temp_divided": "Use divided Temperature value for set temperature command", + "support_color": "Force color support", + "temp_divider": "Temperature values divider (0 = use default)", + "temp_step_override": "Target Temperature step", + "tuya_max_coltemp": "Max color temperature reported by device", + "unit_of_measurement": "Temperature unit used by device" + }, + "description": "Configure options to adjust displayed information for {device_type} device `{device_name}`", + "title": "Configure Tuya Device" + }, + "init": { + "data": { + "discovery_interval": "Discovery device polling interval in seconds", + "list_devices": "Select the devices to configure or leave empty to save configuration", + "query_device": "Select device that will use query method for faster status update", + "query_interval": "Query device polling interval in seconds" + }, + "description": "Do not set pollings interval values too low or the calls will fail generating error message in the log", + "title": "Configure Tuya Options" } } } diff --git a/homeassistant/components/tuya/translations/hu.json b/homeassistant/components/tuya/translations/hu.json index d721a8cd133..9a08d1f5a99 100644 --- a/homeassistant/components/tuya/translations/hu.json +++ b/homeassistant/components/tuya/translations/hu.json @@ -6,7 +6,8 @@ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "error": { - "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "login_error": "Bejelentkez\u00e9si hiba ({code}): {msg}" }, "flow_title": "Tuya konfigur\u00e1ci\u00f3", "step": { @@ -25,9 +26,12 @@ }, "user": { "data": { + "access_id": "Tuya IoT azonos\u00edt\u00f3", + "access_secret": "Tuya IoT hozz\u00e1f\u00e9r\u00e9s", "country_code": "A fi\u00f3k orsz\u00e1gk\u00f3dja (pl. 1 USA, 36 Magyarorsz\u00e1g, vagy 86 K\u00edna)", "password": "Jelsz\u00f3", "platform": "Az alkalmaz\u00e1s, ahol a fi\u00f3k regisztr\u00e1lt", + "region": "R\u00e9gi\u00f3", "tuya_project_type": "Tuya felh\u0151 projekt t\u00edpusa", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, diff --git a/homeassistant/components/tuya/translations/nl.json b/homeassistant/components/tuya/translations/nl.json index 0ceb0c916cc..3519ac126f6 100644 --- a/homeassistant/components/tuya/translations/nl.json +++ b/homeassistant/components/tuya/translations/nl.json @@ -36,7 +36,7 @@ "username": "Gebruikersnaam" }, "description": "Voer uw Tuya-inloggegevens in.", - "title": "Tuya" + "title": "Tuya-integratie" } } }, diff --git a/homeassistant/components/tuya/translations/no.json b/homeassistant/components/tuya/translations/no.json index b5fe4bc1851..254eb1c1230 100644 --- a/homeassistant/components/tuya/translations/no.json +++ b/homeassistant/components/tuya/translations/no.json @@ -6,7 +6,8 @@ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "error": { - "invalid_auth": "Ugyldig godkjenning" + "invalid_auth": "Ugyldig godkjenning", + "login_error": "P\u00e5loggingsfeil ( {code} ): {msg}" }, "flow_title": "Tuya konfigurasjon", "step": { @@ -25,13 +26,16 @@ }, "user": { "data": { + "access_id": "Tuya IoT Access ID", + "access_secret": "Tuya IoT Access Secret", "country_code": "Din landskode for kontoen din (f.eks. 1 for USA eller 86 for Kina)", "password": "Passord", "platform": "Appen der kontoen din er registrert", + "region": "Region", "tuya_project_type": "Tuya -skyprosjekttype", - "username": "Brukernavn" + "username": "Account" }, - "description": "Angi Tuya-legitimasjonen din.", + "description": "Skriv inn Tuya -legitimasjonen din", "title": "Tuya Integrasjon" } } diff --git a/homeassistant/components/tuya/translations/ru.json b/homeassistant/components/tuya/translations/ru.json index 8e00eee568c..cd7b4ee79f5 100644 --- a/homeassistant/components/tuya/translations/ru.json +++ b/homeassistant/components/tuya/translations/ru.json @@ -6,7 +6,8 @@ "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, "error": { - "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "login_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0445\u043e\u0434\u0430 \u0432 \u0441\u0438\u0441\u0442\u0435\u043c\u0443 ({code}): {msg}" }, "flow_title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Tuya", "step": { @@ -25,11 +26,14 @@ }, "user": { "data": { + "access_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 Tuya IoT", + "access_secret": "\u0421\u0435\u043a\u0440\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 Tuya IoT", "country_code": "\u041a\u043e\u0434 \u0441\u0442\u0440\u0430\u043d\u044b \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430 (1 \u0434\u043b\u044f \u0421\u0428\u0410 \u0438\u043b\u0438 86 \u0434\u043b\u044f \u041a\u0438\u0442\u0430\u044f)", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "platform": "\u041f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c", + "region": "\u0420\u0435\u0433\u0438\u043e\u043d", "tuya_project_type": "\u0422\u0438\u043f \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0435\u043a\u0442\u0430 Tuya", - "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + "username": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Tuya.", "title": "Tuya" diff --git a/homeassistant/components/tuya/translations/zh-Hant.json b/homeassistant/components/tuya/translations/zh-Hant.json index e747e50d2c7..f30f08ed991 100644 --- a/homeassistant/components/tuya/translations/zh-Hant.json +++ b/homeassistant/components/tuya/translations/zh-Hant.json @@ -6,7 +6,8 @@ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { - "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "login_error": "\u767b\u5165\u932f\u8aa4\uff08{code}\uff09\uff1a{msg}" }, "flow_title": "Tuya \u8a2d\u5b9a", "step": { @@ -25,13 +26,16 @@ }, "user": { "data": { + "access_id": "Tuya IoT Access ID", + "access_secret": "Tuya IoT Access Secret", "country_code": "\u5e33\u865f\u570b\u5bb6\u4ee3\u78bc\uff08\u4f8b\u5982\uff1a\u7f8e\u570b 1 \u6216\u4e2d\u570b 86\uff09", "password": "\u5bc6\u78bc", "platform": "\u5e33\u6236\u8a3b\u518a\u6240\u5728\u4f4d\u7f6e", + "region": "\u5340\u57df", "tuya_project_type": "Tuya \u96f2\u5c08\u6848\u985e\u578b", - "username": "\u4f7f\u7528\u8005\u540d\u7a31" + "username": "\u5e33\u865f" }, - "description": "\u8f38\u5165 Tuya \u6191\u8b49\u3002", + "description": "\u8f38\u5165 Tuya \u6191\u8b49", "title": "Tuya \u6574\u5408" } } diff --git a/homeassistant/components/yeelight/translations/nl.json b/homeassistant/components/yeelight/translations/nl.json index 6aecb4f0bd9..08a971f0225 100644 --- a/homeassistant/components/yeelight/translations/nl.json +++ b/homeassistant/components/yeelight/translations/nl.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Kon niet verbinden" }, - "flow_title": "{model} {host}", + "flow_title": "{model} {id} ({host})", "step": { "discovery_confirm": { "description": "Wilt u {model} ({host}) instellen?" From f6682ba99dd1f1981f5e755011562f729ba86369 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 6 Oct 2021 02:46:09 +0200 Subject: [PATCH 0136/1038] Block tests from opening sockets (#55516) --- requirements_test.txt | 1 + tests/components/auth/conftest.py | 8 +++ tests/components/emulated_hue/test_upnp.py | 6 ++ tests/components/frontend/test_init.py | 6 ++ tests/components/http/conftest.py | 8 +++ .../components/image_processing/test_init.py | 8 +++ tests/components/motioneye/test_camera.py | 7 +- tests/components/nest/conftest.py | 6 ++ tests/conftest.py | 72 ++++++++++++++++++- tests/test_test_fixtures.py | 18 +++++ 10 files changed, 135 insertions(+), 5 deletions(-) create mode 100644 tests/components/auth/conftest.py create mode 100644 tests/components/http/conftest.py create mode 100644 tests/test_test_fixtures.py diff --git a/requirements_test.txt b/requirements_test.txt index 22ffaa60a0f..713c8820bc1 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -18,6 +18,7 @@ pipdeptree==2.1.0 pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 pytest-cov==2.12.1 +pytest-socket==0.4.1 pytest-test-groups==1.0.3 pytest-sugar==0.9.4 pytest-timeout==1.4.2 diff --git a/tests/components/auth/conftest.py b/tests/components/auth/conftest.py new file mode 100644 index 00000000000..867f44d9f15 --- /dev/null +++ b/tests/components/auth/conftest.py @@ -0,0 +1,8 @@ +"""Test configuration for auth.""" +import pytest + + +@pytest.fixture +def aiohttp_client(loop, aiohttp_client, socket_enabled): + """Return aiohttp_client and allow opening sockets.""" + return aiohttp_client diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index 8ea65380359..d918b378614 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -28,6 +28,12 @@ class MockTransport: self.sends.append((response, addr)) +@pytest.fixture +def aiohttp_client(loop, aiohttp_client, socket_enabled): + """Return aiohttp_client and allow opening sockets.""" + return aiohttp_client + + @pytest.fixture def hue_client(aiohttp_client): """Return a hue API client.""" diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index 9746fc6d838..c508175a846 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -79,6 +79,12 @@ async def frontend_themes(hass): ) +@pytest.fixture +def aiohttp_client(loop, aiohttp_client, socket_enabled): + """Return aiohttp_client and allow opening sockets.""" + return aiohttp_client + + @pytest.fixture async def mock_http_client(hass, aiohttp_client, frontend): """Start the Home Assistant HTTP component.""" diff --git a/tests/components/http/conftest.py b/tests/components/http/conftest.py new file mode 100644 index 00000000000..c796ec50b51 --- /dev/null +++ b/tests/components/http/conftest.py @@ -0,0 +1,8 @@ +"""Test configuration for http.""" +import pytest + + +@pytest.fixture +def aiohttp_client(loop, aiohttp_client, socket_enabled): + """Return aiohttp_client and allow opening sockets.""" + return aiohttp_client diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index c0c57b17a7c..ed8d49e8ddb 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -1,6 +1,8 @@ """The tests for the image_processing component.""" from unittest.mock import PropertyMock, patch +import pytest + import homeassistant.components.http as http import homeassistant.components.image_processing as ip from homeassistant.const import ATTR_ENTITY_PICTURE @@ -11,6 +13,12 @@ from tests.common import assert_setup_component, async_capture_events from tests.components.image_processing import common +@pytest.fixture +def aiohttp_unused_port(loop, aiohttp_unused_port, socket_enabled): + """Return aiohttp_unused_port and allow opening sockets.""" + return aiohttp_unused_port + + def get_url(hass): """Return camera url.""" state = hass.states.get("camera.demo_camera") diff --git a/tests/components/motioneye/test_camera.py b/tests/components/motioneye/test_camera.py index 70c2d44436a..b2264e78556 100644 --- a/tests/components/motioneye/test_camera.py +++ b/tests/components/motioneye/test_camera.py @@ -1,6 +1,5 @@ """Test the motionEye camera.""" import copy -import logging from typing import Any, cast from unittest.mock import AsyncMock, Mock @@ -48,7 +47,11 @@ from . import ( from tests.common import async_fire_time_changed -_LOGGER = logging.getLogger(__name__) + +@pytest.fixture +def aiohttp_server(loop, aiohttp_server, socket_enabled): + """Return aiohttp_server and allow opening sockets.""" + return aiohttp_server async def test_setup_camera(hass: HomeAssistant) -> None: diff --git a/tests/components/nest/conftest.py b/tests/components/nest/conftest.py index 764f037d181..988d9d761fe 100644 --- a/tests/components/nest/conftest.py +++ b/tests/components/nest/conftest.py @@ -48,6 +48,12 @@ class FakeAuth(AbstractAuth): return aiohttp.web.json_response() +@pytest.fixture +def aiohttp_client(loop, aiohttp_client, socket_enabled): + """Return aiohttp_client and allow opening sockets.""" + return aiohttp_client + + @pytest.fixture async def auth(aiohttp_client): """Fixture for an AbstractAuth.""" diff --git a/tests/conftest.py b/tests/conftest.py index 9ee6bbc680b..845145c2ec2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ import asyncio import datetime import functools import logging +import socket import ssl import threading from unittest.mock import MagicMock, patch @@ -10,6 +11,7 @@ from unittest.mock import MagicMock, patch from aiohttp.test_utils import make_mocked_request import multidict import pytest +import pytest_socket import requests_mock as _requests_mock from homeassistant import core as ha, loader, runner, util @@ -61,6 +63,70 @@ def pytest_configure(config): ) +def pytest_runtest_setup(): + """Throw if tests attempt to open sockets. + + allow_unix_socket is set to True because it's needed by asyncio. + Important: socket_allow_hosts must be called before disable_socket, otherwise all + destinations will be allowed. + """ + pytest_socket.socket_allow_hosts(["127.0.0.1"]) + disable_socket(allow_unix_socket=True) + + +@pytest.fixture +def socket_disabled(pytestconfig): + """Disable socket.socket for duration of this test function. + + This incorporates changes from https://github.com/miketheman/pytest-socket/pull/76 + and hardcodes allow_unix_socket to True because it's not passed on the command line. + """ + socket_was_enabled = socket.socket == pytest_socket._true_socket + disable_socket(allow_unix_socket=True) + yield + if socket_was_enabled: + pytest_socket.enable_socket() + + +@pytest.fixture +def socket_enabled(pytestconfig): + """Enable socket.socket for duration of this test function. + + This incorporates changes from https://github.com/miketheman/pytest-socket/pull/76 + and hardcodes allow_unix_socket to True because it's not passed on the command line. + """ + socket_was_disabled = socket.socket != pytest_socket._true_socket + pytest_socket.enable_socket() + yield + if socket_was_disabled: + disable_socket(allow_unix_socket=True) + + +def disable_socket(allow_unix_socket=False): + """Disable socket.socket to disable the Internet. useful in testing. + + This incorporates changes from https://github.com/miketheman/pytest-socket/pull/75 + """ + + class GuardedSocket(socket.socket): + """socket guard to disable socket creation (from pytest-socket).""" + + def __new__(cls, *args, **kwargs): + try: + if len(args) > 0: + is_unix_socket = args[0] == socket.AF_UNIX + else: + is_unix_socket = kwargs.get("family") == socket.AF_UNIX + except AttributeError: + # AF_UNIX not supported on Windows https://bugs.python.org/issue33408 + is_unix_socket = False + if is_unix_socket and allow_unix_socket: + return super().__new__(cls, *args, **kwargs) + raise pytest_socket.SocketBlockedError() + + socket.socket = GuardedSocket + + def check_real(func): """Force a function to require a keyword _test_real to be passed in.""" @@ -319,7 +385,7 @@ def local_auth(hass): @pytest.fixture -def hass_client(hass, aiohttp_client, hass_access_token): +def hass_client(hass, aiohttp_client, hass_access_token, socket_enabled): """Return an authenticated HTTP client.""" async def auth_client(): @@ -332,7 +398,7 @@ def hass_client(hass, aiohttp_client, hass_access_token): @pytest.fixture -def hass_client_no_auth(hass, aiohttp_client): +def hass_client_no_auth(hass, aiohttp_client, socket_enabled): """Return an unauthenticated HTTP client.""" async def client(): @@ -367,7 +433,7 @@ def current_request_with_host(current_request): @pytest.fixture -def hass_ws_client(aiohttp_client, hass_access_token, hass): +def hass_ws_client(aiohttp_client, hass_access_token, hass, socket_enabled): """Websocket client fixture connected to websocket server.""" async def create_client(hass=hass, access_token=hass_access_token): diff --git a/tests/test_test_fixtures.py b/tests/test_test_fixtures.py new file mode 100644 index 00000000000..90362e95819 --- /dev/null +++ b/tests/test_test_fixtures.py @@ -0,0 +1,18 @@ +"""Test test fixture configuration.""" +import socket + +import pytest +import pytest_socket + + +def test_sockets_disabled(): + """Test we can't open sockets.""" + with pytest.raises(pytest_socket.SocketBlockedError): + socket.socket() + + +def test_sockets_enabled(socket_enabled): + """Test we can't connect to an address different from 127.0.0.1.""" + mysocket = socket.socket() + with pytest.raises(pytest_socket.SocketConnectBlockedError): + mysocket.connect(("127.0.0.2", 1234)) From 286ffb2d717c694cffb6973ea2b8bbd66bfad896 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Oct 2021 14:49:42 -1000 Subject: [PATCH 0137/1038] Write flux_led state after turning on/off (#57152) --- homeassistant/components/flux_led/light.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 581d5fbaab6..a480a956a58 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -355,6 +355,7 @@ class FluxLight(CoordinatorEntity, LightEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the specified or all lights on.""" await self.hass.async_add_executor_job(partial(self._turn_on, **kwargs)) + self.async_write_ha_state() await self.coordinator.async_request_refresh() def _turn_on(self, **kwargs: Any) -> None: @@ -446,6 +447,7 @@ class FluxLight(CoordinatorEntity, LightEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the specified or all lights off.""" await self.hass.async_add_executor_job(self._bulb.turnOff) + self.async_write_ha_state() await self.coordinator.async_request_refresh() async def async_added_to_hass(self) -> None: From 7e5dfadc279e675da458519892944cae28a156ae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Oct 2021 14:53:39 -1000 Subject: [PATCH 0138/1038] Add sw_version and model to flux_led device info (#56958) --- homeassistant/components/flux_led/__init__.py | 3 ++ homeassistant/components/flux_led/light.py | 41 +++++++------------ tests/components/flux_led/__init__.py | 1 + tests/components/flux_led/test_init.py | 23 ++++++++++- tests/components/flux_led/test_light.py | 36 ++++++++++++++-- 5 files changed, 72 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index df7334a8ebc..fa3ad23a41c 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -140,3 +140,6 @@ class FluxLedUpdateCoordinator(DataUpdateCoordinator): await self.hass.async_add_executor_job(self.device.update_state) except FLUX_LED_EXCEPTIONS as ex: raise UpdateFailed(ex) from ex + + if not self.device.raw_state: + raise UpdateFailed("The device failed to update") diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index a480a956a58..8c14a1c22b6 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -28,11 +28,11 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.const import ( - ATTR_IDENTIFIERS, ATTR_MANUFACTURER, ATTR_MODE, ATTR_MODEL, ATTR_NAME, + ATTR_SW_VERSION, CONF_DEVICES, CONF_HOST, CONF_MAC, @@ -41,9 +41,8 @@ from homeassistant.const import ( CONF_PROTOCOL, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_platform +from homeassistant.helpers import device_registry as dr, entity_platform import homeassistant.helpers.config_validation as cv -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 @@ -271,23 +270,22 @@ class FluxLight(CoordinatorEntity, LightEntity): """Initialize the light.""" super().__init__(coordinator) self._bulb: WifiLedBulb = coordinator.device - self._name = name - self._unique_id = unique_id + self._attr_name = name + self._attr_unique_id = unique_id self._ip_address = coordinator.host self._mode = mode self._custom_effect_colors = custom_effect_colors self._custom_effect_speed_pct = custom_effect_speed_pct self._custom_effect_transition = custom_effect_transition - - @property - def unique_id(self) -> str | None: - """Return the unique ID of the light.""" - return self._unique_id - - @property - def name(self) -> str: - """Return the name of the device.""" - return self._name + old_protocol = self._bulb.protocol == "LEDENET_ORIGINAL" + if self.unique_id: + self._attr_device_info = { + "connections": {(dr.CONNECTION_NETWORK_MAC, self.unique_id)}, + ATTR_MODEL: f"0x{self._bulb.raw_state[1]:02X}", + ATTR_SW_VERSION: "1" if old_protocol else str(self._bulb.raw_state[10]), + ATTR_NAME: self.name, + ATTR_MANUFACTURER: "FluxLED/Magic Home", + } @property def is_on(self) -> bool: @@ -341,17 +339,6 @@ class FluxLight(CoordinatorEntity, LightEntity): "ip_address": self._ip_address, } - @property - def device_info(self) -> DeviceInfo: - """Return the device information.""" - assert self._unique_id is not None - return { - ATTR_IDENTIFIERS: {(DOMAIN, self._unique_id)}, - ATTR_NAME: self._name, - ATTR_MANUFACTURER: "FluxLED/Magic Home", - ATTR_MODEL: "LED Lights", - } - async def async_turn_on(self, **kwargs: Any) -> None: """Turn the specified or all lights on.""" await self.hass.async_add_executor_job(partial(self._turn_on, **kwargs)) @@ -464,7 +451,7 @@ class FluxLight(CoordinatorEntity, LightEntity): self._mode = MODE_RGB _LOGGER.debug( "Detected mode for %s (%s) with raw_state=%s rgbwcapable=%s is %s", - self._name, + self.name, self.unique_id, self._bulb.raw_state, self._bulb.rgbwcapable, diff --git a/tests/components/flux_led/__init__.py b/tests/components/flux_led/__init__.py index 49b9db491f3..5ca6244655a 100644 --- a/tests/components/flux_led/__init__.py +++ b/tests/components/flux_led/__init__.py @@ -36,6 +36,7 @@ def _mocked_bulb() -> WifiLedBulb: bulb.getRgbw = MagicMock(return_value=[255, 0, 0, 50]) bulb.brightness = 128 bulb.rgbwcapable = True + bulb.raw_state = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] return bulb diff --git a/tests/components/flux_led/test_init.py b/tests/components/flux_led/test_init.py index 7b01088d1f6..734fb64520f 100644 --- a/tests/components/flux_led/test_init.py +++ b/tests/components/flux_led/test_init.py @@ -11,7 +11,14 @@ from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from . import FLUX_DISCOVERY, IP_ADDRESS, MAC_ADDRESS, _patch_discovery, _patch_wifibulb +from . import ( + FLUX_DISCOVERY, + IP_ADDRESS, + MAC_ADDRESS, + _mocked_bulb, + _patch_discovery, + _patch_wifibulb, +) from tests.common import MockConfigEntry, async_fire_time_changed @@ -56,3 +63,17 @@ async def test_config_entry_retry(hass: HomeAssistant) -> None: await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() assert config_entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_config_entry_retry_when_state_missing(hass: HomeAssistant) -> None: + """Test that a config entry is retried when state is missing.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=MAC_ADDRESS + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.raw_state = None + with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.SETUP_RETRY diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index c2e98158b0b..4ddf9a7d04d 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -39,7 +39,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -93,6 +93,35 @@ async def test_light_no_unique_id(hass: HomeAssistant) -> None: assert state.state == STATE_ON +@pytest.mark.parametrize( + "protocol,sw_version,model", [("LEDENET_ORIGINAL", 1, 0x35), ("LEDENET", 8, 0x33)] +) +async def test_light_device_registry( + hass: HomeAssistant, protocol: str, sw_version: int, model: int +) -> None: + """Test a light device registry entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.protocol = protocol + bulb.raw_state[1] = model + bulb.raw_state[10] = sw_version + with _patch_discovery(no_device=True), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device( + identifiers={}, connections={(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)} + ) + assert device.sw_version == str(sw_version) + assert device.model == f"0x{model:02X}" + + async def test_rgb_light(hass: HomeAssistant) -> None: """Test an rgb light.""" config_entry = MockConfigEntry( @@ -276,7 +305,6 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) bulb = _mocked_bulb() - bulb.raw_state = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] bulb.raw_state[9] = 1 bulb.raw_state[11] = 2 @@ -466,7 +494,7 @@ async def test_rgb_light_custom_effects( ) bulb.setCustomPattern.assert_called_with([[0, 0, 255], [255, 0, 0]], 88, "jump") bulb.setCustomPattern.reset_mock() - bulb.raw_state = [0, 0, 0, EFFECT_CUSTOM_CODE, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + bulb.raw_state[3] = EFFECT_CUSTOM_CODE bulb.is_on = True async_fire_time_changed(hass, utcnow() + timedelta(seconds=20)) await hass.async_block_till_done() @@ -484,7 +512,7 @@ async def test_rgb_light_custom_effects( ) bulb.setCustomPattern.assert_called_with([[0, 0, 255], [255, 0, 0]], 88, "jump") bulb.setCustomPattern.reset_mock() - bulb.raw_state = [0, 0, 0, EFFECT_CUSTOM_CODE, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + bulb.raw_state[3] = EFFECT_CUSTOM_CODE bulb.is_on = True async_fire_time_changed(hass, utcnow() + timedelta(seconds=20)) await hass.async_block_till_done() From 9de3bd77d89817b0c8c70b9a9d3d739534de4980 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 6 Oct 2021 05:25:57 +0200 Subject: [PATCH 0139/1038] Fix SamsungTV shutdown race condition (#57149) --- homeassistant/components/samsungtv/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 8644335959e..cab6435af95 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -133,7 +133,7 @@ class SamsungTVDevice(MediaPlayerEntity): def update(self) -> None: """Update state of device.""" - if self._auth_failed: + if self._auth_failed or self.hass.is_stopping: return if self._power_off_in_progress(): self._state = STATE_OFF From d51d70d3beea2631bfb2c861a89eca0261cc7aa7 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 6 Oct 2021 05:26:18 +0200 Subject: [PATCH 0140/1038] Fix Fritz shutdown race condition (#57148) --- homeassistant/components/fritz/common.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 0fb062af2d7..61cff890a93 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -228,7 +228,12 @@ class FritzBoxTools: def _update_hosts_info(self) -> list[HostInfo]: """Retrieve latest hosts information from the FRITZ!Box.""" - return self.fritz_hosts.get_hosts_info() # type: ignore [no-any-return] + try: + return self.fritz_hosts.get_hosts_info() # type: ignore [no-any-return] + except Exception as ex: # pylint: disable=[broad-except] + if not self.hass.is_stopping: + raise HomeAssistantError("Error refreshing hosts info") from ex + return [] def _update_device_info(self) -> tuple[bool, str | None]: """Retrieve latest device information from the FRITZ!Box.""" From 205b40cf164d33172a134d53afb749672e3cf3a2 Mon Sep 17 00:00:00 2001 From: Lawrence Date: Wed, 6 Oct 2021 14:34:23 +1100 Subject: [PATCH 0141/1038] Updated amberelectic attributes to reflect unit change to $/kWh (#57109) --- .../components/amberelectric/sensor.py | 33 +++++++++++-------- tests/components/amberelectric/test_sensor.py | 33 +++++++++---------- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/amberelectric/sensor.py b/homeassistant/components/amberelectric/sensor.py index 0a47615046e..974de2d5c15 100644 --- a/homeassistant/components/amberelectric/sensor.py +++ b/homeassistant/components/amberelectric/sensor.py @@ -37,6 +37,11 @@ ICONS = { UNIT = f"{CURRENCY_DOLLAR}/{ENERGY_KILO_WATT_HOUR}" +def format_cents_to_dollars(cents: float) -> float: + """Return a formatted conversion from cents to dollars.""" + return round(cents / 100, 2) + + def friendly_channel_type(channel_type: str) -> str: """Return a human readable version of the channel type.""" if channel_type == "controlled_load": @@ -70,13 +75,13 @@ class AmberPriceSensor(AmberSensor): """Amber Price Sensor.""" @property - def native_value(self) -> str | None: + def native_value(self) -> float | None: """Return the current price in $/kWh.""" interval = self.coordinator.data[self.entity_description.key][self.channel_type] if interval.channel_type == ChannelType.FEED_IN: - return round(interval.per_kwh, 0) / 100 * -1 - return round(interval.per_kwh, 0) / 100 + return format_cents_to_dollars(interval.per_kwh) * -1 + return format_cents_to_dollars(interval.per_kwh) @property def device_state_attributes(self) -> Mapping[str, Any] | None: @@ -89,11 +94,11 @@ class AmberPriceSensor(AmberSensor): data["duration"] = interval.duration data["date"] = interval.date.isoformat() - data["per_kwh"] = round(interval.per_kwh) + data["per_kwh"] = format_cents_to_dollars(interval.per_kwh) if interval.channel_type == ChannelType.FEED_IN: data["per_kwh"] = data["per_kwh"] * -1 data["nem_date"] = interval.nem_time.isoformat() - data["spot_per_kwh"] = round(interval.spot_per_kwh) + data["spot_per_kwh"] = format_cents_to_dollars(interval.spot_per_kwh) data["start_time"] = interval.start_time.isoformat() data["end_time"] = interval.end_time.isoformat() data["renewables"] = round(interval.renewables) @@ -102,8 +107,8 @@ class AmberPriceSensor(AmberSensor): data["channel_type"] = interval.channel_type.value if interval.range is not None: - data["range_min"] = interval.range.min - data["range_max"] = interval.range.max + data["range_min"] = format_cents_to_dollars(interval.range.min) + data["range_max"] = format_cents_to_dollars(interval.range.max) return data @@ -112,7 +117,7 @@ class AmberForecastSensor(AmberSensor): """Amber Forecast Sensor.""" @property - def native_value(self) -> str | None: + def native_value(self) -> float | None: """Return the first forecast price in $/kWh.""" intervals = self.coordinator.data[self.entity_description.key].get( self.channel_type @@ -122,8 +127,8 @@ class AmberForecastSensor(AmberSensor): interval = intervals[0] if interval.channel_type == ChannelType.FEED_IN: - return round(interval.per_kwh, 0) / 100 * -1 - return round(interval.per_kwh, 0) / 100 + return format_cents_to_dollars(interval.per_kwh) * -1 + return format_cents_to_dollars(interval.per_kwh) @property def device_state_attributes(self) -> Mapping[str, Any] | None: @@ -146,18 +151,18 @@ class AmberForecastSensor(AmberSensor): datum["duration"] = interval.duration datum["date"] = interval.date.isoformat() datum["nem_date"] = interval.nem_time.isoformat() - datum["per_kwh"] = round(interval.per_kwh) + datum["per_kwh"] = format_cents_to_dollars(interval.per_kwh) if interval.channel_type == ChannelType.FEED_IN: datum["per_kwh"] = datum["per_kwh"] * -1 - datum["spot_per_kwh"] = round(interval.spot_per_kwh) + datum["spot_per_kwh"] = format_cents_to_dollars(interval.spot_per_kwh) datum["start_time"] = interval.start_time.isoformat() datum["end_time"] = interval.end_time.isoformat() datum["renewables"] = round(interval.renewables) datum["spike_status"] = interval.spike_status.value if interval.range is not None: - datum["range_min"] = interval.range.min - datum["range_max"] = interval.range.max + datum["range_min"] = format_cents_to_dollars(interval.range.min) + datum["range_max"] = format_cents_to_dollars(interval.range.max) data["forecasts"].append(datum) diff --git a/tests/components/amberelectric/test_sensor.py b/tests/components/amberelectric/test_sensor.py index 865121bd1ee..ccfcd82b3bd 100644 --- a/tests/components/amberelectric/test_sensor.py +++ b/tests/components/amberelectric/test_sensor.py @@ -108,9 +108,9 @@ async def test_general_price_sensor(hass: HomeAssistant, setup_general: Mock) -> attributes = price.attributes assert attributes["duration"] == 30 assert attributes["date"] == "2021-09-21" - assert attributes["per_kwh"] == 8 + assert attributes["per_kwh"] == 0.08 assert attributes["nem_date"] == "2021-09-21T08:30:00+10:00" - assert attributes["spot_per_kwh"] == 1 + assert attributes["spot_per_kwh"] == 0.01 assert attributes["start_time"] == "2021-09-21T08:00:00+10:00" assert attributes["end_time"] == "2021-09-21T08:30:00+10:00" assert attributes["renewables"] == 51 @@ -132,8 +132,8 @@ async def test_general_price_sensor(hass: HomeAssistant, setup_general: Mock) -> price = hass.states.get("sensor.mock_title_general_price") assert price attributes = price.attributes - assert attributes.get("range_min") == 7.8 - assert attributes.get("range_max") == 12.4 + assert attributes.get("range_min") == 0.08 + assert attributes.get("range_max") == 0.12 async def test_general_and_controlled_load_price_sensor( @@ -141,16 +141,15 @@ async def test_general_and_controlled_load_price_sensor( ) -> None: """Test the Controlled Price sensor.""" assert len(hass.states.async_all()) == 6 - print(hass.states) price = hass.states.get("sensor.mock_title_controlled_load_price") assert price assert price.state == "0.08" attributes = price.attributes assert attributes["duration"] == 30 assert attributes["date"] == "2021-09-21" - assert attributes["per_kwh"] == 8 + assert attributes["per_kwh"] == 0.08 assert attributes["nem_date"] == "2021-09-21T08:30:00+10:00" - assert attributes["spot_per_kwh"] == 1 + assert attributes["spot_per_kwh"] == 0.01 assert attributes["start_time"] == "2021-09-21T08:00:00+10:00" assert attributes["end_time"] == "2021-09-21T08:30:00+10:00" assert attributes["renewables"] == 51 @@ -172,9 +171,9 @@ async def test_general_and_feed_in_price_sensor( attributes = price.attributes assert attributes["duration"] == 30 assert attributes["date"] == "2021-09-21" - assert attributes["per_kwh"] == -8 + assert attributes["per_kwh"] == -0.08 assert attributes["nem_date"] == "2021-09-21T08:30:00+10:00" - assert attributes["spot_per_kwh"] == 1 + assert attributes["spot_per_kwh"] == 0.01 assert attributes["start_time"] == "2021-09-21T08:00:00+10:00" assert attributes["end_time"] == "2021-09-21T08:30:00+10:00" assert attributes["renewables"] == 51 @@ -199,9 +198,9 @@ async def test_general_forecast_sensor( first_forecast = attributes["forecasts"][0] assert first_forecast["duration"] == 30 assert first_forecast["date"] == "2021-09-21" - assert first_forecast["per_kwh"] == 9 + assert first_forecast["per_kwh"] == 0.09 assert first_forecast["nem_date"] == "2021-09-21T09:00:00+10:00" - assert first_forecast["spot_per_kwh"] == 1 + assert first_forecast["spot_per_kwh"] == 0.01 assert first_forecast["start_time"] == "2021-09-21T08:30:00+10:00" assert first_forecast["end_time"] == "2021-09-21T09:00:00+10:00" assert first_forecast["renewables"] == 50 @@ -222,8 +221,8 @@ async def test_general_forecast_sensor( assert price attributes = price.attributes first_forecast = attributes["forecasts"][0] - assert first_forecast.get("range_min") == 7.8 - assert first_forecast.get("range_max") == 12.4 + assert first_forecast.get("range_min") == 0.08 + assert first_forecast.get("range_max") == 0.12 async def test_controlled_load_forecast_sensor( @@ -241,9 +240,9 @@ async def test_controlled_load_forecast_sensor( first_forecast = attributes["forecasts"][0] assert first_forecast["duration"] == 30 assert first_forecast["date"] == "2021-09-21" - assert first_forecast["per_kwh"] == 9 + assert first_forecast["per_kwh"] == 0.09 assert first_forecast["nem_date"] == "2021-09-21T09:00:00+10:00" - assert first_forecast["spot_per_kwh"] == 1 + assert first_forecast["spot_per_kwh"] == 0.01 assert first_forecast["start_time"] == "2021-09-21T08:30:00+10:00" assert first_forecast["end_time"] == "2021-09-21T09:00:00+10:00" assert first_forecast["renewables"] == 50 @@ -265,9 +264,9 @@ async def test_feed_in_forecast_sensor( first_forecast = attributes["forecasts"][0] assert first_forecast["duration"] == 30 assert first_forecast["date"] == "2021-09-21" - assert first_forecast["per_kwh"] == -9 + assert first_forecast["per_kwh"] == -0.09 assert first_forecast["nem_date"] == "2021-09-21T09:00:00+10:00" - assert first_forecast["spot_per_kwh"] == 1 + assert first_forecast["spot_per_kwh"] == 0.01 assert first_forecast["start_time"] == "2021-09-21T08:30:00+10:00" assert first_forecast["end_time"] == "2021-09-21T09:00:00+10:00" assert first_forecast["renewables"] == 50 From 222a0c26e078ee997780f71c7cf1321cb731c8fd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 5 Oct 2021 21:31:11 -0700 Subject: [PATCH 0142/1038] Guard upnp create device (#57156) --- homeassistant/components/upnp/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 14a2c39d3d9..6db8b087378 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -8,6 +8,7 @@ from datetime import timedelta from ipaddress import ip_address from typing import Any +from async_upnp_client.exceptions import UpnpConnectionError import voluptuous as vol from homeassistant import config_entries @@ -122,7 +123,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: location = discovery_info[ # pylint: disable=unsubscriptable-object ssdp.ATTR_SSDP_LOCATION ] - device = await Device.async_create_device(hass, location) + try: + device = await Device.async_create_device(hass, location) + except UpnpConnectionError as err: + LOGGER.debug("Error connecting to device %s", location) + raise ConfigEntryNotReady from err # Ensure entry has a unique_id. if not entry.unique_id: From a809f5fcf7383bc77ce6ac226ae26f5d3882a01b Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Wed, 6 Oct 2021 10:07:30 +0200 Subject: [PATCH 0143/1038] Update Daikin config_flow with better error handling (#57069) --- .../components/daikin/config_flow.py | 17 ++++++++------- homeassistant/components/daikin/strings.json | 1 + .../components/daikin/translations/en.json | 1 + tests/components/daikin/test_config_flow.py | 21 ++++++++++++++----- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/daikin/config_flow.py b/homeassistant/components/daikin/config_flow.py index ea0709e5557..43d169e3440 100644 --- a/homeassistant/components/daikin/config_flow.py +++ b/homeassistant/components/daikin/config_flow.py @@ -75,7 +75,8 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): uuid=uuid, password=password, ) - except asyncio.TimeoutError: + except (asyncio.TimeoutError, ClientError): + self.host = None return self.async_show_form( step_id="user", data_schema=self.schema, @@ -87,13 +88,6 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data_schema=self.schema, errors={"base": "invalid_auth"}, ) - except ClientError: - _LOGGER.exception("ClientError") - return self.async_show_form( - step_id="user", - data_schema=self.schema, - errors={"base": "unknown"}, - ) except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected error creating device") return self.async_show_form( @@ -109,6 +103,13 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """User initiated config flow.""" if user_input is None: return self.async_show_form(step_id="user", data_schema=self.schema) + if user_input.get(CONF_API_KEY) and user_input.get(CONF_PASSWORD): + self.host = user_input.get(CONF_HOST) + return self.async_show_form( + step_id="user", + data_schema=self.schema, + errors={"base": "api_password"}, + ) return await self._create_device( user_input[CONF_HOST], user_input.get(CONF_API_KEY), diff --git a/homeassistant/components/daikin/strings.json b/homeassistant/components/daikin/strings.json index fc2b6e79a5e..5c759384795 100644 --- a/homeassistant/components/daikin/strings.json +++ b/homeassistant/components/daikin/strings.json @@ -18,6 +18,7 @@ "error": { "unknown": "[%key:common::config_flow::error::unknown%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "api_password": "[%key:common::config_flow::error::invalid_auth%], use either API Key or Password.", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } } diff --git a/homeassistant/components/daikin/translations/en.json b/homeassistant/components/daikin/translations/en.json index d1db170d769..84843ba8211 100644 --- a/homeassistant/components/daikin/translations/en.json +++ b/homeassistant/components/daikin/translations/en.json @@ -5,6 +5,7 @@ "cannot_connect": "Failed to connect" }, "error": { + "api_password": "Invalid authentication, use either API Key or Password.", "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" diff --git a/tests/components/daikin/test_config_flow.py b/tests/components/daikin/test_config_flow.py index 624268d7ee3..91ea79f4aa7 100644 --- a/tests/components/daikin/test_config_flow.py +++ b/tests/components/daikin/test_config_flow.py @@ -3,13 +3,12 @@ import asyncio from unittest.mock import PropertyMock, patch -from aiohttp import ClientError -from aiohttp.web_exceptions import HTTPForbidden +from aiohttp import ClientError, web_exceptions import pytest from homeassistant.components.daikin.const import KEY_MAC from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY, @@ -84,8 +83,8 @@ async def test_abort_if_already_setup(hass, mock_daikin): "s_effect,reason", [ (asyncio.TimeoutError, "cannot_connect"), - (HTTPForbidden, "invalid_auth"), - (ClientError, "unknown"), + (ClientError, "cannot_connect"), + (web_exceptions.HTTPForbidden, "invalid_auth"), (Exception, "unknown"), ], ) @@ -103,6 +102,18 @@ async def test_device_abort(hass, mock_daikin, s_effect, reason): assert result["step_id"] == "user" +async def test_api_password_abort(hass): + """Test device abort.""" + result = await hass.config_entries.flow.async_init( + "daikin", + context={"source": SOURCE_USER}, + data={CONF_HOST: HOST, CONF_API_KEY: "aa", CONF_PASSWORD: "aa"}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "api_password"} + assert result["step_id"] == "user" + + @pytest.mark.parametrize( "source, data, unique_id", [ From aa03b63884e3230801419d86fa43970ddaa08d4c Mon Sep 17 00:00:00 2001 From: Thomas Schamm Date: Wed, 6 Oct 2021 10:09:02 +0200 Subject: [PATCH 0144/1038] Skip link local addresses in bosch_shc discovery step (#57074) --- .../components/bosch_shc/config_flow.py | 18 ++++++++++---- .../components/bosch_shc/test_config_flow.py | 24 ++++++++++++++++++- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/bosch_shc/config_flow.py b/homeassistant/components/bosch_shc/config_flow.py index 4415a0ff6ef..416dc6cf304 100644 --- a/homeassistant/components/bosch_shc/config_flow.py +++ b/homeassistant/components/bosch_shc/config_flow.py @@ -187,16 +187,26 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="not_bosch_shc") try: - self.info = info = await self._get_info(discovery_info["host"]) + hosts = ( + discovery_info["host"] + if isinstance(discovery_info["host"], list) + else [discovery_info["host"]] + ) + for host in hosts: + if host.startswith("169."): # skip link local address + continue + self.info = await self._get_info(host) + self.host = host + if self.host is None: + return self.async_abort(reason="cannot_connect") except SHCConnectionError: return self.async_abort(reason="cannot_connect") local_name = discovery_info["hostname"][:-1] node_name = local_name[: -len(".local")] - await self.async_set_unique_id(info["unique_id"]) - self._abort_if_unique_id_configured({CONF_HOST: discovery_info["host"]}) - self.host = discovery_info["host"] + await self.async_set_unique_id(self.info["unique_id"]) + self._abort_if_unique_id_configured({CONF_HOST: self.host}) self.context["title_placeholders"] = {"name": node_name} return await self.async_step_confirm_discovery() diff --git a/tests/components/bosch_shc/test_config_flow.py b/tests/components/bosch_shc/test_config_flow.py index 6d8ef9bd32e..543d0438738 100644 --- a/tests/components/bosch_shc/test_config_flow.py +++ b/tests/components/bosch_shc/test_config_flow.py @@ -20,7 +20,7 @@ MOCK_SETTINGS = { "device": {"mac": "test-mac", "hostname": "test-host"}, } DISCOVERY_INFO = { - "host": "1.1.1.1", + "host": ["169.1.1.1", "1.1.1.1"], "port": 0, "hostname": "shc012345.local.", "type": "_http._tcp.local.", @@ -526,6 +526,28 @@ async def test_zeroconf_cannot_connect(hass, mock_zeroconf): assert result["reason"] == "cannot_connect" +async def test_zeroconf_link_local(hass, mock_zeroconf): + """Test we get the form.""" + DISCOVERY_INFO_LINK_LOCAL = { + "host": ["169.1.1.1"], + "port": 0, + "hostname": "shc012345.local.", + "type": "_http._tcp.local.", + "name": "Bosch SHC [test-mac]._http._tcp.local.", + } + + with patch( + "boschshcpy.session.SHCSession.mdns_info", side_effect=SHCConnectionError + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DISCOVERY_INFO_LINK_LOCAL, + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + assert result["type"] == "abort" + assert result["reason"] == "cannot_connect" + + async def test_zeroconf_not_bosch_shc(hass, mock_zeroconf): """Test we filter out non-bosch_shc devices.""" result = await hass.config_entries.flow.async_init( From e9f6bc7364326a98274c8a02514a3914bf347ae8 Mon Sep 17 00:00:00 2001 From: Tomasz Date: Wed, 6 Oct 2021 10:48:11 +0200 Subject: [PATCH 0145/1038] Add missing return type to async_unload_entry and async_setup_entry (#57115) --- homeassistant/components/aemet/__init__.py | 2 +- homeassistant/components/airnow/__init__.py | 2 +- homeassistant/components/asuswrt/__init__.py | 2 +- homeassistant/components/august/__init__.py | 4 ++-- homeassistant/components/aurora/__init__.py | 2 +- homeassistant/components/blebox/__init__.py | 2 +- homeassistant/components/bmw_connected_drive/__init__.py | 2 +- homeassistant/components/coinbase/__init__.py | 2 +- homeassistant/components/control4/__init__.py | 2 +- homeassistant/components/dexcom/__init__.py | 2 +- homeassistant/components/doorbird/__init__.py | 2 +- homeassistant/components/elkm1/__init__.py | 2 +- homeassistant/components/emonitor/__init__.py | 2 +- homeassistant/components/epson/__init__.py | 2 +- homeassistant/components/faa_delays/__init__.py | 2 +- homeassistant/components/flick_electric/__init__.py | 2 +- homeassistant/components/flo/__init__.py | 2 +- homeassistant/components/flume/__init__.py | 2 +- homeassistant/components/foscam/__init__.py | 2 +- homeassistant/components/freebox/__init__.py | 2 +- homeassistant/components/freedompro/__init__.py | 4 ++-- homeassistant/components/google_travel_time/__init__.py | 2 +- homeassistant/components/gree/__init__.py | 2 +- homeassistant/components/habitica/__init__.py | 2 +- homeassistant/components/harmony/__init__.py | 2 +- homeassistant/components/heos/__init__.py | 2 +- homeassistant/components/huisbaasje/__init__.py | 2 +- homeassistant/components/hunterdouglas_powerview/__init__.py | 2 +- homeassistant/components/hvv_departures/__init__.py | 2 +- homeassistant/components/ialarm/__init__.py | 2 +- homeassistant/components/icloud/__init__.py | 2 +- homeassistant/components/juicenet/__init__.py | 2 +- homeassistant/components/kmtronic/__init__.py | 2 +- homeassistant/components/kodi/__init__.py | 2 +- homeassistant/components/konnected/__init__.py | 2 +- homeassistant/components/kulersky/__init__.py | 2 +- homeassistant/components/litejet/__init__.py | 2 +- homeassistant/components/litterrobot/__init__.py | 2 +- homeassistant/components/lyric/__init__.py | 2 +- homeassistant/components/mazda/__init__.py | 2 +- homeassistant/components/meteo_france/__init__.py | 2 +- homeassistant/components/metoffice/__init__.py | 2 +- homeassistant/components/monoprice/__init__.py | 2 +- homeassistant/components/mullvad/__init__.py | 2 +- homeassistant/components/myq/__init__.py | 2 +- homeassistant/components/nexia/__init__.py | 2 +- homeassistant/components/nuheat/__init__.py | 2 +- homeassistant/components/nut/__init__.py | 2 +- homeassistant/components/nws/__init__.py | 2 +- homeassistant/components/omnilogic/__init__.py | 2 +- homeassistant/components/ondilo_ico/__init__.py | 2 +- homeassistant/components/onvif/__init__.py | 2 +- homeassistant/components/openweathermap/__init__.py | 2 +- homeassistant/components/philips_js/__init__.py | 2 +- homeassistant/components/picnic/__init__.py | 2 +- homeassistant/components/plaato/__init__.py | 2 +- homeassistant/components/plugwise/__init__.py | 2 +- homeassistant/components/point/__init__.py | 2 +- homeassistant/components/poolsense/__init__.py | 2 +- homeassistant/components/powerwall/__init__.py | 2 +- homeassistant/components/progettihwsw/__init__.py | 2 +- homeassistant/components/rachio/__init__.py | 2 +- homeassistant/components/risco/__init__.py | 2 +- homeassistant/components/screenlogic/__init__.py | 2 +- homeassistant/components/sense/__init__.py | 2 +- homeassistant/components/smappee/__init__.py | 2 +- homeassistant/components/smart_meter_texas/__init__.py | 2 +- homeassistant/components/smarthab/__init__.py | 2 +- homeassistant/components/smartthings/__init__.py | 2 +- homeassistant/components/sms/__init__.py | 2 +- homeassistant/components/soma/__init__.py | 2 +- homeassistant/components/somfy/__init__.py | 2 +- homeassistant/components/somfy_mylink/__init__.py | 2 +- homeassistant/components/srp_energy/__init__.py | 2 +- homeassistant/components/subaru/__init__.py | 2 +- homeassistant/components/tado/__init__.py | 2 +- homeassistant/components/velbus/__init__.py | 2 +- homeassistant/components/vilfo/__init__.py | 2 +- homeassistant/components/volumio/__init__.py | 2 +- homeassistant/components/wallbox/__init__.py | 2 +- homeassistant/components/whirlpool/__init__.py | 4 ++-- homeassistant/components/wiffi/__init__.py | 2 +- homeassistant/components/wilight/__init__.py | 2 +- homeassistant/components/wolflink/__init__.py | 2 +- homeassistant/components/xbox/__init__.py | 2 +- homeassistant/components/zerproc/__init__.py | 2 +- 86 files changed, 89 insertions(+), 89 deletions(-) diff --git a/homeassistant/components/aemet/__init__.py b/homeassistant/components/aemet/__init__.py index 7a20e77f0b0..a914a23a0da 100644 --- a/homeassistant/components/aemet/__init__.py +++ b/homeassistant/components/aemet/__init__.py @@ -52,7 +52,7 @@ async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None: await hass.config_entries.async_reload(entry.entry_id) -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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) diff --git a/homeassistant/components/airnow/__init__.py b/homeassistant/components/airnow/__init__.py index 52ee1a0e8fc..be385d19645 100644 --- a/homeassistant/components/airnow/__init__.py +++ b/homeassistant/components/airnow/__init__.py @@ -64,7 +64,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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) diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py index c87ed85b759..1305403e1d7 100644 --- a/homeassistant/components/asuswrt/__init__.py +++ b/homeassistant/components/asuswrt/__init__.py @@ -142,7 +142,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index f48068498c6..700b03a85da 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -33,7 +33,7 @@ API_CACHED_ATTRS = ( ) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up August from a config entry.""" august_gateway = AugustGateway(hass) @@ -47,7 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): raise ConfigEntryNotReady from err -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" hass.data[DOMAIN][entry.entry_id][DATA_AUGUST].async_stop() diff --git a/homeassistant/components/aurora/__init__.py b/homeassistant/components/aurora/__init__.py index 0c80cda4bd5..3fa5d5b39a7 100644 --- a/homeassistant/components/aurora/__init__.py +++ b/homeassistant/components/aurora/__init__.py @@ -78,7 +78,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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) diff --git a/homeassistant/components/blebox/__init__.py b/homeassistant/components/blebox/__init__.py index 95b36612add..69ca8e23a73 100644 --- a/homeassistant/components/blebox/__init__.py +++ b/homeassistant/components/blebox/__init__.py @@ -47,7 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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) diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 85a5c9cd02f..f82a04f9355 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -163,7 +163,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms( entry, [platform for platform in PLATFORMS if platform != NOTIFY_DOMAIN] diff --git a/homeassistant/components/coinbase/__init__.py b/homeassistant/components/coinbase/__init__.py index 033c398e09c..111064924af 100644 --- a/homeassistant/components/coinbase/__init__.py +++ b/homeassistant/components/coinbase/__init__.py @@ -81,7 +81,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/control4/__init__.py b/homeassistant/components/control4/__init__.py index b7806e665f3..e48f7fc0ccb 100644 --- a/homeassistant/components/control4/__init__.py +++ b/homeassistant/components/control4/__init__.py @@ -119,7 +119,7 @@ async def update_listener(hass, config_entry): await hass.config_entries.async_reload(config_entry.entry_id) -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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) diff --git a/homeassistant/components/dexcom/__init__.py b/homeassistant/components/dexcom/__init__.py index 68622a23350..8db69b38927 100644 --- a/homeassistant/components/dexcom/__init__.py +++ b/homeassistant/components/dexcom/__init__.py @@ -71,7 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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) hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 255b9e54636..4c720d5b9de 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -154,7 +154,7 @@ def _init_doorbird_device(device): return device.ready(), device.info() -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index a392fbd302a..0eb34b2fc61 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -285,7 +285,7 @@ def _find_elk_by_prefix(hass, prefix): return hass.data[DOMAIN][entry_id]["elk"] -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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) diff --git a/homeassistant/components/emonitor/__init__.py b/homeassistant/components/emonitor/__init__.py index 91263db5127..1cff3c9c30e 100644 --- a/homeassistant/components/emonitor/__init__.py +++ b/homeassistant/components/emonitor/__init__.py @@ -42,7 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/epson/__init__.py b/homeassistant/components/epson/__init__.py index e60df7dc8bc..036b8df7ca9 100644 --- a/homeassistant/components/epson/__init__.py +++ b/homeassistant/components/epson/__init__.py @@ -56,7 +56,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/faa_delays/__init__.py b/homeassistant/components/faa_delays/__init__.py index c270a878d49..e27916ec6c1 100644 --- a/homeassistant/components/faa_delays/__init__.py +++ b/homeassistant/components/faa_delays/__init__.py @@ -34,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/flick_electric/__init__.py b/homeassistant/components/flick_electric/__init__.py index 690cbe03cdd..ca634310659 100644 --- a/homeassistant/components/flick_electric/__init__.py +++ b/homeassistant/components/flick_electric/__init__.py @@ -36,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/flo/__init__.py b/homeassistant/components/flo/__init__.py index 734c4d9e766..32802ba85d3 100644 --- a/homeassistant/components/flo/__init__.py +++ b/homeassistant/components/flo/__init__.py @@ -49,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/flume/__init__.py b/homeassistant/components/flume/__init__.py index 1b441aa6ba5..3ca99a335f2 100644 --- a/homeassistant/components/flume/__init__.py +++ b/homeassistant/components/flume/__init__.py @@ -71,7 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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) diff --git a/homeassistant/components/foscam/__init__.py b/homeassistant/components/foscam/__init__.py index 9d825ed0851..a6714094b0c 100644 --- a/homeassistant/components/foscam/__init__.py +++ b/homeassistant/components/foscam/__init__.py @@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index bb308e154ef..c343e8d629c 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -67,7 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/freedompro/__init__.py b/homeassistant/components/freedompro/__init__.py index 40d440d83eb..6a6253f2e06 100644 --- a/homeassistant/components/freedompro/__init__.py +++ b/homeassistant/components/freedompro/__init__.py @@ -26,7 +26,7 @@ PLATFORMS = [ ] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Freedompro from a config entry.""" hass.data.setdefault(DOMAIN, {}) api_key = entry.data[CONF_API_KEY] @@ -43,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/google_travel_time/__init__.py b/homeassistant/components/google_travel_time/__init__.py index 88fe587fbd0..7ac88b84727 100644 --- a/homeassistant/components/google_travel_time/__init__.py +++ b/homeassistant/components/google_travel_time/__init__.py @@ -25,6 +25,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/gree/__init__.py b/homeassistant/components/gree/__init__.py index b91324ba4b3..761c8e0ab78 100644 --- a/homeassistant/components/gree/__init__.py +++ b/homeassistant/components/gree/__init__.py @@ -45,7 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" if hass.data[DOMAIN].get(DISPATCHERS) is not None: for cleanup in hass.data[DOMAIN][DISPATCHERS]: diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index 1d1536d1679..a08932d9c1b 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -164,7 +164,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py index c541aa0e0e3..00a0feca4f7 100644 --- a/homeassistant/components/harmony/__init__.py +++ b/homeassistant/components/harmony/__init__.py @@ -110,7 +110,7 @@ async def _update_listener(hass: HomeAssistant, entry: ConfigEntry): ) -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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) diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index 35520927e97..a4cdb6cdddc 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -130,7 +130,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" controller_manager = hass.data[DOMAIN][DATA_CONTROLLER_MANAGER] await controller_manager.disconnect() diff --git a/homeassistant/components/huisbaasje/__init__.py b/homeassistant/components/huisbaasje/__init__.py index 8bd07474705..6c8df713344 100644 --- a/homeassistant/components/huisbaasje/__init__.py +++ b/homeassistant/components/huisbaasje/__init__.py @@ -68,7 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" # Forward the unloading of the entry to the platform unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py index 7b945d9bdfe..a8461f4da5c 100644 --- a/homeassistant/components/hunterdouglas_powerview/__init__.py +++ b/homeassistant/components/hunterdouglas_powerview/__init__.py @@ -162,7 +162,7 @@ def _async_map_data_by_id(data): return {entry[ATTR_ID]: entry for entry in data} -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/hvv_departures/__init__.py b/homeassistant/components/hvv_departures/__init__.py index d57fe20f95e..09b9d516bad 100644 --- a/homeassistant/components/hvv_departures/__init__.py +++ b/homeassistant/components/hvv_departures/__init__.py @@ -31,6 +31,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/ialarm/__init__.py b/homeassistant/components/ialarm/__init__.py index 1fb35e23c9e..af8fd0695e4 100644 --- a/homeassistant/components/ialarm/__init__.py +++ b/homeassistant/components/ialarm/__init__.py @@ -44,7 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload iAlarm config.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index 9267170391d..865b77b4ddf 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -218,7 +218,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/juicenet/__init__.py b/homeassistant/components/juicenet/__init__.py index 0480eac80b3..41097589244 100644 --- a/homeassistant/components/juicenet/__init__.py +++ b/homeassistant/components/juicenet/__init__.py @@ -99,7 +99,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/kmtronic/__init__.py b/homeassistant/components/kmtronic/__init__.py index 5226dfb4f26..72c0c772aec 100644 --- a/homeassistant/components/kmtronic/__init__.py +++ b/homeassistant/components/kmtronic/__init__.py @@ -68,7 +68,7 @@ async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry) - await hass.config_entries.async_reload(config_entry.entry_id) -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/kodi/__init__.py b/homeassistant/components/kodi/__init__.py index 0b2b1b8047b..7c7740e8fe8 100644 --- a/homeassistant/components/kodi/__init__.py +++ b/homeassistant/components/kodi/__init__.py @@ -71,7 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index 29502f3878c..8fd439a0355 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -268,7 +268,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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) diff --git a/homeassistant/components/kulersky/__init__.py b/homeassistant/components/kulersky/__init__.py index 03819c360d6..0ddac1850d7 100644 --- a/homeassistant/components/kulersky/__init__.py +++ b/homeassistant/components/kulersky/__init__.py @@ -20,7 +20,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" # Stop discovery unregister_discovery = hass.data[DOMAIN].pop(DATA_DISCOVERY_SUBSCRIPTION, None) diff --git a/homeassistant/components/litejet/__init__.py b/homeassistant/components/litejet/__init__.py index b69df5ffd31..413a886798a 100644 --- a/homeassistant/components/litejet/__init__.py +++ b/homeassistant/components/litejet/__init__.py @@ -63,7 +63,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a LiteJet config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index 25500a1fcfb..04a98a5bf60 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index d253c1b0349..7189c5ce74e 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -122,7 +122,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index c64a3b35993..74a92b3e371 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -198,7 +198,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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) diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index 15a9aa7d3cd..142d90e6284 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -180,7 +180,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" if hass.data[DOMAIN][entry.entry_id][COORDINATOR_ALERT]: diff --git a/homeassistant/components/metoffice/__init__.py b/homeassistant/components/metoffice/__init__.py index 1a55c940d81..4c187a606c7 100644 --- a/homeassistant/components/metoffice/__init__.py +++ b/homeassistant/components/metoffice/__init__.py @@ -90,7 +90,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/monoprice/__init__.py b/homeassistant/components/monoprice/__init__.py index 9ee6128c784..90f1e2976ef 100644 --- a/homeassistant/components/monoprice/__init__.py +++ b/homeassistant/components/monoprice/__init__.py @@ -53,7 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/mullvad/__init__.py b/homeassistant/components/mullvad/__init__.py index 44d10a66d5d..eeab5abed2f 100644 --- a/homeassistant/components/mullvad/__init__.py +++ b/homeassistant/components/mullvad/__init__.py @@ -38,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: dict) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/myq/__init__.py b/homeassistant/components/myq/__init__.py index 253c10544c9..b6ecd8a8e23 100644 --- a/homeassistant/components/myq/__init__.py +++ b/homeassistant/components/myq/__init__.py @@ -67,7 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/nexia/__init__.py b/homeassistant/components/nexia/__init__.py index e9f31749042..6059a639019 100644 --- a/homeassistant/components/nexia/__init__.py +++ b/homeassistant/components/nexia/__init__.py @@ -80,7 +80,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/nuheat/__init__.py b/homeassistant/components/nuheat/__init__.py index 08cacd2bf76..fee23ac496c 100644 --- a/homeassistant/components/nuheat/__init__.py +++ b/homeassistant/components/nuheat/__init__.py @@ -79,7 +79,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index 5b5389f0270..33cbc9fb47c 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -163,7 +163,7 @@ def find_resources_in_config_entry(config_entry): return config_entry.data[CONF_RESOURCES] -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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) diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py index 318ba687d30..b78961911d5 100644 --- a/homeassistant/components/nws/__init__.py +++ b/homeassistant/components/nws/__init__.py @@ -158,7 +158,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/omnilogic/__init__.py b/homeassistant/components/omnilogic/__init__.py index 556c100033b..ee43456285b 100644 --- a/homeassistant/components/omnilogic/__init__.py +++ b/homeassistant/components/omnilogic/__init__.py @@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/ondilo_ico/__init__.py b/homeassistant/components/ondilo_ico/__init__.py index 2b8b2cc22b7..2d00af2a78b 100644 --- a/homeassistant/components/ondilo_ico/__init__.py +++ b/homeassistant/components/ondilo_ico/__init__.py @@ -33,7 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index 67bec21e123..658ee7502f4 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -96,7 +96,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" device = hass.data[DOMAIN][entry.unique_id] diff --git a/homeassistant/components/openweathermap/__init__.py b/homeassistant/components/openweathermap/__init__.py index 49bb870271e..d9643d6a4d3 100644 --- a/homeassistant/components/openweathermap/__init__.py +++ b/homeassistant/components/openweathermap/__init__.py @@ -89,7 +89,7 @@ async def async_update_options(hass: HomeAssistant, entry: ConfigEntry): await hass.config_entries.async_reload(entry.entry_id) -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index 79698ea4136..79999b95d12 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -55,7 +55,7 @@ async def async_update_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: await hass.config_entries.async_reload(entry.entry_id) -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/picnic/__init__.py b/homeassistant/components/picnic/__init__.py index 1a2164dafed..ce6a02a81b5 100644 --- a/homeassistant/components/picnic/__init__.py +++ b/homeassistant/components/picnic/__init__.py @@ -39,7 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/plaato/__init__.py b/homeassistant/components/plaato/__init__.py index 333ee6b6e95..c965c632827 100644 --- a/homeassistant/components/plaato/__init__.py +++ b/homeassistant/components/plaato/__init__.py @@ -147,7 +147,7 @@ def _set_entry_data(entry, hass, coordinator=None, device_id=None): } -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" use_webhook = entry.data[CONF_USE_WEBHOOK] hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() diff --git a/homeassistant/components/plugwise/__init__.py b/homeassistant/components/plugwise/__init__.py index d425cca246e..89349e1f0b5 100644 --- a/homeassistant/components/plugwise/__init__.py +++ b/homeassistant/components/plugwise/__init__.py @@ -15,7 +15,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return False -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload the Plugwise components.""" if entry.data.get(CONF_HOST): return await async_unload_entry_gw(hass, entry) diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index 303282ead54..66f640cb7be 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -138,7 +138,7 @@ async def async_setup_webhook(hass: HomeAssistant, entry: ConfigEntry, session): ) -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) session = hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/poolsense/__init__.py b/homeassistant/components/poolsense/__init__.py index 134be1cefba..fb7927de970 100644 --- a/homeassistant/components/poolsense/__init__.py +++ b/homeassistant/components/poolsense/__init__.py @@ -51,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index 5560c51f72b..6c9de5585b9 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -216,7 +216,7 @@ def _fetch_powerwall_data(power_wall): } -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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) diff --git a/homeassistant/components/progettihwsw/__init__.py b/homeassistant/components/progettihwsw/__init__.py index 55ed9c0241b..e3555079a4d 100644 --- a/homeassistant/components/progettihwsw/__init__.py +++ b/homeassistant/components/progettihwsw/__init__.py @@ -27,7 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index 3e8e26e2a13..2d1caf23c42 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -25,7 +25,7 @@ PLATFORMS = ["switch", "binary_sensor"] CONFIG_SCHEMA = cv.deprecated(DOMAIN) -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index ce43ce09988..0e61d6c7aeb 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -69,7 +69,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index 2ec087d1e61..a9ba02de18d 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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) hass.data[DOMAIN][entry.entry_id]["listener"]() diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index bfc3f42b421..d7953a5a5fe 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -137,7 +137,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/smappee/__init__.py b/homeassistant/components/smappee/__init__.py index 94c5bbcdcac..43c7aca9a34 100644 --- a/homeassistant/components/smappee/__init__.py +++ b/homeassistant/components/smappee/__init__.py @@ -110,7 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/smart_meter_texas/__init__.py b/homeassistant/components/smart_meter_texas/__init__.py index 16379dca8cb..0d636c7558f 100644 --- a/homeassistant/components/smart_meter_texas/__init__.py +++ b/homeassistant/components/smart_meter_texas/__init__.py @@ -120,7 +120,7 @@ class SmartMeterTexasData: return self.meters -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/smarthab/__init__.py b/homeassistant/components/smarthab/__init__.py index 06d4de36b3c..7f8d5a38222 100644 --- a/homeassistant/components/smarthab/__init__.py +++ b/homeassistant/components/smarthab/__init__.py @@ -77,7 +77,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload config entry from SmartHab integration.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index fef2917fb8d..1913751ba78 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -209,7 +209,7 @@ async def async_get_entry_scenes(entry: ConfigEntry, api): return [] -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" broker = hass.data[DOMAIN][DATA_BROKERS].pop(entry.entry_id, None) if broker: diff --git a/homeassistant/components/sms/__init__.py b/homeassistant/components/sms/__init__.py index 52ea32c96d1..e2904825bbb 100644 --- a/homeassistant/components/sms/__init__.py +++ b/homeassistant/components/sms/__init__.py @@ -50,7 +50,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py index 10ebd26bde2..77ecd3af96b 100644 --- a/homeassistant/components/soma/__init__.py +++ b/homeassistant/components/soma/__init__.py @@ -57,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index 71d7f7f790c..5efd4bfaa3a 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -119,6 +119,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/somfy_mylink/__init__.py b/homeassistant/components/somfy_mylink/__init__.py index 5377846a4c1..bf47617a302 100644 --- a/homeassistant/components/somfy_mylink/__init__.py +++ b/homeassistant/components/somfy_mylink/__init__.py @@ -65,7 +65,7 @@ async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): await hass.config_entries.async_reload(entry.entry_id) -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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) diff --git a/homeassistant/components/srp_energy/__init__.py b/homeassistant/components/srp_energy/__init__.py index 0e25e3f21f6..76d344415d6 100644 --- a/homeassistant/components/srp_energy/__init__.py +++ b/homeassistant/components/srp_energy/__init__.py @@ -35,7 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" # unload srp client hass.data[SRP_ENERGY_DOMAIN] = None diff --git a/homeassistant/components/subaru/__init__.py b/homeassistant/components/subaru/__init__.py index 94c1243b710..ce5ac6a95f5 100644 --- a/homeassistant/components/subaru/__init__.py +++ b/homeassistant/components/subaru/__init__.py @@ -93,7 +93,7 @@ async def async_setup_entry(hass, entry): return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 7a1965e31fb..8bb9b72f117 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -103,7 +103,7 @@ async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): await hass.config_entries.async_reload(entry.entry_id) -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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) diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index 265167f574c..acc90116269 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -131,7 +131,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Remove the velbus connection.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) await hass.data[DOMAIN][entry.entry_id]["cntrl"].stop() diff --git a/homeassistant/components/vilfo/__init__.py b/homeassistant/components/vilfo/__init__.py index 9a65ba3c400..b2e460c85f2 100644 --- a/homeassistant/components/vilfo/__init__.py +++ b/homeassistant/components/vilfo/__init__.py @@ -40,7 +40,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/volumio/__init__.py b/homeassistant/components/volumio/__init__.py index 6e0db0f73eb..215cd2e1aff 100644 --- a/homeassistant/components/volumio/__init__.py +++ b/homeassistant/components/volumio/__init__.py @@ -34,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py index aeacee9b943..9f07329a1dd 100644 --- a/homeassistant/components/wallbox/__init__.py +++ b/homeassistant/components/wallbox/__init__.py @@ -112,7 +112,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/whirlpool/__init__.py b/homeassistant/components/whirlpool/__init__.py index 2c51ee07cc4..c53b9a59ef4 100644 --- a/homeassistant/components/whirlpool/__init__.py +++ b/homeassistant/components/whirlpool/__init__.py @@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["climate"] -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Whirlpool Sixth Sense from a config entry.""" hass.data.setdefault(DOMAIN, {}) @@ -36,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/wiffi/__init__.py b/homeassistant/components/wiffi/__init__.py index e155a48fd72..e19fe227ddb 100644 --- a/homeassistant/components/wiffi/__init__.py +++ b/homeassistant/components/wiffi/__init__.py @@ -63,7 +63,7 @@ async def async_update_options(hass: HomeAssistant, entry: ConfigEntry): await hass.config_entries.async_reload(entry.entry_id) -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" api: WiffiIntegrationApi = hass.data[DOMAIN][entry.entry_id] await api.server.close_server() diff --git a/homeassistant/components/wilight/__init__.py b/homeassistant/components/wilight/__init__.py index 3c3c24db793..fd71c7342e8 100644 --- a/homeassistant/components/wilight/__init__.py +++ b/homeassistant/components/wilight/__init__.py @@ -30,7 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload WiLight config entry.""" # Unload entities for this entry/device. diff --git a/homeassistant/components/wolflink/__init__.py b/homeassistant/components/wolflink/__init__.py index ab078e438c6..e15a5225475 100644 --- a/homeassistant/components/wolflink/__init__.py +++ b/homeassistant/components/wolflink/__init__.py @@ -111,7 +111,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/xbox/__init__.py b/homeassistant/components/xbox/__init__.py index d54d79532ca..2a09c465984 100644 --- a/homeassistant/components/xbox/__init__.py +++ b/homeassistant/components/xbox/__init__.py @@ -107,7 +107,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +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: diff --git a/homeassistant/components/zerproc/__init__.py b/homeassistant/components/zerproc/__init__.py index 8643066f59c..ec48fb90345 100644 --- a/homeassistant/components/zerproc/__init__.py +++ b/homeassistant/components/zerproc/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" # Stop discovery unregister_discovery = hass.data[DOMAIN].pop(DATA_DISCOVERY_SUBSCRIPTION, None) From e69a1c35469d622355d2e6a28e94b9e1042a1128 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 6 Oct 2021 11:33:55 +0200 Subject: [PATCH 0146/1038] Update frontend to 20211006.0 (#57164) --- 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 3b047fbe245..121cf6ea754 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20211004.0" + "home-assistant-frontend==20211006.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5e2e891f265..9bcaec32d7c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==3.4.8 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211004.0 +home-assistant-frontend==20211006.0 httpx==0.19.0 ifaddr==0.1.7 jinja2==3.0.2 diff --git a/requirements_all.txt b/requirements_all.txt index 2da77ab175b..28eaa8072fe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -807,7 +807,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211004.0 +home-assistant-frontend==20211006.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6304ec758c9..4bd5e936133 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -488,7 +488,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211004.0 +home-assistant-frontend==20211006.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 9d84d41f815c58bf15bdea76fc78e7ccc8249f7b Mon Sep 17 00:00:00 2001 From: Jean-Yves Avenard Date: Wed, 6 Oct 2021 22:24:17 +1100 Subject: [PATCH 0147/1038] Change energy state class to STATE_CLASS_TOTAL (#56974) --- homeassistant/components/iotawatt/sensor.py | 4 ++-- tests/components/iotawatt/test_sensor.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/iotawatt/sensor.py b/homeassistant/components/iotawatt/sensor.py index ba0ec30caa0..ec2918b0ce6 100644 --- a/homeassistant/components/iotawatt/sensor.py +++ b/homeassistant/components/iotawatt/sensor.py @@ -9,7 +9,7 @@ from iotawattpy.sensor import Sensor from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + STATE_CLASS_TOTAL, SensorEntity, SensorEntityDescription, ) @@ -83,6 +83,7 @@ ENTITY_DESCRIPTION_KEY_MAP: dict[str, IotaWattSensorEntityDescription] = { "WattHours": IotaWattSensorEntityDescription( "WattHours", native_unit_of_measurement=ENERGY_WATT_HOUR, + state_class=STATE_CLASS_TOTAL, device_class=DEVICE_CLASS_ENERGY, ), "VA": IotaWattSensorEntityDescription( @@ -242,7 +243,6 @@ class IotaWattAccumulatingSensor(IotaWattSensor, RestoreEntity): super().__init__(coordinator, key, entity_description) - self._attr_state_class = STATE_CLASS_TOTAL_INCREASING if self._attr_unique_id is not None: self._attr_unique_id += ".accumulated" diff --git a/tests/components/iotawatt/test_sensor.py b/tests/components/iotawatt/test_sensor.py index 8928c012d48..2397338c22c 100644 --- a/tests/components/iotawatt/test_sensor.py +++ b/tests/components/iotawatt/test_sensor.py @@ -6,7 +6,7 @@ from homeassistant.components.sensor import ( ATTR_STATE_CLASS, DEVICE_CLASS_ENERGY, STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL_INCREASING, + STATE_CLASS_TOTAL, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -74,7 +74,7 @@ async def test_sensor_type_output(hass, mock_iotawatt): state = hass.states.get("sensor.my_watthour_sensor") assert state is not None assert state.state == "243" - assert ATTR_STATE_CLASS not in state.attributes + assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL assert state.attributes[ATTR_FRIENDLY_NAME] == "My WattHour Sensor" assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_WATT_HOUR assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_ENERGY @@ -125,7 +125,7 @@ async def test_sensor_type_accumulated_output(hass, mock_iotawatt): state.attributes[ATTR_FRIENDLY_NAME] == "My WattHour Accumulated Output Sensor.wh Accumulated" ) - assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL_INCREASING + assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_WATT_HOUR assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_ENERGY assert state.attributes["type"] == "Output" @@ -166,7 +166,7 @@ async def test_sensor_type_accumulated_output_error_restore(hass, mock_iotawatt) state.attributes[ATTR_FRIENDLY_NAME] == "My WattHour Accumulated Output Sensor.wh Accumulated" ) - assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL_INCREASING + assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_WATT_HOUR assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_ENERGY assert state.attributes["type"] == "Output" @@ -224,7 +224,7 @@ async def test_sensor_type_multiple_accumulated_output(hass, mock_iotawatt): state.attributes[ATTR_FRIENDLY_NAME] == "My WattHour Accumulated Output Sensor.wh Accumulated" ) - assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL_INCREASING + assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_WATT_HOUR assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_ENERGY assert state.attributes["type"] == "Output" From 8337baa35400fcfa289dd99767135c0f9c98b7e0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 6 Oct 2021 13:29:42 +0200 Subject: [PATCH 0148/1038] Correct migration to recorder schema 18 (#57165) --- .../components/recorder/migration.py | 47 +++++++++---------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 1ced8b73207..a3d2955e55b 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -470,17 +470,20 @@ def _apply_update(instance, session, new_version, old_version): # noqa: C901 elif new_version == 18: # Recreate the statistics and statistics meta tables. # - # Order matters! Statistics has a relation with StatisticsMeta, - # so statistics need to be deleted before meta (or in pair depending - # on the SQL backend); and meta needs to be created before statistics. - if sqlalchemy.inspect(engine).has_table( - StatisticsMeta.__tablename__ - ) or sqlalchemy.inspect(engine).has_table(Statistics.__tablename__): - Base.metadata.drop_all( - bind=engine, tables=[Statistics.__table__, StatisticsMeta.__table__] - ) + # Order matters! Statistics and StatisticsShortTerm have a relation with + # StatisticsMeta, so statistics need to be deleted before meta (or in pair + # depending on the SQL backend); and meta needs to be created before statistics. + Base.metadata.drop_all( + bind=engine, + tables=[ + StatisticsShortTerm.__table__, + Statistics.__table__, + StatisticsMeta.__table__, + ], + ) StatisticsMeta.__table__.create(engine) + StatisticsShortTerm.__table__.create(engine) Statistics.__table__.create(engine) elif new_version == 19: # This adds the statistic runs table, insert a fake run to prevent duplicating @@ -527,23 +530,15 @@ def _apply_update(instance, session, new_version, old_version): # noqa: C901 # so statistics need to be deleted before meta (or in pair depending # on the SQL backend); and meta needs to be created before statistics. if engine.dialect.name == "oracle": - if ( - sqlalchemy.inspect(engine).has_table(StatisticsMeta.__tablename__) - or sqlalchemy.inspect(engine).has_table(Statistics.__tablename__) - or sqlalchemy.inspect(engine).has_table(StatisticsRuns.__tablename__) - or sqlalchemy.inspect(engine).has_table( - StatisticsShortTerm.__tablename__ - ) - ): - Base.metadata.drop_all( - bind=engine, - tables=[ - StatisticsShortTerm.__table__, - Statistics.__table__, - StatisticsMeta.__table__, - StatisticsRuns.__table__, - ], - ) + Base.metadata.drop_all( + bind=engine, + tables=[ + StatisticsShortTerm.__table__, + Statistics.__table__, + StatisticsMeta.__table__, + StatisticsRuns.__table__, + ], + ) StatisticsRuns.__table__.create(engine) StatisticsMeta.__table__.create(engine) From 3e89ebb1cb531c9fe4d9ed78d22b00a764eab4d7 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 6 Oct 2021 13:30:13 +0200 Subject: [PATCH 0149/1038] Remove Netgear tracker link_rate check on Orbi (#57032) * Netgear tracker: remove link_rate check on Orbi * fix debug message * Add orbi models * check start of model in V2 check * fix black --- homeassistant/components/netgear/const.py | 19 ++++++++++++++++++- homeassistant/components/netgear/router.py | 11 ++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/netgear/const.py b/homeassistant/components/netgear/const.py index 8b520485e1e..325d9e68cd8 100644 --- a/homeassistant/components/netgear/const.py +++ b/homeassistant/components/netgear/const.py @@ -11,7 +11,24 @@ DEFAULT_CONSIDER_HOME = timedelta(seconds=180) DEFAULT_NAME = "Netgear router" # update method V2 models -MODELS_V2 = ["Orbi"] +MODELS_V2 = [ + "Orbi", + "RBK", + "RBR", + "RBS", + "RBW", + "LBK", + "LBR", + "CBK", + "CBR", + "SRC", + "SRK", + "SRR", + "SRS", + "SXK", + "SXR", + "SXS", +] # Icons DEVICE_ICONS = { diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index 83b1aaa9f32..53cc4f32728 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -146,8 +146,9 @@ class NetgearRouter: self.model = self._info.get("ModelName") self.firmware_version = self._info.get("Firmwareversion") - if self.model in MODELS_V2: - self._method_version = 2 + for model in MODELS_V2: + if self.model.startswith(model): + self._method_version = 2 async def async_setup(self) -> None: """Set up a Netgear router.""" @@ -198,12 +199,12 @@ class NetgearRouter: ntg_devices = await self.async_get_attached_devices() now = dt_util.utcnow() + if _LOGGER.isEnabledFor(logging.DEBUG): + _LOGGER.debug("Netgear scan result: \n%s", ntg_devices) + for ntg_device in ntg_devices: device_mac = format_mac(ntg_device.mac) - if self._method_version == 2 and not ntg_device.link_rate: - continue - if not self.devices.get(device_mac): new_device = True From 8fea54fff7dea8cd64db71dfd4ae018036013392 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 7 Oct 2021 00:10:57 +0000 Subject: [PATCH 0150/1038] [ci skip] Translation update --- .../components/airthings/translations/pl.json | 16 ++++++ .../amberelectric/translations/pl.json | 9 ++++ .../crownstone/translations/pl.json | 50 +++++++++++++++++++ .../components/daikin/translations/ca.json | 1 + .../components/daikin/translations/de.json | 1 + .../components/daikin/translations/et.json | 1 + .../components/daikin/translations/hu.json | 1 + .../components/daikin/translations/ru.json | 1 + .../components/dlna_dmr/translations/pl.json | 5 ++ .../components/flux_led/translations/he.json | 20 ++++++++ .../components/flux_led/translations/pl.json | 5 ++ .../components/geofency/translations/hu.json | 2 +- .../components/gpslogger/translations/hu.json | 2 +- .../components/homekit/translations/pl.json | 1 + .../components/hue/translations/sk.json | 13 +++++ .../components/iotawatt/translations/pl.json | 3 +- .../modem_callerid/translations/pl.json | 19 +++++++ .../components/mqtt/translations/hu.json | 4 +- .../components/mysensors/translations/hu.json | 12 ++--- .../components/netgear/translations/pl.json | 22 ++++++++ .../components/notion/translations/pl.json | 9 +++- .../components/ozw/translations/hu.json | 2 +- .../philips_js/translations/sk.json | 42 ++++++++++++++++ .../components/renault/translations/pl.json | 10 +++- .../components/rfxtrx/translations/pl.json | 5 ++ .../components/shelly/translations/pl.json | 3 +- .../surepetcare/translations/pl.json | 15 ++++++ .../components/switchbot/translations/pl.json | 31 ++++++++++++ .../synology_dsm/translations/pl.json | 6 +++ .../components/tasmota/translations/hu.json | 4 +- .../components/tplink/translations/pl.json | 5 ++ .../components/tuya/translations/ca.json | 2 +- .../components/tuya/translations/de.json | 2 +- .../components/tuya/translations/et.json | 2 +- .../components/tuya/translations/he.json | 5 +- .../components/tuya/translations/hu.json | 2 +- .../components/tuya/translations/pl.json | 11 +++- .../components/tuya/translations/ru.json | 2 +- .../components/tuya/translations/sk.json | 11 ++++ .../components/tuya/translations/zh-Hant.json | 2 +- .../components/watttime/translations/pl.json | 30 +++++++++++ .../components/whirlpool/translations/pl.json | 16 ++++++ .../components/zha/translations/hu.json | 6 +-- .../components/zwave_js/translations/hu.json | 2 +- 44 files changed, 383 insertions(+), 30 deletions(-) create mode 100644 homeassistant/components/airthings/translations/pl.json create mode 100644 homeassistant/components/amberelectric/translations/pl.json create mode 100644 homeassistant/components/crownstone/translations/pl.json create mode 100644 homeassistant/components/dlna_dmr/translations/pl.json create mode 100644 homeassistant/components/flux_led/translations/he.json create mode 100644 homeassistant/components/flux_led/translations/pl.json create mode 100644 homeassistant/components/hue/translations/sk.json create mode 100644 homeassistant/components/modem_callerid/translations/pl.json create mode 100644 homeassistant/components/netgear/translations/pl.json create mode 100644 homeassistant/components/philips_js/translations/sk.json create mode 100644 homeassistant/components/surepetcare/translations/pl.json create mode 100644 homeassistant/components/switchbot/translations/pl.json create mode 100644 homeassistant/components/tuya/translations/sk.json create mode 100644 homeassistant/components/watttime/translations/pl.json create mode 100644 homeassistant/components/whirlpool/translations/pl.json diff --git a/homeassistant/components/airthings/translations/pl.json b/homeassistant/components/airthings/translations/pl.json new file mode 100644 index 00000000000..c3895e64423 --- /dev/null +++ b/homeassistant/components/airthings/translations/pl.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "id": "ID", + "secret": "Sekret" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/amberelectric/translations/pl.json b/homeassistant/components/amberelectric/translations/pl.json new file mode 100644 index 00000000000..1054014ea49 --- /dev/null +++ b/homeassistant/components/amberelectric/translations/pl.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Amber Electric" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/pl.json b/homeassistant/components/crownstone/translations/pl.json new file mode 100644 index 00000000000..948d71471e3 --- /dev/null +++ b/homeassistant/components/crownstone/translations/pl.json @@ -0,0 +1,50 @@ +{ + "config": { + "error": { + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "usb_config": { + "data": { + "usb_path": "\u015acie\u017cka do urz\u0105dzenia USB" + } + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "\u015acie\u017cka do urz\u0105dzenia USB" + } + }, + "user": { + "data": { + "email": "E-mail", + "password": "Has\u0142o" + }, + "title": "Konto Crownstone" + } + } + }, + "options": { + "step": { + "usb_config": { + "data": { + "usb_path": "\u015acie\u017cka do urz\u0105dzenia USB" + } + }, + "usb_config_option": { + "data": { + "usb_path": "\u015acie\u017cka do urz\u0105dzenia USB" + } + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "\u015acie\u017cka do urz\u0105dzenia USB" + } + }, + "usb_manual_config_option": { + "data": { + "usb_manual_path": "\u015acie\u017cka do urz\u0105dzenia USB" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/translations/ca.json b/homeassistant/components/daikin/translations/ca.json index 0b8c2aae7eb..a895697f490 100644 --- a/homeassistant/components/daikin/translations/ca.json +++ b/homeassistant/components/daikin/translations/ca.json @@ -5,6 +5,7 @@ "cannot_connect": "Ha fallat la connexi\u00f3" }, "error": { + "api_password": "Autenticaci\u00f3 inv\u00e0lida, utilitza la clau API o la contrasenya.", "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" diff --git a/homeassistant/components/daikin/translations/de.json b/homeassistant/components/daikin/translations/de.json index 038a997201a..b587adc62d1 100644 --- a/homeassistant/components/daikin/translations/de.json +++ b/homeassistant/components/daikin/translations/de.json @@ -5,6 +5,7 @@ "cannot_connect": "Verbindung fehlgeschlagen" }, "error": { + "api_password": "Ung\u00fcltige Authentifizierung , verwende entweder den API-Schl\u00fcssel oder das Passwort.", "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" diff --git a/homeassistant/components/daikin/translations/et.json b/homeassistant/components/daikin/translations/et.json index 0f303d2014e..9fa1ea3b293 100644 --- a/homeassistant/components/daikin/translations/et.json +++ b/homeassistant/components/daikin/translations/et.json @@ -5,6 +5,7 @@ "cannot_connect": "\u00dchendamine nurjus" }, "error": { + "api_password": "Vigane autentimine , kasuta kas API v\u00f5tit v\u00f5i salas\u00f5na.", "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamise viga", "unknown": "Tundmatu viga" diff --git a/homeassistant/components/daikin/translations/hu.json b/homeassistant/components/daikin/translations/hu.json index 6049890cb53..f5f774e1527 100644 --- a/homeassistant/components/daikin/translations/hu.json +++ b/homeassistant/components/daikin/translations/hu.json @@ -5,6 +5,7 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "error": { + "api_password": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s, haszn\u00e1ljon API-kulcsot vagy jelsz\u00f3t.", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" diff --git a/homeassistant/components/daikin/translations/ru.json b/homeassistant/components/daikin/translations/ru.json index 7365bb0e7bb..45734a361fa 100644 --- a/homeassistant/components/daikin/translations/ru.json +++ b/homeassistant/components/daikin/translations/ru.json @@ -5,6 +5,7 @@ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, "error": { + "api_password": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043a\u043b\u044e\u0447 API \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "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." diff --git a/homeassistant/components/dlna_dmr/translations/pl.json b/homeassistant/components/dlna_dmr/translations/pl.json new file mode 100644 index 00000000000..e8940bef26a --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/pl.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/he.json b/homeassistant/components/flux_led/translations/he.json new file mode 100644 index 00000000000..aa2d7877791 --- /dev/null +++ b/homeassistant/components/flux_led/translations/he.json @@ -0,0 +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_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" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "flow_title": "{model} {id} ({ipaddr})", + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/pl.json b/homeassistant/components/flux_led/translations/pl.json new file mode 100644 index 00000000000..1e321012040 --- /dev/null +++ b/homeassistant/components/flux_led/translations/pl.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "{model} {id} ({ipaddr})" + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/translations/hu.json b/homeassistant/components/geofency/translations/hu.json index de8f368adb3..1b3f17fe700 100644 --- a/homeassistant/components/geofency/translations/hu.json +++ b/homeassistant/components/geofency/translations/hu.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, "create_entry": { - "default": "Az esem\u00e9ny Home Assistantnek val\u00f3 k\u00fcld\u00e9s\u00e9hez be kell \u00e1ll\u00edtanod a webhook funkci\u00f3t a Geofencyben. \n\n Az al\u00e1bbi inform\u00e1ci\u00f3kat haszn\u00e1ld: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Tov\u00e1bbi r\u00e9szletek a [dokument\u00e1ci\u00f3]({docs_url}) linken tal\u00e1lhat\u00f3k." + "default": "Az esem\u00e9ny Home Assistantnek val\u00f3 k\u00fcld\u00e9s\u00e9hez be kell \u00e1ll\u00edtano a webhook funkci\u00f3t a Geofencyben. \n\n Az al\u00e1bbi inform\u00e1ci\u00f3kat haszn\u00e1lja: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Tov\u00e1bbi r\u00e9szletek a [dokument\u00e1ci\u00f3]({docs_url}) linken tal\u00e1lhat\u00f3k." }, "step": { "user": { diff --git a/homeassistant/components/gpslogger/translations/hu.json b/homeassistant/components/gpslogger/translations/hu.json index 45832cf493f..d458e959d0a 100644 --- a/homeassistant/components/gpslogger/translations/hu.json +++ b/homeassistant/components/gpslogger/translations/hu.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "A Home Assistant p\u00e9ld\u00e1ny\u00e1nak el\u00e9rhet\u0151nek kell lennie az internet fel\u0151l a webhook \u00fczenetek fogad\u00e1s\u00e1hoz." }, "create_entry": { - "default": "Az esem\u00e9ny Home Assistantnek val\u00f3 k\u00fcld\u00e9s\u00e9hez be kell \u00e1ll\u00edtanod a webhook funkci\u00f3t a GPSLoggerben. \n\n Az al\u00e1bbi inform\u00e1ci\u00f3kat haszn\u00e1ld: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Tov\u00e1bbi r\u00e9szletek a [dokument\u00e1ci\u00f3]({docs_url}) linken tal\u00e1lhat\u00f3k." + "default": "Az esem\u00e9ny Home Assistantnek val\u00f3 k\u00fcld\u00e9s\u00e9hez be kell \u00e1ll\u00edtani a webhook funkci\u00f3t a GPSLoggerben. \n\n Az al\u00e1bbi inform\u00e1ci\u00f3kat haszn\u00e1lja: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Tov\u00e1bbi r\u00e9szletek a [dokument\u00e1ci\u00f3]({docs_url}) linken tal\u00e1lhat\u00f3k." }, "step": { "user": { diff --git a/homeassistant/components/homekit/translations/pl.json b/homeassistant/components/homekit/translations/pl.json index 300c730e66c..4b25a482cfd 100644 --- a/homeassistant/components/homekit/translations/pl.json +++ b/homeassistant/components/homekit/translations/pl.json @@ -29,6 +29,7 @@ }, "cameras": { "data": { + "camera_audio": "Kamery obs\u0142uguj\u0105ce d\u017awi\u0119k", "camera_copy": "Kamery obs\u0142uguj\u0105ce kodek H.264" }, "description": "Sprawd\u017a, czy wszystkie kamery obs\u0142uguj\u0105 kodek H.264. Je\u015bli kamera nie wysy\u0142a strumienia skompresowanego kodekiem H.264, system b\u0119dzie transkodowa\u0142 wideo do H.264 dla HomeKit. Transkodowanie wymaga wydajnego procesora i jest ma\u0142o prawdopodobne, aby dzia\u0142a\u0142o na komputerach jednop\u0142ytkowych.", diff --git a/homeassistant/components/hue/translations/sk.json b/homeassistant/components/hue/translations/sk.json new file mode 100644 index 00000000000..3912c15c86c --- /dev/null +++ b/homeassistant/components/hue/translations/sk.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "all_configured": "V\u0161etky Philips Hue bridge u\u017e boli nakonfigurovan\u00e9", + "no_bridges": "Neboli objaven\u00fd \u017eiaden Philips Hue bridge" + }, + "step": { + "link": { + "description": "Stla\u010dte tla\u010didlo na Philips Hue bridge pre registr\u00e1ciu Philips Hue s Home Assistant.\n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iotawatt/translations/pl.json b/homeassistant/components/iotawatt/translations/pl.json index 2ea6be8ca81..d6fa1ba5735 100644 --- a/homeassistant/components/iotawatt/translations/pl.json +++ b/homeassistant/components/iotawatt/translations/pl.json @@ -10,7 +10,8 @@ "data": { "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika" - } + }, + "description": "Urz\u0105dzenie IoTawatt wymaga uwierzytelnienia. Wprowad\u017a nazw\u0119 u\u017cytkownika i has\u0142o, a nast\u0119pnie kliknij przycisk Prze\u015blij." }, "user": { "data": { diff --git a/homeassistant/components/modem_callerid/translations/pl.json b/homeassistant/components/modem_callerid/translations/pl.json new file mode 100644 index 00000000000..a07df641d7a --- /dev/null +++ b/homeassistant/components/modem_callerid/translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107" + }, + "step": { + "usb_confirm": { + "description": "Jest to integracja dla po\u0142\u0105cze\u0144 stacjonarnych przy u\u017cyciu modemu g\u0142osowego CX93001. Mo\u017ce ona pobra\u0107 informacje o identyfikatorze dzwoni\u0105cego z opcj\u0105 odrzucenia po\u0142\u0105czenia przychodz\u0105cego." + }, + "user": { + "data": { + "name": "Nazwa", + "port": "Port" + }, + "description": "Jest to integracja dla po\u0142\u0105cze\u0144 stacjonarnych przy u\u017cyciu modemu g\u0142osowego CX93001. Mo\u017ce ona pobra\u0107 informacje o identyfikatorze dzwoni\u0105cego z opcj\u0105 odrzucenia po\u0142\u0105czenia przychodz\u0105cego." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/hu.json b/homeassistant/components/mqtt/translations/hu.json index 9da3c6d9666..26961549b2c 100644 --- a/homeassistant/components/mqtt/translations/hu.json +++ b/homeassistant/components/mqtt/translations/hu.json @@ -51,8 +51,8 @@ }, "options": { "error": { - "bad_birth": "\u00c9rv\u00e9nytelen sz\u00fclet\u00e9si t\u00e9ma.", - "bad_will": "\u00c9rv\u00e9nytelen t\u00e9ma.", + "bad_birth": "\u00c9rv\u00e9nytelen 'birth' topik.", + "bad_will": "\u00c9rv\u00e9nytelen 'will' topik.", "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { diff --git a/homeassistant/components/mysensors/translations/hu.json b/homeassistant/components/mysensors/translations/hu.json index e9d9caaeb4f..a52ac3cd289 100644 --- a/homeassistant/components/mysensors/translations/hu.json +++ b/homeassistant/components/mysensors/translations/hu.json @@ -4,15 +4,15 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "duplicate_persistence_file": "A perzisztencia f\u00e1jl m\u00e1r haszn\u00e1latban van", - "duplicate_topic": "A t\u00e9ma m\u00e1r haszn\u00e1latban van", + "duplicate_topic": "A topik m\u00e1r haszn\u00e1latban van", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "invalid_device": "\u00c9rv\u00e9nytelen eszk\u00f6z", "invalid_ip": "\u00c9rv\u00e9nytelen IP-c\u00edm", "invalid_persistence_file": "\u00c9rv\u00e9nytelen perzisztencia f\u00e1jl", "invalid_port": "\u00c9rv\u00e9nytelen portsz\u00e1m", - "invalid_publish_topic": "\u00c9rv\u00e9nytelen k\u00f6zz\u00e9t\u00e9teli t\u00e9ma", + "invalid_publish_topic": "\u00c9rv\u00e9nytelen k\u00f6zz\u00e9t\u00e9teli (publish) topik", "invalid_serial": "\u00c9rv\u00e9nytelen soros port", - "invalid_subscribe_topic": "\u00c9rv\u00e9nytelen feliratkoz\u00e1si t\u00e9ma", + "invalid_subscribe_topic": "\u00c9rv\u00e9nytelen feliratkoz\u00e1si (subscribe) topik", "invalid_version": "\u00c9rv\u00e9nytelen MySensors verzi\u00f3", "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", @@ -23,15 +23,15 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "duplicate_persistence_file": "A perzisztencia f\u00e1jl m\u00e1r haszn\u00e1latban van", - "duplicate_topic": "A t\u00e9ma m\u00e1r haszn\u00e1latban van", + "duplicate_topic": "A topik m\u00e1r haszn\u00e1latban van", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "invalid_device": "\u00c9rv\u00e9nytelen eszk\u00f6z", "invalid_ip": "\u00c9rv\u00e9nytelen IP-c\u00edm", "invalid_persistence_file": "\u00c9rv\u00e9nytelen perzisztencia f\u00e1jl", "invalid_port": "\u00c9rv\u00e9nytelen portsz\u00e1m", - "invalid_publish_topic": "\u00c9rv\u00e9nytelen k\u00f6zz\u00e9t\u00e9teli t\u00e9ma", + "invalid_publish_topic": "\u00c9rv\u00e9nytelen k\u00f6zz\u00e9t\u00e9teli (publish) topik", "invalid_serial": "\u00c9rv\u00e9nytelen soros port", - "invalid_subscribe_topic": "\u00c9rv\u00e9nytelen feliratkoz\u00e1si t\u00e9ma", + "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": "K\u00e9rj\u00fck, adja meg a sz\u00e1mot", diff --git a/homeassistant/components/netgear/translations/pl.json b/homeassistant/components/netgear/translations/pl.json new file mode 100644 index 00000000000..488f0580ca2 --- /dev/null +++ b/homeassistant/components/netgear/translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "port": "Port (Opcjonalnie)", + "username": "Nazwa u\u017cytkownika (Opcjonalnie)" + }, + "title": "Netgear" + } + } + }, + "options": { + "step": { + "init": { + "description": "Okre\u015bl opcjonalne ustawienia", + "title": "Netgear" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/translations/pl.json b/homeassistant/components/notion/translations/pl.json index 468a4c3d02e..cc74db0f75e 100644 --- a/homeassistant/components/notion/translations/pl.json +++ b/homeassistant/components/notion/translations/pl.json @@ -5,9 +5,16 @@ }, "error": { "invalid_auth": "Niepoprawne uwierzytelnienie", - "no_devices": "Nie znaleziono urz\u0105dze\u0144 na koncie" + "no_devices": "Nie znaleziono urz\u0105dze\u0144 na koncie", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "reauth_confirm": { + "data": { + "password": "Has\u0142o" + }, + "description": "Wprowad\u017a ponownie has\u0142o dla u\u017cytkownika {username}." + }, "user": { "data": { "password": "Has\u0142o", diff --git a/homeassistant/components/ozw/translations/hu.json b/homeassistant/components/ozw/translations/hu.json index 06d921c86d3..d38a8d55d7a 100644 --- a/homeassistant/components/ozw/translations/hu.json +++ b/homeassistant/components/ozw/translations/hu.json @@ -24,7 +24,7 @@ }, "on_supervisor": { "data": { - "use_addon": "Haszn\u00e1ld az OpenZWave Supervisor b\u0151v\u00edtm\u00e9nyt" + "use_addon": "OpenZWave Supervisor b\u0151v\u00edtm\u00e9ny haszn\u00e1lata" }, "description": "Szeretn\u00e9 haszn\u00e1lni az OpenZWave Supervisor b\u0151v\u00edtm\u00e9nyt?", "title": "V\u00e1lassza ki a csatlakoz\u00e1si m\u00f3dot" diff --git a/homeassistant/components/philips_js/translations/sk.json b/homeassistant/components/philips_js/translations/sk.json new file mode 100644 index 00000000000..8332248a6c6 --- /dev/null +++ b/homeassistant/components/philips_js/translations/sk.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie je u\u017e nastaven\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_pin": "Nespr\u00e1vny PIN", + "pairing_failure": "Nepodarilo sa sp\u00e1rova\u0165: {error_id}", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "pair": { + "data": { + "pin": "PIN k\u00f3d" + }, + "description": "Zadajte PIN zobrazen\u00fd na Va\u0161ej TV", + "title": "Sp\u00e1rova\u0165" + }, + "user": { + "data": { + "api_version": "Verzia API", + "host": "Host" + } + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "Vy\u017eiadan\u00e9 zapnutie zariadenia" + } + }, + "options": { + "step": { + "init": { + "data": { + "allow_notify": "Povo\u013ete pou\u017e\u00edvanie notifika\u010dnej slu\u017eby" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/pl.json b/homeassistant/components/renault/translations/pl.json index 1d518cc14fb..087dd45e378 100644 --- a/homeassistant/components/renault/translations/pl.json +++ b/homeassistant/components/renault/translations/pl.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Konto jest ju\u017c skonfigurowane", - "kamereon_no_account": "Nie mo\u017cna znale\u017a\u0107 konta Kamereon." + "kamereon_no_account": "Nie mo\u017cna znale\u017a\u0107 konta Kamereon.", + "reauth_successful": "Ponowna autentykacja powiod\u0142a si\u0119" }, "error": { "invalid_credentials": "Niepoprawne uwierzytelnienie" @@ -14,6 +15,13 @@ }, "title": "Wyb\u00f3r identyfikatora konta Kamereon" }, + "reauth_confirm": { + "data": { + "password": "Has\u0142o" + }, + "description": "Zaktualizuj has\u0142o dla u\u017cytkownika {username}", + "title": "Uwierzytelnij ponownie integracj\u0119" + }, "user": { "data": { "locale": "Ustawienia regionalne", diff --git a/homeassistant/components/rfxtrx/translations/pl.json b/homeassistant/components/rfxtrx/translations/pl.json index e0e69b2a64a..0334ca217f8 100644 --- a/homeassistant/components/rfxtrx/translations/pl.json +++ b/homeassistant/components/rfxtrx/translations/pl.json @@ -43,6 +43,11 @@ } } }, + "device_automation": { + "trigger_type": { + "command": "Otrzymane polecenie: {subtype}" + } + }, "few": "kilka", "many": "wiele", "one": "jeden", diff --git a/homeassistant/components/shelly/translations/pl.json b/homeassistant/components/shelly/translations/pl.json index b0c4dd11b1b..25d00cf1e53 100644 --- a/homeassistant/components/shelly/translations/pl.json +++ b/homeassistant/components/shelly/translations/pl.json @@ -33,7 +33,8 @@ "button": "przycisk", "button1": "pierwszy", "button2": "drugi", - "button3": "trzeci" + "button3": "trzeci", + "button4": "Czwarty przycisk" }, "trigger_type": { "double": "przycisk \"{subtype}\" zostanie dwukrotnie naci\u015bni\u0119ty", diff --git a/homeassistant/components/surepetcare/translations/pl.json b/homeassistant/components/surepetcare/translations/pl.json new file mode 100644 index 00000000000..002e46163db --- /dev/null +++ b/homeassistant/components/surepetcare/translations/pl.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/pl.json b/homeassistant/components/switchbot/translations/pl.json new file mode 100644 index 00000000000..c65ebf08924 --- /dev/null +++ b/homeassistant/components/switchbot/translations/pl.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "error": { + "cannot_connect": "Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "mac": "Adres MAC urz\u0105dzenia", + "name": "Nazwa", + "password": "Has\u0142o" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "retry_count": "Liczba ponownych pr\u00f3b", + "retry_timeout": "Limit czasu mi\u0119dzy kolejnymi pr\u00f3bami", + "update_time": "Czas mi\u0119dzy aktualizacjami (sekundy)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/pl.json b/homeassistant/components/synology_dsm/translations/pl.json index 9150333a171..375d96a14e2 100644 --- a/homeassistant/components/synology_dsm/translations/pl.json +++ b/homeassistant/components/synology_dsm/translations/pl.json @@ -39,6 +39,12 @@ "description": "Pow\u00f3d: {details}", "title": "Ponownie uwierzytelnij integracj\u0119 Synology DSM" }, + "reauth_confirm": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + }, "user": { "data": { "host": "Nazwa hosta lub adres IP", diff --git a/homeassistant/components/tasmota/translations/hu.json b/homeassistant/components/tasmota/translations/hu.json index 3a6c32dbb42..7c77caadc8e 100644 --- a/homeassistant/components/tasmota/translations/hu.json +++ b/homeassistant/components/tasmota/translations/hu.json @@ -4,12 +4,12 @@ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "error": { - "invalid_discovery_topic": "\u00c9rv\u00e9nytelen felfedez\u00e9si t\u00e9ma el\u0151tag." + "invalid_discovery_topic": "\u00c9rv\u00e9nytelen felder\u00edt\u00e9si (discovery) topik el\u0151tag." }, "step": { "config": { "data": { - "discovery_prefix": "Felder\u00edt\u00e9si t\u00e9ma el\u0151tagja" + "discovery_prefix": "Felder\u00edt\u00e9si (discovery) topik el\u0151tagja" }, "description": "Adja meg a Tasmota konfigur\u00e1ci\u00f3t.", "title": "Tasmota" diff --git a/homeassistant/components/tplink/translations/pl.json b/homeassistant/components/tplink/translations/pl.json index a8ee3fa57ac..e0e4e817f04 100644 --- a/homeassistant/components/tplink/translations/pl.json +++ b/homeassistant/components/tplink/translations/pl.json @@ -7,6 +7,11 @@ "step": { "confirm": { "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" + }, + "pick_device": { + "data": { + "device": "Urz\u0105dzenie" + } } } } diff --git a/homeassistant/components/tuya/translations/ca.json b/homeassistant/components/tuya/translations/ca.json index bff9b7a35b9..5a9dd4ac44b 100644 --- a/homeassistant/components/tuya/translations/ca.json +++ b/homeassistant/components/tuya/translations/ca.json @@ -28,7 +28,7 @@ "data": { "access_id": "ID d'acc\u00e9s de Tuya IoT", "access_secret": "Secret d'acc\u00e9s de Tuya IoT", - "country_code": "El teu codi de pa\u00eds (per exemple, 1 per l'EUA o 86 per la Xina)", + "country_code": "Pa\u00eds", "password": "Contrasenya", "platform": "L'aplicaci\u00f3 on es registra el teu compte", "region": "Regi\u00f3", diff --git a/homeassistant/components/tuya/translations/de.json b/homeassistant/components/tuya/translations/de.json index 895c34d60bd..57e429b9de1 100644 --- a/homeassistant/components/tuya/translations/de.json +++ b/homeassistant/components/tuya/translations/de.json @@ -28,7 +28,7 @@ "data": { "access_id": "Tuya IoT-Zugriffs-ID", "access_secret": "Tuya IoT-Zugriffsgeheimnis", - "country_code": "L\u00e4ndercode deines Kontos (z. B. 1 f\u00fcr USA oder 86 f\u00fcr China)", + "country_code": "Land", "password": "Passwort", "platform": "Die App, in der dein Konto registriert ist", "region": "Region", diff --git a/homeassistant/components/tuya/translations/et.json b/homeassistant/components/tuya/translations/et.json index 96f081621b8..bd23136cb1b 100644 --- a/homeassistant/components/tuya/translations/et.json +++ b/homeassistant/components/tuya/translations/et.json @@ -28,7 +28,7 @@ "data": { "access_id": "Tuya IoT kasutajatunnus", "access_secret": "Tuya IoT salas\u00f5na", - "country_code": "Konto riigikood (nt 1 USA v\u00f5i 372 Eesti)", + "country_code": "Riik", "password": "Salas\u00f5na", "platform": "\u00c4pp kus konto registreeriti", "region": "Piirkond", diff --git a/homeassistant/components/tuya/translations/he.json b/homeassistant/components/tuya/translations/he.json index 548e623533a..87168fcce41 100644 --- a/homeassistant/components/tuya/translations/he.json +++ b/homeassistant/components/tuya/translations/he.json @@ -25,11 +25,12 @@ }, "user": { "data": { - "country_code": "\u05e7\u05d5\u05d3 \u05de\u05d3\u05d9\u05e0\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05e9\u05dc\u05da (\u05dc\u05de\u05e9\u05dc, 1 \u05dc\u05d0\u05e8\u05d4\"\u05d1 \u05d0\u05d5 972 \u05dc\u05d9\u05e9\u05e8\u05d0\u05dc)", + "country_code": "\u05de\u05d3\u05d9\u05e0\u05d4", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "platform": "\u05d4\u05d9\u05d9\u05e9\u05d5\u05dd \u05d1\u05d5 \u05e8\u05e9\u05d5\u05dd \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05e9\u05dc\u05da", + "region": "\u05d0\u05d9\u05d6\u05d5\u05e8", "tuya_project_type": "\u05e1\u05d5\u05d2 \u05e4\u05e8\u05d5\u05d9\u05d9\u05e7\u05d8 \u05d4\u05e2\u05e0\u05df \u05e9\u05dc Tuya", - "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + "username": "\u05d7\u05e9\u05d1\u05d5\u05df" }, "description": "\u05d4\u05d6\u05e0\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8\u05d9 \u05d4-Tuya \u05e9\u05dc\u05da.", "title": "\u05e9\u05d9\u05dc\u05d5\u05d1 Tuya" diff --git a/homeassistant/components/tuya/translations/hu.json b/homeassistant/components/tuya/translations/hu.json index 9a08d1f5a99..6f2ae6e4219 100644 --- a/homeassistant/components/tuya/translations/hu.json +++ b/homeassistant/components/tuya/translations/hu.json @@ -27,7 +27,7 @@ "user": { "data": { "access_id": "Tuya IoT azonos\u00edt\u00f3", - "access_secret": "Tuya IoT hozz\u00e1f\u00e9r\u00e9s", + "access_secret": "Tuya IoT hozz\u00e1f\u00e9r\u00e9si jelsz\u00f3", "country_code": "A fi\u00f3k orsz\u00e1gk\u00f3dja (pl. 1 USA, 36 Magyarorsz\u00e1g, vagy 86 K\u00edna)", "password": "Jelsz\u00f3", "platform": "Az alkalmaz\u00e1s, ahol a fi\u00f3k regisztr\u00e1lt", diff --git a/homeassistant/components/tuya/translations/pl.json b/homeassistant/components/tuya/translations/pl.json index 92ced00e733..28993f5b659 100644 --- a/homeassistant/components/tuya/translations/pl.json +++ b/homeassistant/components/tuya/translations/pl.json @@ -6,15 +6,24 @@ "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, "error": { - "invalid_auth": "Niepoprawne uwierzytelnienie" + "invalid_auth": "Niepoprawne uwierzytelnienie", + "login_error": "B\u0142\u0105d logowania ({code}): {msg}" }, "flow_title": "Konfiguracja integracji Tuya", "step": { + "login": { + "data": { + "password": "Has\u0142o", + "username": "Konto" + }, + "title": "Tuya" + }, "user": { "data": { "country_code": "Kod kraju twojego konta (np. 1 dla USA lub 86 dla Chin)", "password": "Has\u0142o", "platform": "Aplikacja, w kt\u00f3rej zarejestrowane jest Twoje konto", + "region": "Region", "username": "Nazwa u\u017cytkownika" }, "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce", diff --git a/homeassistant/components/tuya/translations/ru.json b/homeassistant/components/tuya/translations/ru.json index cd7b4ee79f5..b02562813c6 100644 --- a/homeassistant/components/tuya/translations/ru.json +++ b/homeassistant/components/tuya/translations/ru.json @@ -28,7 +28,7 @@ "data": { "access_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 Tuya IoT", "access_secret": "\u0421\u0435\u043a\u0440\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 Tuya IoT", - "country_code": "\u041a\u043e\u0434 \u0441\u0442\u0440\u0430\u043d\u044b \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430 (1 \u0434\u043b\u044f \u0421\u0428\u0410 \u0438\u043b\u0438 86 \u0434\u043b\u044f \u041a\u0438\u0442\u0430\u044f)", + "country_code": "\u0421\u0442\u0440\u0430\u043d\u0430", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "platform": "\u041f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c", "region": "\u0420\u0435\u0433\u0438\u043e\u043d", diff --git a/homeassistant/components/tuya/translations/sk.json b/homeassistant/components/tuya/translations/sk.json new file mode 100644 index 00000000000..2724fad6898 --- /dev/null +++ b/homeassistant/components/tuya/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "region": "Oblas\u0165" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/zh-Hant.json b/homeassistant/components/tuya/translations/zh-Hant.json index f30f08ed991..042c3a929e1 100644 --- a/homeassistant/components/tuya/translations/zh-Hant.json +++ b/homeassistant/components/tuya/translations/zh-Hant.json @@ -28,7 +28,7 @@ "data": { "access_id": "Tuya IoT Access ID", "access_secret": "Tuya IoT Access Secret", - "country_code": "\u5e33\u865f\u570b\u5bb6\u4ee3\u78bc\uff08\u4f8b\u5982\uff1a\u7f8e\u570b 1 \u6216\u4e2d\u570b 86\uff09", + "country_code": "\u570b\u5bb6", "password": "\u5bc6\u78bc", "platform": "\u5e33\u6236\u8a3b\u518a\u6240\u5728\u4f4d\u7f6e", "region": "\u5340\u57df", diff --git a/homeassistant/components/watttime/translations/pl.json b/homeassistant/components/watttime/translations/pl.json new file mode 100644 index 00000000000..f9c3ac32ac5 --- /dev/null +++ b/homeassistant/components/watttime/translations/pl.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "unknown": "Nieoczekiwany b\u0142\u0105d", + "unknown_coordinates": "Brak danych dla szeroko\u015bci/d\u0142ugo\u015bci geograficznej" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna" + }, + "description": "Wprowad\u017a szeroko\u015b\u0107 i d\u0142ugo\u015b\u0107 geograficzn\u0105 do monitorowania:" + }, + "location": { + "data": { + "location_type": "Lokalizacja" + }, + "description": "Wybierz lokalizacj\u0119 do monitorowania:" + }, + "user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Wprowad\u017a swoj\u0105 nazw\u0119 u\u017cytkownika i has\u0142o:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/pl.json b/homeassistant/components/whirlpool/translations/pl.json new file mode 100644 index 00000000000..e6575c9f9a7 --- /dev/null +++ b/homeassistant/components/whirlpool/translations/pl.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107", + "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/zha/translations/hu.json b/homeassistant/components/zha/translations/hu.json index cc480bb413e..d446de8e17a 100644 --- a/homeassistant/components/zha/translations/hu.json +++ b/homeassistant/components/zha/translations/hu.json @@ -46,10 +46,10 @@ "title": "Riaszt\u00f3 vez\u00e9rl\u0151panel opci\u00f3k" }, "zha_options": { - "consider_unavailable_battery": "Fontolja meg, hogy az akkumul\u00e1toros eszk\u00f6z\u00f6k (m\u00e1sodpercek m\u00falva) nem \u00e9rhet\u0151k el", - "consider_unavailable_mains": "Fontolja meg, hogy a h\u00e1l\u00f3zati t\u00e1pell\u00e1t\u00e1s\u00fa eszk\u00f6z\u00f6k (m\u00e1sodpercek m\u00falva) nem \u00e9rhet\u0151k el", + "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 enged\u00e9lyez\u00e9se, amikor az eszk\u00f6z\u00f6k csatlakoznak a h\u00e1l\u00f3zathoz", + "enable_identify_on_join": "Azonos\u00edt\u00f3 hat\u00e1s, amikor az eszk\u00f6z\u00f6k csatlakoznak a h\u00e1l\u00f3zathoz", "title": "Glob\u00e1lis be\u00e1ll\u00edt\u00e1sok" } }, diff --git a/homeassistant/components/zwave_js/translations/hu.json b/homeassistant/components/zwave_js/translations/hu.json index 715881fb329..da3bed46f19 100644 --- a/homeassistant/components/zwave_js/translations/hu.json +++ b/homeassistant/components/zwave_js/translations/hu.json @@ -48,7 +48,7 @@ }, "on_supervisor": { "data": { - "use_addon": "Haszn\u00e1ld a Z-Wave JS Supervisor b\u0151v\u00edtm\u00e9nyt" + "use_addon": "Z-Wave JS Supervisor b\u0151v\u00edtm\u00e9ny haszn\u00e1lata" }, "description": "Szeretn\u00e9 haszn\u00e1lni az Z-Wave JS Supervisor b\u0151v\u00edtm\u00e9nyt?", "title": "V\u00e1lassza ki a csatlakoz\u00e1si m\u00f3dot" From 947ae23749ed3a1a031951b2bc7ceaa4771ef36e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 7 Oct 2021 08:56:43 +0200 Subject: [PATCH 0151/1038] Add home-assistant/core as codeowner for recorder (#57224) --- CODEOWNERS | 1 + homeassistant/components/recorder/manifest.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 606c71f0a1c..73c6e63eb02 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -423,6 +423,7 @@ homeassistant/components/rainforest_eagle/* @gtdiehl @jcalbert homeassistant/components/rainmachine/* @bachya homeassistant/components/random/* @fabaff homeassistant/components/recollect_waste/* @bachya +homeassistant/components/recorder/* @home-assistant/core homeassistant/components/rejseplanen/* @DarkFox homeassistant/components/renault/* @epenet homeassistant/components/repetier/* @MTrab diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 4558e11c076..9ea08cc1f64 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -3,7 +3,7 @@ "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", "requirements": ["sqlalchemy==1.4.23"], - "codeowners": [], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "local_push" } From 19b0212ff076b79080132f7a8f3c14fa1ac21577 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Oct 2021 10:46:43 +0200 Subject: [PATCH 0152/1038] Upgrade coverage to 6.0.1 (#57235) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 713c8820bc1..27d9a29ad74 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.0 +coverage==6.0.1 jsonpickle==1.4.1 mock-open==1.4.0 mypy==0.910 From 24fa2a714a5f3cb1aa0e7902cc9d3c03fbb85dfd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Oct 2021 11:59:04 +0200 Subject: [PATCH 0153/1038] Toon, support Energy dashboard by default (#57233) --- homeassistant/components/toon/sensor.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/toon/sensor.py b/homeassistant/components/toon/sensor.py index cf7546c3fa6..35f317a2638 100644 --- a/homeassistant/components/toon/sensor.py +++ b/homeassistant/components/toon/sensor.py @@ -186,7 +186,6 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = ( native_unit_of_measurement=VOLUME_CUBIC_METERS, state_class=STATE_CLASS_TOTAL_INCREASING, device_class=DEVICE_CLASS_GAS, - entity_registry_enabled_default=False, cls=ToonGasMeterDeviceSensor, ), ToonSensorEntityDescription( @@ -244,7 +243,6 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = ( native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, - entity_registry_enabled_default=False, cls=ToonElectricityMeterDeviceSensor, ), ToonSensorEntityDescription( @@ -255,7 +253,6 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = ( native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, - entity_registry_enabled_default=False, cls=ToonElectricityMeterDeviceSensor, ), ToonSensorEntityDescription( @@ -276,7 +273,6 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = ( native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, - entity_registry_enabled_default=False, cls=ToonElectricityMeterDeviceSensor, ), ToonSensorEntityDescription( @@ -287,7 +283,6 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = ( native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, - entity_registry_enabled_default=False, cls=ToonElectricityMeterDeviceSensor, ), ToonSensorEntityDescription( @@ -391,6 +386,7 @@ SENSOR_ENTITIES_SOLAR: tuple[ToonSensorEntityDescription, ...] = ( measurement="day_produced_solar", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, cls=ToonSolarDeviceSensor, ), ToonSensorEntityDescription( From 2ba8e1030c9b60f9640a004bf3328e17335cec01 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 7 Oct 2021 04:40:10 -0600 Subject: [PATCH 0154/1038] Ensure that WattTime is strictly typed (#57130) --- .strict-typing | 1 + homeassistant/components/watttime/sensor.py | 4 ++-- mypy.ini | 11 +++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.strict-typing b/.strict-typing index dec244ece15..b5ae496fcf0 100644 --- a/.strict-typing +++ b/.strict-typing @@ -122,6 +122,7 @@ homeassistant.components.uptimerobot.* homeassistant.components.vacuum.* homeassistant.components.vallox.* homeassistant.components.water_heater.* +homeassistant.components.watttime.* homeassistant.components.weather.* homeassistant.components.websocket_api.* homeassistant.components.zodiac.* diff --git a/homeassistant/components/watttime/sensor.py b/homeassistant/components/watttime/sensor.py index 6a6d05701c4..50389de35c6 100644 --- a/homeassistant/components/watttime/sensor.py +++ b/homeassistant/components/watttime/sensor.py @@ -1,7 +1,7 @@ """Support for WattTime sensors.""" from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, @@ -100,4 +100,4 @@ class RealtimeEmissionsSensor(CoordinatorEntity, SensorEntity): @property def native_value(self) -> StateType: """Return the value reported by the sensor.""" - return self.coordinator.data[self.entity_description.key] + return cast(StateType, self.coordinator.data[self.entity_description.key]) diff --git a/mypy.ini b/mypy.ini index cc491043c3a..0d4ca87ac64 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1353,6 +1353,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.watttime.*] +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.weather.*] check_untyped_defs = true disallow_incomplete_defs = true From 750dd9186efcf1e43eaf3a1377e08086d23ea128 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 7 Oct 2021 12:48:27 +0200 Subject: [PATCH 0155/1038] Improve deCONZ signal strings (#57140) --- .../components/deconz/alarm_control_panel.py | 3 +- .../components/deconz/binary_sensor.py | 6 ++- homeassistant/components/deconz/climate.py | 6 ++- homeassistant/components/deconz/const.py | 5 -- homeassistant/components/deconz/cover.py | 5 +- .../components/deconz/deconz_event.py | 6 ++- homeassistant/components/deconz/fan.py | 5 +- homeassistant/components/deconz/gateway.py | 52 +++++++++---------- homeassistant/components/deconz/light.py | 10 ++-- homeassistant/components/deconz/lock.py | 7 +-- homeassistant/components/deconz/scene.py | 5 +- homeassistant/components/deconz/sensor.py | 8 +-- homeassistant/components/deconz/services.py | 14 ++--- homeassistant/components/deconz/siren.py | 5 +- homeassistant/components/deconz/switch.py | 6 ++- 15 files changed, 72 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/deconz/alarm_control_panel.py b/homeassistant/components/deconz/alarm_control_panel.py index 73e85f13713..c16a074bc06 100644 --- a/homeassistant/components/deconz/alarm_control_panel.py +++ b/homeassistant/components/deconz/alarm_control_panel.py @@ -35,7 +35,6 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import NEW_SENSOR from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry @@ -89,7 +88,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities) -> None: config_entry.async_on_unload( async_dispatcher_connect( hass, - gateway.async_signal_new_device(NEW_SENSOR), + gateway.signal_new_sensor, async_add_alarm_control_panel, ) ) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index e0479d9a0c6..d79e8708d4b 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -26,7 +26,7 @@ from homeassistant.const import ATTR_TEMPERATURE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR +from .const import ATTR_DARK, ATTR_ON from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry @@ -105,7 +105,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config_entry.async_on_unload( async_dispatcher_connect( - hass, gateway.async_signal_new_device(NEW_SENSOR), async_add_sensor + hass, + gateway.signal_new_sensor, + async_add_sensor, ) ) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index d80ecc6cc77..b9401e6d5a3 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -46,7 +46,7 @@ from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import ATTR_LOCKED, ATTR_OFFSET, ATTR_VALVE, NEW_SENSOR +from .const import ATTR_LOCKED, ATTR_OFFSET, ATTR_VALVE from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry @@ -118,7 +118,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config_entry.async_on_unload( async_dispatcher_connect( - hass, gateway.async_signal_new_device(NEW_SENSOR), async_add_climate + hass, + gateway.signal_new_sensor, + async_add_climate, ) ) diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index 67753aa0355..f0372273253 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -46,11 +46,6 @@ PLATFORMS = [ SWITCH_DOMAIN, ] -NEW_GROUP = "groups" -NEW_LIGHT = "lights" -NEW_SCENE = "scenes" -NEW_SENSOR = "sensors" - ATTR_DARK = "dark" ATTR_LOCKED = "locked" ATTR_OFFSET = "offset" diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index abf1fe4eea4..5cf90c4dca1 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -21,7 +21,6 @@ from homeassistant.components.cover import ( from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import NEW_LIGHT from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry @@ -54,7 +53,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config_entry.async_on_unload( async_dispatcher_connect( - hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_cover + hass, + gateway.signal_new_light, + async_add_cover, ) ) diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index 2d9799c4d02..b04e393103f 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -20,7 +20,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import slugify -from .const import CONF_ANGLE, CONF_GESTURE, LOGGER, NEW_SENSOR +from .const import CONF_ANGLE, CONF_GESTURE, LOGGER from .deconz_device import DeconzBase CONF_DECONZ_EVENT = "deconz_event" @@ -63,7 +63,9 @@ async def async_setup_events(gateway) -> None: gateway.config_entry.async_on_unload( async_dispatcher_connect( - gateway.hass, gateway.async_signal_new_device(NEW_SENSOR), async_add_sensor + gateway.hass, + gateway.signal_new_sensor, + async_add_sensor, ) ) diff --git a/homeassistant/components/deconz/fan.py b/homeassistant/components/deconz/fan.py index 3b1f3bd256d..af73135cd2a 100644 --- a/homeassistant/components/deconz/fan.py +++ b/homeassistant/components/deconz/fan.py @@ -26,7 +26,6 @@ from homeassistant.util.percentage import ( percentage_to_ordered_list_item, ) -from .const import NEW_LIGHT from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry @@ -74,7 +73,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities) -> None: config_entry.async_on_unload( async_dispatcher_connect( - hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_fan + hass, + gateway.signal_new_light, + async_add_fan, ) ) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index ecbc36ebadc..17c927b300e 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -2,7 +2,7 @@ import asyncio import async_timeout -from pydeconz import DeconzSession, errors +from pydeconz import DeconzSession, errors, group, light, sensor from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.core import callback @@ -21,10 +21,6 @@ from .const import ( DEFAULT_ALLOW_NEW_DEVICES, DOMAIN as DECONZ_DOMAIN, LOGGER, - NEW_GROUP, - NEW_LIGHT, - NEW_SCENE, - NEW_SENSOR, PLATFORMS, ) from .deconz_event import async_setup_events, async_unload_events @@ -50,6 +46,20 @@ class DeconzGateway: self.available = True self.ignore_state_updates = False + self.signal_reachable = f"deconz-reachable-{config_entry.entry_id}" + + self.signal_new_group = f"deconz_new_group_{config_entry.entry_id}" + self.signal_new_light = f"deconz_new_light_{config_entry.entry_id}" + self.signal_new_scene = f"deconz_new_scene_{config_entry.entry_id}" + self.signal_new_sensor = f"deconz_new_sensor_{config_entry.entry_id}" + + self.deconz_resource_type_to_signal_new_device = { + group.RESOURCE_TYPE: self.signal_new_group, + light.RESOURCE_TYPE: self.signal_new_light, + group.RESOURCE_TYPE_SCENE: self.signal_new_scene, + sensor.RESOURCE_TYPE: self.signal_new_sensor, + } + self.deconz_ids = {} self.entities = {} self.events = [] @@ -92,24 +102,6 @@ class DeconzGateway: CONF_ALLOW_NEW_DEVICES, DEFAULT_ALLOW_NEW_DEVICES ) - # Signals - - @property - def signal_reachable(self) -> str: - """Gateway specific event to signal a change in connection status.""" - return f"deconz-reachable-{self.bridgeid}" - - @callback - def async_signal_new_device(self, device_type) -> str: - """Gateway specific event to signal new device.""" - new_device = { - NEW_GROUP: f"deconz_new_group_{self.bridgeid}", - NEW_LIGHT: f"deconz_new_light_{self.bridgeid}", - NEW_SCENE: f"deconz_new_scene_{self.bridgeid}", - NEW_SENSOR: f"deconz_new_sensor_{self.bridgeid}", - } - return new_device[device_type] - # Callbacks @callback @@ -121,10 +113,14 @@ class DeconzGateway: @callback def async_add_device_callback( - self, device_type, device=None, force: bool = False + self, resource_type, device=None, force: bool = False ) -> None: """Handle event of new device creation in deCONZ.""" - if not force and not self.option_allow_new_devices: + if ( + not force + and not self.option_allow_new_devices + or resource_type not in self.deconz_resource_type_to_signal_new_device + ): return args = [] @@ -134,7 +130,7 @@ class DeconzGateway: async_dispatcher_send( self.hass, - self.async_signal_new_device(device_type), + self.deconz_resource_type_to_signal_new_device[resource_type], *args, # Don't send device if None, it would override default value in listeners ) @@ -207,7 +203,7 @@ class DeconzGateway: deconz_ids = [] if self.option_allow_clip_sensor: - self.async_add_device_callback(NEW_SENSOR) + self.async_add_device_callback(sensor.RESOURCE_TYPE) else: deconz_ids += [ @@ -217,7 +213,7 @@ class DeconzGateway: ] if self.option_allow_deconz_groups: - self.async_add_device_callback(NEW_GROUP) + self.async_add_device_callback(group.RESOURCE_TYPE) else: deconz_ids += [group.deconz_id for group in self.api.groups.values()] diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 1a3cca6df05..0069e0fb7d9 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -37,7 +37,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.color import color_hs_to_xy -from .const import DOMAIN as DECONZ_DOMAIN, NEW_GROUP, NEW_LIGHT, POWER_PLUGS +from .const import DOMAIN as DECONZ_DOMAIN, POWER_PLUGS from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry @@ -69,7 +69,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config_entry.async_on_unload( async_dispatcher_connect( - hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_light + hass, + gateway.signal_new_light, + async_add_light, ) ) @@ -95,7 +97,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config_entry.async_on_unload( async_dispatcher_connect( - hass, gateway.async_signal_new_device(NEW_GROUP), async_add_group + hass, + gateway.signal_new_group, + async_add_group, ) ) diff --git a/homeassistant/components/deconz/lock.py b/homeassistant/components/deconz/lock.py index bb23ec4be7a..fb344e54176 100644 --- a/homeassistant/components/deconz/lock.py +++ b/homeassistant/components/deconz/lock.py @@ -7,7 +7,6 @@ from homeassistant.components.lock import DOMAIN, LockEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import NEW_LIGHT, NEW_SENSOR from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry @@ -35,7 +34,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config_entry.async_on_unload( async_dispatcher_connect( - hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_lock_from_light + hass, + gateway.signal_new_light, + async_add_lock_from_light, ) ) @@ -58,7 +59,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config_entry.async_on_unload( async_dispatcher_connect( hass, - gateway.async_signal_new_device(NEW_SENSOR), + gateway.signal_new_sensor, async_add_lock_from_sensor, ) ) diff --git a/homeassistant/components/deconz/scene.py b/homeassistant/components/deconz/scene.py index 45e891add28..69f3d48c82c 100644 --- a/homeassistant/components/deconz/scene.py +++ b/homeassistant/components/deconz/scene.py @@ -5,7 +5,6 @@ from homeassistant.components.scene import Scene from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import NEW_SCENE from .gateway import get_gateway_from_config_entry @@ -23,7 +22,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config_entry.async_on_unload( async_dispatcher_connect( - hass, gateway.async_signal_new_device(NEW_SCENE), async_add_scene + hass, + gateway.signal_new_scene, + async_add_scene, ) ) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index ba8e5c8adec..aed4d2df7ee 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -45,7 +45,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) -from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR +from .const import ATTR_DARK, ATTR_ON from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry @@ -167,7 +167,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config_entry.async_on_unload( async_dispatcher_connect( - hass, gateway.async_signal_new_device(NEW_SENSOR), async_add_sensor + hass, + gateway.signal_new_sensor, + async_add_sensor, ) ) @@ -340,7 +342,7 @@ class DeconzSensorStateTracker: if "battery" in self.sensor.changed_keys: async_dispatcher_send( self.gateway.hass, - self.gateway.async_signal_new_device(NEW_SENSOR), + self.gateway.signal_new_sensor, [self.sensor], ) diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index 5ae2875305d..88006a851f2 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -14,15 +14,7 @@ from homeassistant.helpers.entity_registry import ( ) from .config_flow import get_master_gateway -from .const import ( - CONF_BRIDGE_ID, - DOMAIN, - LOGGER, - NEW_GROUP, - NEW_LIGHT, - NEW_SCENE, - NEW_SENSOR, -) +from .const import CONF_BRIDGE_ID, DOMAIN, LOGGER DECONZ_SERVICES = "deconz_services" @@ -145,8 +137,8 @@ async def async_refresh_devices_service(gateway): await gateway.api.refresh_state() gateway.ignore_state_updates = False - for new_device_type in (NEW_GROUP, NEW_LIGHT, NEW_SCENE, NEW_SENSOR): - gateway.async_add_device_callback(new_device_type, force=True) + for resource_type in gateway.deconz_resource_type_to_signal_new_device: + gateway.async_add_device_callback(resource_type, force=True) async def async_remove_orphaned_entries_service(gateway): diff --git a/homeassistant/components/deconz/siren.py b/homeassistant/components/deconz/siren.py index 9138bb3ac14..c3679b6ad89 100644 --- a/homeassistant/components/deconz/siren.py +++ b/homeassistant/components/deconz/siren.py @@ -13,7 +13,6 @@ from homeassistant.components.siren import ( from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import NEW_LIGHT from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry @@ -41,7 +40,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config_entry.async_on_unload( async_dispatcher_connect( - hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_siren + hass, + gateway.signal_new_light, + async_add_siren, ) ) diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index f7559e37838..a00def33b72 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -6,7 +6,7 @@ from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import DOMAIN as DECONZ_DOMAIN, NEW_LIGHT, POWER_PLUGS +from .const import DOMAIN as DECONZ_DOMAIN, POWER_PLUGS from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry @@ -48,7 +48,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config_entry.async_on_unload( async_dispatcher_connect( - hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_switch + hass, + gateway.signal_new_light, + async_add_switch, ) ) From a4d9019ffc2c36c4abfb6c68a086198c3e0392ba Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Oct 2021 03:58:00 -0700 Subject: [PATCH 0156/1038] Refactor persistent notification to no longer route all data via a service (#57157) * Convert persistent notification tests to async * Create/dismiss persistent notifications in exposed functions, not service calls * Fix notify persistent_notification * Remove setting up persistent_notification * Drop more setups * Empty methods * Undeprecate sync methods because too big task * Fix setup clearing notifications * Fix a bunch of tests * Fix more tests * Uno mas * Test persistent notification events * Clean up stale comment Co-authored-by: Martin Hjelmare --- homeassistant/components/notify/__init__.py | 16 +- .../persistent_notification/__init__.py | 216 +++++++++--------- .../config_flow/tests/test_config_flow.py | 3 +- tests/components/airnow/test_config_flow.py | 4 +- .../components/airthings/test_config_flow.py | 4 +- tests/components/august/test_config_flow.py | 4 +- tests/components/august/test_init.py | 8 - tests/components/aurora/test_config_flow.py | 7 +- tests/components/blink/test_config_flow.py | 12 +- tests/components/bond/test_config_flow.py | 15 +- .../components/bosch_shc/test_config_flow.py | 11 +- .../components/broadlink/test_config_flow.py | 14 +- .../canary/test_alarm_control_panel.py | 2 - tests/components/canary/test_config_flow.py | 2 - tests/components/canary/test_sensor.py | 3 - .../components/cloudflare/test_config_flow.py | 2 - .../components/co2signal/test_config_flow.py | 15 +- tests/components/coinbase/test_config_flow.py | 4 +- tests/components/control4/test_config_flow.py | 4 +- .../coronavirus/test_config_flow.py | 6 +- .../components/deconz/test_device_trigger.py | 1 - .../devolo_home_control/test_config_flow.py | 8 +- tests/components/dexcom/test_config_flow.py | 4 +- tests/components/doorbird/test_config_flow.py | 8 +- tests/components/dsmr/test_config_flow.py | 18 +- tests/components/elkm1/test_config_flow.py | 11 +- tests/components/emonitor/test_config_flow.py | 10 +- .../enphase_envoy/test_config_flow.py | 15 +- tests/components/epson/test_config_flow.py | 4 +- tests/components/ezviz/test_config_flow.py | 11 - .../components/faa_delays/test_config_flow.py | 4 +- tests/components/filesize/test_sensor.py | 4 +- tests/components/firmata/test_config_flow.py | 6 +- .../fjaraskupan/test_config_flow.py | 3 +- .../flick_electric/test_config_flow.py | 4 +- tests/components/flipr/test_config_flow.py | 4 +- tests/components/flo/test_config_flow.py | 4 +- tests/components/flume/test_config_flow.py | 6 +- tests/components/flux_led/test_config_flow.py | 6 +- tests/components/folder/test_sensor.py | 2 +- tests/components/forecast_solar/conftest.py | 7 - tests/components/foscam/test_config_flow.py | 15 +- tests/components/freebox/test_init.py | 1 - .../garages_amsterdam/test_config_flow.py | 4 +- tests/components/gdacs/test_geo_location.py | 35 ++- tests/components/gdacs/test_sensor.py | 28 ++- tests/components/geofency/test_init.py | 2 - .../geonetnz_quakes/test_geo_location.py | 37 ++- .../components/geonetnz_quakes/test_sensor.py | 28 ++- .../geonetnz_volcano/test_sensor.py | 42 +++- tests/components/goalzero/test_config_flow.py | 3 +- .../components/gogogate2/test_config_flow.py | 7 +- tests/components/gpslogger/test_init.py | 2 - tests/components/habitica/test_config_flow.py | 4 +- tests/components/harmony/test_config_flow.py | 7 +- tests/components/hassio/test_config_flow.py | 3 +- .../here_travel_time/test_sensor.py | 5 - tests/components/hive/test_config_flow.py | 3 +- tests/components/hlk_sw16/test_config_flow.py | 6 +- tests/components/homekit/test_config_flow.py | 11 +- tests/components/homekit/test_homekit.py | 38 +-- tests/components/homekit/test_util.py | 44 ++-- tests/components/http/test_ban.py | 10 +- tests/components/hue/test_init.py | 2 +- .../components/huisbaasje/test_config_flow.py | 4 +- .../test_config_flow.py | 9 +- tests/components/ialarm/test_config_flow.py | 4 +- tests/components/insteon/test_config_flow.py | 18 +- tests/components/iotawatt/test_config_flow.py | 5 +- tests/components/isy994/test_config_flow.py | 21 +- tests/components/juicenet/test_config_flow.py | 4 +- tests/components/kmtronic/test_config_flow.py | 4 +- .../kostal_plenticore/test_config_flow.py | 4 +- tests/components/kulersky/test_config_flow.py | 8 +- tests/components/kulersky/test_light.py | 3 - tests/components/lcn/test_config_flow.py | 8 +- tests/components/lcn/test_init.py | 2 - .../litterrobot/test_config_flow.py | 4 +- .../lutron_caseta/test_config_flow.py | 15 +- .../lutron_caseta/test_device_trigger.py | 13 +- tests/components/mazda/test_config_flow.py | 4 +- .../components/metoffice/test_config_flow.py | 3 +- .../components/monoprice/test_config_flow.py | 4 +- .../components/motioneye/test_config_flow.py | 11 +- tests/components/mqtt/test_light.py | 4 +- tests/components/mullvad/test_config_flow.py | 2 +- tests/components/mutesync/test_config_flow.py | 4 +- tests/components/myq/test_config_flow.py | 4 +- .../components/mysensors/test_config_flow.py | 8 +- tests/components/mysensors/test_init.py | 2 +- tests/components/nexia/test_config_flow.py | 4 +- .../components/nightscout/test_config_flow.py | 4 +- .../nmap_tracker/test_config_flow.py | 15 +- tests/components/notify/test_init.py | 1 - tests/components/nuheat/test_config_flow.py | 4 +- tests/components/nuki/mock.py | 3 +- tests/components/nuki/test_config_flow.py | 5 +- tests/components/nut/test_config_flow.py | 6 +- tests/components/nws/test_config_flow.py | 3 +- tests/components/nzbget/test_config_flow.py | 3 - .../components/omnilogic/test_config_flow.py | 9 +- tests/components/onboarding/test_views.py | 5 - .../components/onewire/test_binary_sensor.py | 3 +- tests/components/onewire/test_init.py | 3 +- tests/components/onewire/test_sensor.py | 6 +- tests/components/onewire/test_switch.py | 3 +- .../components/opengarage/test_config_flow.py | 4 +- .../opentherm_gw/test_config_flow.py | 6 +- .../owntracks/test_device_tracker.py | 3 - tests/components/ozw/test_config_flow.py | 17 +- .../persistent_notification/test_init.py | 214 ++++++++--------- tests/components/philips_js/conftest.py | 2 - tests/components/picnic/test_config_flow.py | 4 +- tests/components/plaato/test_config_flow.py | 4 +- tests/components/plex/test_config_flow.py | 2 - tests/components/plex/test_init.py | 2 - tests/components/plugwise/test_config_flow.py | 11 +- .../plum_lightpad/test_config_flow.py | 6 +- .../components/powerwall/test_config_flow.py | 8 +- tests/components/profiler/test_config_flow.py | 6 +- tests/components/profiler/test_init.py | 7 - .../progettihwsw/test_config_flow.py | 4 +- tests/components/prosegur/test_config_flow.py | 4 +- tests/components/rachio/test_config_flow.py | 5 +- .../rainforest_eagle/test_config_flow.py | 4 +- .../components/rainforest_eagle/test_init.py | 3 +- tests/components/recorder/test_migrate.py | 43 ++-- .../components/renault/test_binary_sensor.py | 11 +- .../components/renault/test_device_tracker.py | 11 +- tests/components/renault/test_select.py | 11 +- tests/components/renault/test_sensor.py | 11 +- tests/components/rest/test_binary_sensor.py | 30 +-- tests/components/rest/test_sensor.py | 50 ++-- tests/components/rfxtrx/test_config_flow.py | 11 +- tests/components/rfxtrx/test_device_action.py | 6 +- .../components/rfxtrx/test_device_trigger.py | 5 +- tests/components/ring/test_config_flow.py | 4 +- tests/components/roku/test_config_flow.py | 3 +- tests/components/roomba/test_config_flow.py | 20 +- tests/components/roon/test_config_flow.py | 6 +- tests/components/samsungtv/__init__.py | 3 +- tests/components/samsungtv/test_init.py | 6 +- .../screenlogic/test_config_flow.py | 12 +- tests/components/sense/test_config_flow.py | 4 +- tests/components/sentry/test_config_flow.py | 3 +- .../components/seventeentrack/test_sensor.py | 2 +- tests/components/sharkiq/test_config_flow.py | 4 +- tests/components/shelly/test_config_flow.py | 16 +- .../components/shelly/test_device_trigger.py | 11 +- tests/components/sma/test_config_flow.py | 5 +- .../smart_meter_texas/test_config_flow.py | 4 +- tests/components/smarthab/test_config_flow.py | 5 +- tests/components/smartthings/test_init.py | 5 +- tests/components/solarlog/test_config_flow.py | 4 +- .../somfy_mylink/test_config_flow.py | 11 +- tests/components/sonos/test_config_flow.py | 8 +- tests/components/spider/test_config_flow.py | 6 +- .../surepetcare/test_config_flow.py | 4 +- .../components/switchbot/test_config_flow.py | 4 - .../components/syncthing/test_config_flow.py | 4 +- tests/components/syncthru/test_config_flow.py | 8 +- .../synology_dsm/test_config_flow.py | 5 +- .../system_bridge/test_config_flow.py | 8 +- tests/components/tado/test_config_flow.py | 5 +- .../template/test_alarm_control_panel.py | 2 +- .../components/template/test_binary_sensor.py | 2 +- tests/components/template/test_cover.py | 2 +- tests/components/template/test_fan.py | 2 +- tests/components/template/test_init.py | 4 +- tests/components/template/test_light.py | 8 +- tests/components/template/test_lock.py | 4 +- tests/components/template/test_number.py | 2 +- tests/components/template/test_select.py | 2 +- tests/components/template/test_sensor.py | 2 +- tests/components/template/test_switch.py | 14 +- tests/components/template/test_vacuum.py | 8 +- tests/components/tplink/test_config_flow.py | 3 - tests/components/traccar/test_init.py | 1 - tests/components/tractive/test_config_flow.py | 4 +- tests/components/trend/test_binary_sensor.py | 6 +- tests/components/unifi/test_config_flow.py | 9 +- tests/components/upb/test_config_flow.py | 7 +- .../uptimerobot/test_config_flow.py | 4 +- tests/components/vilfo/test_config_flow.py | 4 +- tests/components/watttime/test_config_flow.py | 14 +- tests/components/wled/conftest.py | 7 - tests/components/wolflink/test_config_flow.py | 4 +- .../xiaomi_miio/test_config_flow.py | 1 - .../yale_smart_alarm/test_config_flow.py | 4 +- tests/components/yeelight/test_config_flow.py | 7 +- tests/components/zerproc/test_config_flow.py | 8 +- tests/components/zerproc/test_light.py | 4 - tests/components/zha/test_config_flow.py | 11 +- tests/components/zha/test_init.py | 1 - tests/components/zwave_js/test_config_flow.py | 28 +-- tests/helpers/test_translation.py | 4 +- tests/test_config_entries.py | 9 - tests/test_loader.py | 7 +- 198 files changed, 848 insertions(+), 1114 deletions(-) diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index f3c2778bc59..ef78676027d 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -273,21 +273,17 @@ async def async_setup(hass, config): async def persistent_notification(service: ServiceCall) -> None: """Send notification via the built-in persistsent_notify integration.""" - payload = {} message = service.data[ATTR_MESSAGE] message.hass = hass _check_templates_warn(hass, message) - payload[ATTR_MESSAGE] = message.async_render(parse_result=False) - title = service.data.get(ATTR_TITLE) - if title: - _check_templates_warn(hass, title) - title.hass = hass - payload[ATTR_TITLE] = title.async_render(parse_result=False) + title = None + if title_tpl := service.data.get(ATTR_TITLE): + _check_templates_warn(hass, title_tpl) + title_tpl.hass = hass + title = title_tpl.async_render(parse_result=False) - await hass.services.async_call( - pn.DOMAIN, pn.SERVICE_CREATE, payload, blocking=True - ) + pn.async_create(hass, message.async_render(parse_result=False), title) async def async_setup_platform( integration_name, p_config=None, discovery_info=None diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index ec2c5f7512d..8725878797b 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -1,27 +1,24 @@ """Support for displaying persistent notifications.""" from __future__ import annotations -from collections import OrderedDict -from collections.abc import Mapping, MutableMapping +from collections.abc import Mapping import logging -from typing import Any +from typing import Any, cast import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.const import ATTR_FRIENDLY_NAME -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Context, HomeAssistant, ServiceCall, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id -from homeassistant.helpers.template import Template +from homeassistant.helpers.template import Template, is_template_string from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util import slugify import homeassistant.util.dt as dt_util -# mypy: allow-untyped-calls, allow-untyped-defs - ATTR_CREATED_AT = "created_at" ATTR_MESSAGE = "message" ATTR_NOTIFICATION_ID = "notification_id" @@ -34,22 +31,10 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}" EVENT_PERSISTENT_NOTIFICATIONS_UPDATED = "persistent_notifications_updated" -SERVICE_CREATE = "create" -SERVICE_DISMISS = "dismiss" -SERVICE_MARK_READ = "mark_read" - -SCHEMA_SERVICE_CREATE = vol.Schema( - { - vol.Required(ATTR_MESSAGE): vol.Any(cv.dynamic_template, cv.string), - vol.Optional(ATTR_TITLE): vol.Any(cv.dynamic_template, cv.string), - vol.Optional(ATTR_NOTIFICATION_ID): cv.string, - } +SCHEMA_SERVICE_NOTIFICATION = vol.Schema( + {vol.Required(ATTR_NOTIFICATION_ID): cv.string} ) -SCHEMA_SERVICE_DISMISS = vol.Schema({vol.Required(ATTR_NOTIFICATION_ID): cv.string}) - -SCHEMA_SERVICE_MARK_READ = vol.Schema({vol.Required(ATTR_NOTIFICATION_ID): cv.string}) - DEFAULT_OBJECT_ID = "notification" _LOGGER = logging.getLogger(__name__) @@ -59,13 +44,18 @@ STATUS_READ = "read" @bind_hass -def create(hass, message, title=None, notification_id=None): +def create( + hass: HomeAssistant, + message: str, + title: str | None = None, + notification_id: str | None = None, +) -> None: """Generate a notification.""" hass.add_job(async_create, hass, message, title, notification_id) @bind_hass -def dismiss(hass, notification_id): +def dismiss(hass: HomeAssistant, notification_id: str) -> None: """Remove a notification.""" hass.add_job(async_dismiss, hass, notification_id) @@ -77,108 +67,115 @@ def async_create( message: str, title: str | None = None, notification_id: str | None = None, + *, + context: Context | None = None, ) -> None: """Generate a notification.""" - data = { - key: value - for key, value in ( - (ATTR_TITLE, title), - (ATTR_MESSAGE, message), - (ATTR_NOTIFICATION_ID, notification_id), + notifications = hass.data.get(DOMAIN) + if notifications is None: + notifications = hass.data[DOMAIN] = {} + + if notification_id is not None: + entity_id = ENTITY_ID_FORMAT.format(slugify(notification_id)) + else: + entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, DEFAULT_OBJECT_ID, hass=hass ) - if value is not None + notification_id = entity_id.split(".")[1] + + warn = False + + attr: dict[str, str] = {} + if title is not None: + if is_template_string(title): + warn = True + try: + title = cast( + str, Template(title, hass).async_render(parse_result=False) # type: ignore[no-untyped-call] + ) + except TemplateError as ex: + _LOGGER.error("Error rendering title %s: %s", title, ex) + + attr[ATTR_TITLE] = title + attr[ATTR_FRIENDLY_NAME] = title + + if is_template_string(message): + warn = True + try: + message = Template(message, hass).async_render(parse_result=False) # type: ignore[no-untyped-call] + except TemplateError as ex: + _LOGGER.error("Error rendering message %s: %s", message, ex) + + attr[ATTR_MESSAGE] = message + + if warn: + _LOGGER.warning( + "Passing a template string to persistent_notification.async_create function is deprecated" + ) + + hass.states.async_set(entity_id, STATE, attr, context=context) + + # Store notification and fire event + # This will eventually replace state machine storage + notifications[entity_id] = { + ATTR_MESSAGE: message, + ATTR_NOTIFICATION_ID: notification_id, + ATTR_STATUS: STATUS_UNREAD, + ATTR_TITLE: title, + ATTR_CREATED_AT: dt_util.utcnow(), } - hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_CREATE, data)) + hass.bus.async_fire(EVENT_PERSISTENT_NOTIFICATIONS_UPDATED, context=context) @callback @bind_hass -def async_dismiss(hass: HomeAssistant, notification_id: str) -> None: +def async_dismiss( + hass: HomeAssistant, notification_id: str, *, context: Context | None = None +) -> None: """Remove a notification.""" - data = {ATTR_NOTIFICATION_ID: notification_id} + notifications = hass.data.get(DOMAIN) + if notifications is None: + notifications = hass.data[DOMAIN] = {} - hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_DISMISS, data)) + entity_id = ENTITY_ID_FORMAT.format(slugify(notification_id)) + + if entity_id not in notifications: + return + + hass.states.async_remove(entity_id, context) + + del notifications[entity_id] + hass.bus.async_fire(EVENT_PERSISTENT_NOTIFICATIONS_UPDATED) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the persistent notification component.""" - persistent_notifications: MutableMapping[str, MutableMapping] = OrderedDict() - hass.data[DOMAIN] = {"notifications": persistent_notifications} + notifications = hass.data.setdefault(DOMAIN, {}) @callback - def create_service(call): + def create_service(call: ServiceCall) -> None: """Handle a create notification service call.""" - title = call.data.get(ATTR_TITLE) - message = call.data.get(ATTR_MESSAGE) - notification_id = call.data.get(ATTR_NOTIFICATION_ID) - - if notification_id is not None: - entity_id = ENTITY_ID_FORMAT.format(slugify(notification_id)) - else: - entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, DEFAULT_OBJECT_ID, hass=hass - ) - notification_id = entity_id.split(".")[1] - - attr = {} - if title is not None: - if isinstance(title, Template): - try: - title.hass = hass - title = title.async_render(parse_result=False) - except TemplateError as ex: - _LOGGER.error("Error rendering title %s: %s", title, ex) - title = title.template - - attr[ATTR_TITLE] = title - attr[ATTR_FRIENDLY_NAME] = title - - if isinstance(message, Template): - try: - message.hass = hass - message = message.async_render(parse_result=False) - except TemplateError as ex: - _LOGGER.error("Error rendering message %s: %s", message, ex) - message = message.template - - attr[ATTR_MESSAGE] = message - - hass.states.async_set(entity_id, STATE, attr) - - # Store notification and fire event - # This will eventually replace state machine storage - persistent_notifications[entity_id] = { - ATTR_MESSAGE: message, - ATTR_NOTIFICATION_ID: notification_id, - ATTR_STATUS: STATUS_UNREAD, - ATTR_TITLE: title, - ATTR_CREATED_AT: dt_util.utcnow(), - } - - hass.bus.async_fire(EVENT_PERSISTENT_NOTIFICATIONS_UPDATED) + async_create( + hass, + call.data[ATTR_MESSAGE], + call.data.get(ATTR_TITLE), + call.data.get(ATTR_NOTIFICATION_ID), + context=call.context, + ) @callback - def dismiss_service(call): + def dismiss_service(call: ServiceCall) -> None: """Handle the dismiss notification service call.""" - notification_id = call.data.get(ATTR_NOTIFICATION_ID) - entity_id = ENTITY_ID_FORMAT.format(slugify(notification_id)) - - if entity_id not in persistent_notifications: - return - - hass.states.async_remove(entity_id, call.context) - - del persistent_notifications[entity_id] - hass.bus.async_fire(EVENT_PERSISTENT_NOTIFICATIONS_UPDATED) + async_dismiss(hass, call.data[ATTR_NOTIFICATION_ID], context=call.context) @callback - def mark_read_service(call): + def mark_read_service(call: ServiceCall) -> None: """Handle the mark_read notification service call.""" notification_id = call.data.get(ATTR_NOTIFICATION_ID) entity_id = ENTITY_ID_FORMAT.format(slugify(notification_id)) - if entity_id not in persistent_notifications: + if entity_id not in notifications: _LOGGER.error( "Marking persistent_notification read failed: " "Notification ID %s not found", @@ -186,19 +183,30 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) return - persistent_notifications[entity_id][ATTR_STATUS] = STATUS_READ - hass.bus.async_fire(EVENT_PERSISTENT_NOTIFICATIONS_UPDATED) + notifications[entity_id][ATTR_STATUS] = STATUS_READ + hass.bus.async_fire( + EVENT_PERSISTENT_NOTIFICATIONS_UPDATED, context=call.context + ) hass.services.async_register( - DOMAIN, SERVICE_CREATE, create_service, SCHEMA_SERVICE_CREATE + DOMAIN, + "create", + create_service, + vol.Schema( + { + vol.Required(ATTR_MESSAGE): vol.Any(cv.dynamic_template, cv.string), + vol.Optional(ATTR_TITLE): vol.Any(cv.dynamic_template, cv.string), + vol.Optional(ATTR_NOTIFICATION_ID): cv.string, + } + ), ) hass.services.async_register( - DOMAIN, SERVICE_DISMISS, dismiss_service, SCHEMA_SERVICE_DISMISS + DOMAIN, "dismiss", dismiss_service, SCHEMA_SERVICE_NOTIFICATION ) hass.services.async_register( - DOMAIN, SERVICE_MARK_READ, mark_read_service, SCHEMA_SERVICE_MARK_READ + DOMAIN, "mark_read", mark_read_service, SCHEMA_SERVICE_NOTIFICATION ) hass.components.websocket_api.async_register_command(websocket_get_notifications) @@ -228,7 +236,7 @@ def websocket_get_notifications( ATTR_CREATED_AT, ) } - for data in hass.data[DOMAIN]["notifications"].values() + for data in hass.data[DOMAIN].values() ], ) ) diff --git a/script/scaffold/templates/config_flow/tests/test_config_flow.py b/script/scaffold/templates/config_flow/tests/test_config_flow.py index c6a6ec6b629..d1ddc177690 100644 --- a/script/scaffold/templates/config_flow/tests/test_config_flow.py +++ b/script/scaffold/templates/config_flow/tests/test_config_flow.py @@ -1,7 +1,7 @@ """Test the NEW_NAME config flow.""" from unittest.mock import patch -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.NEW_DOMAIN.config_flow import CannotConnect, InvalidAuth from homeassistant.components.NEW_DOMAIN.const import DOMAIN from homeassistant.core import HomeAssistant @@ -10,7 +10,6 @@ from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_ async def test_form(hass: HomeAssistant) -> None: """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/airnow/test_config_flow.py b/tests/components/airnow/test_config_flow.py index 9937e899643..b26775d7051 100644 --- a/tests/components/airnow/test_config_flow.py +++ b/tests/components/airnow/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch from pyairnow.errors import AirNowError, InvalidKeyError -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.airnow.const import DOMAIN from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS @@ -68,7 +68,7 @@ MOCK_RESPONSE = [ async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/airthings/test_config_flow.py b/tests/components/airthings/test_config_flow.py index ad9d44a054a..0ecb2c7a8dc 100644 --- a/tests/components/airthings/test_config_flow.py +++ b/tests/components/airthings/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch import airthings -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.airthings.const import CONF_ID, CONF_SECRET, DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM @@ -18,7 +18,7 @@ TEST_DATA = { async def test_form(hass: HomeAssistant) -> None: """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/august/test_config_flow.py b/tests/components/august/test_config_flow.py index ab5ea1e216b..8aa9c3e3759 100644 --- a/tests/components/august/test_config_flow.py +++ b/tests/components/august/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch from yalexs.authenticator import ValidationResult -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.august.const import ( CONF_ACCESS_TOKEN_CACHE_FILE, CONF_INSTALL_ID, @@ -23,7 +23,7 @@ from tests.common import MockConfigEntry async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index 44594239d74..4d73eec7f1f 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -6,7 +6,6 @@ from aiohttp import ClientResponseError from yalexs.authenticator_common import AuthenticationState from yalexs.exceptions import AugustApiAIOHTTPError -from homeassistant import setup from homeassistant.components.august.const import DOMAIN from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -41,7 +40,6 @@ async def test_august_is_offline(hass): ) config_entry.add_to_hass(hass) - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate", side_effect=asyncio.TimeoutError, @@ -147,7 +145,6 @@ async def test_auth_fails(hass): config_entry.add_to_hass(hass) assert hass.config_entries.flow.async_progress() == [] - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate", side_effect=ClientResponseError(None, None, status=401), @@ -173,7 +170,6 @@ async def test_bad_password(hass): config_entry.add_to_hass(hass) assert hass.config_entries.flow.async_progress() == [] - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate", return_value=_mock_august_authentication( @@ -201,7 +197,6 @@ async def test_http_failure(hass): config_entry.add_to_hass(hass) assert hass.config_entries.flow.async_progress() == [] - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate", side_effect=ClientResponseError(None, None, status=500), @@ -225,7 +220,6 @@ async def test_unknown_auth_state(hass): config_entry.add_to_hass(hass) assert hass.config_entries.flow.async_progress() == [] - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate", return_value=_mock_august_authentication("original_token", 1234, None), @@ -251,7 +245,6 @@ async def test_requires_validation_state(hass): config_entry.add_to_hass(hass) assert hass.config_entries.flow.async_progress() == [] - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate", return_value=_mock_august_authentication( @@ -278,7 +271,6 @@ async def test_unknown_auth_http_401(hass): config_entry.add_to_hass(hass) assert hass.config_entries.flow.async_progress() == [] - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate", return_value=_mock_august_authentication("original_token", 1234, None), diff --git a/tests/components/aurora/test_config_flow.py b/tests/components/aurora/test_config_flow.py index d839b024468..9e83810978a 100644 --- a/tests/components/aurora/test_config_flow.py +++ b/tests/components/aurora/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch from aiohttp import ClientError -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.aurora.const import DOMAIN from tests.common import MockConfigEntry @@ -18,7 +18,7 @@ DATA = { async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -47,7 +47,6 @@ async def test_form(hass): async def test_form_cannot_connect(hass): """Test if invalid response or no connection returned from the API.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -68,7 +67,7 @@ async def test_form_cannot_connect(hass): async def test_with_unknown_error(hass): """Test with unknown error response from the API.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/blink/test_config_flow.py b/tests/components/blink/test_config_flow.py index 7da395a6f1f..5e3b89002bf 100644 --- a/tests/components/blink/test_config_flow.py +++ b/tests/components/blink/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import Mock, patch from blinkpy.auth import LoginError from blinkpy.blinkpy import BlinkSetupError -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.blink import DOMAIN from tests.common import MockConfigEntry @@ -12,7 +12,7 @@ from tests.common import MockConfigEntry async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -50,7 +50,7 @@ async def test_form(hass): async def test_form_2fa(hass): """Test we get the 2fa form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -92,7 +92,7 @@ async def test_form_2fa(hass): async def test_form_2fa_connect_error(hass): """Test we report a connect error during 2fa setup.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -131,7 +131,7 @@ async def test_form_2fa_connect_error(hass): async def test_form_2fa_invalid_key(hass): """Test we report an error if key is invalid.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -170,7 +170,7 @@ async def test_form_2fa_invalid_key(hass): async def test_form_2fa_unknown_error(hass): """Test we report an unknown error during 2fa setup.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/bond/test_config_flow.py b/tests/components/bond/test_config_flow.py index 89c183ec1ba..8dd379ed3a7 100644 --- a/tests/components/bond/test_config_flow.py +++ b/tests/components/bond/test_config_flow.py @@ -6,7 +6,7 @@ from unittest.mock import Mock, patch from aiohttp import ClientConnectionError, ClientResponseError -from homeassistant import config_entries, core, setup +from homeassistant import config_entries, core from homeassistant.components.bond.const import DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST @@ -24,7 +24,7 @@ from tests.common import MockConfigEntry async def test_user_form(hass: core.HomeAssistant): """Test we get the user initiated form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -53,7 +53,7 @@ async def test_user_form(hass: core.HomeAssistant): async def test_user_form_with_non_bridge(hass: core.HomeAssistant): """Test setup a smart by bond fan.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -170,8 +170,6 @@ async def test_user_form_one_entry_per_device_allowed(hass: core.HomeAssistant): data={CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, ).add_to_hass(hass) - await setup.async_setup_component(hass, "persistent_notification", {}) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -193,7 +191,7 @@ async def test_user_form_one_entry_per_device_allowed(hass: core.HomeAssistant): async def test_zeroconf_form(hass: core.HomeAssistant): """Test we get the discovery form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, @@ -222,7 +220,7 @@ async def test_zeroconf_form(hass: core.HomeAssistant): async def test_zeroconf_form_token_unavailable(hass: core.HomeAssistant): """Test we get the discovery form and we handle the token being unavailable.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + with patch_bond_version(), patch_bond_token(): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -251,7 +249,7 @@ async def test_zeroconf_form_token_unavailable(hass: core.HomeAssistant): async def test_zeroconf_form_with_token_available(hass: core.HomeAssistant): """Test we get the discovery form when we can get the token.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + with patch_bond_version(return_value={"bondid": "test-bond-id"}), patch_bond_token( return_value={"token": "discovered-token"} ), patch_bond_bridge( @@ -284,7 +282,6 @@ async def test_zeroconf_form_with_token_available(hass: core.HomeAssistant): async def test_zeroconf_already_configured(hass: core.HomeAssistant): """Test starting a flow from discovery when already configured.""" - await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/bosch_shc/test_config_flow.py b/tests/components/bosch_shc/test_config_flow.py index 543d0438738..be5c3d76b53 100644 --- a/tests/components/bosch_shc/test_config_flow.py +++ b/tests/components/bosch_shc/test_config_flow.py @@ -9,7 +9,7 @@ from boschshcpy.exceptions import ( ) from boschshcpy.information import SHCInformation -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.bosch_shc.config_flow import write_tls_asset from homeassistant.components.bosch_shc.const import CONF_SHC_CERT, CONF_SHC_KEY, DOMAIN @@ -30,7 +30,7 @@ DISCOVERY_INFO = { async def test_form_user(hass, mock_zeroconf): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -376,7 +376,7 @@ async def test_form_validate_exception(hass, mock_zeroconf): async def test_form_already_configured(hass, mock_zeroconf): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain="bosch_shc", unique_id="test-mac", data={"host": "0.0.0.0"} ) @@ -412,7 +412,6 @@ async def test_form_already_configured(hass, mock_zeroconf): async def test_zeroconf(hass, mock_zeroconf): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "boschshcpy.session.SHCSession.mdns_info", @@ -481,7 +480,7 @@ async def test_zeroconf(hass, mock_zeroconf): async def test_zeroconf_already_configured(hass, mock_zeroconf): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain="bosch_shc", unique_id="test-mac", data={"host": "0.0.0.0"} ) @@ -561,7 +560,7 @@ async def test_zeroconf_not_bosch_shc(hass, mock_zeroconf): async def test_reauth(hass, mock_zeroconf): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + mock_config = MockConfigEntry( domain=DOMAIN, unique_id="test-mac", diff --git a/tests/components/broadlink/test_config_flow.py b/tests/components/broadlink/test_config_flow.py index 68f1c54f697..ed27d497d23 100644 --- a/tests/components/broadlink/test_config_flow.py +++ b/tests/components/broadlink/test_config_flow.py @@ -6,7 +6,7 @@ from unittest.mock import call, patch import broadlink.exceptions as blke import pytest -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.broadlink.const import DOMAIN from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.helpers import device_registry @@ -825,7 +825,6 @@ async def test_flow_reauth_valid_host(hass): async def test_dhcp_can_finish(hass): """Test DHCP discovery flow can finish right away.""" - await setup.async_setup_component(hass, "persistent_notification", {}) device = get_device("Living Room") device.host = "1.2.3.4" @@ -864,7 +863,7 @@ async def test_dhcp_can_finish(hass): async def test_dhcp_fails_to_connect(hass): """Test DHCP discovery flow that fails to connect.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + with patch(DEVICE_HELLO, side_effect=blke.NetworkTimeoutError()): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -883,7 +882,7 @@ async def test_dhcp_fails_to_connect(hass): async def test_dhcp_unreachable(hass): """Test DHCP discovery flow that fails to connect.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + with patch(DEVICE_HELLO, side_effect=OSError(errno.ENETUNREACH, None)): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -902,7 +901,7 @@ async def test_dhcp_unreachable(hass): async def test_dhcp_connect_unknown_error(hass): """Test DHCP discovery flow that fails to connect with an OSError.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + with patch(DEVICE_HELLO, side_effect=OSError()): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -921,7 +920,7 @@ async def test_dhcp_connect_unknown_error(hass): async def test_dhcp_device_not_supported(hass): """Test DHCP discovery flow that fails because the device is not supported.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + device = get_device("Kitchen") mock_api = device.get_mock_api() @@ -942,7 +941,7 @@ async def test_dhcp_device_not_supported(hass): async def test_dhcp_already_exists(hass): """Test DHCP discovery flow that fails to connect.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + device = get_device("Living Room") mock_entry = device.get_mock_entry() mock_entry.add_to_hass(hass) @@ -967,7 +966,6 @@ async def test_dhcp_already_exists(hass): async def test_dhcp_updates_host(hass): """Test DHCP updates host.""" - await setup.async_setup_component(hass, "persistent_notification", {}) device = get_device("Living Room") device.host = "1.2.3.4" diff --git a/tests/components/canary/test_alarm_control_panel.py b/tests/components/canary/test_alarm_control_panel.py index a21284ec376..84cef7e81ff 100644 --- a/tests/components/canary/test_alarm_control_panel.py +++ b/tests/components/canary/test_alarm_control_panel.py @@ -25,7 +25,6 @@ from tests.common import mock_registry async def test_alarm_control_panel(hass, canary) -> None: """Test the creation and values of the alarm_control_panel for Canary.""" - await async_setup_component(hass, "persistent_notification", {}) registry = mock_registry(hass) online_device_at_home = mock_device(20, "Dining Room", True, "Canary Pro") @@ -109,7 +108,6 @@ async def test_alarm_control_panel(hass, canary) -> None: async def test_alarm_control_panel_services(hass, canary) -> None: """Test the services of the alarm_control_panel for Canary.""" - await async_setup_component(hass, "persistent_notification", {}) online_device_at_home = mock_device(20, "Dining Room", True, "Canary Pro") diff --git a/tests/components/canary/test_config_flow.py b/tests/components/canary/test_config_flow.py index d194ae21185..b37c81407f7 100644 --- a/tests/components/canary/test_config_flow.py +++ b/tests/components/canary/test_config_flow.py @@ -16,14 +16,12 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM, ) -from homeassistant.setup import async_setup_component from . import USER_INPUT, _patch_async_setup, _patch_async_setup_entry, init_integration async def test_user_form(hass, canary_config_flow): """Test we get the user initiated form.""" - await async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} diff --git a/tests/components/canary/test_sensor.py b/tests/components/canary/test_sensor.py index 67d4a724584..c59810ac72f 100644 --- a/tests/components/canary/test_sensor.py +++ b/tests/components/canary/test_sensor.py @@ -29,7 +29,6 @@ from tests.common import async_fire_time_changed, mock_device_registry, mock_reg async def test_sensors_pro(hass, canary) -> None: """Test the creation and values of the sensors for Canary Pro.""" - await async_setup_component(hass, "persistent_notification", {}) registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -97,7 +96,6 @@ async def test_sensors_pro(hass, canary) -> None: async def test_sensors_attributes_pro(hass, canary) -> None: """Test the creation and values of the sensors attributes for Canary Pro.""" - await async_setup_component(hass, "persistent_notification", {}) online_device_at_home = mock_device(20, "Dining Room", True, "Canary Pro") @@ -158,7 +156,6 @@ async def test_sensors_attributes_pro(hass, canary) -> None: async def test_sensors_flex(hass, canary) -> None: """Test the creation and values of the sensors for Canary Flex.""" - await async_setup_component(hass, "persistent_notification", {}) registry = mock_registry(hass) device_registry = mock_device_registry(hass) diff --git a/tests/components/cloudflare/test_config_flow.py b/tests/components/cloudflare/test_config_flow.py index 16177850ad5..df8cff0f3a4 100644 --- a/tests/components/cloudflare/test_config_flow.py +++ b/tests/components/cloudflare/test_config_flow.py @@ -13,7 +13,6 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM, ) -from homeassistant.setup import async_setup_component from . import ( ENTRY_CONFIG, @@ -28,7 +27,6 @@ from tests.common import MockConfigEntry async def test_user_form(hass, cfupdate_flow): """Test we get the user initiated form.""" - await async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} diff --git a/tests/components/co2signal/test_config_flow.py b/tests/components/co2signal/test_config_flow.py index 668c72e9b04..b2530decb12 100644 --- a/tests/components/co2signal/test_config_flow.py +++ b/tests/components/co2signal/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.co2signal import DOMAIN, config_flow from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM @@ -16,7 +16,7 @@ from tests.common import MockConfigEntry async def test_form_home(hass: HomeAssistant) -> None: """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -46,7 +46,7 @@ async def test_form_home(hass: HomeAssistant) -> None: async def test_form_coordinates(hass: HomeAssistant) -> None: """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -87,7 +87,7 @@ async def test_form_coordinates(hass: HomeAssistant) -> None: async def test_form_country(hass: HomeAssistant) -> None: """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -200,7 +200,6 @@ async def test_form_error_unexpected_data(hass: HomeAssistant) -> None: async def test_import(hass: HomeAssistant) -> None: """Test we import correctly.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "CO2Signal.get_latest", @@ -230,7 +229,7 @@ async def test_import(hass: HomeAssistant) -> None: async def test_import_abort_existing_home(hass: HomeAssistant) -> None: """Test we abort if home entry found.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + MockConfigEntry(domain="co2signal", data={"api_key": "abcd"}).add_to_hass(hass) with patch( @@ -247,7 +246,7 @@ async def test_import_abort_existing_home(hass: HomeAssistant) -> None: async def test_import_abort_existing_country(hass: HomeAssistant) -> None: """Test we abort if existing country found.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + MockConfigEntry( domain="co2signal", data={"api_key": "abcd", "country_code": "nl"} ).add_to_hass(hass) @@ -274,7 +273,7 @@ async def test_import_abort_existing_country(hass: HomeAssistant) -> None: async def test_import_abort_existing_coordinates(hass: HomeAssistant) -> None: """Test we abort if existing coordinates found.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + MockConfigEntry( domain="co2signal", data={"api_key": "abcd", "latitude": 1, "longitude": 2} ).add_to_hass(hass) diff --git a/tests/components/coinbase/test_config_flow.py b/tests/components/coinbase/test_config_flow.py index fa13648ee71..e487cb5d837 100644 --- a/tests/components/coinbase/test_config_flow.py +++ b/tests/components/coinbase/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch from coinbase.wallet.error import AuthenticationError from requests.models import Response -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.coinbase.const import ( CONF_CURRENCIES, CONF_EXCHANGE_RATES, @@ -26,7 +26,7 @@ from tests.common import MockConfigEntry async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/control4/test_config_flow.py b/tests/components/control4/test_config_flow.py index 5ffdc4f1ea8..8a4791ab579 100644 --- a/tests/components/control4/test_config_flow.py +++ b/tests/components/control4/test_config_flow.py @@ -6,7 +6,7 @@ from pyControl4.account import C4Account from pyControl4.director import C4Director from pyControl4.error_handling import Unauthorized -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.control4.const import DEFAULT_SCAN_INTERVAL, DOMAIN from homeassistant.const import ( CONF_HOST, @@ -46,7 +46,7 @@ def _get_mock_c4_director(getAllItemInfo={}): async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/coronavirus/test_config_flow.py b/tests/components/coronavirus/test_config_flow.py index bfc69200893..e641c0e0011 100644 --- a/tests/components/coronavirus/test_config_flow.py +++ b/tests/components/coronavirus/test_config_flow.py @@ -3,14 +3,14 @@ from unittest.mock import MagicMock, patch from aiohttp import ClientError -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.coronavirus.const import DOMAIN, OPTION_WORLDWIDE from homeassistant.core import HomeAssistant async def test_form(hass: HomeAssistant) -> None: """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -39,7 +39,7 @@ async def test_abort_on_connection_error( mock_get_cases: MagicMock, hass: HomeAssistant ) -> None: """Test we abort on connection error.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py index 640a4b61ce3..265aec78270 100644 --- a/tests/components/deconz/test_device_trigger.py +++ b/tests/components/deconz/test_device_trigger.py @@ -169,7 +169,6 @@ async def test_functional_device_trigger( hass, aioclient_mock, mock_deconz_websocket, automation_calls ): """Test proper matching and attachment of device trigger automation.""" - await async_setup_component(hass, "persistent_notification", {}) data = { "sensors": { diff --git a/tests/components/devolo_home_control/test_config_flow.py b/tests/components/devolo_home_control/test_config_flow.py index 054b613f3a0..d2a359b9438 100644 --- a/tests/components/devolo_home_control/test_config_flow.py +++ b/tests/components/devolo_home_control/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.devolo_home_control.const import DEFAULT_MYDEVOLO, DOMAIN from .const import ( @@ -17,7 +17,7 @@ from tests.common import MockConfigEntry async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -31,7 +31,7 @@ async def test_form(hass): @pytest.mark.credentials_invalid async def test_form_invalid_credentials_user(hass): """Test if we get the error message on invalid credentials.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -117,7 +117,7 @@ async def test_form_zeroconf(hass): @pytest.mark.credentials_invalid async def test_form_invalid_credentials_zeroconf(hass): """Test if we get the error message on invalid credentials.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, diff --git a/tests/components/dexcom/test_config_flow.py b/tests/components/dexcom/test_config_flow.py index 65db7ca16ac..48544ca0158 100644 --- a/tests/components/dexcom/test_config_flow.py +++ b/tests/components/dexcom/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch from pydexcom import AccountError, SessionError -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.dexcom.const import DOMAIN, MG_DL, MMOL_L from homeassistant.const import CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME @@ -13,7 +13,7 @@ from tests.components.dexcom import CONFIG async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py index 915955da652..e9f43bf7af2 100644 --- a/tests/components/doorbird/test_config_flow.py +++ b/tests/components/doorbird/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, Mock, patch import pytest import requests -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.doorbird.const import CONF_EVENTS, DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME @@ -38,7 +38,7 @@ def _get_mock_doorbirdapi_side_effects(ready=None, info=None): async def test_user_form(hass): """Test we get the user form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -77,7 +77,6 @@ async def test_user_form(hass): async def test_form_zeroconf_wrong_oui(hass): """Test we abort when we get the wrong OUI via zeroconf.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, @@ -94,7 +93,6 @@ async def test_form_zeroconf_wrong_oui(hass): async def test_form_zeroconf_link_local_ignored(hass): """Test we abort when we get a link local address via zeroconf.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, @@ -114,7 +112,6 @@ async def test_form_zeroconf_correct_oui(hass): doorbirdapi = _get_mock_doorbirdapi_return_values( ready=[True], info={"WIFI_MAC_ADDR": "macaddr"} ) - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.doorbird.config_flow.DoorBird", @@ -174,7 +171,6 @@ async def test_form_zeroconf_correct_oui_wrong_device(hass, doorbell_state_side_ ready=[True], info={"WIFI_MAC_ADDR": "macaddr"} ) type(doorbirdapi).doorbell_state = MagicMock(side_effect=doorbell_state_side_effect) - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.doorbird.config_flow.DoorBird", diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py index d56cd3f2eb8..9a2d1fe8481 100644 --- a/tests/components/dsmr/test_config_flow.py +++ b/tests/components/dsmr/test_config_flow.py @@ -7,7 +7,7 @@ from unittest.mock import DEFAULT, AsyncMock, MagicMock, patch, sentinel import serial import serial.tools.list_ports -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.dsmr import DOMAIN, config_flow from tests.common import MockConfigEntry @@ -150,8 +150,6 @@ async def test_setup_serial_fail(com_mock, hass, dsmr_connection_send_validate_f """Test failed serial connection.""" (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture - await setup.async_setup_component(hass, "persistent_notification", {}) - port = com_port() result = await hass.config_entries.flow.async_init( @@ -197,8 +195,6 @@ async def test_setup_serial_wrong_telegram( """Test failed telegram data.""" (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture - await setup.async_setup_component(hass, "persistent_notification", {}) - port = com_port() result = await hass.config_entries.flow.async_init( @@ -231,7 +227,6 @@ async def test_setup_serial_wrong_telegram( async def test_import_usb(hass, dsmr_connection_send_validate_fixture): """Test we can import.""" - await setup.async_setup_component(hass, "persistent_notification", {}) entry_data = { "port": "/dev/ttyUSB0", @@ -258,8 +253,6 @@ async def test_import_usb_failed_connection( """Test we can import.""" (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture - await setup.async_setup_component(hass, "persistent_notification", {}) - entry_data = { "port": "/dev/ttyUSB0", "dsmr_version": "2.2", @@ -293,8 +286,6 @@ async def test_import_usb_no_data(hass, dsmr_connection_send_validate_fixture): """Test we can import.""" (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture - await setup.async_setup_component(hass, "persistent_notification", {}) - entry_data = { "port": "/dev/ttyUSB0", "dsmr_version": "2.2", @@ -325,8 +316,6 @@ async def test_import_usb_wrong_telegram(hass, dsmr_connection_send_validate_fix """Test we can import.""" (connection_factory, transport, protocol) = dsmr_connection_send_validate_fixture - await setup.async_setup_component(hass, "persistent_notification", {}) - entry_data = { "port": "/dev/ttyUSB0", "dsmr_version": "2.2", @@ -349,7 +338,6 @@ async def test_import_usb_wrong_telegram(hass, dsmr_connection_send_validate_fix async def test_import_network(hass, dsmr_connection_send_validate_fixture): """Test we can import from network.""" - await setup.async_setup_component(hass, "persistent_notification", {}) entry_data = { "host": "localhost", @@ -373,7 +361,6 @@ async def test_import_network(hass, dsmr_connection_send_validate_fixture): async def test_import_update(hass, dsmr_connection_send_validate_fixture): """Test we can import.""" - await setup.async_setup_component(hass, "persistent_notification", {}) entry_data = { "port": "/dev/ttyUSB0", @@ -422,7 +409,6 @@ async def test_import_update(hass, dsmr_connection_send_validate_fixture): async def test_options_flow(hass): """Test options flow.""" - await setup.async_setup_component(hass, "persistent_notification", {}) entry_data = { "port": "/dev/ttyUSB0", @@ -462,7 +448,6 @@ async def test_options_flow(hass): async def test_import_luxembourg(hass, dsmr_connection_send_validate_fixture): """Test we can import.""" - await setup.async_setup_component(hass, "persistent_notification", {}) entry_data = { "port": "/dev/ttyUSB0", @@ -485,7 +470,6 @@ async def test_import_luxembourg(hass, dsmr_connection_send_validate_fixture): async def test_import_sweden(hass, dsmr_connection_send_validate_fixture): """Test we can import.""" - await setup.async_setup_component(hass, "persistent_notification", {}) entry_data = { "port": "/dev/ttyUSB0", diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index d0498496bf2..ab5ebba79eb 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock, patch -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.elkm1.const import DOMAIN @@ -25,7 +25,7 @@ def mock_elk(invalid_auth=None, sync_complete=None): async def test_form_user_with_secure_elk(hass): """Test we can setup a secure elk.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -72,7 +72,7 @@ async def test_form_user_with_secure_elk(hass): async def test_form_user_with_tls_elk(hass): """Test we can setup a secure elk.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -119,7 +119,7 @@ async def test_form_user_with_tls_elk(hass): async def test_form_user_with_non_secure_elk(hass): """Test we can setup a non-secure elk.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -164,7 +164,7 @@ async def test_form_user_with_non_secure_elk(hass): async def test_form_user_with_serial_elk(hass): """Test we can setup a serial elk.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -268,7 +268,6 @@ async def test_form_invalid_auth(hass): async def test_form_import(hass): """Test we get the form with import source.""" - await setup.async_setup_component(hass, "persistent_notification", {}) mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) with patch( diff --git a/tests/components/emonitor/test_config_flow.py b/tests/components/emonitor/test_config_flow.py index ab2f62578b4..a030f242d8d 100644 --- a/tests/components/emonitor/test_config_flow.py +++ b/tests/components/emonitor/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch from aioemonitor.monitor import EmonitorNetwork, EmonitorStatus import aiohttp -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.components.emonitor.const import DOMAIN from homeassistant.const import CONF_HOST @@ -20,7 +20,7 @@ def _mock_emonitor(): async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -94,7 +94,6 @@ async def test_form_cannot_connect(hass): async def test_dhcp_can_confirm(hass): """Test DHCP discovery flow can confirm right away.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.emonitor.config_flow.Emonitor.async_get_status", @@ -138,7 +137,6 @@ async def test_dhcp_can_confirm(hass): async def test_dhcp_fails_to_connect(hass): """Test DHCP discovery flow that fails to connect.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.emonitor.config_flow.Emonitor.async_get_status", @@ -161,7 +159,7 @@ async def test_dhcp_fails_to_connect(hass): async def test_dhcp_already_exists(hass): """Test DHCP discovery flow that fails to connect.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "1.2.3.4"}, @@ -190,7 +188,7 @@ async def test_dhcp_already_exists(hass): async def test_user_unique_id_already_exists(hass): """Test creating an entry where the unique_id already exists.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "1.2.3.4"}, diff --git a/tests/components/enphase_envoy/test_config_flow.py b/tests/components/enphase_envoy/test_config_flow.py index a4eb8a574dc..6e32cf88975 100644 --- a/tests/components/enphase_envoy/test_config_flow.py +++ b/tests/components/enphase_envoy/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock, patch import httpx -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.enphase_envoy.const import DOMAIN from homeassistant.core import HomeAssistant @@ -12,7 +12,7 @@ from tests.common import MockConfigEntry async def test_form(hass: HomeAssistant) -> None: """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -120,7 +120,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: async def test_import(hass: HomeAssistant) -> None: """Test we can import from yaml.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( "homeassistant.components.enphase_envoy.config_flow.EnvoyReader.getData", return_value=True, @@ -153,7 +153,7 @@ async def test_import(hass: HomeAssistant) -> None: async def test_zeroconf(hass: HomeAssistant) -> None: """Test we can setup from zeroconf.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, @@ -198,7 +198,6 @@ async def test_zeroconf(hass: HomeAssistant) -> None: async def test_form_host_already_exists(hass: HomeAssistant) -> None: """Test host already exists.""" - await setup.async_setup_component(hass, "persistent_notification", {}) config_entry = MockConfigEntry( domain=DOMAIN, @@ -237,7 +236,7 @@ async def test_form_host_already_exists(hass: HomeAssistant) -> None: async def test_zeroconf_serial_already_exists(hass: HomeAssistant) -> None: """Test serial number already exists from zeroconf.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + config_entry = MockConfigEntry( domain=DOMAIN, data={ @@ -266,7 +265,7 @@ async def test_zeroconf_serial_already_exists(hass: HomeAssistant) -> None: async def test_zeroconf_host_already_exists(hass: HomeAssistant) -> None: """Test hosts already exists from zeroconf.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + config_entry = MockConfigEntry( domain=DOMAIN, data={ @@ -306,7 +305,7 @@ async def test_zeroconf_host_already_exists(hass: HomeAssistant) -> None: async def test_reauth(hass: HomeAssistant) -> None: """Test we reauth auth.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + config_entry = MockConfigEntry( domain=DOMAIN, data={ diff --git a/tests/components/epson/test_config_flow.py b/tests/components/epson/test_config_flow.py index 088b1a5435c..bf58307f81c 100644 --- a/tests/components/epson/test_config_flow.py +++ b/tests/components/epson/test_config_flow.py @@ -3,14 +3,14 @@ from unittest.mock import patch from epson_projector.const import PWR_OFF_STATE -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.epson.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME, STATE_UNAVAILABLE async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + with patch("homeassistant.components.epson.Projector.get_power", return_value="01"): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} diff --git a/tests/components/ezviz/test_config_flow.py b/tests/components/ezviz/test_config_flow.py index fe3c271b390..4dffe1d7e25 100644 --- a/tests/components/ezviz/test_config_flow.py +++ b/tests/components/ezviz/test_config_flow.py @@ -34,7 +34,6 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM, ) -from homeassistant.setup import async_setup_component from . import ( DISCOVERY_INFO, @@ -52,7 +51,6 @@ from . import ( async def test_user_form(hass, ezviz_config_flow): """Test the user initiated form.""" - await async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -115,7 +113,6 @@ async def test_user_custom_url(hass, ezviz_config_flow): async def test_async_step_import(hass, ezviz_config_flow): """Test the config import flow.""" - await async_setup_component(hass, "persistent_notification", {}) with _patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_init( @@ -129,7 +126,6 @@ async def test_async_step_import(hass, ezviz_config_flow): async def test_async_step_import_camera(hass, ezviz_config_flow): """Test the config import camera flow.""" - await async_setup_component(hass, "persistent_notification", {}) with _patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_init( @@ -143,7 +139,6 @@ async def test_async_step_import_camera(hass, ezviz_config_flow): async def test_async_step_import_2nd_form_returns_camera(hass, ezviz_config_flow): """Test we get the user initiated form.""" - await async_setup_component(hass, "persistent_notification", {}) with _patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_init( @@ -168,7 +163,6 @@ async def test_async_step_import_2nd_form_returns_camera(hass, ezviz_config_flow async def test_async_step_import_abort(hass, ezviz_config_flow): """Test the config import flow with invalid data.""" - await async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=YAML_INVALID @@ -179,7 +173,6 @@ async def test_async_step_import_abort(hass, ezviz_config_flow): async def test_step_discovery_abort_if_cloud_account_missing(hass): """Test discovery and confirm step, abort if cloud account was removed.""" - await async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DISCOVERY}, data=DISCOVERY_INFO @@ -207,7 +200,6 @@ async def test_async_step_discovery( """Test discovery and confirm step.""" with patch("homeassistant.components.ezviz.PLATFORMS", []): await init_integration(hass) - await async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DISCOVERY}, data=DISCOVERY_INFO @@ -359,8 +351,6 @@ async def test_discover_exception_step1( with patch("homeassistant.components.ezviz.PLATFORMS", []): await init_integration(hass) - await async_setup_component(hass, "persistent_notification", {}) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DISCOVERY}, @@ -435,7 +425,6 @@ async def test_discover_exception_step3( """Test we handle unexpected exception on discovery.""" with patch("homeassistant.components.ezviz.PLATFORMS", []): await init_integration(hass) - await async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/faa_delays/test_config_flow.py b/tests/components/faa_delays/test_config_flow.py index df79c3953c3..2ab4aaf6dd6 100644 --- a/tests/components/faa_delays/test_config_flow.py +++ b/tests/components/faa_delays/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch from aiohttp import ClientConnectionError import faadelays -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.faa_delays.const import DOMAIN from homeassistant.const import CONF_ID from homeassistant.exceptions import HomeAssistantError @@ -19,7 +19,7 @@ async def mock_valid_airport(self, *args, **kwargs): async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/filesize/test_sensor.py b/tests/components/filesize/test_sensor.py index 5649a678e3a..8bb79a8088a 100644 --- a/tests/components/filesize/test_sensor.py +++ b/tests/components/filesize/test_sensor.py @@ -33,7 +33,7 @@ async def test_invalid_path(hass): config = {"sensor": {"platform": "filesize", CONF_FILE_PATHS: ["invalid_path"]}} assert await async_setup_component(hass, "sensor", config) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids()) == 0 + assert len(hass.states.async_entity_ids("sensor")) == 0 async def test_valid_path(hass): @@ -43,7 +43,7 @@ async def test_valid_path(hass): hass.config.allowlist_external_dirs = {TEST_DIR} assert await async_setup_component(hass, "sensor", config) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids()) == 1 + assert len(hass.states.async_entity_ids("sensor")) == 1 state = hass.states.get("sensor.mock_file_test_filesize_txt") assert state.state == "0.0" assert state.attributes.get("bytes") == 4 diff --git a/tests/components/firmata/test_config_flow.py b/tests/components/firmata/test_config_flow.py index 91db94052cc..22f8b326781 100644 --- a/tests/components/firmata/test_config_flow.py +++ b/tests/components/firmata/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch from pymata_express.pymata_express_serial import serial -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.firmata.const import CONF_SERIAL_PORT, DOMAIN from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant @@ -11,7 +11,6 @@ from homeassistant.core import HomeAssistant async def test_import_cannot_connect_pymata(hass: HomeAssistant) -> None: """Test we fail with an invalid board.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.firmata.board.PymataExpress.start_aio", @@ -29,7 +28,6 @@ async def test_import_cannot_connect_pymata(hass: HomeAssistant) -> None: async def test_import_cannot_connect_serial(hass: HomeAssistant) -> None: """Test we fail with an invalid board.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.firmata.board.PymataExpress.start_aio", @@ -47,7 +45,6 @@ async def test_import_cannot_connect_serial(hass: HomeAssistant) -> None: async def test_import_cannot_connect_serial_timeout(hass: HomeAssistant) -> None: """Test we fail with an invalid board.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.firmata.board.PymataExpress.start_aio", @@ -65,7 +62,6 @@ async def test_import_cannot_connect_serial_timeout(hass: HomeAssistant) -> None async def test_import(hass: HomeAssistant) -> None: """Test we create an entry from config.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.firmata.board.PymataExpress", autospec=True diff --git a/tests/components/fjaraskupan/test_config_flow.py b/tests/components/fjaraskupan/test_config_flow.py index 7244042d356..22808382c49 100644 --- a/tests/components/fjaraskupan/test_config_flow.py +++ b/tests/components/fjaraskupan/test_config_flow.py @@ -6,7 +6,7 @@ from unittest.mock import patch from bleak.backends.device import BLEDevice from pytest import fixture -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.fjaraskupan.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( @@ -19,7 +19,6 @@ from homeassistant.data_entry_flow import ( @fixture(name="mock_setup_entry", autouse=True) async def fixture_mock_setup_entry(hass): """Fixture for config entry.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.fjaraskupan.async_setup_entry", return_value=True diff --git a/tests/components/flick_electric/test_config_flow.py b/tests/components/flick_electric/test_config_flow.py index 580db390afb..be4f240efa3 100644 --- a/tests/components/flick_electric/test_config_flow.py +++ b/tests/components/flick_electric/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch from pyflick.authentication import AuthException -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.flick_electric.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -23,7 +23,7 @@ async def _flow_submit(hass): async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/flipr/test_config_flow.py b/tests/components/flipr/test_config_flow.py index 66410938aab..00c8d7e2401 100644 --- a/tests/components/flipr/test_config_flow.py +++ b/tests/components/flipr/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest from requests.exceptions import HTTPError, Timeout -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.flipr.const import CONF_FLIPR_ID, DOMAIN from homeassistant.const import CONF_EMAIL, CONF_PASSWORD @@ -21,7 +21,7 @@ def mock_setups(): async def test_show_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/flo/test_config_flow.py b/tests/components/flo/test_config_flow.py index 3fd68979b05..f1cbd46ba70 100644 --- a/tests/components/flo/test_config_flow.py +++ b/tests/components/flo/test_config_flow.py @@ -3,7 +3,7 @@ import json import time from unittest.mock import patch -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.flo.const import DOMAIN from homeassistant.const import CONTENT_TYPE_JSON @@ -12,7 +12,7 @@ from .common import TEST_EMAIL_ADDRESS, TEST_PASSWORD, TEST_TOKEN, TEST_USER_ID async def test_form(hass, aioclient_mock_fixture): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/flume/test_config_flow.py b/tests/components/flume/test_config_flow.py index 5c439933b0b..70ee359b7b4 100644 --- a/tests/components/flume/test_config_flow.py +++ b/tests/components/flume/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock, patch import requests.exceptions -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.flume.const import DOMAIN from homeassistant.const import ( CONF_CLIENT_ID, @@ -23,7 +23,7 @@ def _get_mocked_flume_device_list(): async def test_form(hass): """Test we get the form and can setup from user input.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -66,7 +66,7 @@ async def test_form(hass): async def test_form_import(hass): """Test we can import the sensor platform config.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + mock_flume_device_list = _get_mocked_flume_device_list() with patch( diff --git a/tests/components/flux_led/test_config_flow.py b/tests/components/flux_led/test_config_flow.py index 1c239108f41..029ba0f972b 100644 --- a/tests/components/flux_led/test_config_flow.py +++ b/tests/components/flux_led/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.flux_led.const import ( CONF_CUSTOM_EFFECT_COLORS, CONF_CUSTOM_EFFECT_SPEED_PCT, @@ -317,7 +317,6 @@ async def test_manual_no_discovery_data(hass: HomeAssistant): async def test_discovered_by_discovery_and_dhcp(hass): """Test we get the form with discovery and abort for dhcp source when we get both.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with _patch_discovery(), _patch_wifibulb(): result = await hass.config_entries.flow.async_init( @@ -364,8 +363,6 @@ async def test_discovered_by_discovery_and_dhcp(hass): async def test_discovered_by_dhcp_or_discovery(hass, source, data): """Test we can setup when discovered from dhcp or discovery.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - with _patch_discovery(), _patch_wifibulb(): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, data=data @@ -402,7 +399,6 @@ async def test_discovered_by_dhcp_or_discovery_adds_missing_unique_id( """Test we can setup when discovered from dhcp or discovery.""" config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}) config_entry.add_to_hass(hass) - await setup.async_setup_component(hass, "persistent_notification", {}) with _patch_discovery(), _patch_wifibulb(): result = await hass.config_entries.flow.async_init( diff --git a/tests/components/folder/test_sensor.py b/tests/components/folder/test_sensor.py index b047aae49ac..f2d70b31b8f 100644 --- a/tests/components/folder/test_sensor.py +++ b/tests/components/folder/test_sensor.py @@ -28,7 +28,7 @@ async def test_invalid_path(hass): """Test that an invalid path is caught.""" config = {"sensor": {"platform": "folder", CONF_FOLDER_PATHS: "invalid_path"}} assert await async_setup_component(hass, "sensor", config) - assert len(hass.states.async_entity_ids()) == 0 + assert len(hass.states.async_entity_ids("sensor")) == 0 async def test_valid_path(hass): diff --git a/tests/components/forecast_solar/conftest.py b/tests/components/forecast_solar/conftest.py index 0bf080535f6..0be3f4bde0b 100644 --- a/tests/components/forecast_solar/conftest.py +++ b/tests/components/forecast_solar/conftest.py @@ -16,18 +16,11 @@ from homeassistant.components.forecast_solar.const import ( ) from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry -@pytest.fixture(autouse=True) -async def mock_persistent_notification(hass: HomeAssistant) -> None: - """Set up component for persistent notifications.""" - await async_setup_component(hass, "persistent_notification", {}) - - @pytest.fixture def mock_config_entry() -> MockConfigEntry: """Return the default mocked config entry.""" diff --git a/tests/components/foscam/test_config_flow.py b/tests/components/foscam/test_config_flow.py index 2f72000aaed..3a108b539d8 100644 --- a/tests/components/foscam/test_config_flow.py +++ b/tests/components/foscam/test_config_flow.py @@ -8,7 +8,7 @@ from libpyfoscam.foscam import ( ERROR_FOSCAM_UNKNOWN, ) -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.foscam import config_flow from tests.common import MockConfigEntry @@ -76,7 +76,6 @@ def setup_mock_foscam_camera(mock_foscam_camera): async def test_user_valid(hass): """Test valid config from user input.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -108,7 +107,6 @@ async def test_user_valid(hass): async def test_user_invalid_auth(hass): """Test we handle invalid auth from user input.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -137,7 +135,6 @@ async def test_user_invalid_auth(hass): async def test_user_cannot_connect(hass): """Test we handle cannot connect error from user input.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -166,7 +163,6 @@ async def test_user_cannot_connect(hass): async def test_user_invalid_response(hass): """Test we handle invalid response error from user input.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -197,7 +193,6 @@ async def test_user_invalid_response(hass): async def test_user_already_configured(hass): """Test we handle already configured from user input.""" - await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry( domain=config_flow.DOMAIN, @@ -229,7 +224,6 @@ async def test_user_already_configured(hass): async def test_user_unknown_exception(hass): """Test we handle unknown exceptions from user input.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -255,7 +249,6 @@ async def test_user_unknown_exception(hass): async def test_import_user_valid(hass): """Test valid config from import.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.foscam.config_flow.FoscamCamera", @@ -282,7 +275,6 @@ async def test_import_user_valid(hass): async def test_import_user_valid_with_name(hass): """Test valid config with extra name from import.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.foscam.config_flow.FoscamCamera", @@ -313,7 +305,6 @@ async def test_import_user_valid_with_name(hass): async def test_import_invalid_auth(hass): """Test we handle invalid auth from import.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.foscam.config_flow.FoscamCamera", @@ -337,7 +328,6 @@ async def test_import_invalid_auth(hass): async def test_import_cannot_connect(hass): """Test we handle cannot connect error from import.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.foscam.config_flow.FoscamCamera", @@ -361,7 +351,6 @@ async def test_import_cannot_connect(hass): async def test_import_invalid_response(hass): """Test we handle invalid response error from import.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.foscam.config_flow.FoscamCamera", @@ -387,7 +376,6 @@ async def test_import_invalid_response(hass): async def test_import_already_configured(hass): """Test we handle already configured from import.""" - await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry( domain=config_flow.DOMAIN, @@ -414,7 +402,6 @@ async def test_import_already_configured(hass): async def test_import_unknown_exception(hass): """Test we handle unknown exceptions from import.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.foscam.config_flow.FoscamCamera", diff --git a/tests/components/freebox/test_init.py b/tests/components/freebox/test_init.py index 44af000f79a..2f33832f518 100644 --- a/tests/components/freebox/test_init.py +++ b/tests/components/freebox/test_init.py @@ -46,7 +46,6 @@ async def test_setup(hass: HomeAssistant, router: Mock): async def test_setup_import(hass: HomeAssistant, router: Mock): """Test setup of integration from import.""" - await async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/garages_amsterdam/test_config_flow.py b/tests/components/garages_amsterdam/test_config_flow.py index 464fcb799ad..4ff064ca9d4 100644 --- a/tests/components/garages_amsterdam/test_config_flow.py +++ b/tests/components/garages_amsterdam/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch from aiohttp import ClientResponseError import pytest -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.garages_amsterdam.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( @@ -16,7 +16,6 @@ from homeassistant.data_entry_flow import ( async def test_full_flow(hass: HomeAssistant) -> None: """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -52,7 +51,6 @@ async def test_error_handling( side_effect: Exception, reason: str, hass: HomeAssistant ) -> None: """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.garages_amsterdam.config_flow.garages_amsterdam.get_garages", diff --git a/tests/components/gdacs/test_geo_location.py b/tests/components/gdacs/test_geo_location.py index d93db73aa79..bc73484b90f 100644 --- a/tests/components/gdacs/test_geo_location.py +++ b/tests/components/gdacs/test_geo_location.py @@ -96,9 +96,12 @@ async def test_setup(hass, legacy_patchable_time): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - all_states = hass.states.async_all() # 3 geolocation and 1 sensor entities - assert len(all_states) == 4 + assert ( + len(hass.states.async_entity_ids("geo_location")) + + len(hass.states.async_entity_ids("sensor")) + == 4 + ) entity_registry = er.async_get(hass) assert len(entity_registry.entities) == 4 @@ -169,8 +172,11 @@ async def test_setup(hass, legacy_patchable_time): async_fire_time_changed(hass, utcnow + DEFAULT_SCAN_INTERVAL) await hass.async_block_till_done() - all_states = hass.states.async_all() - assert len(all_states) == 4 + assert ( + len(hass.states.async_entity_ids("geo_location")) + + len(hass.states.async_entity_ids("sensor")) + == 4 + ) # Simulate an update - empty data, but successful update, # so no changes to entities. @@ -178,16 +184,22 @@ async def test_setup(hass, legacy_patchable_time): async_fire_time_changed(hass, utcnow + 2 * DEFAULT_SCAN_INTERVAL) await hass.async_block_till_done() - all_states = hass.states.async_all() - assert len(all_states) == 4 + assert ( + len(hass.states.async_entity_ids("geo_location")) + + len(hass.states.async_entity_ids("sensor")) + == 4 + ) # Simulate an update - empty data, removes all entities mock_feed_update.return_value = "ERROR", None async_fire_time_changed(hass, utcnow + 3 * DEFAULT_SCAN_INTERVAL) await hass.async_block_till_done() - all_states = hass.states.async_all() - assert len(all_states) == 1 + assert ( + len(hass.states.async_entity_ids("geo_location")) + + len(hass.states.async_entity_ids("sensor")) + == 1 + ) assert len(entity_registry.entities) == 1 @@ -219,8 +231,11 @@ async def test_setup_imperial(hass, legacy_patchable_time): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - all_states = hass.states.async_all() - assert len(all_states) == 2 + assert ( + len(hass.states.async_entity_ids("geo_location")) + + len(hass.states.async_entity_ids("sensor")) + == 2 + ) # Test conversion of 200 miles to kilometers. feeds = hass.data[DOMAIN][FEED] diff --git a/tests/components/gdacs/test_sensor.py b/tests/components/gdacs/test_sensor.py index ac53d88478b..87c14b1cd66 100644 --- a/tests/components/gdacs/test_sensor.py +++ b/tests/components/gdacs/test_sensor.py @@ -61,9 +61,12 @@ async def test_setup(hass, legacy_patchable_time): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - all_states = hass.states.async_all() # 3 geolocation and 1 sensor entities - assert len(all_states) == 4 + assert ( + len(hass.states.async_entity_ids("geo_location")) + + len(hass.states.async_entity_ids("sensor")) + == 4 + ) state = hass.states.get("sensor.gdacs_32_87336_117_22743") assert state is not None @@ -83,8 +86,11 @@ async def test_setup(hass, legacy_patchable_time): async_fire_time_changed(hass, utcnow + DEFAULT_SCAN_INTERVAL) await hass.async_block_till_done() - all_states = hass.states.async_all() - assert len(all_states) == 4 + assert ( + len(hass.states.async_entity_ids("geo_location")) + + len(hass.states.async_entity_ids("sensor")) + == 4 + ) state = hass.states.get("sensor.gdacs_32_87336_117_22743") attributes = state.attributes @@ -98,16 +104,22 @@ async def test_setup(hass, legacy_patchable_time): async_fire_time_changed(hass, utcnow + 2 * DEFAULT_SCAN_INTERVAL) await hass.async_block_till_done() - all_states = hass.states.async_all() - assert len(all_states) == 4 + assert ( + len(hass.states.async_entity_ids("geo_location")) + + len(hass.states.async_entity_ids("sensor")) + == 4 + ) # Simulate an update - empty data, removes all entities mock_feed_update.return_value = "ERROR", None async_fire_time_changed(hass, utcnow + 3 * DEFAULT_SCAN_INTERVAL) await hass.async_block_till_done() - all_states = hass.states.async_all() - assert len(all_states) == 1 + assert ( + len(hass.states.async_entity_ids("geo_location")) + + len(hass.states.async_entity_ids("sensor")) + == 1 + ) state = hass.states.get("sensor.gdacs_32_87336_117_22743") attributes = state.attributes diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index 8646eac19a2..44d4e954d28 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -114,13 +114,11 @@ BEACON_EXIT_CAR = { @pytest.fixture(autouse=True) def mock_dev_track(mock_device_tracker_conf): """Mock device tracker config loading.""" - pass @pytest.fixture async def geofency_client(loop, hass, hass_client_no_auth): """Geofency mock client (unauthenticated).""" - assert await async_setup_component(hass, "persistent_notification", {}) assert await async_setup_component( hass, DOMAIN, {DOMAIN: {CONF_MOBILE_BEACONS: ["Car 1"]}} diff --git a/tests/components/geonetnz_quakes/test_geo_location.py b/tests/components/geonetnz_quakes/test_geo_location.py index 9c700c2b38e..0690da5bf7b 100644 --- a/tests/components/geonetnz_quakes/test_geo_location.py +++ b/tests/components/geonetnz_quakes/test_geo_location.py @@ -72,9 +72,12 @@ async def test_setup(hass): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - all_states = hass.states.async_all() # 3 geolocation and 1 sensor entities - assert len(all_states) == 4 + assert ( + len(hass.states.async_entity_ids("geo_location")) + + len(hass.states.async_entity_ids("sensor")) + == 4 + ) entity_registry = er.async_get(hass) assert len(entity_registry.entities) == 4 @@ -136,25 +139,32 @@ async def test_setup(hass): async_fire_time_changed(hass, utcnow + DEFAULT_SCAN_INTERVAL) await hass.async_block_till_done() - all_states = hass.states.async_all() - assert len(all_states) == 4 - + assert ( + len(hass.states.async_entity_ids("geo_location")) + + len(hass.states.async_entity_ids("sensor")) + == 4 + ) # Simulate an update - empty data, but successful update, # so no changes to entities. mock_feed_update.return_value = "OK_NO_DATA", None async_fire_time_changed(hass, utcnow + 2 * DEFAULT_SCAN_INTERVAL) await hass.async_block_till_done() - all_states = hass.states.async_all() - assert len(all_states) == 4 - + assert ( + len(hass.states.async_entity_ids("geo_location")) + + len(hass.states.async_entity_ids("sensor")) + == 4 + ) # Simulate an update - empty data, removes all entities mock_feed_update.return_value = "ERROR", None async_fire_time_changed(hass, utcnow + 3 * DEFAULT_SCAN_INTERVAL) await hass.async_block_till_done() - all_states = hass.states.async_all() - assert len(all_states) == 1 + assert ( + len(hass.states.async_entity_ids("geo_location")) + + len(hass.states.async_entity_ids("sensor")) + == 1 + ) assert len(entity_registry.entities) == 1 @@ -178,8 +188,11 @@ async def test_setup_imperial(hass): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - all_states = hass.states.async_all() - assert len(all_states) == 2 + assert ( + len(hass.states.async_entity_ids("geo_location")) + + len(hass.states.async_entity_ids("sensor")) + == 2 + ) # Test conversion of 200 miles to kilometers. feeds = hass.data[DOMAIN][FEED] diff --git a/tests/components/geonetnz_quakes/test_sensor.py b/tests/components/geonetnz_quakes/test_sensor.py index 8226fd91898..c1746565854 100644 --- a/tests/components/geonetnz_quakes/test_sensor.py +++ b/tests/components/geonetnz_quakes/test_sensor.py @@ -62,9 +62,12 @@ async def test_setup(hass, legacy_patchable_time): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - all_states = hass.states.async_all() # 3 geolocation and 1 sensor entities - assert len(all_states) == 4 + assert ( + len(hass.states.async_entity_ids("geo_location")) + + len(hass.states.async_entity_ids("sensor")) + == 4 + ) state = hass.states.get("sensor.geonet_nz_quakes_32_87336_117_22743") assert state is not None @@ -84,8 +87,11 @@ async def test_setup(hass, legacy_patchable_time): async_fire_time_changed(hass, utcnow + DEFAULT_SCAN_INTERVAL) await hass.async_block_till_done() - all_states = hass.states.async_all() - assert len(all_states) == 4 + assert ( + len(hass.states.async_entity_ids("geo_location")) + + len(hass.states.async_entity_ids("sensor")) + == 4 + ) state = hass.states.get("sensor.geonet_nz_quakes_32_87336_117_22743") attributes = state.attributes @@ -99,16 +105,22 @@ async def test_setup(hass, legacy_patchable_time): async_fire_time_changed(hass, utcnow + 2 * DEFAULT_SCAN_INTERVAL) await hass.async_block_till_done() - all_states = hass.states.async_all() - assert len(all_states) == 4 + assert ( + len(hass.states.async_entity_ids("geo_location")) + + len(hass.states.async_entity_ids("sensor")) + == 4 + ) # Simulate an update - empty data, removes all entities mock_feed_update.return_value = "ERROR", None async_fire_time_changed(hass, utcnow + 3 * DEFAULT_SCAN_INTERVAL) await hass.async_block_till_done() - all_states = hass.states.async_all() - assert len(all_states) == 1 + assert ( + len(hass.states.async_entity_ids("geo_location")) + + len(hass.states.async_entity_ids("sensor")) + == 1 + ) state = hass.states.get("sensor.geonet_nz_quakes_32_87336_117_22743") attributes = state.attributes diff --git a/tests/components/geonetnz_volcano/test_sensor.py b/tests/components/geonetnz_volcano/test_sensor.py index 824fc059ace..f12887ad761 100644 --- a/tests/components/geonetnz_volcano/test_sensor.py +++ b/tests/components/geonetnz_volcano/test_sensor.py @@ -57,9 +57,12 @@ async def test_setup(hass, legacy_patchable_time): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - all_states = hass.states.async_all() # 3 sensor entities - assert len(all_states) == 3 + assert ( + len(hass.states.async_entity_ids("geo_location")) + + len(hass.states.async_entity_ids("sensor")) + == 3 + ) state = hass.states.get("sensor.volcano_title_1") assert state is not None @@ -101,8 +104,11 @@ async def test_setup(hass, legacy_patchable_time): async_fire_time_changed(hass, utcnow + DEFAULT_SCAN_INTERVAL) await hass.async_block_till_done() - all_states = hass.states.async_all() - assert len(all_states) == 4 + assert ( + len(hass.states.async_entity_ids("geo_location")) + + len(hass.states.async_entity_ids("sensor")) + == 4 + ) # Simulate an update - empty data, but successful update, # so no changes to entities. @@ -110,24 +116,33 @@ async def test_setup(hass, legacy_patchable_time): async_fire_time_changed(hass, utcnow + 2 * DEFAULT_SCAN_INTERVAL) await hass.async_block_till_done() - all_states = hass.states.async_all() - assert len(all_states) == 4 + assert ( + len(hass.states.async_entity_ids("geo_location")) + + len(hass.states.async_entity_ids("sensor")) + == 4 + ) # Simulate an update - empty data, keep all entities mock_feed_update.return_value = "ERROR", None async_fire_time_changed(hass, utcnow + 3 * DEFAULT_SCAN_INTERVAL) await hass.async_block_till_done() - all_states = hass.states.async_all() - assert len(all_states) == 4 + assert ( + len(hass.states.async_entity_ids("geo_location")) + + len(hass.states.async_entity_ids("sensor")) + == 4 + ) # Simulate an update - regular data for 3 entries mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_2, mock_entry_3] async_fire_time_changed(hass, utcnow + 4 * DEFAULT_SCAN_INTERVAL) await hass.async_block_till_done() - all_states = hass.states.async_all() - assert len(all_states) == 4 + assert ( + len(hass.states.async_entity_ids("geo_location")) + + len(hass.states.async_entity_ids("sensor")) + == 4 + ) async def test_setup_imperial(hass): @@ -149,8 +164,11 @@ async def test_setup_imperial(hass): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - all_states = hass.states.async_all() - assert len(all_states) == 1 + assert ( + len(hass.states.async_entity_ids("geo_location")) + + len(hass.states.async_entity_ids("sensor")) + == 1 + ) # Test conversion of 200 miles to kilometers. assert mock_feed_init.call_args[1].get("filter_radius") == 321.8688 diff --git a/tests/components/goalzero/test_config_flow.py b/tests/components/goalzero/test_config_flow.py index 6df5465eff9..838a0f8a124 100644 --- a/tests/components/goalzero/test_config_flow.py +++ b/tests/components/goalzero/test_config_flow.py @@ -10,7 +10,6 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM, ) -from homeassistant.setup import async_setup_component from . import ( CONF_CONFIG_FLOW, @@ -120,7 +119,7 @@ async def test_flow_user_unknown_error(hass): async def test_dhcp_discovery(hass): """Test we can process the discovery from dhcp.""" - await async_setup_component(hass, "persistent_notification", {}) + mocked_yeti = await _create_mocked_yeti() with _patch_config_flow_yeti(mocked_yeti), _patch_setup(): result = await hass.config_entries.flow.async_init( diff --git a/tests/components/gogogate2/test_config_flow.py b/tests/components/gogogate2/test_config_flow.py index 0722874e9e5..18ed7334b8d 100644 --- a/tests/components/gogogate2/test_config_flow.py +++ b/tests/components/gogogate2/test_config_flow.py @@ -5,7 +5,7 @@ from ismartgate import GogoGate2Api, ISmartGateApi from ismartgate.common import ApiError from ismartgate.const import GogoGate2ApiErrorCode -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.gogogate2.const import ( DEVICE_TYPE_GOGOGATE2, DEVICE_TYPE_ISMARTGATE, @@ -102,7 +102,6 @@ async def test_auth_fail( async def test_form_homekit_unique_id_already_setup(hass): """Test that we abort from homekit if gogogate2 is already setup.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, @@ -134,7 +133,6 @@ async def test_form_homekit_unique_id_already_setup(hass): async def test_form_homekit_ip_address_already_setup(hass): """Test that we abort from homekit if gogogate2 is already setup.""" - await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry( domain=DOMAIN, @@ -152,7 +150,6 @@ async def test_form_homekit_ip_address_already_setup(hass): async def test_form_homekit_ip_address(hass): """Test homekit includes the defaults ip address.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, @@ -181,7 +178,6 @@ async def test_discovered_dhcp( ismartgateapi_mock.return_value = api api.reset_mock() - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, @@ -227,7 +223,6 @@ async def test_discovered_dhcp( async def test_discovered_by_homekit_and_dhcp(hass): """Test we get the form with homekit and abort for dhcp source when we get both.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/gpslogger/test_init.py b/tests/components/gpslogger/test_init.py index 4305b8d5642..dc9a3720709 100644 --- a/tests/components/gpslogger/test_init.py +++ b/tests/components/gpslogger/test_init.py @@ -27,13 +27,11 @@ HOME_LONGITUDE = -115.815811 @pytest.fixture(autouse=True) def mock_dev_track(mock_device_tracker_conf): """Mock device tracker config loading.""" - pass @pytest.fixture async def gpslogger_client(loop, hass, hass_client_no_auth): """Mock client for GPSLogger (unauthenticated).""" - assert await async_setup_component(hass, "persistent_notification", {}) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) diff --git a/tests/components/habitica/test_config_flow.py b/tests/components/habitica/test_config_flow.py index d02a9031d63..06eafd3262a 100644 --- a/tests/components/habitica/test_config_flow.py +++ b/tests/components/habitica/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from aiohttp import ClientResponseError -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.habitica.const import DEFAULT_URL, DOMAIN from tests.common import MockConfigEntry @@ -11,7 +11,7 @@ from tests.common import MockConfigEntry async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py index d81adabb916..be0d78242ac 100644 --- a/tests/components/harmony/test_config_flow.py +++ b/tests/components/harmony/test_config_flow.py @@ -1,7 +1,7 @@ """Test the Logitech Harmony Hub config flow.""" from unittest.mock import AsyncMock, MagicMock, patch -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.harmony.config_flow import CannotConnect from homeassistant.components.harmony.const import DOMAIN, PREVIOUS_ACTIVE_ACTIVITY from homeassistant.const import CONF_HOST, CONF_NAME @@ -19,7 +19,7 @@ def _get_mock_harmonyapi(connect=None, close=None): async def test_user_form(hass): """Test we get the user form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -48,7 +48,6 @@ async def test_user_form(hass): async def test_form_ssdp(hass): """Test we get the form with ssdp source.""" - await setup.async_setup_component(hass, "persistent_notification", {}) harmonyapi = _get_mock_harmonyapi(connect=True) @@ -97,7 +96,7 @@ async def test_form_ssdp(hass): async def test_form_ssdp_aborts_before_checking_remoteid_if_host_known(hass): """Test we abort without connecting if the host is already known.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + config_entry = MockConfigEntry( domain=DOMAIN, data={"host": "2.2.2.2", "name": "any"}, diff --git a/tests/components/hassio/test_config_flow.py b/tests/components/hassio/test_config_flow.py index 2b4b8a88914..be773db6155 100644 --- a/tests/components/hassio/test_config_flow.py +++ b/tests/components/hassio/test_config_flow.py @@ -1,13 +1,12 @@ """Test the Home Assistant Supervisor config flow.""" from unittest.mock import patch -from homeassistant import setup from homeassistant.components.hassio import DOMAIN async def test_config_flow(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( "homeassistant.components.hassio.async_setup", return_value=True ) as mock_setup, patch( diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index 3e5b2aeaaed..ce0c2d9ca6d 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -786,7 +786,6 @@ async def test_location_device_tracker_added_after_update( await hass.async_block_till_done() sensor = hass.states.get("sensor.test") - assert len(caplog.records) == 2 assert "Unable to find entity" in caplog.text caplog.clear() @@ -908,7 +907,6 @@ async def test_pattern_origin(hass, caplog): } assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() - assert len(caplog.records) == 2 assert "invalid latitude" in caplog.text @@ -928,7 +926,6 @@ async def test_pattern_destination(hass, caplog): } assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() - assert len(caplog.records) == 2 assert "invalid latitude" in caplog.text @@ -1179,7 +1176,6 @@ async def test_arrival_only_allowed_for_timetable(hass, caplog): } assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() - assert len(caplog.records) == 2 assert "[arrival] is an invalid option" in caplog.text @@ -1204,5 +1200,4 @@ async def test_exclusive_arrival_and_departure(hass, caplog): } assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() - assert len(caplog.records) == 2 assert "two or more values in the same group of exclusion" in caplog.text diff --git a/tests/components/hive/test_config_flow.py b/tests/components/hive/test_config_flow.py index dae69eebd96..ce13e52fe96 100644 --- a/tests/components/hive/test_config_flow.py +++ b/tests/components/hive/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch from apyhiveapi.helper import hive_exceptions -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.hive.const import CONF_CODE, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME @@ -181,7 +181,6 @@ async def test_user_flow_2fa(hass): async def test_reauth_flow(hass): """Test the reauth flow.""" - await setup.async_setup_component(hass, "persistent_notification", {}) mock_config = MockConfigEntry( domain=DOMAIN, unique_id=USERNAME, diff --git a/tests/components/hlk_sw16/test_config_flow.py b/tests/components/hlk_sw16/test_config_flow.py index 7a57bc20417..a325295f480 100644 --- a/tests/components/hlk_sw16/test_config_flow.py +++ b/tests/components/hlk_sw16/test_config_flow.py @@ -2,7 +2,7 @@ import asyncio from unittest.mock import patch -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.hlk_sw16.const import DOMAIN @@ -49,7 +49,7 @@ async def create_mock_hlk_sw16_connection(fail): async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -110,7 +110,7 @@ async def test_form(hass): async def test_import(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT} ) diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index 3cbe49f664b..7fa40b00f9c 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.homekit.const import DOMAIN, SHORT_BRIDGE_NAME from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_IMPORT from homeassistant.const import CONF_NAME, CONF_PORT @@ -35,7 +35,7 @@ def _mock_config_entry_with_options_populated(): async def test_setup_in_bridge_mode(hass, mock_get_source_ip): """Test we can setup a new instance in bridge mode.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -85,7 +85,6 @@ async def test_setup_in_bridge_mode(hass, mock_get_source_ip): async def test_setup_in_bridge_mode_name_taken(hass, mock_get_source_ip): """Test we can setup a new instance in bridge mode when the name is taken.""" - await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry( domain=DOMAIN, @@ -175,7 +174,6 @@ async def test_setup_creates_entries_for_accessory_mode_devices( ) accessory_mode_entry.add_to_hass(hass) - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -235,7 +233,6 @@ async def test_setup_creates_entries_for_accessory_mode_devices( async def test_import(hass, mock_get_source_ip): """Test we can import instance.""" - await setup.async_setup_component(hass, "persistent_notification", {}) ignored_entry = MockConfigEntry(domain=DOMAIN, data={}, source=SOURCE_IGNORE) ignored_entry.add_to_hass(hass) @@ -379,7 +376,6 @@ async def test_options_flow_devices( demo_config_entry = MockConfigEntry(domain="domain") demo_config_entry.add_to_hass(hass) - assert await async_setup_component(hass, "persistent_notification", {}) assert await async_setup_component(hass, "demo", {"demo": {}}) assert await async_setup_component(hass, "homekit", {"homekit": {}}) @@ -460,7 +456,6 @@ async def test_options_flow_devices_preserved_when_advanced_off( demo_config_entry = MockConfigEntry(domain="domain") demo_config_entry.add_to_hass(hass) - assert await async_setup_component(hass, "persistent_notification", {}) assert await async_setup_component(hass, "homekit", {"homekit": {}}) hass.states.async_set("climate.old", "off") @@ -1006,7 +1001,7 @@ async def test_options_flow_include_mode_basic_accessory(hass, mock_get_source_i async def test_converting_bridge_to_accessory_mode(hass, hk_driver, mock_get_source_ip): """Test we can convert a bridge to accessory mode.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 039bd1c11c3..fbb715f1c39 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -147,7 +147,7 @@ def _mock_pyhap_bridge(): async def test_setup_min(hass, mock_zeroconf): """Test async_setup with min config options.""" - await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_PORT}, @@ -186,7 +186,7 @@ async def test_setup_min(hass, mock_zeroconf): async def test_setup_auto_start_disabled(hass, mock_zeroconf): """Test async_setup with auto start disabled and test service calls.""" - await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "Test Name", CONF_PORT: 11111, CONF_IP_ADDRESS: "172.0.0.0"}, @@ -370,7 +370,7 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_zeroconf): async def test_homekit_add_accessory(hass, mock_zeroconf): """Add accessory if config exists and get_acc returns an accessory.""" - await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} ) @@ -408,7 +408,7 @@ async def test_homekit_warn_add_accessory_bridge( hass, acc_category, mock_zeroconf, caplog ): """Test we warn when adding cameras or tvs to a bridge.""" - await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} ) @@ -673,7 +673,7 @@ async def test_homekit_stop(hass): async def test_homekit_reset_accessories(hass, mock_zeroconf): """Test resetting HomeKit accessories.""" - await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} ) @@ -718,7 +718,7 @@ async def test_homekit_reset_accessories(hass, mock_zeroconf): async def test_homekit_unpair(hass, device_reg, mock_zeroconf): """Test unpairing HomeKit accessories.""" - await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} ) @@ -760,7 +760,7 @@ async def test_homekit_unpair(hass, device_reg, mock_zeroconf): async def test_homekit_unpair_missing_device_id(hass, device_reg, mock_zeroconf): """Test unpairing HomeKit accessories with invalid device id.""" - await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} ) @@ -798,7 +798,7 @@ async def test_homekit_unpair_missing_device_id(hass, device_reg, mock_zeroconf) async def test_homekit_unpair_not_homekit_device(hass, device_reg, mock_zeroconf): """Test unpairing HomeKit accessories with a non-homekit device id.""" - await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} ) @@ -846,7 +846,7 @@ async def test_homekit_unpair_not_homekit_device(hass, device_reg, mock_zeroconf async def test_homekit_reset_accessories_not_supported(hass, mock_zeroconf): """Test resetting HomeKit accessories with an unsupported entity.""" - await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} ) @@ -890,7 +890,7 @@ async def test_homekit_reset_accessories_not_supported(hass, mock_zeroconf): async def test_homekit_reset_accessories_state_missing(hass, mock_zeroconf): """Test resetting HomeKit accessories when the state goes missing.""" - await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} ) @@ -932,7 +932,7 @@ async def test_homekit_reset_accessories_state_missing(hass, mock_zeroconf): async def test_homekit_reset_accessories_not_bridged(hass, mock_zeroconf): """Test resetting HomeKit accessories when the state is not bridged.""" - await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} ) @@ -974,7 +974,7 @@ async def test_homekit_reset_accessories_not_bridged(hass, mock_zeroconf): async def test_homekit_reset_single_accessory(hass, mock_zeroconf): """Test resetting HomeKit single accessory.""" - await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} ) @@ -1013,7 +1013,7 @@ async def test_homekit_reset_single_accessory(hass, mock_zeroconf): async def test_homekit_reset_single_accessory_unsupported(hass, mock_zeroconf): """Test resetting HomeKit single accessory with an unsupported entity.""" - await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} ) @@ -1050,7 +1050,7 @@ async def test_homekit_reset_single_accessory_unsupported(hass, mock_zeroconf): async def test_homekit_reset_single_accessory_state_missing(hass, mock_zeroconf): """Test resetting HomeKit single accessory when the state goes missing.""" - await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} ) @@ -1086,7 +1086,7 @@ async def test_homekit_reset_single_accessory_state_missing(hass, mock_zeroconf) async def test_homekit_reset_single_accessory_no_match(hass, mock_zeroconf): """Test resetting HomeKit single accessory when the entity id does not match.""" - await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} ) @@ -1292,7 +1292,7 @@ async def test_homekit_async_get_integration_fails( async def test_yaml_updates_update_config_entry_for_name(hass, mock_zeroconf): """Test async_setup with imported config.""" - await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=DOMAIN, source=SOURCE_IMPORT, @@ -1369,7 +1369,7 @@ async def test_homekit_ignored_missing_devices( hass, hk_driver, device_reg, entity_reg, mock_zeroconf ): """Test HomeKit handles a device in the entity registry but missing from the device registry.""" - await async_setup_component(hass, "persistent_notification", {}) + entry = await async_init_integration(hass) homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE) @@ -1564,7 +1564,7 @@ async def test_homekit_finds_linked_humidity_sensors( async def test_reload(hass, mock_zeroconf): """Test we can reload from yaml.""" - await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=DOMAIN, source=SOURCE_IMPORT, @@ -1729,7 +1729,7 @@ async def test_homekit_start_in_accessory_mode_missing_entity( async def test_wait_for_port_to_free(hass, hk_driver, mock_zeroconf, caplog): """Test we wait for the port to free before declaring unload success.""" - await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_PORT}, diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 94936e3e2c2..0bb3d2053d4 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -39,11 +39,7 @@ from homeassistant.components.homekit.util import ( validate_entity_config as vec, validate_media_player_features, ) -from homeassistant.components.persistent_notification import ( - ATTR_MESSAGE, - ATTR_NOTIFICATION_ID, - DOMAIN as PERSISTENT_NOTIFICATION_DOMAIN, -) +from homeassistant.components.persistent_notification import create, dismiss from homeassistant.const import ( ATTR_CODE, ATTR_SUPPORTED_FEATURES, @@ -58,7 +54,7 @@ from homeassistant.core import State from .util import async_init_integration -from tests.common import MockConfigEntry, async_mock_service +from tests.common import MockConfigEntry def _mock_socket(failure_attempts: int = 0) -> MagicMock: @@ -242,33 +238,31 @@ async def test_show_setup_msg(hass, hk_driver, mock_get_source_ip): entry = await async_init_integration(hass) assert entry - call_create_notification = async_mock_service( - hass, PERSISTENT_NOTIFICATION_DOMAIN, "create" - ) - - await hass.async_add_executor_job( - show_setup_message, hass, entry.entry_id, "bridge_name", pincode, "X-HM://0" - ) - await hass.async_block_till_done() + with patch( + "homeassistant.components.persistent_notification.create", side_effect=create + ) as mock_create: + await hass.async_add_executor_job( + show_setup_message, hass, entry.entry_id, "bridge_name", pincode, "X-HM://0" + ) + await hass.async_block_till_done() assert hass.data[DOMAIN][entry.entry_id][HOMEKIT_PAIRING_QR_SECRET] assert hass.data[DOMAIN][entry.entry_id][HOMEKIT_PAIRING_QR] - assert call_create_notification - assert call_create_notification[0].data[ATTR_NOTIFICATION_ID] == entry.entry_id - assert pincode.decode() in call_create_notification[0].data[ATTR_MESSAGE] + assert len(mock_create.mock_calls) == 1 + assert mock_create.mock_calls[0][1][3] == entry.entry_id + assert pincode.decode() in mock_create.mock_calls[0][1][1] async def test_dismiss_setup_msg(hass): """Test dismiss setup message.""" - call_dismiss_notification = async_mock_service( - hass, PERSISTENT_NOTIFICATION_DOMAIN, "dismiss" - ) + with patch( + "homeassistant.components.persistent_notification.dismiss", side_effect=dismiss + ) as mock_dismiss: + await hass.async_add_executor_job(dismiss_setup_message, hass, "entry_id") + await hass.async_block_till_done() - await hass.async_add_executor_job(dismiss_setup_message, hass, "entry_id") - await hass.async_block_till_done() - - assert call_dismiss_notification - assert call_dismiss_notification[0].data[ATTR_NOTIFICATION_ID] == "entry_id" + assert len(mock_dismiss.mock_calls) == 1 + assert mock_dismiss.mock_calls[0][1][1] == "entry_id" async def test_port_is_available(hass): diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index e3c2abf2293..ed3c2ad23af 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -24,8 +24,6 @@ from homeassistant.setup import async_setup_component from . import mock_real_ip -from tests.common import async_mock_service - SUPERVISOR_IP = "1.2.3.4" BANNED_IPS = ["200.201.202.203", "100.64.0.2"] BANNED_IPS_WITH_SUPERVISOR = BANNED_IPS + [SUPERVISOR_IP] @@ -136,8 +134,6 @@ async def test_ban_middleware_loaded_by_default(hass): async def test_ip_bans_file_creation(hass, aiohttp_client): """Testing if banned IP file created.""" - notification_calls = async_mock_service(hass, "persistent_notification", "create") - app = web.Application() app["hass"] = hass @@ -174,9 +170,11 @@ async def test_ip_bans_file_creation(hass, aiohttp_client): assert resp.status == HTTP_FORBIDDEN assert m_open.call_count == 1 - assert len(notification_calls) == 3 assert ( - notification_calls[0].data["message"] + len(notifications := hass.states.async_all("persistent_notification")) == 2 + ) + assert ( + notifications[0].attributes["message"] == "Login attempt or request with invalid authentication from example.com (200.201.202.204). See the log for details." ) diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index afc920a6667..05a7ade6948 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -108,7 +108,7 @@ async def test_fixing_unique_id_other_correct(hass, mock_bridge_setup): async def test_security_vuln_check(hass): """Test that we report security vulnerabilities.""" - assert await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry(domain=hue.DOMAIN, data={"host": "0.0.0.0"}) entry.add_to_hass(hass) diff --git a/tests/components/huisbaasje/test_config_flow.py b/tests/components/huisbaasje/test_config_flow.py index 5da159b282c..4b0ae908a2d 100644 --- a/tests/components/huisbaasje/test_config_flow.py +++ b/tests/components/huisbaasje/test_config_flow.py @@ -1,7 +1,7 @@ """Test the Huisbaasje config flow.""" from unittest.mock import patch -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.huisbaasje.config_flow import ( HuisbaasjeConnectionException, HuisbaasjeException, @@ -13,7 +13,7 @@ from tests.common import MockConfigEntry async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/hunterdouglas_powerview/test_config_flow.py b/tests/components/hunterdouglas_powerview/test_config_flow.py index 6423daff3f8..e3cb9aa4c8b 100644 --- a/tests/components/hunterdouglas_powerview/test_config_flow.py +++ b/tests/components/hunterdouglas_powerview/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.hunterdouglas_powerview.const import DOMAIN from tests.common import MockConfigEntry, load_fixture @@ -73,7 +73,7 @@ def _get_mock_powerview_fwversion(fwversion=None, get_resources=None): async def test_user_form(hass): """Test we get the user form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -116,7 +116,7 @@ async def test_user_form(hass): async def test_user_form_legacy(hass): """Test we get the user form with a legacy device.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -164,7 +164,6 @@ async def test_user_form_legacy(hass): @pytest.mark.parametrize("source, discovery_info", DISCOVERY_DATA) async def test_form_homekit_and_dhcp_cannot_connect(hass, source, discovery_info): """Test we get the form with homekit and dhcp source.""" - await setup.async_setup_component(hass, "persistent_notification", {}) ignored_config_entry = MockConfigEntry( domain=DOMAIN, data={}, source=config_entries.SOURCE_IGNORE @@ -191,7 +190,6 @@ async def test_form_homekit_and_dhcp_cannot_connect(hass, source, discovery_info @pytest.mark.parametrize("source, discovery_info", DISCOVERY_DATA) async def test_form_homekit_and_dhcp(hass, source, discovery_info): """Test we get the form with homekit and dhcp source.""" - await setup.async_setup_component(hass, "persistent_notification", {}) ignored_config_entry = MockConfigEntry( domain=DOMAIN, data={}, source=config_entries.SOURCE_IGNORE @@ -244,7 +242,6 @@ async def test_form_homekit_and_dhcp(hass, source, discovery_info): async def test_discovered_by_homekit_and_dhcp(hass): """Test we get the form with homekit and abort for dhcp source when we get both.""" - await setup.async_setup_component(hass, "persistent_notification", {}) mock_powerview_userdata = _get_mock_powerview_userdata() with patch( diff --git a/tests/components/ialarm/test_config_flow.py b/tests/components/ialarm/test_config_flow.py index 54da9a18b1a..102b2edb0a6 100644 --- a/tests/components/ialarm/test_config_flow.py +++ b/tests/components/ialarm/test_config_flow.py @@ -1,7 +1,7 @@ """Test the Antifurto365 iAlarm config flow.""" from unittest.mock import patch -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.ialarm.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PORT @@ -14,7 +14,7 @@ TEST_MAC = "00:00:54:12:34:56" async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/insteon/test_config_flow.py b/tests/components/insteon/test_config_flow.py index 172cb6936b3..3fb1cb2d48b 100644 --- a/tests/components/insteon/test_config_flow.py +++ b/tests/components/insteon/test_config_flow.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.insteon.config_flow import ( HUB1, HUB2, @@ -95,7 +95,7 @@ async def _device_form(hass, flow_id, connection, user_input): async def test_form_select_modem(hass: HomeAssistant): """Test we get a modem form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await _init_form(hass, HUB2) assert result["step_id"] == STEP_HUB_V2 assert result["type"] == "form" @@ -123,7 +123,7 @@ async def test_fail_on_existing(hass: HomeAssistant): async def test_form_select_plm(hass: HomeAssistant): """Test we set up the PLM correctly.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await _init_form(hass, PLM) result2, mock_setup, mock_setup_entry = await _device_form( @@ -138,7 +138,7 @@ async def test_form_select_plm(hass: HomeAssistant): async def test_form_select_hub_v1(hass: HomeAssistant): """Test we set up the Hub v1 correctly.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await _init_form(hass, HUB1) result2, mock_setup, mock_setup_entry = await _device_form( @@ -156,7 +156,7 @@ async def test_form_select_hub_v1(hass: HomeAssistant): async def test_form_select_hub_v2(hass: HomeAssistant): """Test we set up the Hub v2 correctly.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await _init_form(hass, HUB2) result2, mock_setup, mock_setup_entry = await _device_form( @@ -174,7 +174,7 @@ async def test_form_select_hub_v2(hass: HomeAssistant): async def test_failed_connection_plm(hass: HomeAssistant): """Test a failed connection with the PLM.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await _init_form(hass, PLM) result2, _, _ = await _device_form( @@ -186,7 +186,7 @@ async def test_failed_connection_plm(hass: HomeAssistant): async def test_failed_connection_hub(hass: HomeAssistant): """Test a failed connection with a Hub.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await _init_form(hass, HUB2) result2, _, _ = await _device_form( @@ -208,7 +208,6 @@ async def _import_config(hass, config): async def test_import_plm(hass: HomeAssistant): """Test importing a minimum PLM config from yaml.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await _import_config(hass, MOCK_IMPORT_CONFIG_PLM) @@ -235,7 +234,6 @@ async def _options_init_form(hass, entry_id, step): async def test_import_min_hub_v2(hass: HomeAssistant): """Test importing a minimum Hub v2 config from yaml.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await _import_config( hass, {**MOCK_IMPORT_MINIMUM_HUB_V2, CONF_PORT: 25105, CONF_HUB_VERSION: 2} @@ -253,7 +251,6 @@ async def test_import_min_hub_v2(hass: HomeAssistant): async def test_import_min_hub_v1(hass: HomeAssistant): """Test importing a minimum Hub v1 config from yaml.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await _import_config( hass, {**MOCK_IMPORT_MINIMUM_HUB_V1, CONF_PORT: 9761, CONF_HUB_VERSION: 1} @@ -287,7 +284,6 @@ async def test_import_existing(hass: HomeAssistant): async def test_import_failed_connection(hass: HomeAssistant): """Test a failed connection on import.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch(PATCH_CONNECTION, new=mock_failed_connection,), patch( PATCH_ASYNC_SETUP, return_value=True diff --git a/tests/components/iotawatt/test_config_flow.py b/tests/components/iotawatt/test_config_flow.py index e028f365431..6adb3534b15 100644 --- a/tests/components/iotawatt/test_config_flow.py +++ b/tests/components/iotawatt/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch import httpx -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.iotawatt.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM @@ -11,7 +11,7 @@ from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_ async def test_form(hass: HomeAssistant) -> None: """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -43,7 +43,6 @@ async def test_form(hass: HomeAssistant) -> None: async def test_form_auth(hass: HomeAssistant) -> None: """Test we handle auth.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/isy994/test_config_flow.py b/tests/components/isy994/test_config_flow.py index 09e6e26e777..62a779293be 100644 --- a/tests/components/isy994/test_config_flow.py +++ b/tests/components/isy994/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch from pyisy import ISYConnectionError, ISYInvalidAuthError -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components import dhcp, ssdp from homeassistant.components.isy994.const import ( CONF_IGNORE_STRING, @@ -92,7 +92,7 @@ PATCH_ASYNC_SETUP_ENTRY = f"{INTEGRATION}.async_setup_entry" async def test_form(hass: HomeAssistant): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -233,7 +233,7 @@ async def test_form_no_name_in_response(hass: HomeAssistant): async def test_form_existing_config_entry(hass: HomeAssistant): """Test if config entry already exists.""" MockConfigEntry(domain=DOMAIN, unique_id=MOCK_UUID).add_to_hass(hass) - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -316,7 +316,6 @@ async def test_import_flow_all_fields(hass: HomeAssistant) -> None: async def test_form_ssdp_already_configured(hass: HomeAssistant) -> None: """Test ssdp abort when the serial number is already configured.""" - await setup.async_setup_component(hass, "persistent_notification", {}) MockConfigEntry( domain=DOMAIN, @@ -338,7 +337,6 @@ async def test_form_ssdp_already_configured(hass: HomeAssistant) -> None: async def test_form_ssdp(hass: HomeAssistant): """Test we can setup from ssdp.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, @@ -375,7 +373,7 @@ async def test_form_ssdp(hass: HomeAssistant): async def test_form_ssdp_existing_entry(hass: HomeAssistant): """Test we update the ip of an existing entry from ssdp.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: f"http://{MOCK_HOSTNAME}{ISY_URL_POSTFIX}"}, @@ -402,7 +400,7 @@ async def test_form_ssdp_existing_entry(hass: HomeAssistant): async def test_form_ssdp_existing_entry_with_no_port(hass: HomeAssistant): """Test we update the ip of an existing entry from ssdp with no port.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: f"http://{MOCK_HOSTNAME}:1443/{ISY_URL_POSTFIX}"}, @@ -429,7 +427,7 @@ async def test_form_ssdp_existing_entry_with_no_port(hass: HomeAssistant): async def test_form_ssdp_existing_entry_with_alternate_port(hass: HomeAssistant): """Test we update the ip of an existing entry from ssdp with an alternate port.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: f"http://{MOCK_HOSTNAME}:1443/{ISY_URL_POSTFIX}"}, @@ -456,7 +454,7 @@ async def test_form_ssdp_existing_entry_with_alternate_port(hass: HomeAssistant) async def test_form_ssdp_existing_entry_no_port_https(hass: HomeAssistant): """Test we update the ip of an existing entry from ssdp with no port and https.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: f"https://{MOCK_HOSTNAME}/{ISY_URL_POSTFIX}"}, @@ -483,7 +481,6 @@ async def test_form_ssdp_existing_entry_no_port_https(hass: HomeAssistant): async def test_form_dhcp(hass: HomeAssistant): """Test we can setup from dhcp.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, @@ -520,7 +517,7 @@ async def test_form_dhcp(hass: HomeAssistant): async def test_form_dhcp_existing_entry(hass: HomeAssistant): """Test we update the ip of an existing entry from dhcp.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: f"http://{MOCK_HOSTNAME}{ISY_URL_POSTFIX}"}, @@ -547,7 +544,7 @@ async def test_form_dhcp_existing_entry(hass: HomeAssistant): async def test_form_dhcp_existing_entry_preserves_port(hass: HomeAssistant): """Test we update the ip of an existing entry from dhcp preserves port.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=DOMAIN, data={ diff --git a/tests/components/juicenet/test_config_flow.py b/tests/components/juicenet/test_config_flow.py index 817ae04aa79..abda068b622 100644 --- a/tests/components/juicenet/test_config_flow.py +++ b/tests/components/juicenet/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch import aiohttp from pyjuicenet import TokenError -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.juicenet.const import DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN @@ -17,7 +17,7 @@ def _mock_juicenet_return_value(get_devices=None): async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/kmtronic/test_config_flow.py b/tests/components/kmtronic/test_config_flow.py index b2f0f4b0515..510157ad9dd 100644 --- a/tests/components/kmtronic/test_config_flow.py +++ b/tests/components/kmtronic/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import Mock, patch from aiohttp import ClientConnectorError, ClientResponseError -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.kmtronic.const import CONF_REVERSE, DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -12,7 +12,7 @@ from tests.common import MockConfigEntry async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/kostal_plenticore/test_config_flow.py b/tests/components/kostal_plenticore/test_config_flow.py index 2e9b25f5721..17ff8ef3e03 100644 --- a/tests/components/kostal_plenticore/test_config_flow.py +++ b/tests/components/kostal_plenticore/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import ANY, AsyncMock, MagicMock, patch from kostal.plenticore import PlenticoreAuthenticationException -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.kostal_plenticore.const import DOMAIN from tests.common import MockConfigEntry @@ -12,7 +12,7 @@ from tests.common import MockConfigEntry async def test_formx(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/kulersky/test_config_flow.py b/tests/components/kulersky/test_config_flow.py index 75b1326d338..ed27faeddd6 100644 --- a/tests/components/kulersky/test_config_flow.py +++ b/tests/components/kulersky/test_config_flow.py @@ -3,13 +3,13 @@ from unittest.mock import MagicMock, patch import pykulersky -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.kulersky.config_flow import DOMAIN async def test_flow_success(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -41,7 +41,7 @@ async def test_flow_success(hass): async def test_flow_no_devices_found(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -68,7 +68,7 @@ async def test_flow_no_devices_found(hass): async def test_flow_exceptions_caught(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/kulersky/test_light.py b/tests/components/kulersky/test_light.py index f51c79168d7..3315286967d 100644 --- a/tests/components/kulersky/test_light.py +++ b/tests/components/kulersky/test_light.py @@ -5,7 +5,6 @@ import pykulersky import pytest from pytest import approx -from homeassistant import setup from homeassistant.components.kulersky.const import ( DATA_ADDRESSES, DATA_DISCOVERY_SUBSCRIPTION, @@ -44,7 +43,6 @@ async def mock_entry(hass): @pytest.fixture async def mock_light(hass, mock_entry): """Create a mock light entity.""" - await setup.async_setup_component(hass, "persistent_notification", {}) light = MagicMock(spec=pykulersky.Light) light.address = "AA:BB:CC:11:22:33" @@ -102,7 +100,6 @@ async def test_remove_entry_exceptions_caught(hass, mock_light, mock_entry): async def test_update_exception(hass, mock_light): """Test platform setup.""" - await setup.async_setup_component(hass, "persistent_notification", {}) mock_light.get_color.side_effect = pykulersky.PykulerskyException await hass.helpers.entity_component.async_update_entity("light.bedroom") diff --git a/tests/components/lcn/test_config_flow.py b/tests/components/lcn/test_config_flow.py index 325552f62d3..5f83ab27762 100644 --- a/tests/components/lcn/test_config_flow.py +++ b/tests/components/lcn/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch from pypck.connection import PchkAuthenticationError, PchkLicenseError import pytest -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.lcn.const import CONF_DIM_MODE, CONF_SK_NUM_TRIES, DOMAIN from homeassistant.const import ( CONF_HOST, @@ -29,7 +29,7 @@ IMPORT_DATA = { async def test_step_import(hass): """Test for import step.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + with patch("pypck.connection.PchkConnectionManager.async_connect"), patch( "homeassistant.components.lcn.async_setup", return_value=True ), patch("homeassistant.components.lcn.async_setup_entry", return_value=True): @@ -46,7 +46,7 @@ async def test_step_import(hass): async def test_step_import_existing_host(hass): """Test for update of config_entry if imported host already exists.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + # Create config entry and add it to hass mock_data = IMPORT_DATA.copy() mock_data.update({CONF_SK_NUM_TRIES: 3, CONF_DIM_MODE: 50}) @@ -77,7 +77,7 @@ async def test_step_import_existing_host(hass): ) async def test_step_import_error(hass, error, reason): """Test for authentication error is handled correctly.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( "pypck.connection.PchkConnectionManager.async_connect", side_effect=error ): diff --git a/tests/components/lcn/test_init.py b/tests/components/lcn/test_init.py index eef02b681d8..79f0eed4e46 100644 --- a/tests/components/lcn/test_init.py +++ b/tests/components/lcn/test_init.py @@ -11,7 +11,6 @@ from homeassistant import config_entries from homeassistant.components.lcn.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.helpers import entity_registry as er -from homeassistant.setup import async_setup_component from .conftest import MockPchkConnectionManager, init_integration, setup_component @@ -98,7 +97,6 @@ async def test_async_setup_entry_raises_timeout_error(hass, entry): @patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) async def test_async_setup_from_configuration_yaml(hass): """Test a successful setup using data from configuration.yaml.""" - await async_setup_component(hass, "persistent_notification", {}) with patch("homeassistant.components.lcn.async_setup_entry") as async_setup_entry: await setup_component(hass) diff --git a/tests/components/litterrobot/test_config_flow.py b/tests/components/litterrobot/test_config_flow.py index 8d39f7ac9e8..7bfb1321d9e 100644 --- a/tests/components/litterrobot/test_config_flow.py +++ b/tests/components/litterrobot/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components import litterrobot from .common import CONF_USERNAME, CONFIG, DOMAIN @@ -13,7 +13,7 @@ from tests.common import MockConfigEntry async def test_form(hass, mock_account): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/lutron_caseta/test_config_flow.py b/tests/components/lutron_caseta/test_config_flow.py index 36e86778d0a..2697bb363f6 100644 --- a/tests/components/lutron_caseta/test_config_flow.py +++ b/tests/components/lutron_caseta/test_config_flow.py @@ -7,7 +7,7 @@ from pylutron_caseta.pairing import PAIR_CA, PAIR_CERT, PAIR_KEY from pylutron_caseta.smartbridge import Smartbridge import pytest -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.lutron_caseta import DOMAIN import homeassistant.components.lutron_caseta.config_flow as CasetaConfigFlow from homeassistant.components.lutron_caseta.const import ( @@ -195,7 +195,6 @@ async def test_duplicate_bridge_import(hass): async def test_already_configured_with_ignored(hass): """Test ignored entries do not break checking for existing entries.""" - await setup.async_setup_component(hass, "persistent_notification", {}) config_entry = MockConfigEntry( domain=DOMAIN, data={}, source=config_entries.SOURCE_IGNORE @@ -217,7 +216,7 @@ async def test_already_configured_with_ignored(hass): async def test_form_user(hass, tmpdir): """Test we get the form and can pair.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + hass.config.config_dir = await hass.async_add_executor_job( tmpdir.mkdir, "tls_assets" ) @@ -268,7 +267,7 @@ async def test_form_user(hass, tmpdir): async def test_form_user_pairing_fails(hass, tmpdir): """Test we get the form and we handle pairing failure.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + hass.config.config_dir = await hass.async_add_executor_job( tmpdir.mkdir, "tls_assets" ) @@ -313,7 +312,7 @@ async def test_form_user_pairing_fails(hass, tmpdir): async def test_form_user_reuses_existing_assets_when_pairing_again(hass, tmpdir): """Test the tls assets saved on disk are reused when pairing again.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + hass.config.config_dir = await hass.async_add_executor_job( tmpdir.mkdir, "tls_assets" ) @@ -413,7 +412,7 @@ async def test_form_user_reuses_existing_assets_when_pairing_again(hass, tmpdir) async def test_zeroconf_host_already_configured(hass, tmpdir): """Test starting a flow from discovery when the host is already configured.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + hass.config.config_dir = await hass.async_add_executor_job( tmpdir.mkdir, "tls_assets" ) @@ -438,7 +437,6 @@ async def test_zeroconf_host_already_configured(hass, tmpdir): async def test_zeroconf_lutron_id_already_configured(hass): """Test starting a flow from discovery when lutron id already configured.""" - await setup.async_setup_component(hass, "persistent_notification", {}) config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "4.5.6.7"}, unique_id="abc" @@ -463,7 +461,6 @@ async def test_zeroconf_lutron_id_already_configured(hass): async def test_zeroconf_not_lutron_device(hass): """Test starting a flow from discovery when it is not a lutron device.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, @@ -484,7 +481,7 @@ async def test_zeroconf_not_lutron_device(hass): ) async def test_zeroconf(hass, source, tmpdir): """Test starting a flow from discovery.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + hass.config.config_dir = await hass.async_add_executor_job( tmpdir.mkdir, "tls_assets" ) diff --git a/tests/components/lutron_caseta/test_device_trigger.py b/tests/components/lutron_caseta/test_device_trigger.py index 9370edd48bf..32d6eb3dc5f 100644 --- a/tests/components/lutron_caseta/test_device_trigger.py +++ b/tests/components/lutron_caseta/test_device_trigger.py @@ -1,7 +1,6 @@ """The tests for Lutron Caséta device triggers.""" import pytest -from homeassistant import setup from homeassistant.components import automation from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, @@ -186,7 +185,7 @@ async def test_get_triggers_for_invalid_device_id(hass, device_reg): async def test_if_fires_on_button_event(hass, calls, device_reg): """Test for press trigger firing.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + config_entry_id = await _async_setup_lutron_with_picos(hass, device_reg) dr_button_devices = hass.data[DOMAIN][config_entry_id][BUTTON_DEVICES] device_id = list(dr_button_devices)[0] @@ -230,7 +229,6 @@ async def test_if_fires_on_button_event(hass, calls, device_reg): async def test_validate_trigger_config_no_device(hass, calls, device_reg): """Test for no press with no device.""" - await setup.async_setup_component(hass, "persistent_notification", {}) assert await async_setup_component( hass, @@ -269,7 +267,7 @@ async def test_validate_trigger_config_no_device(hass, calls, device_reg): async def test_validate_trigger_config_unknown_device(hass, calls, device_reg): """Test for no press with an unknown device.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + config_entry_id = await _async_setup_lutron_with_picos(hass, device_reg) dr_button_devices = hass.data[DOMAIN][config_entry_id][BUTTON_DEVICES] device_id = list(dr_button_devices)[0] @@ -313,7 +311,6 @@ async def test_validate_trigger_config_unknown_device(hass, calls, device_reg): async def test_validate_trigger_invalid_triggers(hass, device_reg): """Test for click_event with invalid triggers.""" - notification_calls = async_mock_service(hass, "persistent_notification", "create") config_entry_id = await _async_setup_lutron_with_picos(hass, device_reg) dr_button_devices = hass.data[DOMAIN][config_entry_id][BUTTON_DEVICES] device_id = list(dr_button_devices)[0] @@ -339,8 +336,10 @@ async def test_validate_trigger_invalid_triggers(hass, device_reg): }, ) - assert len(notification_calls) == 1 + assert ( + len(entity_ids := hass.states.async_entity_ids("persistent_notification")) == 1 + ) assert ( "The following integrations and platforms could not be set up" - in notification_calls[0].data["message"] + in hass.states.get(entity_ids[0]).attributes["message"] ) diff --git a/tests/components/mazda/test_config_flow.py b/tests/components/mazda/test_config_flow.py index f6ea8c73a9b..c2628f2aa11 100644 --- a/tests/components/mazda/test_config_flow.py +++ b/tests/components/mazda/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch import aiohttp -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.mazda.config_flow import ( MazdaAccountLockedException, MazdaAuthenticationException, @@ -179,7 +179,7 @@ async def test_form_unknown_error(hass): async def test_reauth_flow(hass: HomeAssistant) -> None: """Test reauth works.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + mock_config = MockConfigEntry( domain=DOMAIN, unique_id=FIXTURE_USER_INPUT[CONF_EMAIL], diff --git a/tests/components/metoffice/test_config_flow.py b/tests/components/metoffice/test_config_flow.py index 55dd54d1c87..af911bb5bde 100644 --- a/tests/components/metoffice/test_config_flow.py +++ b/tests/components/metoffice/test_config_flow.py @@ -2,7 +2,7 @@ import json from unittest.mock import patch -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.metoffice.const import DOMAIN from .const import ( @@ -26,7 +26,6 @@ async def test_form(hass, requests_mock): all_sites = json.dumps(mock_json["all_sites"]) requests_mock.get("/public/data/val/wxfcs/all/json/sitelist/", text=all_sites) - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/monoprice/test_config_flow.py b/tests/components/monoprice/test_config_flow.py index 3e133519d1b..0ed4ac35eaa 100644 --- a/tests/components/monoprice/test_config_flow.py +++ b/tests/components/monoprice/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch from serial import SerialException -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.monoprice.const import ( CONF_SOURCE_1, CONF_SOURCE_4, @@ -25,7 +25,7 @@ CONFIG = { async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/motioneye/test_config_flow.py b/tests/components/motioneye/test_config_flow.py index 604085cec8f..dffcc4660d8 100644 --- a/tests/components/motioneye/test_config_flow.py +++ b/tests/components/motioneye/test_config_flow.py @@ -8,7 +8,7 @@ from motioneye_client.client import ( MotionEyeClientRequestError, ) -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.motioneye.const import ( CONF_ADMIN_PASSWORD, CONF_ADMIN_USERNAME, @@ -30,7 +30,7 @@ _LOGGER = logging.getLogger(__name__) async def test_user_success(hass: HomeAssistant) -> None: """Test successful user flow.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -73,7 +73,7 @@ async def test_user_success(hass: HomeAssistant) -> None: async def test_hassio_success(hass: HomeAssistant) -> None: """Test successful Supervisor flow.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, data={"addon": "motionEye", "url": TEST_URL}, @@ -157,7 +157,7 @@ async def test_user_invalid_auth(hass: HomeAssistant) -> None: async def test_user_invalid_url(hass: HomeAssistant) -> None: """Test invalid url is handled correctly.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -254,7 +254,6 @@ async def test_reauth(hass: HomeAssistant) -> None: config_entry = create_mock_motioneye_config_entry(hass, data=config_data) - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={ @@ -312,7 +311,6 @@ async def test_duplicate(hass: HomeAssistant) -> None: # Now do the usual config entry process, and verify it is rejected. create_mock_motioneye_config_entry(hass, data=config_data) - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -393,7 +391,6 @@ async def test_hassio_abort_if_already_in_progress(hass: HomeAssistant) -> None: async def test_hassio_clean_up_on_user_flow(hass: HomeAssistant) -> None: """Test Supervisor discovered flow is clean up when doing user flow.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index ae6a58c7d22..109d6961a0d 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -3396,7 +3396,7 @@ async def test_reloadable(hass, mqtt_mock): await hass.async_block_till_done() assert hass.states.get("light.test") - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("light")) == 1 yaml_path = path.join( _get_fixtures_base_path(), @@ -3412,7 +3412,7 @@ async def test_reloadable(hass, mqtt_mock): ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("light")) == 1 assert hass.states.get("light.test") is None assert hass.states.get("light.reload") diff --git a/tests/components/mullvad/test_config_flow.py b/tests/components/mullvad/test_config_flow.py index e34af4eb83b..967ad0dbcc4 100644 --- a/tests/components/mullvad/test_config_flow.py +++ b/tests/components/mullvad/test_config_flow.py @@ -41,7 +41,7 @@ async def test_form_user(hass): async def test_form_user_only_once(hass): """Test we can setup by the user only once.""" MockConfigEntry(domain=DOMAIN).add_to_hass(hass) - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/mutesync/test_config_flow.py b/tests/components/mutesync/test_config_flow.py index 39a8feb2472..934eb190742 100644 --- a/tests/components/mutesync/test_config_flow.py +++ b/tests/components/mutesync/test_config_flow.py @@ -5,14 +5,14 @@ from unittest.mock import patch import aiohttp import pytest -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.mutesync.const import DOMAIN from homeassistant.core import HomeAssistant async def test_form(hass: HomeAssistant) -> None: """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/myq/test_config_flow.py b/tests/components/myq/test_config_flow.py index 3b0d79f6f03..2e62b4aa5c0 100644 --- a/tests/components/myq/test_config_flow.py +++ b/tests/components/myq/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch from pymyq.errors import InvalidCredentialsError, MyQError -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.myq.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -12,7 +12,7 @@ from tests.common import MockConfigEntry async def test_form_user(hass): """Test we get the user form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/mysensors/test_config_flow.py b/tests/components/mysensors/test_config_flow.py index daee8a37eba..ca13a6d9cef 100644 --- a/tests/components/mysensors/test_config_flow.py +++ b/tests/components/mysensors/test_config_flow.py @@ -6,7 +6,7 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.mysensors.const import ( CONF_BAUD_RATE, CONF_DEVICE, @@ -34,7 +34,7 @@ async def get_form( hass: HomeAssistant, gatway_type: ConfGatewayType, expected_step_id: str ) -> FlowResult: """Get a form for the given gateway type.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + stepuser = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -92,7 +92,7 @@ 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.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -442,7 +442,6 @@ async def test_config_invalid( ) async def test_import(hass: HomeAssistant, mqtt: None, user_input: dict) -> None: """Test importing a gateway.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch("sys.platform", "win32"), patch( "homeassistant.components.mysensors.config_flow.try_connect", return_value=True @@ -738,7 +737,6 @@ async def test_duplicate( expected_result: tuple[str, str] | None, ) -> None: """Test duplicate detection.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch("sys.platform", "win32"), patch( "homeassistant.components.mysensors.config_flow.try_connect", return_value=True diff --git a/tests/components/mysensors/test_init.py b/tests/components/mysensors/test_init.py index 05e3df76285..7c83334d8f3 100644 --- a/tests/components/mysensors/test_init.py +++ b/tests/components/mysensors/test_init.py @@ -324,7 +324,7 @@ async def test_import( expected_config_entry_data: list[dict[str, Any]], ) -> None: """Test importing a gateway.""" - await async_setup_component(hass, "persistent_notification", {}) + with patch("sys.platform", "win32"), patch( "homeassistant.components.mysensors.config_flow.try_connect", return_value=True ), patch( diff --git a/tests/components/nexia/test_config_flow.py b/tests/components/nexia/test_config_flow.py index 2dd5c270c07..bd3eb9180e7 100644 --- a/tests/components/nexia/test_config_flow.py +++ b/tests/components/nexia/test_config_flow.py @@ -5,7 +5,7 @@ from nexia.const import BRAND_ASAIR, BRAND_NEXIA import pytest from requests.exceptions import ConnectTimeout, HTTPError -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.nexia.const import CONF_BRAND, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -13,7 +13,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @pytest.mark.parametrize("brand", [BRAND_ASAIR, BRAND_NEXIA]) async def test_form(hass, brand): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/nightscout/test_config_flow.py b/tests/components/nightscout/test_config_flow.py index 9be3c95ef42..61f064a3e29 100644 --- a/tests/components/nightscout/test_config_flow.py +++ b/tests/components/nightscout/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch from aiohttp import ClientConnectionError, ClientResponseError -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.nightscout.const import DOMAIN from homeassistant.components.nightscout.utils import hash_from_url from homeassistant.const import CONF_URL @@ -20,7 +20,7 @@ CONFIG = {CONF_URL: "https://some.url:1234"} async def test_form(hass): """Test we get the user initiated form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/nmap_tracker/test_config_flow.py b/tests/components/nmap_tracker/test_config_flow.py index 0c6e8f8f7e8..aae48b80d10 100644 --- a/tests/components/nmap_tracker/test_config_flow.py +++ b/tests/components/nmap_tracker/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.device_tracker.const import ( CONF_CONSIDER_HOME, CONF_SCAN_INTERVAL, @@ -25,7 +25,7 @@ from tests.common import MockConfigEntry ) async def test_form(hass: HomeAssistant, hosts: str, mock_get_source_ip) -> None: """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -64,7 +64,7 @@ async def test_form(hass: HomeAssistant, hosts: str, mock_get_source_ip) -> None async def test_form_range(hass: HomeAssistant, mock_get_source_ip) -> None: """Test we get the form and can take an ip range.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -100,7 +100,7 @@ async def test_form_range(hass: HomeAssistant, mock_get_source_ip) -> None: async def test_form_invalid_hosts(hass: HomeAssistant, mock_get_source_ip) -> None: """Test invalid hosts passed in.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -124,7 +124,7 @@ async def test_form_invalid_hosts(hass: HomeAssistant, mock_get_source_ip) -> No async def test_form_already_configured(hass: HomeAssistant, mock_get_source_ip) -> None: """Test duplicate host list.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + config_entry = MockConfigEntry( domain=DOMAIN, data={}, @@ -159,7 +159,7 @@ async def test_form_already_configured(hass: HomeAssistant, mock_get_source_ip) async def test_form_invalid_excludes(hass: HomeAssistant, mock_get_source_ip) -> None: """Test invalid excludes passed in.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -245,7 +245,7 @@ async def test_options_flow(hass: HomeAssistant, mock_get_source_ip) -> None: async def test_import(hass: HomeAssistant, mock_get_source_ip) -> None: """Test we can import from yaml.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( "homeassistant.components.nmap_tracker.async_setup_entry", return_value=True, @@ -293,7 +293,6 @@ async def test_import_aborts_if_matching( }, ) config_entry.add_to_hass(hass) - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/notify/test_init.py b/tests/components/notify/test_init.py index 92b71091e07..9dbbcf9b9b9 100644 --- a/tests/components/notify/test_init.py +++ b/tests/components/notify/test_init.py @@ -87,7 +87,6 @@ class NotificationService(notify.BaseNotificationService): async def test_warn_template(hass, caplog): """Test warning when template used.""" assert await async_setup_component(hass, "notify", {}) - assert await async_setup_component(hass, "persistent_notification", {}) await hass.services.async_call( "notify", diff --git a/tests/components/nuheat/test_config_flow.py b/tests/components/nuheat/test_config_flow.py index 525ab18726a..b89147a0024 100644 --- a/tests/components/nuheat/test_config_flow.py +++ b/tests/components/nuheat/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock, patch import requests -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.nuheat.const import CONF_SERIAL_NUMBER, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, HTTP_INTERNAL_SERVER_ERROR @@ -12,7 +12,7 @@ from .mocks import _get_mock_thermostat_run async def test_form_user(hass): """Test we get the form with user source.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/nuki/mock.py b/tests/components/nuki/mock.py index a7870ce0906..30315915a73 100644 --- a/tests/components/nuki/mock.py +++ b/tests/components/nuki/mock.py @@ -1,5 +1,4 @@ """Mockup Nuki device.""" -from homeassistant import setup from tests.common import MockConfigEntry @@ -14,7 +13,7 @@ MOCK_INFO = {"ids": {"hardwareId": HW_ID}} async def setup_nuki_integration(hass): """Create the Nuki device.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain="nuki", unique_id=HW_ID, diff --git a/tests/components/nuki/test_config_flow.py b/tests/components/nuki/test_config_flow.py index 4039eef5984..8e3636dbdef 100644 --- a/tests/components/nuki/test_config_flow.py +++ b/tests/components/nuki/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch from pynuki.bridge import InvalidCredentialsException from requests.exceptions import RequestException -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.components.nuki.const import DOMAIN from homeassistant.const import CONF_TOKEN @@ -14,7 +14,7 @@ from .mock import HOST, MAC, MOCK_INFO, NAME, setup_nuki_integration async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -53,7 +53,6 @@ async def test_form(hass): async def test_import(hass): """Test that the import works.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.nuki.config_flow.NukiBridge.info", diff --git a/tests/components/nut/test_config_flow.py b/tests/components/nut/test_config_flow.py index 8543c68c2b8..135d0ef9efc 100644 --- a/tests/components/nut/test_config_flow.py +++ b/tests/components/nut/test_config_flow.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.nut.const import DOMAIN from homeassistant.const import CONF_RESOURCES, CONF_SCAN_INTERVAL @@ -73,7 +73,7 @@ async def test_form_zeroconf(hass): async def test_form_user_one_ups(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -128,7 +128,6 @@ async def test_form_user_one_ups(hass): async def test_form_user_multiple_ups(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) config_entry = MockConfigEntry( domain=DOMAIN, @@ -210,7 +209,6 @@ async def test_form_user_one_ups_with_ignored_entry(hass): ) ignored_entry.add_to_hass(hass) - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/nws/test_config_flow.py b/tests/components/nws/test_config_flow.py index 6945cb380d3..d2344ee2b4a 100644 --- a/tests/components/nws/test_config_flow.py +++ b/tests/components/nws/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch import aiohttp -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.nws.const import DOMAIN @@ -12,7 +12,6 @@ async def test_form(hass, mock_simple_nws_config): hass.config.latitude = 35 hass.config.longitude = -90 - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/nzbget/test_config_flow.py b/tests/components/nzbget/test_config_flow.py index 68488c376f6..f6e91d13d9e 100644 --- a/tests/components/nzbget/test_config_flow.py +++ b/tests/components/nzbget/test_config_flow.py @@ -11,7 +11,6 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM, ) -from homeassistant.setup import async_setup_component from . import ( ENTRY_CONFIG, @@ -28,7 +27,6 @@ from tests.common import MockConfigEntry async def test_user_form(hass): """Test we get the user initiated form.""" - await async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -53,7 +51,6 @@ async def test_user_form(hass): async def test_user_form_show_advanced_options(hass): """Test we get the user initiated form with advanced options shown.""" - await async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True} diff --git a/tests/components/omnilogic/test_config_flow.py b/tests/components/omnilogic/test_config_flow.py index eda83b45455..bd8f32b05bd 100644 --- a/tests/components/omnilogic/test_config_flow.py +++ b/tests/components/omnilogic/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch from omnilogic import LoginException, OmniLogicException -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.omnilogic.const import DOMAIN from tests.common import MockConfigEntry @@ -13,7 +13,7 @@ DATA = {"username": "test-username", "password": "test-password"} async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -43,7 +43,6 @@ async def test_already_configured(hass): """Test config flow when Omnilogic component is already setup.""" MockConfigEntry(domain="omnilogic", data=DATA).add_to_hass(hass) - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -55,7 +54,6 @@ async def test_already_configured(hass): async def test_with_invalid_credentials(hass): """Test with invalid credentials.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -77,7 +75,6 @@ async def test_with_invalid_credentials(hass): async def test_form_cannot_connect(hass): """Test if invalid response or no connection returned from Hayward.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -98,7 +95,7 @@ async def test_form_cannot_connect(hass): async def test_with_unknown_error(hass): """Test with unknown error response from Hayward.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 206cafa197b..582ad5a436b 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -20,7 +20,6 @@ from tests.components.met.conftest import mock_weather # noqa: F401 @pytest.fixture(autouse=True) def always_mock_weather(mock_weather): # noqa: F811 """Mock the Met weather provider.""" - pass @pytest.fixture(autouse=True) @@ -386,7 +385,6 @@ async def test_onboarding_core_sets_up_rpi_power( ): """Test that the core step sets up rpi_power on RPi.""" mock_storage(hass_storage, {"done": [const.STEP_USER]}) - await async_setup_component(hass, "persistent_notification", {}) assert await async_setup_component(hass, "onboarding", {}) await hass.async_block_till_done() @@ -411,7 +409,6 @@ async def test_onboarding_core_no_rpi_power( ): """Test that the core step do not set up rpi_power on non RPi.""" mock_storage(hass_storage, {"done": [const.STEP_USER]}) - await async_setup_component(hass, "persistent_notification", {}) assert await async_setup_component(hass, "onboarding", {}) await hass.async_block_till_done() @@ -453,7 +450,6 @@ async def test_onboarding_analytics(hass, hass_storage, hass_client, hass_admin_ async def test_onboarding_installation_type(hass, hass_storage, hass_client): """Test returning installation type during onboarding.""" mock_storage(hass_storage, {"done": []}) - await async_setup_component(hass, "persistent_notification", {}) assert await async_setup_component(hass, "onboarding", {}) await hass.async_block_till_done() @@ -475,7 +471,6 @@ async def test_onboarding_installation_type(hass, hass_storage, hass_client): async def test_onboarding_installation_type_after_done(hass, hass_storage, hass_client): """Test raising for installation type after onboarding.""" mock_storage(hass_storage, {"done": [const.STEP_USER]}) - await async_setup_component(hass, "persistent_notification", {}) assert await async_setup_component(hass, "onboarding", {}) await hass.async_block_till_done() diff --git a/tests/components/onewire/test_binary_sensor.py b/tests/components/onewire/test_binary_sensor.py index c82bc88c3a6..7a34fe44a94 100644 --- a/tests/components/onewire/test_binary_sensor.py +++ b/tests/components/onewire/test_binary_sensor.py @@ -6,7 +6,6 @@ import pytest from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.onewire.binary_sensor import DEVICE_BINARY_SENSORS -from homeassistant.setup import async_setup_component from . import setup_onewire_patched_owserver_integration, setup_owproxy_mock_devices from .const import MOCK_OWPROXY_DEVICES @@ -27,7 +26,7 @@ async def test_owserver_binary_sensor(owproxy, hass, device_id): This test forces all entities to be enabled. """ - await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) setup_owproxy_mock_devices(owproxy, BINARY_SENSOR_DOMAIN, [device_id]) diff --git a/tests/components/onewire/test_init.py b/tests/components/onewire/test_init.py index 01426e1faf1..38f63cda09a 100644 --- a/tests/components/onewire/test_init.py +++ b/tests/components/onewire/test_init.py @@ -8,7 +8,6 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.setup import async_setup_component from . import ( setup_onewire_owserver_integration, @@ -94,7 +93,7 @@ async def test_registry_cleanup(owproxy, hass): As they would be on a clean setup: all binary-sensors and switches disabled. """ - await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) diff --git a/tests/components/onewire/test_sensor.py b/tests/components/onewire/test_sensor.py index eacaa148b45..f70d2912da3 100644 --- a/tests/components/onewire/test_sensor.py +++ b/tests/components/onewire/test_sensor.py @@ -75,7 +75,7 @@ async def test_setup_owserver_with_port(hass): @patch("homeassistant.components.onewire.onewirehub.protocol.proxy") async def test_sensors_on_owserver_coupler(owproxy, hass, device_id): """Test for 1-Wire sensors connected to DS2409 coupler.""" - await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) mock_coupler = MOCK_COUPLERS[device_id] @@ -138,7 +138,7 @@ async def test_owserver_setup_valid_device(owproxy, hass, device_id, platform): As they would be on a clean setup: all binary-sensors and switches disabled. """ - await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -184,7 +184,7 @@ async def test_owserver_setup_valid_device(owproxy, hass, device_id, platform): @pytest.mark.parametrize("device_id", MOCK_SYSBUS_DEVICES.keys()) async def test_onewiredirect_setup_valid_device(hass, device_id): """Test that sysbus config entry works correctly.""" - await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) diff --git a/tests/components/onewire/test_switch.py b/tests/components/onewire/test_switch.py index bfc4550cdc7..7abb8a5e9bd 100644 --- a/tests/components/onewire/test_switch.py +++ b/tests/components/onewire/test_switch.py @@ -7,7 +7,6 @@ import pytest from homeassistant.components.onewire.switch import DEVICE_SWITCHES from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TOGGLE, STATE_OFF, STATE_ON -from homeassistant.setup import async_setup_component from . import setup_onewire_patched_owserver_integration, setup_owproxy_mock_devices from .const import MOCK_OWPROXY_DEVICES @@ -28,7 +27,7 @@ async def test_owserver_switch(owproxy, hass, device_id): This test forces all entities to be enabled. """ - await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) setup_owproxy_mock_devices(owproxy, SWITCH_DOMAIN, [device_id]) diff --git a/tests/components/opengarage/test_config_flow.py b/tests/components/opengarage/test_config_flow.py index ca89d07cedc..d421fb6a129 100644 --- a/tests/components/opengarage/test_config_flow.py +++ b/tests/components/opengarage/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch import aiohttp -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.opengarage.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( @@ -17,7 +17,7 @@ from tests.common import MockConfigEntry async def test_form(hass: HomeAssistant) -> None: """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py index 6713bfecdaa..99133cf17c3 100644 --- a/tests/components/opentherm_gw/test_config_flow.py +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch from pyotgw.vars import OTGW, OTGW_ABOUT from serial import SerialException -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.opentherm_gw.const import ( CONF_FLOOR_TEMP, CONF_PRECISION, @@ -29,7 +29,7 @@ MINIMAL_STATUS = {OTGW: {OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}} async def test_form_user(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -67,7 +67,7 @@ async def test_form_user(hass): async def test_form_import(hass): """Test import from existing config.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( "homeassistant.components.opentherm_gw.async_setup", return_value=True, diff --git a/tests/components/owntracks/test_device_tracker.py b/tests/components/owntracks/test_device_tracker.py index c21361c5fff..d2ed1b64779 100644 --- a/tests/components/owntracks/test_device_tracker.py +++ b/tests/components/owntracks/test_device_tracker.py @@ -283,9 +283,6 @@ BAD_JSON_SUFFIX = "** and it ends here ^^" @pytest.fixture def setup_comp(hass, mock_device_tracker_conf, mqtt_mock): """Initialize components.""" - assert hass.loop.run_until_complete( - async_setup_component(hass, "persistent_notification", {}) - ) hass.loop.run_until_complete(async_setup_component(hass, "device_tracker", {})) hass.states.async_set("zone.inner", "zoning", INNER_ZONE) diff --git a/tests/components/ozw/test_config_flow.py b/tests/components/ozw/test_config_flow.py index 004c492bb2d..384e16b57ed 100644 --- a/tests/components/ozw/test_config_flow.py +++ b/tests/components/ozw/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.ozw.config_flow import TITLE from homeassistant.components.ozw.const import DOMAIN @@ -81,7 +81,6 @@ def mock_start_addon(): async def test_user_not_supervisor_create_entry(hass, mqtt): """Test the user step creates an entry not on Supervisor.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.ozw.async_setup_entry", @@ -126,7 +125,6 @@ async def test_one_instance_allowed(hass): async def test_not_addon(hass, supervisor, mqtt): """Test opting out of add-on on Supervisor.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -156,7 +154,6 @@ async def test_addon_running(hass, supervisor, addon_running, addon_options): """Test add-on already running on Supervisor.""" addon_options["device"] = "/test" addon_options["network_key"] = "abc123" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -185,7 +182,6 @@ async def test_addon_running(hass, supervisor, addon_running, addon_options): async def test_addon_info_failure(hass, supervisor, addon_info): """Test add-on info failure.""" addon_info.side_effect = HassioAPIError() - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -203,7 +199,6 @@ async def test_addon_installed( hass, supervisor, addon_installed, addon_options, set_addon_options, start_addon ): """Test add-on already installed but not running on Supervisor.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -237,7 +232,6 @@ async def test_set_addon_config_failure( ): """Test add-on set config failure.""" set_addon_options.side_effect = HassioAPIError() - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -259,7 +253,6 @@ async def test_start_addon_failure( ): """Test add-on start failure.""" start_addon.side_effect = HassioAPIError() - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -287,7 +280,6 @@ async def test_addon_not_installed( ): """Test add-on not installed.""" addon_installed.return_value["version"] = None - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -330,7 +322,6 @@ async def test_install_addon_failure(hass, supervisor, addon_installed, install_ """Test add-on install failure.""" addon_installed.return_value["version"] = None install_addon.side_effect = HassioAPIError() - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -352,7 +343,6 @@ async def test_install_addon_failure(hass, supervisor, addon_installed, install_ async def test_supervisor_discovery(hass, supervisor, addon_running, addon_options): """Test flow started from Supervisor discovery.""" - await setup.async_setup_component(hass, "persistent_notification", {}) addon_options["device"] = "/test" addon_options["network_key"] = "abc123" @@ -385,7 +375,6 @@ async def test_clean_discovery_on_user_create( hass, supervisor, addon_running, addon_options ): """Test discovery flow is cleaned up when a user flow is finished.""" - await setup.async_setup_component(hass, "persistent_notification", {}) addon_options["device"] = "/test" addon_options["network_key"] = "abc123" @@ -427,7 +416,6 @@ async def test_abort_discovery_with_user_flow( hass, supervisor, addon_running, addon_options ): """Test discovery flow is aborted if a user flow is in progress.""" - await setup.async_setup_component(hass, "persistent_notification", {}) await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -448,7 +436,6 @@ async def test_abort_discovery_with_existing_entry( hass, supervisor, addon_running, addon_options ): """Test discovery flow is aborted if an entry already exists.""" - await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry(domain=DOMAIN, data={}, title=TITLE, unique_id=DOMAIN) entry.add_to_hass(hass) @@ -468,7 +455,6 @@ async def test_discovery_addon_not_running( ): """Test discovery with add-on already installed but not running.""" addon_options["device"] = None - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, @@ -490,7 +476,6 @@ async def test_discovery_addon_not_installed( ): """Test discovery with add-on not installed.""" addon_installed.return_value["version"] = None - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/persistent_notification/test_init.py b/tests/components/persistent_notification/test_init.py index daf671bd52b..bc552b71770 100644 --- a/tests/components/persistent_notification/test_init.py +++ b/tests/components/persistent_notification/test_init.py @@ -1,142 +1,146 @@ """The tests for the persistent notification component.""" +import pytest + import homeassistant.components.persistent_notification as pn from homeassistant.components.websocket_api.const import TYPE_RESULT -from homeassistant.setup import async_setup_component, setup_component +from homeassistant.setup import async_setup_component -from tests.common import get_test_home_assistant +from tests.common import async_capture_events -class TestPersistentNotification: - """Test persistent notification component.""" +@pytest.fixture(autouse=True) +async def setup_integration(hass): + """Set up persistent notification integration.""" + assert await async_setup_component(hass, pn.DOMAIN, {}) - def setup_method(self, method): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - setup_component(self.hass, pn.DOMAIN, {}) - def teardown_method(self, method): - """Stop everything that was started.""" - self.hass.stop() +async def test_create(hass): + """Test creating notification without title or notification id.""" + notifications = hass.data[pn.DOMAIN] + assert len(hass.states.async_entity_ids(pn.DOMAIN)) == 0 + assert len(notifications) == 0 - def test_create(self): - """Test creating notification without title or notification id.""" - notifications = self.hass.data[pn.DOMAIN]["notifications"] - assert len(self.hass.states.entity_ids(pn.DOMAIN)) == 0 - assert len(notifications) == 0 + pn.async_create(hass, "Hello World {{ 1 + 1 }}", title="{{ 1 + 1 }} beers") - pn.create(self.hass, "Hello World {{ 1 + 1 }}", title="{{ 1 + 1 }} beers") - self.hass.block_till_done() + entity_ids = hass.states.async_entity_ids(pn.DOMAIN) + assert len(entity_ids) == 1 + assert len(notifications) == 1 - entity_ids = self.hass.states.entity_ids(pn.DOMAIN) - assert len(entity_ids) == 1 - assert len(notifications) == 1 + state = hass.states.get(entity_ids[0]) + assert state.state == pn.STATE + assert state.attributes.get("message") == "Hello World 2" + assert state.attributes.get("title") == "2 beers" - state = self.hass.states.get(entity_ids[0]) - assert state.state == pn.STATE - assert state.attributes.get("message") == "Hello World 2" - assert state.attributes.get("title") == "2 beers" + notification = notifications.get(entity_ids[0]) + assert notification["status"] == pn.STATUS_UNREAD + assert notification["message"] == "Hello World 2" + assert notification["title"] == "2 beers" + assert notification["created_at"] is not None - notification = notifications.get(entity_ids[0]) - assert notification["status"] == pn.STATUS_UNREAD - assert notification["message"] == "Hello World 2" - assert notification["title"] == "2 beers" - assert notification["created_at"] is not None - notifications.clear() - def test_create_notification_id(self): - """Ensure overwrites existing notification with same id.""" - notifications = self.hass.data[pn.DOMAIN]["notifications"] - assert len(self.hass.states.entity_ids(pn.DOMAIN)) == 0 - assert len(notifications) == 0 +async def test_create_notification_id(hass): + """Ensure overwrites existing notification with same id.""" + notifications = hass.data[pn.DOMAIN] + assert len(hass.states.async_entity_ids(pn.DOMAIN)) == 0 + assert len(notifications) == 0 - pn.create(self.hass, "test", notification_id="Beer 2") - self.hass.block_till_done() + pn.async_create(hass, "test", notification_id="Beer 2") - assert len(self.hass.states.entity_ids()) == 1 - assert len(notifications) == 1 + assert len(hass.states.async_entity_ids()) == 1 + assert len(notifications) == 1 - entity_id = "persistent_notification.beer_2" - state = self.hass.states.get(entity_id) - assert state.attributes.get("message") == "test" + entity_id = "persistent_notification.beer_2" + state = hass.states.get(entity_id) + assert state.attributes.get("message") == "test" - notification = notifications.get(entity_id) - assert notification["message"] == "test" - assert notification["title"] is None + notification = notifications.get(entity_id) + assert notification["message"] == "test" + assert notification["title"] is None - pn.create(self.hass, "test 2", notification_id="Beer 2") - self.hass.block_till_done() + pn.async_create(hass, "test 2", notification_id="Beer 2") - # We should have overwritten old one - assert len(self.hass.states.entity_ids()) == 1 - state = self.hass.states.get(entity_id) - assert state.attributes.get("message") == "test 2" + # We should have overwritten old one + assert len(hass.states.async_entity_ids()) == 1 + state = hass.states.get(entity_id) + assert state.attributes.get("message") == "test 2" - notification = notifications.get(entity_id) - assert notification["message"] == "test 2" - notifications.clear() + notification = notifications.get(entity_id) + assert notification["message"] == "test 2" - def test_create_template_error(self): - """Ensure we output templates if contain error.""" - notifications = self.hass.data[pn.DOMAIN]["notifications"] - assert len(self.hass.states.entity_ids(pn.DOMAIN)) == 0 - assert len(notifications) == 0 - pn.create(self.hass, "{{ message + 1 }}", "{{ title + 1 }}") - self.hass.block_till_done() +async def test_create_template_error(hass): + """Ensure we output templates if contain error.""" + notifications = hass.data[pn.DOMAIN] + assert len(hass.states.async_entity_ids(pn.DOMAIN)) == 0 + assert len(notifications) == 0 - entity_ids = self.hass.states.entity_ids(pn.DOMAIN) - assert len(entity_ids) == 1 - assert len(notifications) == 1 + pn.async_create(hass, "{{ message + 1 }}", "{{ title + 1 }}") - state = self.hass.states.get(entity_ids[0]) - assert state.attributes.get("message") == "{{ message + 1 }}" - assert state.attributes.get("title") == "{{ title + 1 }}" + entity_ids = hass.states.async_entity_ids(pn.DOMAIN) + assert len(entity_ids) == 1 + assert len(notifications) == 1 - notification = notifications.get(entity_ids[0]) - assert notification["message"] == "{{ message + 1 }}" - assert notification["title"] == "{{ title + 1 }}" - notifications.clear() + state = hass.states.get(entity_ids[0]) + assert state.attributes.get("message") == "{{ message + 1 }}" + assert state.attributes.get("title") == "{{ title + 1 }}" - def test_dismiss_notification(self): - """Ensure removal of specific notification.""" - notifications = self.hass.data[pn.DOMAIN]["notifications"] - assert len(self.hass.states.entity_ids(pn.DOMAIN)) == 0 - assert len(notifications) == 0 + notification = notifications.get(entity_ids[0]) + assert notification["message"] == "{{ message + 1 }}" + assert notification["title"] == "{{ title + 1 }}" - pn.create(self.hass, "test", notification_id="Beer 2") - self.hass.block_till_done() - assert len(self.hass.states.entity_ids(pn.DOMAIN)) == 1 - assert len(notifications) == 1 - pn.dismiss(self.hass, notification_id="Beer 2") - self.hass.block_till_done() +async def test_dismiss_notification(hass): + """Ensure removal of specific notification.""" + notifications = hass.data[pn.DOMAIN] + assert len(hass.states.async_entity_ids(pn.DOMAIN)) == 0 + assert len(notifications) == 0 - assert len(self.hass.states.entity_ids(pn.DOMAIN)) == 0 - assert len(notifications) == 0 - notifications.clear() + pn.async_create(hass, "test", notification_id="Beer 2") - def test_mark_read(self): - """Ensure notification is marked as Read.""" - notifications = self.hass.data[pn.DOMAIN]["notifications"] - assert len(notifications) == 0 + assert len(hass.states.async_entity_ids(pn.DOMAIN)) == 1 + assert len(notifications) == 1 + pn.async_dismiss(hass, notification_id="Beer 2") - pn.create(self.hass, "test", notification_id="Beer 2") - self.hass.block_till_done() + assert len(hass.states.async_entity_ids(pn.DOMAIN)) == 0 + assert len(notifications) == 0 - entity_id = "persistent_notification.beer_2" - assert len(notifications) == 1 - notification = notifications.get(entity_id) - assert notification["status"] == pn.STATUS_UNREAD - self.hass.services.call( - pn.DOMAIN, pn.SERVICE_MARK_READ, {"notification_id": "Beer 2"} - ) - self.hass.block_till_done() +async def test_mark_read(hass): + """Ensure notification is marked as Read.""" + events = async_capture_events(hass, pn.EVENT_PERSISTENT_NOTIFICATIONS_UPDATED) + notifications = hass.data[pn.DOMAIN] + assert len(notifications) == 0 - assert len(notifications) == 1 - notification = notifications.get(entity_id) - assert notification["status"] == pn.STATUS_READ - notifications.clear() + await hass.services.async_call( + pn.DOMAIN, + "create", + {"notification_id": "Beer 2", "message": "test"}, + blocking=True, + ) + + entity_id = "persistent_notification.beer_2" + assert len(notifications) == 1 + notification = notifications.get(entity_id) + assert notification["status"] == pn.STATUS_UNREAD + assert len(events) == 1 + + await hass.services.async_call( + pn.DOMAIN, "mark_read", {"notification_id": "Beer 2"}, blocking=True + ) + + assert len(notifications) == 1 + notification = notifications.get(entity_id) + assert notification["status"] == pn.STATUS_READ + assert len(events) == 2 + + await hass.services.async_call( + pn.DOMAIN, + "dismiss", + {"notification_id": "Beer 2"}, + blocking=True, + ) + assert len(notifications) == 0 + assert len(events) == 3 async def test_ws_get_notifications(hass, hass_ws_client): @@ -173,7 +177,7 @@ async def test_ws_get_notifications(hass, hass_ws_client): # Mark Read await hass.services.async_call( - pn.DOMAIN, pn.SERVICE_MARK_READ, {"notification_id": "Beer 2"} + pn.DOMAIN, "mark_read", {"notification_id": "Beer 2"} ) await client.send_json({"id": 7, "type": "persistent_notification/get"}) msg = await client.receive_json() diff --git a/tests/components/philips_js/conftest.py b/tests/components/philips_js/conftest.py index 4b6150f9f81..fc7e142bf53 100644 --- a/tests/components/philips_js/conftest.py +++ b/tests/components/philips_js/conftest.py @@ -4,7 +4,6 @@ from unittest.mock import create_autospec, patch from haphilipsjs import PhilipsTV from pytest import fixture -from homeassistant import setup from homeassistant.components.philips_js.const import DOMAIN from . import MOCK_CONFIG, MOCK_ENTITY_ID, MOCK_NAME, MOCK_SERIAL_NO, MOCK_SYSTEM @@ -15,7 +14,6 @@ from tests.common import MockConfigEntry, mock_device_registry @fixture(autouse=True) async def setup_notification(hass): """Configure notification system.""" - await setup.async_setup_component(hass, "persistent_notification", {}) @fixture(autouse=True) diff --git a/tests/components/picnic/test_config_flow.py b/tests/components/picnic/test_config_flow.py index 7cdc04e4a39..b6bcc17a03d 100644 --- a/tests/components/picnic/test_config_flow.py +++ b/tests/components/picnic/test_config_flow.py @@ -4,14 +4,14 @@ from unittest.mock import patch from python_picnic_api.session import PicnicAuthError import requests -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.picnic.const import CONF_COUNTRY_CODE, DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/plaato/test_config_flow.py b/tests/components/plaato/test_config_flow.py index 9f0f6de5cd6..887b2b2bb01 100644 --- a/tests/components/plaato/test_config_flow.py +++ b/tests/components/plaato/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch from pyplaato.models.device import PlaatoDeviceType import pytest -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.plaato.const import ( CONF_DEVICE_NAME, CONF_DEVICE_TYPE, @@ -34,7 +34,7 @@ def mock_webhook_id(): async def test_show_config_form(hass): """Test show configuration form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 9f9e4e1cdfb..1760a498a6f 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -36,7 +36,6 @@ from homeassistant.const import ( CONF_URL, CONF_VERIFY_SSL, ) -from homeassistant.setup import async_setup_component from .const import DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN, PLEX_DIRECT_URL from .helpers import trigger_plex_update, wait_for_debouncer @@ -755,7 +754,6 @@ async def test_trigger_reauth( hass, entry, mock_plex_server, mock_websocket, current_request_with_host ): """Test setup and reauthorization of a Plex token.""" - await async_setup_component(hass, "persistent_notification", {}) assert entry.state is ConfigEntryState.LOADED diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index 081adb845f4..c9bcce0ac83 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -21,7 +21,6 @@ from homeassistant.const import ( STATE_IDLE, STATE_PLAYING, ) -from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from .const import DEFAULT_DATA, DEFAULT_OPTIONS, PLEX_DIRECT_URL @@ -186,7 +185,6 @@ async def test_setup_when_certificate_changed( plextv_shared_users, ): """Test setup component when the Plex certificate has changed.""" - await async_setup_component(hass, "persistent_notification", {}) class WrongCertHostnameException(requests.exceptions.SSLError): """Mock the exception showing a mismatched hostname.""" diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index 04c42e0ba83..75851f5c15a 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -8,7 +8,6 @@ from plugwise.exceptions import ( ) import pytest -from homeassistant import setup from homeassistant.components.plugwise.const import ( API, DEFAULT_PORT, @@ -79,7 +78,7 @@ def mock_smile(): async def test_form_flow_gateway(hass): """Test we get the form for Plugwise Gateway product type.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) @@ -97,7 +96,7 @@ async def test_form_flow_gateway(hass): async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={FLOW_TYPE: FLOW_NET} ) @@ -132,7 +131,7 @@ async def test_form(hass): async def test_zeroconf_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_ZEROCONF}, @@ -169,7 +168,7 @@ async def test_zeroconf_form(hass): async def test_zeroconf_stretch_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_ZEROCONF}, @@ -206,7 +205,7 @@ async def test_zeroconf_stretch_form(hass): async def test_form_username(hass): """Test we get the username data back.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={FLOW_TYPE: FLOW_NET} ) diff --git a/tests/components/plum_lightpad/test_config_flow.py b/tests/components/plum_lightpad/test_config_flow.py index 29539b5886f..20c3f5df411 100644 --- a/tests/components/plum_lightpad/test_config_flow.py +++ b/tests/components/plum_lightpad/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch from requests.exceptions import ConnectTimeout -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.plum_lightpad.const import DOMAIN from tests.common import MockConfigEntry @@ -11,7 +11,6 @@ from tests.common import MockConfigEntry async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -70,8 +69,6 @@ async def test_form_one_entry_per_email_allowed(hass): data={"username": "test-plum-username", "password": "test-plum-password"}, ).add_to_hass(hass) - await setup.async_setup_component(hass, "persistent_notification", {}) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -94,7 +91,6 @@ async def test_form_one_entry_per_email_allowed(hass): async def test_import(hass): """Test configuring the flow using configuration.yaml.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.plum_lightpad.utils.Plum.loadCloudData" diff --git a/tests/components/powerwall/test_config_flow.py b/tests/components/powerwall/test_config_flow.py index 61230f59e52..afad08f3cd2 100644 --- a/tests/components/powerwall/test_config_flow.py +++ b/tests/components/powerwall/test_config_flow.py @@ -8,7 +8,7 @@ from tesla_powerwall import ( PowerwallUnreachableError, ) -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.components.powerwall.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD @@ -22,7 +22,7 @@ VALID_CONFIG = {CONF_IP_ADDRESS: "1.2.3.4", CONF_PASSWORD: "00GGX"} async def test_form_source_user(hass): """Test we get config flow setup form as a user.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -137,7 +137,6 @@ async def test_form_wrong_version(hass): async def test_already_configured(hass): """Test we abort when already configured.""" - await setup.async_setup_component(hass, "persistent_notification", {}) config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_IP_ADDRESS: "1.1.1.1"}) config_entry.add_to_hass(hass) @@ -157,7 +156,6 @@ async def test_already_configured(hass): async def test_already_configured_with_ignored(hass): """Test ignored entries do not break checking for existing entries.""" - await setup.async_setup_component(hass, "persistent_notification", {}) config_entry = MockConfigEntry( domain=DOMAIN, data={}, source=config_entries.SOURCE_IGNORE @@ -178,7 +176,7 @@ async def test_already_configured_with_ignored(hass): async def test_dhcp_discovery(hass): """Test we can process the discovery from dhcp.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, diff --git a/tests/components/profiler/test_config_flow.py b/tests/components/profiler/test_config_flow.py index 2b33472e93a..77a6f3e4575 100644 --- a/tests/components/profiler/test_config_flow.py +++ b/tests/components/profiler/test_config_flow.py @@ -1,7 +1,7 @@ """Test the Profiler config flow.""" from unittest.mock import patch -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.profiler.const import DOMAIN from tests.common import MockConfigEntry @@ -9,7 +9,7 @@ from tests.common import MockConfigEntry async def test_form_user(hass): """Test we can setup by the user.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -35,7 +35,7 @@ async def test_form_user(hass): async def test_form_user_only_once(hass): """Test we can setup by the user only once.""" MockConfigEntry(domain=DOMAIN).add_to_hass(hass) - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/profiler/test_init.py b/tests/components/profiler/test_init.py index 809a6164ce2..37e48763131 100644 --- a/tests/components/profiler/test_init.py +++ b/tests/components/profiler/test_init.py @@ -3,7 +3,6 @@ from datetime import timedelta import os from unittest.mock import patch -from homeassistant import setup from homeassistant.components.profiler import ( CONF_SECONDS, SERVICE_DUMP_LOG_OBJECTS, @@ -25,7 +24,6 @@ async def test_basic_usage(hass, tmpdir): """Test we can setup and the service is registered.""" test_dir = tmpdir.mkdir("profiles") - await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry(domain=DOMAIN) entry.add_to_hass(hass) @@ -57,7 +55,6 @@ async def test_memory_usage(hass, tmpdir): """Test we can setup and the service is registered.""" test_dir = tmpdir.mkdir("profiles") - await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry(domain=DOMAIN) entry.add_to_hass(hass) @@ -88,7 +85,6 @@ async def test_memory_usage(hass, tmpdir): async def test_object_growth_logging(hass, caplog): """Test we can setup and the service and we can dump objects to the log.""" - await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry(domain=DOMAIN) entry.add_to_hass(hass) @@ -129,7 +125,6 @@ async def test_object_growth_logging(hass, caplog): async def test_dump_log_object(hass, caplog): """Test we can setup and the service is registered and logging works.""" - await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry(domain=DOMAIN) entry.add_to_hass(hass) @@ -153,7 +148,6 @@ async def test_dump_log_object(hass, caplog): async def test_log_thread_frames(hass, caplog): """Test we can log thread frames.""" - await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry(domain=DOMAIN) entry.add_to_hass(hass) @@ -175,7 +169,6 @@ async def test_log_thread_frames(hass, caplog): async def test_log_scheduled(hass, caplog): """Test we can log scheduled items in the event loop.""" - await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry(domain=DOMAIN) entry.add_to_hass(hass) diff --git a/tests/components/progettihwsw/test_config_flow.py b/tests/components/progettihwsw/test_config_flow.py index 5d256af35c8..8c850e0807a 100644 --- a/tests/components/progettihwsw/test_config_flow.py +++ b/tests/components/progettihwsw/test_config_flow.py @@ -1,7 +1,7 @@ """Test the ProgettiHWSW Automation config flow.""" from unittest.mock import patch -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.progettihwsw.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.data_entry_flow import ( @@ -22,7 +22,7 @@ mock_value_step_user = { async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/prosegur/test_config_flow.py b/tests/components/prosegur/test_config_flow.py index f345d4c7fa3..46832146cf8 100644 --- a/tests/components/prosegur/test_config_flow.py +++ b/tests/components/prosegur/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock, patch from pytest import mark -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.prosegur.config_flow import CannotConnect, InvalidAuth from homeassistant.components.prosegur.const import DOMAIN from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM @@ -13,7 +13,7 @@ from tests.common import MockConfigEntry async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/rachio/test_config_flow.py b/tests/components/rachio/test_config_flow.py index 324cfd0fbf1..182f5e45dd7 100644 --- a/tests/components/rachio/test_config_flow.py +++ b/tests/components/rachio/test_config_flow.py @@ -1,7 +1,7 @@ """Test the Rachio config flow.""" from unittest.mock import MagicMock, patch -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.rachio.const import ( CONF_CUSTOM_URL, CONF_MANUAL_RUN_MINS, @@ -23,7 +23,7 @@ def _mock_rachio_return_value(get=None, info=None): async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -107,7 +107,6 @@ async def test_form_cannot_connect(hass): async def test_form_homekit(hass): """Test that we abort from homekit if rachio is already setup.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/rainforest_eagle/test_config_flow.py b/tests/components/rainforest_eagle/test_config_flow.py index a54fbdac4db..370dfebee90 100644 --- a/tests/components/rainforest_eagle/test_config_flow.py +++ b/tests/components/rainforest_eagle/test_config_flow.py @@ -1,7 +1,7 @@ """Test the Rainforest Eagle config flow.""" from unittest.mock import patch -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.rainforest_eagle.const import ( CONF_CLOUD_ID, CONF_HARDWARE_ADDRESS, @@ -17,7 +17,7 @@ from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_ async def test_form(hass: HomeAssistant) -> None: """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/rainforest_eagle/test_init.py b/tests/components/rainforest_eagle/test_init.py index 0c3305732cb..f4ce029231a 100644 --- a/tests/components/rainforest_eagle/test_init.py +++ b/tests/components/rainforest_eagle/test_init.py @@ -1,7 +1,7 @@ """Tests for the Rainforest Eagle integration.""" from unittest.mock import patch -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.rainforest_eagle.const import ( CONF_CLOUD_ID, CONF_HARDWARE_ADDRESS, @@ -17,7 +17,6 @@ from homeassistant.setup import async_setup_component async def test_import(hass: HomeAssistant) -> None: """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.rainforest_eagle.data.async_get_type", diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 42122983007..5586e06d337 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -16,7 +16,7 @@ from sqlalchemy.orm import Session from sqlalchemy.pool import StaticPool from homeassistant.bootstrap import async_setup_component -from homeassistant.components import recorder +from homeassistant.components import persistent_notification as pn, recorder from homeassistant.components.recorder import RecorderRuns, migration, models from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.components.recorder.models import States @@ -25,7 +25,7 @@ import homeassistant.util.dt as dt_util from .common import async_wait_recording_done_without_instance -from tests.common import async_fire_time_changed, async_mock_service +from tests.common import async_fire_time_changed from tests.components.recorder import models_original @@ -50,7 +50,7 @@ def create_engine_test(*args, **kwargs): async def test_schema_update_calls(hass): """Test that schema migrations occur in correct order.""" assert await recorder.async_migration_in_progress(hass) is False - await async_setup_component(hass, "persistent_notification", {}) + with patch( "homeassistant.components.recorder.create_engine", new=create_engine_test ), patch( @@ -74,7 +74,6 @@ async def test_schema_update_calls(hass): async def test_migration_in_progress(hass): """Test that we can check for migration in progress.""" assert await recorder.async_migration_in_progress(hass) is False - await async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.recorder.create_engine", new=create_engine_test @@ -91,9 +90,6 @@ async def test_migration_in_progress(hass): async def test_database_migration_failed(hass): """Test we notify if the migration fails.""" - await async_setup_component(hass, "persistent_notification", {}) - create_calls = async_mock_service(hass, "persistent_notification", "create") - dismiss_calls = async_mock_service(hass, "persistent_notification", "dismiss") assert await recorder.async_migration_in_progress(hass) is False with patch( @@ -101,7 +97,12 @@ async def test_database_migration_failed(hass): ), patch( "homeassistant.components.recorder.migration._apply_update", side_effect=ValueError, - ): + ), patch( + "homeassistant.components.persistent_notification.create", side_effect=pn.create + ) as mock_create, patch( + "homeassistant.components.persistent_notification.dismiss", + side_effect=pn.dismiss, + ) as mock_dismiss: await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} ) @@ -112,13 +113,13 @@ async def test_database_migration_failed(hass): await hass.async_block_till_done() assert await recorder.async_migration_in_progress(hass) is False - assert len(create_calls) == 2 - assert len(dismiss_calls) == 1 + assert len(mock_create.mock_calls) == 2 + assert len(mock_dismiss.mock_calls) == 1 async def test_database_migration_encounters_corruption(hass): """Test we move away the database if its corrupt.""" - await async_setup_component(hass, "persistent_notification", {}) + assert await recorder.async_migration_in_progress(hass) is False sqlite3_exception = DatabaseError("statement", {}, []) @@ -146,9 +147,6 @@ async def test_database_migration_encounters_corruption(hass): async def test_database_migration_encounters_corruption_not_sqlite(hass): """Test we fail on database error when we cannot recover.""" - await async_setup_component(hass, "persistent_notification", {}) - create_calls = async_mock_service(hass, "persistent_notification", "create") - dismiss_calls = async_mock_service(hass, "persistent_notification", "dismiss") assert await recorder.async_migration_in_progress(hass) is False with patch( @@ -159,7 +157,12 @@ async def test_database_migration_encounters_corruption_not_sqlite(hass): side_effect=DatabaseError("statement", {}, []), ), patch( "homeassistant.components.recorder.move_away_broken_database" - ) as move_away: + ) as move_away, patch( + "homeassistant.components.persistent_notification.create", side_effect=pn.create + ) as mock_create, patch( + "homeassistant.components.persistent_notification.dismiss", + side_effect=pn.dismiss, + ) as mock_dismiss: await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} ) @@ -171,15 +174,15 @@ async def test_database_migration_encounters_corruption_not_sqlite(hass): assert await recorder.async_migration_in_progress(hass) is False assert not move_away.called - assert len(create_calls) == 2 - assert len(dismiss_calls) == 1 + assert len(mock_create.mock_calls) == 2 + assert len(mock_dismiss.mock_calls) == 1 async def test_events_during_migration_are_queued(hass): """Test that events during migration are queued.""" assert await recorder.async_migration_in_progress(hass) is False - await async_setup_component(hass, "persistent_notification", {}) + with patch( "homeassistant.components.recorder.create_engine", new=create_engine_test ): @@ -202,7 +205,7 @@ async def test_events_during_migration_are_queued(hass): async def test_events_during_migration_queue_exhausted(hass): """Test that events during migration takes so long the queue is exhausted.""" - await async_setup_component(hass, "persistent_notification", {}) + assert await recorder.async_migration_in_progress(hass) is False with patch( @@ -238,8 +241,6 @@ async def test_schema_migrate(hass): inspection could quickly become quite cumbersome. """ - await async_setup_component(hass, "persistent_notification", {}) - def _mock_setup_run(self): self.run_info = RecorderRuns( start=self.recording_start, created=dt_util.utcnow() diff --git a/tests/components/renault/test_binary_sensor.py b/tests/components/renault/test_binary_sensor.py index a89c1da7808..d0fb6e544ad 100644 --- a/tests/components/renault/test_binary_sensor.py +++ b/tests/components/renault/test_binary_sensor.py @@ -8,7 +8,6 @@ from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAI from homeassistant.components.renault.renault_entities import ATTR_LAST_UPDATE from homeassistant.const import ATTR_ICON, STATE_OFF, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from . import ( check_device_registry, @@ -25,7 +24,7 @@ from tests.common import mock_device_registry, mock_registry @pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) async def test_binary_sensors(hass: HomeAssistant, vehicle_type: str): """Test for Renault binary sensors.""" - await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -52,7 +51,7 @@ async def test_binary_sensors(hass: HomeAssistant, vehicle_type: str): @pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) async def test_binary_sensor_empty(hass: HomeAssistant, vehicle_type: str): """Test for Renault binary sensors with empty data from Renault.""" - await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -82,7 +81,7 @@ async def test_binary_sensor_empty(hass: HomeAssistant, vehicle_type: str): @pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) async def test_binary_sensor_errors(hass: HomeAssistant, vehicle_type: str): """Test for Renault binary sensors with temporary failure.""" - await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -118,7 +117,7 @@ async def test_binary_sensor_errors(hass: HomeAssistant, vehicle_type: str): async def test_binary_sensor_access_denied(hass): """Test for Renault binary sensors with access denied failure.""" - await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -142,7 +141,7 @@ async def test_binary_sensor_access_denied(hass): async def test_binary_sensor_not_supported(hass): """Test for Renault binary sensors with not supported failure.""" - await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) diff --git a/tests/components/renault/test_device_tracker.py b/tests/components/renault/test_device_tracker.py index f6cac06380b..2232af18a22 100644 --- a/tests/components/renault/test_device_tracker.py +++ b/tests/components/renault/test_device_tracker.py @@ -8,7 +8,6 @@ from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOM from homeassistant.components.renault.renault_entities import ATTR_LAST_UPDATE from homeassistant.const import ATTR_ICON, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from . import ( check_device_registry, @@ -25,7 +24,7 @@ from tests.common import mock_device_registry, mock_registry @pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) async def test_device_trackers(hass: HomeAssistant, vehicle_type: str): """Test for Renault device trackers.""" - await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -52,7 +51,7 @@ async def test_device_trackers(hass: HomeAssistant, vehicle_type: str): @pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) async def test_device_tracker_empty(hass: HomeAssistant, vehicle_type: str): """Test for Renault device trackers with empty data from Renault.""" - await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -82,7 +81,7 @@ async def test_device_tracker_empty(hass: HomeAssistant, vehicle_type: str): @pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) async def test_device_tracker_errors(hass: HomeAssistant, vehicle_type: str): """Test for Renault device trackers with temporary failure.""" - await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -118,7 +117,7 @@ async def test_device_tracker_errors(hass: HomeAssistant, vehicle_type: str): async def test_device_tracker_access_denied(hass: HomeAssistant): """Test for Renault device trackers with access denied failure.""" - await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -142,7 +141,7 @@ async def test_device_tracker_access_denied(hass: HomeAssistant): async def test_device_tracker_not_supported(hass: HomeAssistant): """Test for Renault device trackers with not supported failure.""" - await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) diff --git a/tests/components/renault/test_select.py b/tests/components/renault/test_select.py index 113db099447..0bc7c4ddc68 100644 --- a/tests/components/renault/test_select.py +++ b/tests/components/renault/test_select.py @@ -14,7 +14,6 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from . import ( check_device_registry, @@ -31,7 +30,7 @@ from tests.common import load_fixture, mock_device_registry, mock_registry @pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) async def test_selects(hass: HomeAssistant, vehicle_type: str): """Test for Renault selects.""" - await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -58,7 +57,7 @@ async def test_selects(hass: HomeAssistant, vehicle_type: str): @pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) async def test_select_empty(hass: HomeAssistant, vehicle_type: str): """Test for Renault selects with empty data from Renault.""" - await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -88,7 +87,7 @@ async def test_select_empty(hass: HomeAssistant, vehicle_type: str): @pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) async def test_select_errors(hass: HomeAssistant, vehicle_type: str): """Test for Renault selects with temporary failure.""" - await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -124,7 +123,7 @@ async def test_select_errors(hass: HomeAssistant, vehicle_type: str): async def test_select_access_denied(hass: HomeAssistant): """Test for Renault selects with access denied failure.""" - await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -148,7 +147,7 @@ async def test_select_access_denied(hass: HomeAssistant): async def test_select_not_supported(hass: HomeAssistant): """Test for Renault selects with access denied failure.""" - await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) diff --git a/tests/components/renault/test_sensor.py b/tests/components/renault/test_sensor.py index 370721bb0dd..e3c758f088a 100644 --- a/tests/components/renault/test_sensor.py +++ b/tests/components/renault/test_sensor.py @@ -8,7 +8,6 @@ from homeassistant.components.renault.renault_entities import ATTR_LAST_UPDATE from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ATTR_ICON, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from . import ( check_device_registry, @@ -25,7 +24,7 @@ from tests.common import mock_device_registry, mock_registry @pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) async def test_sensors(hass: HomeAssistant, vehicle_type: str): """Test for Renault sensors.""" - await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -52,7 +51,7 @@ async def test_sensors(hass: HomeAssistant, vehicle_type: str): @pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) async def test_sensor_empty(hass: HomeAssistant, vehicle_type: str): """Test for Renault sensors with empty data from Renault.""" - await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -82,7 +81,7 @@ async def test_sensor_empty(hass: HomeAssistant, vehicle_type: str): @pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) async def test_sensor_errors(hass: HomeAssistant, vehicle_type: str): """Test for Renault sensors with temporary failure.""" - await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -118,7 +117,7 @@ async def test_sensor_errors(hass: HomeAssistant, vehicle_type: str): async def test_sensor_access_denied(hass: HomeAssistant): """Test for Renault sensors with access denied failure.""" - await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -142,7 +141,7 @@ async def test_sensor_access_denied(hass: HomeAssistant): async def test_sensor_not_supported(hass: HomeAssistant): """Test for Renault sensors with access denied failure.""" - await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py index f6445c25022..d443710f9b2 100644 --- a/tests/components/rest/test_binary_sensor.py +++ b/tests/components/rest/test_binary_sensor.py @@ -26,7 +26,7 @@ async def test_setup_missing_basic_config(hass): hass, binary_sensor.DOMAIN, {"binary_sensor": {"platform": "rest"}} ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 + assert len(hass.states.async_all("binary_sensor")) == 0 async def test_setup_missing_config(hass): @@ -43,7 +43,7 @@ async def test_setup_missing_config(hass): }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 + assert len(hass.states.async_all("binary_sensor")) == 0 @respx.mock @@ -65,7 +65,7 @@ async def test_setup_failed_connect(hass, caplog): }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 + assert len(hass.states.async_all("binary_sensor")) == 0 assert "server offline" in caplog.text @@ -85,7 +85,7 @@ async def test_setup_timeout(hass): }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 + assert len(hass.states.async_all("binary_sensor")) == 0 @respx.mock @@ -104,7 +104,7 @@ async def test_setup_minimum(hass): }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("binary_sensor")) == 1 @respx.mock @@ -122,7 +122,7 @@ async def test_setup_minimum_resource_template(hass): }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("binary_sensor")) == 1 @respx.mock @@ -141,7 +141,7 @@ async def test_setup_duplicate_resource_template(hass): }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 + assert len(hass.states.async_all("binary_sensor")) == 0 @respx.mock @@ -169,7 +169,7 @@ async def test_setup_get(hass): ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("binary_sensor")) == 1 @respx.mock @@ -197,7 +197,7 @@ async def test_setup_get_digest_auth(hass): ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("binary_sensor")) == 1 @respx.mock @@ -225,7 +225,7 @@ async def test_setup_post(hass): }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("binary_sensor")) == 1 @respx.mock @@ -252,7 +252,7 @@ async def test_setup_get_off(hass): }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("binary_sensor")) == 1 state = hass.states.get("binary_sensor.foo") assert state.state == STATE_OFF @@ -282,7 +282,7 @@ async def test_setup_get_on(hass): }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("binary_sensor")) == 1 state = hass.states.get("binary_sensor.foo") assert state.state == STATE_ON @@ -308,7 +308,7 @@ async def test_setup_with_exception(hass): }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("binary_sensor")) == 1 state = hass.states.get("binary_sensor.foo") assert state.state == STATE_OFF @@ -352,7 +352,7 @@ async def test_reload(hass): await hass.async_start() await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("binary_sensor")) == 1 assert hass.states.get("binary_sensor.mockrest") @@ -391,7 +391,7 @@ async def test_setup_query_params(hass): }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("binary_sensor")) == 1 def _get_fixtures_base_path(): diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index 50b959be36b..f50f5aba3bc 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -26,7 +26,7 @@ async def test_setup_missing_config(hass): hass, sensor.DOMAIN, {"sensor": {"platform": "rest"}} ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 + assert len(hass.states.async_all("sensor")) == 0 async def test_setup_missing_schema(hass): @@ -37,7 +37,7 @@ async def test_setup_missing_schema(hass): {"sensor": {"platform": "rest", "resource": "localhost", "method": "GET"}}, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 + assert len(hass.states.async_all("sensor")) == 0 @respx.mock @@ -58,7 +58,7 @@ async def test_setup_failed_connect(hass, caplog): }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 + assert len(hass.states.async_all("sensor")) == 0 assert "server offline" in caplog.text @@ -72,7 +72,7 @@ async def test_setup_timeout(hass): {"sensor": {"platform": "rest", "resource": "localhost", "method": "GET"}}, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 + assert len(hass.states.async_all("sensor")) == 0 @respx.mock @@ -91,7 +91,7 @@ async def test_setup_minimum(hass): }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("sensor")) == 1 @respx.mock @@ -113,7 +113,7 @@ async def test_manual_update(hass): }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("sensor")) == 1 assert hass.states.get("sensor.mysensor").state == "first" respx.get("http://localhost").respond(status_code=200, json={"data": "second"}) @@ -141,7 +141,7 @@ async def test_setup_minimum_resource_template(hass): }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("sensor")) == 1 @respx.mock @@ -160,7 +160,7 @@ async def test_setup_duplicate_resource_template(hass): }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 + assert len(hass.states.async_all("sensor")) == 0 @respx.mock @@ -190,7 +190,7 @@ async def test_setup_get(hass): await async_setup_component(hass, "homeassistant", {}) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("sensor")) == 1 assert hass.states.get("sensor.foo").state == "" await hass.services.async_call( @@ -229,7 +229,7 @@ async def test_setup_get_digest_auth(hass): ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("sensor")) == 1 @respx.mock @@ -258,7 +258,7 @@ async def test_setup_post(hass): }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("sensor")) == 1 @respx.mock @@ -286,7 +286,7 @@ async def test_setup_get_xml(hass): }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("sensor")) == 1 state = hass.states.get("sensor.foo") assert state.state == "abc" @@ -310,7 +310,7 @@ async def test_setup_query_params(hass): }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("sensor")) == 1 @respx.mock @@ -339,7 +339,7 @@ async def test_update_with_json_attrs(hass): }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("sensor")) == 1 state = hass.states.get("sensor.foo") assert state.state == "some_json_value" @@ -372,7 +372,7 @@ async def test_update_with_no_template(hass): }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("sensor")) == 1 state = hass.states.get("sensor.foo") assert state.state == '{"key": "some_json_value"}' @@ -406,7 +406,7 @@ async def test_update_with_json_attrs_no_data(hass, caplog): }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("sensor")) == 1 state = hass.states.get("sensor.foo") assert state.state == STATE_UNKNOWN @@ -441,7 +441,7 @@ async def test_update_with_json_attrs_not_dict(hass, caplog): }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("sensor")) == 1 state = hass.states.get("sensor.foo") assert state.state == "" @@ -477,7 +477,7 @@ async def test_update_with_json_attrs_bad_JSON(hass, caplog): }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("sensor")) == 1 state = hass.states.get("sensor.foo") assert state.state == STATE_UNKNOWN @@ -521,7 +521,7 @@ async def test_update_with_json_attrs_with_json_attrs_path(hass): }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("sensor")) == 1 state = hass.states.get("sensor.foo") assert state.state == "master" @@ -557,7 +557,7 @@ async def test_update_with_xml_convert_json_attrs_with_json_attrs_path(hass): }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("sensor")) == 1 state = hass.states.get("sensor.foo") assert state.state == "master" @@ -593,7 +593,7 @@ async def test_update_with_xml_convert_json_attrs_with_jsonattr_template(hass): }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("sensor")) == 1 state = hass.states.get("sensor.foo") assert state.state == "bogus" @@ -634,7 +634,7 @@ async def test_update_with_application_xml_convert_json_attrs_with_jsonattr_temp }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("sensor")) == 1 state = hass.states.get("sensor.foo") assert state.state == "1" @@ -669,7 +669,7 @@ async def test_update_with_xml_convert_bad_xml(hass, caplog): }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("sensor")) == 1 state = hass.states.get("sensor.foo") assert state.state == STATE_UNKNOWN @@ -704,7 +704,7 @@ async def test_update_with_failed_get(hass, caplog): }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("sensor")) == 1 state = hass.states.get("sensor.foo") assert state.state == STATE_UNKNOWN @@ -734,7 +734,7 @@ async def test_reload(hass): await hass.async_start() await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("sensor")) == 1 assert hass.states.get("sensor.mockrest") diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index 07c316618e3..4aa275fa3b4 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch, sentinel import serial.tools.list_ports -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.rfxtrx import DOMAIN, config_flow from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -277,7 +277,6 @@ async def test_setup_serial_manual_fail(com_mock, hass): async def test_options_global(hass): """Test if we can set global options.""" - await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry( domain=DOMAIN, @@ -310,7 +309,6 @@ async def test_options_global(hass): async def test_options_add_device(hass): """Test we can add a device.""" - await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry( domain=DOMAIN, @@ -376,7 +374,6 @@ async def test_options_add_device(hass): async def test_options_add_duplicate_device(hass): """Test we can add a device.""" - await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry( domain=DOMAIN, @@ -413,7 +410,6 @@ async def test_options_add_duplicate_device(hass): async def test_options_add_remove_device(hass): """Test we can add a device.""" - await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry( domain=DOMAIN, @@ -497,7 +493,6 @@ async def test_options_add_remove_device(hass): async def test_options_replace_sensor_device(hass): """Test we can replace a sensor device.""" - await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry( domain=DOMAIN, @@ -658,7 +653,6 @@ async def test_options_replace_sensor_device(hass): async def test_options_replace_control_device(hass): """Test we can replace a control device.""" - await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry( domain=DOMAIN, @@ -767,7 +761,6 @@ async def test_options_replace_control_device(hass): async def test_options_remove_multiple_devices(hass): """Test we can add a device.""" - await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry( domain=DOMAIN, @@ -838,7 +831,6 @@ async def test_options_remove_multiple_devices(hass): async def test_options_add_and_configure_device(hass): """Test we can add a device.""" - await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry( domain=DOMAIN, @@ -960,7 +952,6 @@ async def test_options_add_and_configure_device(hass): async def test_options_configure_rfy_cover_device(hass): """Test we can configure the venetion blind mode of an Rfy cover.""" - await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/rfxtrx/test_device_action.py b/tests/components/rfxtrx/test_device_action.py index cedf2082fb2..6714bfdaea9 100644 --- a/tests/components/rfxtrx/test_device_action.py +++ b/tests/components/rfxtrx/test_device_action.py @@ -15,7 +15,6 @@ from tests.common import ( MockConfigEntry, assert_lists_same, async_get_device_automations, - async_mock_service, mock_device_registry, mock_registry, ) @@ -169,7 +168,6 @@ async def test_action( async def test_invalid_action(hass, device_reg: DeviceRegistry): """Test for invalid actions.""" device = DEVICE_LIGHTING_1 - notification_calls = async_mock_service(hass, "persistent_notification", "create") await setup_entry(hass, {device.code: {"signal_repetitions": 1}}) @@ -199,8 +197,8 @@ async def test_invalid_action(hass, device_reg: DeviceRegistry): ) await hass.async_block_till_done() - assert len(notification_calls) == 1 + assert len(notifications := hass.states.async_all("persistent_notification")) == 1 assert ( "The following integrations and platforms could not be set up" - in notification_calls[0].data["message"] + in notifications[0].attributes["message"] ) diff --git a/tests/components/rfxtrx/test_device_trigger.py b/tests/components/rfxtrx/test_device_trigger.py index 9ac2c7e9819..90be97bd56f 100644 --- a/tests/components/rfxtrx/test_device_trigger.py +++ b/tests/components/rfxtrx/test_device_trigger.py @@ -148,7 +148,6 @@ async def test_firing_event(hass, device_reg: DeviceRegistry, rfxtrx, event): async def test_invalid_trigger(hass, device_reg: DeviceRegistry): """Test for invalid actions.""" event = EVENT_LIGHTING_1 - notification_calls = async_mock_service(hass, "persistent_notification", "create") await setup_entry(hass, {event.code: {"fire_event": True, "signal_repetitions": 1}}) @@ -179,8 +178,8 @@ async def test_invalid_trigger(hass, device_reg: DeviceRegistry): ) await hass.async_block_till_done() - assert len(notification_calls) == 1 + assert len(notifications := hass.states.async_all("persistent_notification")) == 1 assert ( "The following integrations and platforms could not be set up" - in notification_calls[0].data["message"] + in notifications[0].attributes["message"] ) diff --git a/tests/components/ring/test_config_flow.py b/tests/components/ring/test_config_flow.py index 85ca4ffb558..34ff53ba8a3 100644 --- a/tests/components/ring/test_config_flow.py +++ b/tests/components/ring/test_config_flow.py @@ -1,14 +1,14 @@ """Test the Ring config flow.""" from unittest.mock import Mock, patch -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.ring import DOMAIN from homeassistant.components.ring.config_flow import InvalidAuth async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/roku/test_config_flow.py b/tests/components/roku/test_config_flow.py index fde46b6621c..743d69167fe 100644 --- a/tests/components/roku/test_config_flow.py +++ b/tests/components/roku/test_config_flow.py @@ -10,7 +10,6 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM, ) -from homeassistant.setup import async_setup_component from tests.components.roku import ( HOMEKIT_HOST, @@ -59,7 +58,7 @@ async def test_duplicate_error( async def test_form(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None: """Test the user step.""" - await async_setup_component(hass, "persistent_notification", {}) + mock_connection(aioclient_mock) result = await hass.config_entries.flow.async_init( diff --git a/tests/components/roomba/test_config_flow.py b/tests/components/roomba/test_config_flow.py index cdb1c681f5c..54554af6ecb 100644 --- a/tests/components/roomba/test_config_flow.py +++ b/tests/components/roomba/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, PropertyMock, patch import pytest from roombapy import RoombaConnectionError, RoombaInfo -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.components.roomba import config_flow from homeassistant.components.roomba.const import CONF_BLID, CONF_CONTINUOUS, DOMAIN @@ -114,7 +114,6 @@ def _mocked_connection_refused_on_getpassword(*_): async def test_form_user_discovery_and_password_fetch(hass): """Test we can discovery and fetch the password.""" - await setup.async_setup_component(hass, "persistent_notification", {}) mocked_roomba = _create_mocked_roomba( roomba_connected=True, @@ -173,7 +172,6 @@ async def test_form_user_discovery_and_password_fetch(hass): async def test_form_user_discovery_skips_known(hass): """Test discovery proceeds to manual if all discovered are already known.""" - await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry(domain=DOMAIN, data=VALID_CONFIG, unique_id="BLID") entry.add_to_hass(hass) @@ -193,7 +191,6 @@ async def test_form_user_discovery_skips_known(hass): async def test_form_user_no_devices_found_discovery_aborts_already_configured(hass): """Test if we manually configure an existing host we abort.""" - await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry(domain=DOMAIN, data=VALID_CONFIG, unique_id="BLID") entry.add_to_hass(hass) @@ -222,7 +219,6 @@ async def test_form_user_no_devices_found_discovery_aborts_already_configured(ha async def test_form_user_discovery_manual_and_auto_password_fetch(hass): """Test discovery skipped and we can auto fetch the password.""" - await setup.async_setup_component(hass, "persistent_notification", {}) mocked_roomba = _create_mocked_roomba( roomba_connected=True, @@ -293,7 +289,6 @@ async def test_form_user_discovery_manual_and_auto_password_fetch(hass): async def test_form_user_discover_fails_aborts_already_configured(hass): """Test if we manually configure an existing host we abort after failed discovery.""" - await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry(domain=DOMAIN, data=VALID_CONFIG, unique_id="BLID") entry.add_to_hass(hass) @@ -324,7 +319,6 @@ async def test_form_user_discovery_manual_and_auto_password_fetch_but_cannot_con hass, ): """Test discovery skipped and we can auto fetch the password then we fail to connect.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery @@ -363,7 +357,6 @@ async def test_form_user_discovery_manual_and_auto_password_fetch_but_cannot_con async def test_form_user_discovery_no_devices_found_and_auto_password_fetch(hass): """Test discovery finds no devices and we can auto fetch the password.""" - await setup.async_setup_component(hass, "persistent_notification", {}) mocked_roomba = _create_mocked_roomba( roomba_connected=True, @@ -425,7 +418,6 @@ async def test_form_user_discovery_no_devices_found_and_auto_password_fetch(hass async def test_form_user_discovery_no_devices_found_and_password_fetch_fails(hass): """Test discovery finds no devices and password fetch fails.""" - await setup.async_setup_component(hass, "persistent_notification", {}) mocked_roomba = _create_mocked_roomba( roomba_connected=True, @@ -496,7 +488,6 @@ async def test_form_user_discovery_not_devices_found_and_password_fetch_fails_an hass, ): """Test discovery finds no devices and password fetch fails then we cannot connect.""" - await setup.async_setup_component(hass, "persistent_notification", {}) mocked_roomba = _create_mocked_roomba( connect=RoombaConnectionError, @@ -558,7 +549,6 @@ async def test_form_user_discovery_not_devices_found_and_password_fetch_fails_an async def test_form_user_discovery_and_password_fetch_gets_connection_refused(hass): """Test we can discovery and fetch the password manually.""" - await setup.async_setup_component(hass, "persistent_notification", {}) mocked_roomba = _create_mocked_roomba( roomba_connected=True, @@ -625,7 +615,6 @@ async def test_form_user_discovery_and_password_fetch_gets_connection_refused(ha @pytest.mark.parametrize("discovery_data", DHCP_DISCOVERY_DEVICES) async def test_dhcp_discovery_and_roomba_discovery_finds(hass, discovery_data): """Test we can process the discovery from dhcp and roomba discovery matches the device.""" - await setup.async_setup_component(hass, "persistent_notification", {}) mocked_roomba = _create_mocked_roomba( roomba_connected=True, @@ -679,7 +668,6 @@ async def test_dhcp_discovery_and_roomba_discovery_finds(hass, discovery_data): @pytest.mark.parametrize("discovery_data", DHCP_DISCOVERY_DEVICES_WITHOUT_MATCHING_IP) async def test_dhcp_discovery_falls_back_to_manual(hass, discovery_data): """Test we can process the discovery from dhcp but roomba discovery cannot find the specific device.""" - await setup.async_setup_component(hass, "persistent_notification", {}) mocked_roomba = _create_mocked_roomba( roomba_connected=True, @@ -752,7 +740,6 @@ async def test_dhcp_discovery_falls_back_to_manual(hass, discovery_data): @pytest.mark.parametrize("discovery_data", DHCP_DISCOVERY_DEVICES_WITHOUT_MATCHING_IP) async def test_dhcp_discovery_no_devices_falls_back_to_manual(hass, discovery_data): """Test we can process the discovery from dhcp but roomba discovery cannot find any devices.""" - await setup.async_setup_component(hass, "persistent_notification", {}) mocked_roomba = _create_mocked_roomba( roomba_connected=True, @@ -816,7 +803,6 @@ async def test_dhcp_discovery_no_devices_falls_back_to_manual(hass, discovery_da async def test_dhcp_discovery_with_ignored(hass): """Test ignored entries do not break checking for existing entries.""" - await setup.async_setup_component(hass, "persistent_notification", {}) config_entry = MockConfigEntry( domain=DOMAIN, data={}, source=config_entries.SOURCE_IGNORE @@ -842,7 +828,6 @@ async def test_dhcp_discovery_with_ignored(hass): async def test_dhcp_discovery_already_configured_host(hass): """Test we abort if the host is already configured.""" - await setup.async_setup_component(hass, "persistent_notification", {}) config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_HOST: MOCK_IP}) config_entry.add_to_hass(hass) @@ -867,7 +852,6 @@ async def test_dhcp_discovery_already_configured_host(hass): async def test_dhcp_discovery_already_configured_blid(hass): """Test we abort if the blid is already configured.""" - await setup.async_setup_component(hass, "persistent_notification", {}) config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_BLID: "BLID"}, unique_id="BLID" @@ -894,7 +878,6 @@ async def test_dhcp_discovery_already_configured_blid(hass): async def test_dhcp_discovery_not_irobot(hass): """Test we abort if the discovered device is not an irobot device.""" - await setup.async_setup_component(hass, "persistent_notification", {}) config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_BLID: "BLID"}, unique_id="BLID" @@ -921,7 +904,6 @@ async def test_dhcp_discovery_not_irobot(hass): async def test_dhcp_discovery_partial_hostname(hass): """Test we abort flows when we have a partial hostname.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery diff --git a/tests/components/roon/test_config_flow.py b/tests/components/roon/test_config_flow.py index a30441c24ff..686109f968e 100644 --- a/tests/components/roon/test_config_flow.py +++ b/tests/components/roon/test_config_flow.py @@ -1,7 +1,7 @@ """Test the roon config flow.""" from unittest.mock import patch -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.roon.const import DOMAIN @@ -64,7 +64,6 @@ class RoonDiscoveryFailedMock(RoonDiscoveryMock): async def test_successful_discovery_and_auth(hass): """Test when discovery and auth both work ok.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.roon.config_flow.RoonApi", return_value=RoonApiMock(), @@ -102,7 +101,6 @@ async def test_successful_discovery_and_auth(hass): async def test_unsuccessful_discovery_user_form_and_auth(hass): """Test unsuccessful discover, user adding the host via the form and then successful auth.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.roon.config_flow.RoonApi", return_value=RoonApiMock(), @@ -143,7 +141,6 @@ async def test_unsuccessful_discovery_user_form_and_auth(hass): async def test_successful_discovery_no_auth(hass): """Test successful discover, but failed auth.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.roon.config_flow.RoonApi", return_value=RoonApiMockNoToken(), @@ -182,7 +179,6 @@ async def test_successful_discovery_no_auth(hass): async def test_unexpected_exception(hass): """Test successful discover, and unexpected exception during auth.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.roon.config_flow.RoonApi", return_value=RoonApiMockException(), diff --git a/tests/components/samsungtv/__init__.py b/tests/components/samsungtv/__init__.py index 84328736822..89768221665 100644 --- a/tests/components/samsungtv/__init__.py +++ b/tests/components/samsungtv/__init__.py @@ -1,14 +1,13 @@ """Tests for the samsungtv component.""" from homeassistant.components.samsungtv.const import DOMAIN as SAMSUNGTV_DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry async def setup_samsungtv(hass: HomeAssistant, config: dict): """Set up mock Samsung TV.""" - await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry(domain=SAMSUNGTV_DOMAIN, data=config) entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index 1f6c13809cb..500c39d677a 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -125,7 +125,7 @@ async def test_setup_duplicate_config(hass: HomeAssistant, remote: Mock, caplog) await async_setup_component(hass, SAMSUNGTV_DOMAIN, DUPLICATE) await hass.async_block_till_done() assert hass.states.get(ENTITY_ID) is None - assert len(hass.states.async_all()) == 0 + assert len(hass.states.async_all("media_player")) == 0 assert "duplicate host entries found" in caplog.text @@ -136,6 +136,6 @@ async def test_setup_duplicate_entries( await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) await hass.async_block_till_done() assert hass.states.get(ENTITY_ID) - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("media_player")) == 1 await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("media_player")) == 1 diff --git a/tests/components/screenlogic/test_config_flow.py b/tests/components/screenlogic/test_config_flow.py index a24ce36e7a1..d1333cb7514 100644 --- a/tests/components/screenlogic/test_config_flow.py +++ b/tests/components/screenlogic/test_config_flow.py @@ -10,7 +10,7 @@ from screenlogicpy.const import ( SL_GATEWAY_TYPE, ) -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS from homeassistant.components.screenlogic.config_flow import ( GATEWAY_MANUAL_ENTRY, @@ -28,7 +28,7 @@ from tests.common import MockConfigEntry async def test_flow_discovery(hass): """Test the flow works with basic discovery.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( "homeassistant.components.screenlogic.config_flow.discovery.async_discover", return_value=[ @@ -72,7 +72,7 @@ async def test_flow_discovery(hass): async def test_flow_discover_none(hass): """Test when nothing is discovered.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( "homeassistant.components.screenlogic.config_flow.discovery.async_discover", return_value=[], @@ -88,7 +88,7 @@ async def test_flow_discover_none(hass): async def test_flow_discover_error(hass): """Test when discovery errors.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( "homeassistant.components.screenlogic.config_flow.discovery.async_discover", side_effect=ScreenLogicError("Fake error"), @@ -134,7 +134,7 @@ async def test_flow_discover_error(hass): async def test_dhcp(hass): """Test DHCP discovery flow.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, @@ -180,7 +180,7 @@ async def test_dhcp(hass): async def test_form_manual_entry(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( "homeassistant.components.screenlogic.config_flow.discovery.async_discover", return_value=[ diff --git a/tests/components/sense/test_config_flow.py b/tests/components/sense/test_config_flow.py index a56422dcb84..0058c05bf80 100644 --- a/tests/components/sense/test_config_flow.py +++ b/tests/components/sense/test_config_flow.py @@ -3,13 +3,13 @@ from unittest.mock import patch from sense_energy import SenseAPITimeoutException, SenseAuthenticationException -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.sense.const import DOMAIN async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/sentry/test_config_flow.py b/tests/components/sentry/test_config_flow.py index 2876f6e3a17..77dd440a2da 100644 --- a/tests/components/sentry/test_config_flow.py +++ b/tests/components/sentry/test_config_flow.py @@ -22,14 +22,13 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM, ) -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry async def test_full_user_flow_implementation(hass: HomeAssistant) -> None: """Test we get the form.""" - await async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) diff --git a/tests/components/seventeentrack/test_sensor.py b/tests/components/seventeentrack/test_sensor.py index 5ad904530b9..cef92496561 100644 --- a/tests/components/seventeentrack/test_sensor.py +++ b/tests/components/seventeentrack/test_sensor.py @@ -166,7 +166,7 @@ async def test_invalid_config(hass): """Ensure nothing is created when config is wrong.""" assert await async_setup_component(hass, "sensor", INVALID_CONFIG) - assert not hass.states.async_entity_ids() + assert not hass.states.async_entity_ids("sensor") async def test_add_package(hass): diff --git a/tests/components/sharkiq/test_config_flow.py b/tests/components/sharkiq/test_config_flow.py index dc631f48a46..ef69f793d7c 100644 --- a/tests/components/sharkiq/test_config_flow.py +++ b/tests/components/sharkiq/test_config_flow.py @@ -5,7 +5,7 @@ import aiohttp import pytest from sharkiqpy import AylaApi, SharkIqAuthError -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.sharkiq.const import DOMAIN from homeassistant.core import HomeAssistant @@ -16,7 +16,7 @@ from tests.common import MockConfigEntry async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 1cc102715c5..f17e01e118b 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -6,7 +6,7 @@ import aiohttp import aioshelly import pytest -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.shelly.const import DOMAIN from tests.common import MockConfigEntry @@ -28,7 +28,7 @@ MOCK_CONFIG = { @pytest.mark.parametrize("gen", [1, 2]) async def test_form(hass, gen): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -81,7 +81,7 @@ async def test_form(hass, gen): async def test_title_without_name(hass): """Test we set the title to the hostname when the device doesn't have a name.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -227,7 +227,7 @@ async def test_form_errors_test_connection(hass, error): async def test_form_already_configured(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain="shelly", unique_id="test-mac", data={"host": "0.0.0.0"} ) @@ -255,7 +255,7 @@ async def test_form_already_configured(hass): async def test_user_setup_ignored_device(hass): """Test user can successfully setup an ignored device.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain="shelly", unique_id="test-mac", @@ -361,7 +361,6 @@ async def test_form_auth_errors_test_connection(hass, error): async def test_zeroconf(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "aioshelly.common.get_info", @@ -415,7 +414,6 @@ async def test_zeroconf(hass): async def test_zeroconf_sleeping_device(hass): """Test sleeping device configuration via zeroconf.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "aioshelly.common.get_info", @@ -489,7 +487,6 @@ async def test_zeroconf_sleeping_device(hass): async def test_zeroconf_sleeping_device_error(hass, error): """Test sleeping device configuration via zeroconf with error.""" exc = error - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "aioshelly.common.get_info", @@ -514,7 +511,7 @@ async def test_zeroconf_sleeping_device_error(hass, error): async def test_zeroconf_already_configured(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain="shelly", unique_id="test-mac", data={"host": "0.0.0.0"} ) @@ -566,7 +563,6 @@ async def test_zeroconf_cannot_connect(hass): async def test_zeroconf_require_auth(hass): """Test zeroconf if auth is required.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "aioshelly.common.get_info", diff --git a/tests/components/shelly/test_device_trigger.py b/tests/components/shelly/test_device_trigger.py index 67e4660d167..41fbad2f8e3 100644 --- a/tests/components/shelly/test_device_trigger.py +++ b/tests/components/shelly/test_device_trigger.py @@ -3,7 +3,6 @@ from unittest.mock import AsyncMock, Mock import pytest -from homeassistant import setup from homeassistant.components import automation from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, @@ -26,7 +25,6 @@ from tests.common import ( MockConfigEntry, assert_lists_same, async_get_device_automations, - async_mock_service, ) @@ -187,7 +185,6 @@ async def test_get_triggers_for_invalid_device_id(hass, device_reg, coap_wrapper async def test_if_fires_on_click_event_block_device(hass, calls, coap_wrapper): """Test for click_event trigger firing for block device.""" assert coap_wrapper - await setup.async_setup_component(hass, "persistent_notification", {}) assert await async_setup_component( hass, @@ -226,7 +223,6 @@ async def test_if_fires_on_click_event_block_device(hass, calls, coap_wrapper): async def test_if_fires_on_click_event_rpc_device(hass, calls, rpc_wrapper): """Test for click_event trigger firing for rpc device.""" assert rpc_wrapper - await setup.async_setup_component(hass, "persistent_notification", {}) assert await async_setup_component( hass, @@ -265,7 +261,6 @@ async def test_if_fires_on_click_event_rpc_device(hass, calls, rpc_wrapper): async def test_validate_trigger_block_device_not_ready(hass, calls, coap_wrapper): """Test validate trigger config when block device is not ready.""" assert coap_wrapper - await setup.async_setup_component(hass, "persistent_notification", {}) assert await async_setup_component( hass, @@ -303,7 +298,6 @@ async def test_validate_trigger_block_device_not_ready(hass, calls, coap_wrapper async def test_validate_trigger_rpc_device_not_ready(hass, calls, rpc_wrapper): """Test validate trigger config when RPC device is not ready.""" assert rpc_wrapper - await setup.async_setup_component(hass, "persistent_notification", {}) assert await async_setup_component( hass, @@ -341,7 +335,6 @@ async def test_validate_trigger_rpc_device_not_ready(hass, calls, rpc_wrapper): async def test_validate_trigger_invalid_triggers(hass, coap_wrapper): """Test for click_event with invalid triggers.""" assert coap_wrapper - notification_calls = async_mock_service(hass, "persistent_notification", "create") assert await async_setup_component( hass, @@ -365,8 +358,8 @@ async def test_validate_trigger_invalid_triggers(hass, coap_wrapper): }, ) - assert len(notification_calls) == 1 + assert len(notifications := hass.states.async_all("persistent_notification")) == 1 assert ( "The following integrations and platforms could not be set up" - in notification_calls[0].data["message"] + in notifications[0].attributes["message"] ) diff --git a/tests/components/sma/test_config_flow.py b/tests/components/sma/test_config_flow.py index f262f7eeba1..9194bc15d6f 100644 --- a/tests/components/sma/test_config_flow.py +++ b/tests/components/sma/test_config_flow.py @@ -7,7 +7,6 @@ from pysma.exceptions import ( SmaReadException, ) -from homeassistant import setup from homeassistant.components.sma.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.data_entry_flow import ( @@ -28,7 +27,7 @@ from . import ( async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -153,7 +152,6 @@ async def test_form_already_configured(hass, mock_config_entry): async def test_import(hass): """Test we can import.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch("pysma.SMA.new_session", return_value=True), patch( "pysma.SMA.device_info", return_value=MOCK_DEVICE @@ -174,7 +172,6 @@ async def test_import(hass): async def test_import_sensor_dict(hass): """Test we can import.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with patch("pysma.SMA.new_session", return_value=True), patch( "pysma.SMA.device_info", return_value=MOCK_DEVICE diff --git a/tests/components/smart_meter_texas/test_config_flow.py b/tests/components/smart_meter_texas/test_config_flow.py index 246ae4edc7d..3d0662f5b44 100644 --- a/tests/components/smart_meter_texas/test_config_flow.py +++ b/tests/components/smart_meter_texas/test_config_flow.py @@ -9,7 +9,7 @@ from smart_meter_texas.exceptions import ( SmartMeterTexasAuthError, ) -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.smart_meter_texas.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -20,7 +20,7 @@ TEST_LOGIN = {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"} async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/smarthab/test_config_flow.py b/tests/components/smarthab/test_config_flow.py index 6201d6f6f28..e6e9dc86101 100644 --- a/tests/components/smarthab/test_config_flow.py +++ b/tests/components/smarthab/test_config_flow.py @@ -3,14 +3,14 @@ from unittest.mock import patch import pysmarthab -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.smarthab import DOMAIN from homeassistant.const import CONF_EMAIL, CONF_PASSWORD async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -98,7 +98,6 @@ async def test_form_unknown_error(hass): async def test_import(hass): """Test import.""" - await setup.async_setup_component(hass, "persistent_notification", {}) imported_conf = { CONF_EMAIL: "mock@example.com", diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index 7504a1536d1..43f88dfb7af 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -22,14 +22,13 @@ from homeassistant.components.smartthings.const import ( from homeassistant.config import async_process_ha_core_config from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry async def test_migration_creates_new_flow(hass, smartthings_mock, config_entry): """Test migration deletes app and creates new flow.""" - assert await async_setup_component(hass, "persistent_notification", {}) + config_entry.version = 1 config_entry.add_to_hass(hass) @@ -55,7 +54,7 @@ async def test_unrecoverable_api_errors_create_new_flow( 403 (forbidden/not found): Occurs when the app or installed app could not be retrieved/found (likely deleted?) """ - assert await async_setup_component(hass, "persistent_notification", {}) + config_entry.add_to_hass(hass) request_info = Mock(real_url="http://example.com") smartthings_mock.app.side_effect = ClientResponseError( diff --git a/tests/components/solarlog/test_config_flow.py b/tests/components/solarlog/test_config_flow.py index d3ba3c3c84a..ccbc5412562 100644 --- a/tests/components/solarlog/test_config_flow.py +++ b/tests/components/solarlog/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.solarlog import config_flow from homeassistant.components.solarlog.const import DEFAULT_HOST, DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME @@ -16,7 +16,7 @@ HOST = "http://1.1.1.1" async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/somfy_mylink/test_config_flow.py b/tests/components/somfy_mylink/test_config_flow.py index d74b4392f31..38cb2a52dfd 100644 --- a/tests/components/somfy_mylink/test_config_flow.py +++ b/tests/components/somfy_mylink/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.components.somfy_mylink.const import ( CONF_REVERSED_TARGET_IDS, @@ -18,7 +18,7 @@ from tests.common import MockConfigEntry async def test_form_user(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -54,7 +54,6 @@ async def test_form_user(hass): async def test_form_user_already_configured(hass): """Test we abort if already configured.""" - await setup.async_setup_component(hass, "persistent_notification", {}) config_entry = MockConfigEntry( domain=DOMAIN, @@ -163,7 +162,6 @@ async def test_form_unknown_error(hass): async def test_options_not_loaded(hass): """Test options will not display until loaded.""" - await setup.async_setup_component(hass, "persistent_notification", {}) config_entry = MockConfigEntry( domain=DOMAIN, @@ -183,7 +181,6 @@ async def test_options_not_loaded(hass): @pytest.mark.parametrize("reversed", [True, False]) async def test_options_with_targets(hass, reversed): """Test we can configure reverse for a target.""" - await setup.async_setup_component(hass, "persistent_notification", {}) config_entry = MockConfigEntry( domain=DOMAIN, @@ -238,7 +235,6 @@ async def test_options_with_targets(hass, reversed): async def test_form_user_already_configured_from_dhcp(hass): """Test we abort if already configured from dhcp.""" - await setup.async_setup_component(hass, "persistent_notification", {}) config_entry = MockConfigEntry( domain=DOMAIN, @@ -271,7 +267,6 @@ async def test_form_user_already_configured_from_dhcp(hass): async def test_already_configured_with_ignored(hass): """Test ignored entries do not break checking for existing entries.""" - await setup.async_setup_component(hass, "persistent_notification", {}) config_entry = MockConfigEntry( domain=DOMAIN, data={}, source=config_entries.SOURCE_IGNORE @@ -292,7 +287,7 @@ async def test_already_configured_with_ignored(hass): async def test_dhcp_discovery(hass): """Test we can process the discovery from dhcp.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, diff --git a/tests/components/sonos/test_config_flow.py b/tests/components/sonos/test_config_flow.py index faf4e07ac9c..39f3966e2ce 100644 --- a/tests/components/sonos/test_config_flow.py +++ b/tests/components/sonos/test_config_flow.py @@ -3,14 +3,14 @@ from __future__ import annotations from unittest.mock import MagicMock, patch -from homeassistant import config_entries, core, setup +from homeassistant import config_entries, core from homeassistant.components.sonos.const import DATA_SONOS_DISCOVERY_MANAGER, DOMAIN @patch("homeassistant.components.sonos.config_flow.soco.discover", return_value=True) async def test_user_form(discover_mock: MagicMock, hass: core.HomeAssistant): """Test we get the user initiated form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -38,7 +38,7 @@ async def test_user_form(discover_mock: MagicMock, hass: core.HomeAssistant): async def test_zeroconf_form(hass: core.HomeAssistant): """Test we pass sonos devices to the discovery manager.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + mock_manager = hass.data[DATA_SONOS_DISCOVERY_MANAGER] = MagicMock() result = await hass.config_entries.flow.async_init( DOMAIN, @@ -78,7 +78,7 @@ async def test_zeroconf_form(hass: core.HomeAssistant): async def test_zeroconf_form_not_sonos(hass: core.HomeAssistant): """Test we abort on non-sonos devices.""" mock_manager = hass.data[DATA_SONOS_DISCOVERY_MANAGER] = MagicMock() - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, diff --git a/tests/components/spider/test_config_flow.py b/tests/components/spider/test_config_flow.py index e8d10b51cf3..d00ab53645d 100644 --- a/tests/components/spider/test_config_flow.py +++ b/tests/components/spider/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import Mock, patch import pytest -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.spider.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -27,7 +27,7 @@ def spider_fixture() -> Mock: async def test_user(hass, spider): """Test user config.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -57,7 +57,7 @@ async def test_user(hass, spider): async def test_import(hass, spider): """Test import step.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( "homeassistant.components.spider.async_setup", return_value=True, diff --git a/tests/components/surepetcare/test_config_flow.py b/tests/components/surepetcare/test_config_flow.py index d52dd025148..e504a807f08 100644 --- a/tests/components/surepetcare/test_config_flow.py +++ b/tests/components/surepetcare/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import NonCallableMagicMock, patch from surepy.exceptions import SurePetcareAuthenticationError, SurePetcareError -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.surepetcare.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( @@ -22,7 +22,7 @@ INPUT_DATA = { async def test_form(hass: HomeAssistant, surepetcare: NonCallableMagicMock) -> None: """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/switchbot/test_config_flow.py b/tests/components/switchbot/test_config_flow.py index fad0769a7b8..edd35238034 100644 --- a/tests/components/switchbot/test_config_flow.py +++ b/tests/components/switchbot/test_config_flow.py @@ -14,7 +14,6 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM, ) -from homeassistant.setup import async_setup_component from . import ( USER_INPUT, @@ -29,7 +28,6 @@ DOMAIN = "switchbot" async def test_user_form_valid_mac(hass): """Test the user initiated form with password and valid mac.""" - await async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -94,7 +92,6 @@ async def test_user_form_valid_mac(hass): async def test_async_step_import(hass): """Test the config import flow.""" - await async_setup_component(hass, "persistent_notification", {}) with _patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_init( @@ -113,7 +110,6 @@ async def test_async_step_import(hass): async def test_user_form_exception(hass, switchbot_config_flow): """Test we handle exception on user form.""" - await async_setup_component(hass, "persistent_notification", {}) switchbot_config_flow.side_effect = NotConnectedError diff --git a/tests/components/syncthing/test_config_flow.py b/tests/components/syncthing/test_config_flow.py index 7cdf728c07f..80656c75990 100644 --- a/tests/components/syncthing/test_config_flow.py +++ b/tests/components/syncthing/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch from aiosyncthing.exceptions import UnauthorizedError -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.syncthing.const import DOMAIN from homeassistant.const import CONF_NAME, CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL @@ -25,7 +25,7 @@ MOCK_ENTRY = { async def test_show_setup_form(hass): """Test that the setup form is served.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/syncthru/test_config_flow.py b/tests/components/syncthru/test_config_flow.py index 61bc2992f52..8ac91770741 100644 --- a/tests/components/syncthru/test_config_flow.py +++ b/tests/components/syncthru/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch from pysyncthru import SyncThruAPINotSupported -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components import ssdp from homeassistant.components.syncthru.config_flow import SyncThru from homeassistant.components.syncthru.const import DOMAIN @@ -49,7 +49,7 @@ async def test_show_setup_form(hass): async def test_already_configured_by_url(hass, aioclient_mock): """Test we match and update already configured devices by URL.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + udn = "uuid:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" MockConfigEntry( domain=DOMAIN, @@ -103,7 +103,7 @@ async def test_unknown_state(hass): async def test_success(hass, aioclient_mock): """Test successful flow provides entry creation data.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + mock_connection(aioclient_mock) with patch( @@ -123,7 +123,7 @@ async def test_success(hass, aioclient_mock): async def test_ssdp(hass, aioclient_mock): """Test SSDP discovery initiates config properly.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + mock_connection(aioclient_mock) url = "http://192.168.1.2/" diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py index dec720cfd72..cc761a4b06a 100644 --- a/tests/components/synology_dsm/test_config_flow.py +++ b/tests/components/synology_dsm/test_config_flow.py @@ -10,7 +10,7 @@ from synology_dsm.exceptions import ( SynologyDSMRequestException, ) -from homeassistant import data_entry_flow, setup +from homeassistant import data_entry_flow from homeassistant.components import ssdp from homeassistant.components.synology_dsm.config_flow import CONF_OTP_CODE from homeassistant.components.synology_dsm.const import ( @@ -383,7 +383,6 @@ async def test_missing_data_after_login(hass: HomeAssistant, service_failed: Mag async def test_form_ssdp(hass: HomeAssistant, service: MagicMock): """Test we can setup from ssdp.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, @@ -419,7 +418,6 @@ async def test_form_ssdp(hass: HomeAssistant, service: MagicMock): async def test_reconfig_ssdp(hass: HomeAssistant, service: MagicMock): """Test re-configuration of already existing entry by ssdp.""" - await setup.async_setup_component(hass, "persistent_notification", {}) MockConfigEntry( domain=DOMAIN, @@ -447,7 +445,6 @@ async def test_reconfig_ssdp(hass: HomeAssistant, service: MagicMock): async def test_existing_ssdp(hass: HomeAssistant, service: MagicMock): """Test abort of already existing entry by ssdp.""" - await setup.async_setup_component(hass, "persistent_notification", {}) MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/system_bridge/test_config_flow.py b/tests/components/system_bridge/test_config_flow.py index 96603c39bcd..1b46bb45a6d 100644 --- a/tests/components/system_bridge/test_config_flow.py +++ b/tests/components/system_bridge/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch from aiohttp.client_exceptions import ClientConnectionError from systembridge.exceptions import BridgeAuthenticationException -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.system_bridge.const import DOMAIN from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT @@ -279,7 +279,7 @@ async def test_zeroconf_flow( hass, aiohttp_client, aioclient_mock, current_request_with_host ) -> None: """Test zeroconf flow.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, @@ -314,7 +314,7 @@ async def test_zeroconf_cannot_connect( hass, aiohttp_client, aioclient_mock, current_request_with_host ) -> None: """Test zeroconf cannot connect flow.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, @@ -342,7 +342,7 @@ async def test_zeroconf_bad_zeroconf_info( hass, aiohttp_client, aioclient_mock, current_request_with_host ) -> None: """Test zeroconf cannot connect flow.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, diff --git a/tests/components/tado/test_config_flow.py b/tests/components/tado/test_config_flow.py index 77656f1c81f..6b0a7c62179 100644 --- a/tests/components/tado/test_config_flow.py +++ b/tests/components/tado/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock, patch import requests -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.tado.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -21,7 +21,7 @@ def _get_mock_tado_api(getMe=None): async def test_form(hass): """Test we can setup though the user path.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -121,7 +121,6 @@ async def test_no_homes(hass): async def test_form_homekit(hass): """Test that we abort from homekit if tado is already setup.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/template/test_alarm_control_panel.py b/tests/components/template/test_alarm_control_panel.py index e7a898efc49..a7502576de1 100644 --- a/tests/components/template/test_alarm_control_panel.py +++ b/tests/components/template/test_alarm_control_panel.py @@ -284,7 +284,7 @@ async def test_no_action_scripts(hass, start_ha): ) async def test_template_syntax_error(hass, msg, start_ha, caplog_setup_text): """Test templating syntax error.""" - assert len(hass.states.async_all()) == 0 + assert len(hass.states.async_all("alarm_control_panel")) == 0 assert (msg) in caplog_setup_text diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index 98d76776242..981ff63af50 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -78,7 +78,7 @@ async def test_setup_legacy(hass, start_ha): ) async def test_setup_invalid_sensors(hass, count, start_ha): """Test setup with no sensors.""" - assert len(hass.states.async_entity_ids()) == count + assert len(hass.states.async_entity_ids("binary_sensor")) == count @pytest.mark.parametrize("count,domain", [(1, binary_sensor.DOMAIN)]) diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index 0f629fdd239..b4bc00ee6a2 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -327,7 +327,7 @@ async def test_template_out_of_bounds(hass, start_ha): ) async def test_template_open_or_position(hass, start_ha, caplog_setup_text): """Test that at least one of open_cover or set_position is used.""" - assert hass.states.async_all() == [] + assert hass.states.async_all("cover") == [] assert "Invalid config for [cover.template]" in caplog_setup_text diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index 82f89be9b0a..91910a11e1f 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -126,7 +126,7 @@ async def test_missing_optional_config(hass, start_ha): ) async def test_wrong_template_config(hass, start_ha): """Test: missing 'value_template' will fail.""" - assert hass.states.async_all() == [] + assert hass.states.async_all("fan") == [] @pytest.mark.parametrize("count,domain", [(1, DOMAIN)]) diff --git a/tests/components/template/test_init.py b/tests/components/template/test_init.py index c179123e035..28caf673d01 100644 --- a/tests/components/template/test_init.py +++ b/tests/components/template/test_init.py @@ -163,10 +163,10 @@ async def test_reloadable_handles_partial_valid_config(hass, start_ha): hass.states.async_set("sensor.test_sensor", "mytest") await hass.async_block_till_done() assert hass.states.get("sensor.state").state == "mytest" - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_all("sensor")) == 2 await async_yaml_patch_helper(hass, "broken_configuration.yaml") - assert len(hass.states.async_all()) == 3 + assert len(hass.states.async_all("sensor")) == 3 assert hass.states.get("sensor.state") is None assert hass.states.get("sensor.watching_tv_in_master_bedroom").state == "off" diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index bbf866c78a3..e0ee5422439 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -210,7 +210,7 @@ async def test_templatex_state_boolean(hass, expected_state, start_ha): ) async def test_template_syntax_error(hass, start_ha): """Test templating syntax error.""" - assert hass.states.async_all() == [] + assert hass.states.async_all("light") == [] SET_VAL1 = '"value_template": "{{ 1== 1}}",' @@ -241,9 +241,9 @@ SET_VAL3 = '"turn_off": {"service": "light.turn_off","entity_id": "light.test_st async def test_missing_key(hass, count, start_ha): """Test missing template.""" if count: - assert hass.states.async_all() != [] + assert hass.states.async_all("light") != [] else: - assert hass.states.async_all() == [] + assert hass.states.async_all("light") == [] @pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) @@ -1471,4 +1471,4 @@ async def test_invalid_availability_template_keeps_component_available( ) async def test_unique_id(hass, start_ha): """Test unique_id option only creates one light per id.""" - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("light")) == 1 diff --git a/tests/components/template/test_lock.py b/tests/components/template/test_lock.py index 109e4b348b3..80c83e0885a 100644 --- a/tests/components/template/test_lock.py +++ b/tests/components/template/test_lock.py @@ -146,7 +146,7 @@ async def test_template_state_boolean_off(hass, start_ha): ) async def test_template_syntax_error(hass, start_ha): """Test templating syntax error.""" - assert hass.states.async_all() == [] + assert hass.states.async_all("lock") == [] @pytest.mark.parametrize("count,domain", [(1, lock.DOMAIN)]) @@ -376,4 +376,4 @@ async def test_unique_id(hass, start_ha): await hass.async_start() await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("lock")) == 1 diff --git a/tests/components/template/test_number.py b/tests/components/template/test_number.py index 1f317c06330..98460335047 100644 --- a/tests/components/template/test_number.py +++ b/tests/components/template/test_number.py @@ -109,7 +109,7 @@ async def test_missing_required_keys(hass, calls): await hass.async_start() await hass.async_block_till_done() - assert hass.states.async_all() == [] + assert hass.states.async_all("number") == [] async def test_all_optional_config(hass, calls): diff --git a/tests/components/template/test_select.py b/tests/components/template/test_select.py index ca4a30b1cd6..4deb02986b8 100644 --- a/tests/components/template/test_select.py +++ b/tests/components/template/test_select.py @@ -140,7 +140,7 @@ async def test_missing_required_keys(hass, calls): await hass.async_start() await hass.async_block_till_done() - assert hass.states.async_all() == [] + assert hass.states.async_all("select") == [] async def test_templates_with_entities(hass, calls): diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 242ac09d3d0..5b179957d92 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -234,7 +234,7 @@ async def test_friendly_name_template(hass, attribute, start_ha): ) async def test_template_syntax_error(hass, start_ha): """Test setup with invalid device_class.""" - assert hass.states.async_all() == [] + assert hass.states.async_all("sensor") == [] @pytest.mark.parametrize("count,domain", [(1, sensor.DOMAIN)]) diff --git a/tests/components/template/test_switch.py b/tests/components/template/test_switch.py index 9bf7ef99956..2628b0afa49 100644 --- a/tests/components/template/test_switch.py +++ b/tests/components/template/test_switch.py @@ -256,7 +256,7 @@ async def test_template_syntax_error(hass): await hass.async_start() await hass.async_block_till_done() - assert hass.states.async_all() == [] + assert hass.states.async_all("switch") == [] async def test_invalid_name_does_not_create(hass): @@ -289,7 +289,7 @@ async def test_invalid_name_does_not_create(hass): await hass.async_start() await hass.async_block_till_done() - assert hass.states.async_all() == [] + assert hass.states.async_all("switch") == [] async def test_invalid_switch_does_not_create(hass): @@ -310,7 +310,7 @@ async def test_invalid_switch_does_not_create(hass): await hass.async_start() await hass.async_block_till_done() - assert hass.states.async_all() == [] + assert hass.states.async_all("switch") == [] async def test_no_switches_does_not_create(hass): @@ -324,7 +324,7 @@ async def test_no_switches_does_not_create(hass): await hass.async_start() await hass.async_block_till_done() - assert hass.states.async_all() == [] + assert hass.states.async_all("switch") == [] async def test_missing_on_does_not_create(hass): @@ -357,7 +357,7 @@ async def test_missing_on_does_not_create(hass): await hass.async_start() await hass.async_block_till_done() - assert hass.states.async_all() == [] + assert hass.states.async_all("switch") == [] async def test_missing_off_does_not_create(hass): @@ -390,7 +390,7 @@ async def test_missing_off_does_not_create(hass): await hass.async_start() await hass.async_block_till_done() - assert hass.states.async_all() == [] + assert hass.states.async_all("switch") == [] async def test_on_action(hass, calls): @@ -721,4 +721,4 @@ async def test_unique_id(hass): await hass.async_start() await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("switch")) == 1 diff --git a/tests/components/template/test_vacuum.py b/tests/components/template/test_vacuum.py index 2bd6063b6ef..8b283f247e5 100644 --- a/tests/components/template/test_vacuum.py +++ b/tests/components/template/test_vacuum.py @@ -92,7 +92,7 @@ _BATTERY_LEVEL_INPUT_NUMBER = "input_number.battery_level" ) async def test_valid_configs(hass, count, parm1, parm2, start_ha): """Test: configs.""" - assert len(hass.states.async_all()) == count + assert len(hass.states.async_all("vacuum")) == count _verify(hass, parm1, parm2) @@ -114,7 +114,7 @@ async def test_valid_configs(hass, count, parm1, parm2, start_ha): ) async def test_invalid_configs(hass, count, start_ha): """Test: configs.""" - assert len(hass.states.async_all()) == count + assert len(hass.states.async_all("vacuum")) == count @pytest.mark.parametrize( @@ -276,7 +276,7 @@ async def test_attribute_templates(hass, start_ha): ) async def test_invalid_attribute_template(hass, start_ha, caplog_setup_text): """Test that errors are logged if rendering template fails.""" - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("vacuum")) == 1 assert "test_attribute" in caplog_setup_text assert "TemplateError" in caplog_setup_text @@ -309,7 +309,7 @@ async def test_invalid_attribute_template(hass, start_ha, caplog_setup_text): ) async def test_unique_id(hass, start_ha): """Test unique_id option only creates one vacuum per id.""" - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all("vacuum")) == 1 async def test_unused_services(hass): diff --git a/tests/components/tplink/test_config_flow.py b/tests/components/tplink/test_config_flow.py index 3c875f623dd..03757efa0ca 100644 --- a/tests/components/tplink/test_config_flow.py +++ b/tests/components/tplink/test_config_flow.py @@ -293,7 +293,6 @@ async def test_manual_no_capabilities(hass: HomeAssistant): async def test_discovered_by_discovery_and_dhcp(hass): """Test we get the form with discovery and abort for dhcp source when we get both.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with _patch_discovery(), _patch_single_discovery(): result = await hass.config_entries.flow.async_init( @@ -351,7 +350,6 @@ async def test_discovered_by_discovery_and_dhcp(hass): ) async def test_discovered_by_dhcp_or_discovery(hass, source, data): """Test we can setup when discovered from dhcp or discovery.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with _patch_discovery(), _patch_single_discovery(): result = await hass.config_entries.flow.async_init( @@ -393,7 +391,6 @@ async def test_discovered_by_dhcp_or_discovery(hass, source, data): ) async def test_discovered_by_dhcp_or_discovery_failed_to_get_device(hass, source, data): """Test we abort if we cannot get the unique id when discovered from dhcp.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with _patch_discovery(no_device=True), _patch_single_discovery(no_device=True): result = await hass.config_entries.flow.async_init( diff --git a/tests/components/traccar/test_init.py b/tests/components/traccar/test_init.py index 2bc46bc94a7..53433baf9ae 100644 --- a/tests/components/traccar/test_init.py +++ b/tests/components/traccar/test_init.py @@ -30,7 +30,6 @@ def mock_dev_track(mock_device_tracker_conf): @pytest.fixture(name="client") async def traccar_client(loop, hass, hass_client_no_auth): """Mock client for Traccar (unauthenticated).""" - assert await async_setup_component(hass, "persistent_notification", {}) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) diff --git a/tests/components/tractive/test_config_flow.py b/tests/components/tractive/test_config_flow.py index 115df39175c..fdd1750e0e3 100644 --- a/tests/components/tractive/test_config_flow.py +++ b/tests/components/tractive/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch import aiotractive -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.tractive.const import DOMAIN from homeassistant.core import HomeAssistant @@ -17,7 +17,7 @@ USER_INPUT = { async def test_form(hass: HomeAssistant) -> None: """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/trend/test_binary_sensor.py b/tests/components/trend/test_binary_sensor.py index d1d77001bff..be414b73042 100644 --- a/tests/components/trend/test_binary_sensor.py +++ b/tests/components/trend/test_binary_sensor.py @@ -347,7 +347,7 @@ class TestTrendBinarySensor: } }, ) - assert self.hass.states.all() == [] + assert self.hass.states.all("binary_sensor") == [] def test_invalid_sensor_does_not_create(self): """Test invalid sensor.""" @@ -364,7 +364,7 @@ class TestTrendBinarySensor: } }, ) - assert self.hass.states.all() == [] + assert self.hass.states.all("binary_sensor") == [] def test_no_sensors_does_not_create(self): """Test no sensors.""" @@ -372,7 +372,7 @@ class TestTrendBinarySensor: assert setup.setup_component( self.hass, "binary_sensor", {"binary_sensor": {"platform": "trend"}} ) - assert self.hass.states.all() == [] + assert self.hass.states.all("binary_sensor") == [] async def test_reload(hass): diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 8f88b6adb4c..24628fae60e 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch import aiounifi -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.unifi.config_flow import async_discover_unifi from homeassistant.components.unifi.const import ( CONF_ALLOW_BANDWIDTH_SENSORS, @@ -543,7 +543,6 @@ async def test_simple_option_flow(hass, aioclient_mock): async def test_form_ssdp(hass): """Test we get the form with ssdp source.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( UNIFI_DOMAIN, @@ -571,7 +570,7 @@ async def test_form_ssdp(hass): async def test_form_ssdp_aborts_if_host_already_exists(hass): """Test we abort if the host is already configured.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=UNIFI_DOMAIN, data={"host": "192.168.208.1", "site": "site_id"}, @@ -593,7 +592,7 @@ async def test_form_ssdp_aborts_if_host_already_exists(hass): async def test_form_ssdp_aborts_if_serial_already_exists(hass): """Test we abort if the serial is already configured.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=UNIFI_DOMAIN, data={"controller": {"host": "1.2.3.4", "site": "site_id"}}, @@ -616,7 +615,7 @@ async def test_form_ssdp_aborts_if_serial_already_exists(hass): async def test_form_ssdp_gets_form_with_ignored_entry(hass): """Test we can still setup if there is an ignored entry.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=UNIFI_DOMAIN, data={"not_controller_key": None}, diff --git a/tests/components/upb/test_config_flow.py b/tests/components/upb/test_config_flow.py index 1abcaa958b7..b24e42fd51f 100644 --- a/tests/components/upb/test_config_flow.py +++ b/tests/components/upb/test_config_flow.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock, PropertyMock, patch -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.upb.const import DOMAIN @@ -24,7 +24,7 @@ def mocked_upb(sync_complete=True, config_ok=True): async def valid_tcp_flow(hass, sync_complete=True, config_ok=True): """Get result dict that are standard for most tests.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + with mocked_upb(sync_complete, config_ok), patch( "homeassistant.components.upb.async_setup_entry", return_value=True ): @@ -40,7 +40,6 @@ async def valid_tcp_flow(hass, sync_complete=True, config_ok=True): async def test_full_upb_flow_with_serial_port(hass): """Test a full UPB config flow with serial port.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with mocked_upb(), patch( "homeassistant.components.upb.async_setup_entry", return_value=True @@ -110,7 +109,6 @@ async def test_form_user_with_already_configured(hass): async def test_form_import(hass): """Test we get the form with import source.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with mocked_upb(), patch( "homeassistant.components.upb.async_setup_entry", return_value=True @@ -131,7 +129,6 @@ async def test_form_import(hass): async def test_form_junk_input(hass): """Test we get the form with import source.""" - await setup.async_setup_component(hass, "persistent_notification", {}) with mocked_upb(): result = await hass.config_entries.flow.async_init( diff --git a/tests/components/uptimerobot/test_config_flow.py b/tests/components/uptimerobot/test_config_flow.py index 966483970d0..8c5225ad38c 100644 --- a/tests/components/uptimerobot/test_config_flow.py +++ b/tests/components/uptimerobot/test_config_flow.py @@ -5,7 +5,7 @@ import pytest from pytest import LogCaptureFixture from pyuptimerobot import UptimeRobotAuthenticationException, UptimeRobotException -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.uptimerobot.const import DOMAIN from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant @@ -29,7 +29,7 @@ from tests.common import MockConfigEntry async def test_form(hass: HomeAssistant) -> None: """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/vilfo/test_config_flow.py b/tests/components/vilfo/test_config_flow.py index d3face657de..571399949b4 100644 --- a/tests/components/vilfo/test_config_flow.py +++ b/tests/components/vilfo/test_config_flow.py @@ -3,14 +3,14 @@ from unittest.mock import Mock, patch import vilfo -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.vilfo.const import DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_ID, CONF_MAC async def test_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + mock_mac = "FF-00-00-00-00-00" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} diff --git a/tests/components/watttime/test_config_flow.py b/tests/components/watttime/test_config_flow.py index efee2429d59..e50e89dfb26 100644 --- a/tests/components/watttime/test_config_flow.py +++ b/tests/components/watttime/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, patch from aiowatttime.errors import CoordinatesNotFoundError, InvalidCredentialsError import pytest -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.watttime.config_flow import ( CONF_LOCATION_TYPE, LOCATION_TYPE_COORDINATES, @@ -121,7 +121,7 @@ async def test_step_coordinates_unknown_coordinates( hass: HomeAssistant, client_login ) -> None: """Test that providing coordinates with no data is handled.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, @@ -146,7 +146,7 @@ async def test_step_coordinates_unknown_error( hass: HomeAssistant, client_login ) -> None: """Test that providing coordinates with no data is handled.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, @@ -164,7 +164,7 @@ async def test_step_coordinates_unknown_error( async def test_step_login_coordinates(hass: HomeAssistant, client_login) -> None: """Test a full login flow (inputting custom coordinates).""" - await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( "homeassistant.components.watttime.async_setup_entry", return_value=True, @@ -198,7 +198,7 @@ async def test_step_login_coordinates(hass: HomeAssistant, client_login) -> None async def test_step_user_home(hass: HomeAssistant, client_login) -> None: """Test a full login flow (selecting the home location).""" - await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( "homeassistant.components.watttime.async_setup_entry", return_value=True, @@ -228,7 +228,7 @@ async def test_step_user_home(hass: HomeAssistant, client_login) -> None: async def test_step_user_invalid_credentials(hass: HomeAssistant) -> None: """Test that invalid credentials are handled.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( "homeassistant.components.watttime.config_flow.Client.async_login", AsyncMock(side_effect=InvalidCredentialsError), @@ -247,7 +247,7 @@ async def test_step_user_invalid_credentials(hass: HomeAssistant) -> None: @pytest.mark.parametrize("get_grid_region", [AsyncMock(side_effect=Exception)]) async def test_step_user_unknown_error(hass: HomeAssistant, client_login) -> None: """Test that an unknown error during the login step is handled.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( "homeassistant.components.watttime.config_flow.Client.async_login", AsyncMock(side_effect=Exception), diff --git a/tests/components/wled/conftest.py b/tests/components/wled/conftest.py index 80b351a20f1..527dc84af59 100644 --- a/tests/components/wled/conftest.py +++ b/tests/components/wled/conftest.py @@ -9,18 +9,11 @@ from wled import Device as WLEDDevice from homeassistant.components.wled.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_MAC from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, load_fixture from tests.components.light.conftest import mock_light_profiles # noqa: F401 -@pytest.fixture(autouse=True) -async def mock_persistent_notification(hass: HomeAssistant) -> None: - """Set up component for persistent notifications.""" - await async_setup_component(hass, "persistent_notification", {}) - - @pytest.fixture def mock_config_entry() -> MockConfigEntry: """Return the default mocked config entry.""" diff --git a/tests/components/wolflink/test_config_flow.py b/tests/components/wolflink/test_config_flow.py index 5108883ed81..f0530524805 100644 --- a/tests/components/wolflink/test_config_flow.py +++ b/tests/components/wolflink/test_config_flow.py @@ -5,7 +5,7 @@ from httpcore import ConnectError from wolf_smartset.models import Device from wolf_smartset.token_auth import InvalidAuth -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.wolflink.const import ( DEVICE_GATEWAY, DEVICE_ID, @@ -34,7 +34,7 @@ DEVICE = Device(CONFIG[DEVICE_ID], CONFIG[DEVICE_GATEWAY], CONFIG[DEVICE_NAME]) async def test_show_form(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index 68091efffa1..454940f1356 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -797,7 +797,6 @@ async def test_options_flow_incomplete(hass): async def test_reauth(hass): """Test a reauth flow.""" - # await setup.async_setup_component(hass, "persistent_notification", {}) config_entry = MockConfigEntry( domain=const.DOMAIN, unique_id=TEST_GATEWAY_ID, diff --git a/tests/components/yale_smart_alarm/test_config_flow.py b/tests/components/yale_smart_alarm/test_config_flow.py index 142e1ac5b5d..a2a0e41ba40 100644 --- a/tests/components/yale_smart_alarm/test_config_flow.py +++ b/tests/components/yale_smart_alarm/test_config_flow.py @@ -6,7 +6,7 @@ from unittest.mock import patch import pytest from yalesmartalarmclient.client import AuthenticationError -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.yale_smart_alarm.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM @@ -16,7 +16,7 @@ from tests.common import MockConfigEntry async def test_form(hass: HomeAssistant) -> None: """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index 8d4b7f48543..99dd233678f 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.yeelight import ( CONF_MODE_MUSIC, CONF_MODEL, @@ -402,7 +402,6 @@ async def test_manual_no_capabilities(hass: HomeAssistant): async def test_discovered_by_homekit_and_dhcp(hass): """Test we get the form with homekit and abort for dhcp source when we get both.""" - await setup.async_setup_component(hass, "persistent_notification", {}) mocked_bulb = _mocked_bulb() with _patch_discovery(), _patch_discovery_interval(), patch( @@ -471,7 +470,6 @@ async def test_discovered_by_homekit_and_dhcp(hass): ) async def test_discovered_by_dhcp_or_homekit(hass, source, data): """Test we can setup when discovered from dhcp or homekit.""" - await setup.async_setup_component(hass, "persistent_notification", {}) mocked_bulb = _mocked_bulb() with _patch_discovery(), _patch_discovery_interval(), patch( @@ -518,7 +516,6 @@ async def test_discovered_by_dhcp_or_homekit(hass, source, data): ) async def test_discovered_by_dhcp_or_homekit_failed_to_get_id(hass, source, data): """Test we abort if we cannot get the unique id when discovered from dhcp or homekit.""" - await setup.async_setup_component(hass, "persistent_notification", {}) mocked_bulb = _mocked_bulb() with _patch_discovery( @@ -535,7 +532,6 @@ async def test_discovered_by_dhcp_or_homekit_failed_to_get_id(hass, source, data async def test_discovered_ssdp(hass): """Test we can setup when discovered from ssdp.""" - await setup.async_setup_component(hass, "persistent_notification", {}) mocked_bulb = _mocked_bulb() with _patch_discovery(), _patch_discovery_interval(), patch( @@ -581,7 +577,6 @@ async def test_discovered_ssdp(hass): async def test_discovered_zeroconf(hass): """Test we can setup when discovered from zeroconf.""" - await setup.async_setup_component(hass, "persistent_notification", {}) mocked_bulb = _mocked_bulb() with _patch_discovery(), _patch_discovery_interval(), patch( diff --git a/tests/components/zerproc/test_config_flow.py b/tests/components/zerproc/test_config_flow.py index 53b90e0364e..9454742b030 100644 --- a/tests/components/zerproc/test_config_flow.py +++ b/tests/components/zerproc/test_config_flow.py @@ -3,13 +3,13 @@ from unittest.mock import patch import pyzerproc -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.zerproc.config_flow import DOMAIN async def test_flow_success(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -41,7 +41,7 @@ async def test_flow_success(hass): async def test_flow_no_devices_found(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -71,7 +71,7 @@ async def test_flow_no_devices_found(hass): async def test_flow_exceptions_caught(hass): """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/zerproc/test_light.py b/tests/components/zerproc/test_light.py index 0fec6be6451..e72ba2bcb66 100644 --- a/tests/components/zerproc/test_light.py +++ b/tests/components/zerproc/test_light.py @@ -4,7 +4,6 @@ from unittest.mock import MagicMock, patch import pytest import pyzerproc -from homeassistant import setup from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_MODE, @@ -45,7 +44,6 @@ async def mock_entry(hass): @pytest.fixture async def mock_light(hass, mock_entry): """Create a mock light entity.""" - await setup.async_setup_component(hass, "persistent_notification", {}) mock_entry.add_to_hass(hass) @@ -72,7 +70,6 @@ async def mock_light(hass, mock_entry): async def test_init(hass, mock_entry): """Test platform setup.""" - await setup.async_setup_component(hass, "persistent_notification", {}) mock_entry.add_to_hass(hass) @@ -133,7 +130,6 @@ async def test_init(hass, mock_entry): async def test_discovery_exception(hass, mock_entry): """Test platform setup.""" - await setup.async_setup_component(hass, "persistent_notification", {}) mock_entry.add_to_hass(hass) diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 5aef30c854d..bb91ccbeda3 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -7,7 +7,7 @@ import serial.tools.list_ports import zigpy.config from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.ssdp import ( ATTR_SSDP_LOCATION, ATTR_UPNP_MANUFACTURER_URL, @@ -208,7 +208,7 @@ async def test_discovery_via_usb_no_radio(detect_mock, hass): @patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True) async def test_discovery_via_usb_already_setup(detect_mock, hass): """Test usb flow -- already setup.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + MockConfigEntry( domain=DOMAIN, data={CONF_DEVICE: {CONF_DEVICE_PATH: "/dev/ttyUSB1"}} ).add_to_hass(hass) @@ -233,7 +233,7 @@ async def test_discovery_via_usb_already_setup(detect_mock, hass): @patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) async def test_discovery_via_usb_path_changes(hass): """Test usb flow already setup and the path changes.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( domain=DOMAIN, unique_id="AAAA:AAAA_1234_test_zigbee radio", @@ -386,7 +386,7 @@ async def test_discovery_already_setup(detect_mock, hass): "hostname": "_tube_zb_gw._tcp.local.", "properties": {"name": "tube_123456"}, } - await setup.async_setup_component(hass, "persistent_notification", {}) + MockConfigEntry( domain=DOMAIN, data={CONF_DEVICE: {CONF_DEVICE_PATH: "/dev/ttyUSB1"}} ).add_to_hass(hass) @@ -498,7 +498,7 @@ async def test_user_flow_existing_config_entry(hass): MockConfigEntry( domain=DOMAIN, data={CONF_DEVICE: {CONF_DEVICE_PATH: "/dev/ttyUSB1"}} ).add_to_hass(hass) - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) @@ -599,7 +599,6 @@ async def test_user_port_config_fail(probe_mock, hass): @patch("bellows.zigbee.application.ControllerApplication.probe", return_value=True) async def test_user_port_config(probe_mock, hass): """Test port config.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/zha/test_init.py b/tests/components/zha/test_init.py index bb8c502562e..0615eeef623 100644 --- a/tests/components/zha/test_init.py +++ b/tests/components/zha/test_init.py @@ -89,7 +89,6 @@ async def test_migration_from_v1_wrong_baudrate(hass, config_entry_v1): ) async def test_config_depreciation(hass, zha_config): """Test config option depreciation.""" - await async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.zha.async_setup", return_value=True diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index 5dcbee4c5ee..a61d13be8eb 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -6,7 +6,7 @@ import aiohttp import pytest from zwave_js_server.version import VersionInfo -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.zwave_js.config_flow import SERVER_VERSION_TIMEOUT, TITLE from homeassistant.components.zwave_js.const import DOMAIN @@ -48,12 +48,6 @@ CP2652_ZIGBEE_DISCOVERY_INFO = { } -@pytest.fixture(name="persistent_notification", autouse=True) -async def setup_persistent_notification(hass): - """Set up persistent notification integration.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - @pytest.fixture(name="setup_entry") def setup_entry_fixture(): """Mock entry setup.""" @@ -139,7 +133,7 @@ def mock_addon_setup_time(): async def test_manual(hass): """Test we create an entry with manual step.""" - await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -251,7 +245,6 @@ async def test_manual_already_configured(hass): ) entry.add_to_hass(hass) - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -278,7 +271,6 @@ async def test_supervisor_discovery( hass, supervisor, addon_running, addon_options, get_addon_discovery_info ): """Test flow started from Supervisor discovery.""" - await setup.async_setup_component(hass, "persistent_notification", {}) addon_options["device"] = "/test" addon_options["s0_legacy_key"] = "new123" @@ -325,7 +317,6 @@ async def test_supervisor_discovery_cannot_connect( hass, supervisor, get_addon_discovery_info ): """Test Supervisor discovery and cannot connect.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, @@ -342,7 +333,6 @@ async def test_clean_discovery_on_user_create( hass, supervisor, addon_running, addon_options, get_addon_discovery_info ): """Test discovery flow is cleaned up when a user flow is finished.""" - await setup.async_setup_component(hass, "persistent_notification", {}) addon_options["device"] = "/test" addon_options["s0_legacy_key"] = "new123" @@ -407,7 +397,6 @@ async def test_abort_discovery_with_existing_entry( hass, supervisor, addon_running, addon_options ): """Test discovery flow is aborted if an entry already exists.""" - await setup.async_setup_component(hass, "persistent_notification", {}) entry = MockConfigEntry( domain=DOMAIN, data={"url": "ws://localhost:3000"}, title=TITLE, unique_id=1234 @@ -635,7 +624,6 @@ async def test_discovery_addon_not_running( ): """Test discovery with add-on already installed but not running.""" addon_options["device"] = None - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, @@ -718,7 +706,6 @@ async def test_discovery_addon_not_installed( ): """Test discovery with add-on not installed.""" addon_installed.return_value["version"] = None - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, @@ -876,7 +863,6 @@ async def test_abort_usb_discovery_aborts_specific_devices( async def test_not_addon(hass, supervisor): """Test opting out of add-on on Supervisor.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -936,7 +922,6 @@ async def test_addon_running( addon_options["s2_access_control_key"] = "new456" addon_options["s2_authenticated_key"] = "new789" addon_options["s2_unauthenticated_key"] = "new987" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -1017,7 +1002,6 @@ async def test_addon_running_failures( """Test all failures when add-on is running.""" addon_options["device"] = "/test" addon_options["network_key"] = "abc123" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -1060,7 +1044,6 @@ async def test_addon_running_already_configured( ) entry.add_to_hass(hass) - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -1093,7 +1076,6 @@ async def test_addon_installed( get_addon_discovery_info, ): """Test add-on already installed but not running on Supervisor.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -1179,7 +1161,6 @@ async def test_addon_installed_start_failure( get_addon_discovery_info, ): """Test add-on start failure when add-on is installed.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -1255,7 +1236,6 @@ async def test_addon_installed_failures( get_addon_discovery_info, ): """Test all failures when add-on is installed.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -1322,7 +1302,6 @@ async def test_addon_installed_set_options_failure( get_addon_discovery_info, ): """Test all failures when add-on is installed.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -1396,7 +1375,6 @@ async def test_addon_installed_already_configured( ) entry.add_to_hass(hass) - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -1467,7 +1445,6 @@ async def test_addon_not_installed( ): """Test add-on not installed.""" addon_installed.return_value["version"] = None - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -1553,7 +1530,6 @@ async def test_install_addon_failure(hass, supervisor, addon_installed, install_ """Test add-on install failure.""" addon_installed.return_value["version"] = None install_addon.side_effect = HassioAPIError() - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index 3a08c423d76..be57b2d52e7 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -9,7 +9,7 @@ import pytest from homeassistant.generated import config_flows from homeassistant.helpers import translation from homeassistant.loader import async_get_integration -from homeassistant.setup import async_setup_component, setup_component +from homeassistant.setup import async_setup_component @pytest.fixture @@ -202,7 +202,7 @@ async def test_get_translations_while_loading_components(hass): nonlocal load_count load_count += 1 # Mimic race condition by loading a component during setup - setup_component(hass, "persistent_notification", {}) + return {"component1": {"title": "world"}} with patch( diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 2ae4ad036d4..0b146c2f612 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -673,7 +673,6 @@ async def test_discovery_notification(hass): """Test that we create/dismiss a notification when source is discovery.""" mock_integration(hass, MockModule("test")) mock_entity_platform(hass, "config_flow.test", None) - await async_setup_component(hass, "persistent_notification", {}) with patch.dict(config_entries.HANDLERS): @@ -726,7 +725,6 @@ async def test_reauth_notification(hass): """Test that we create/dismiss a notification when source is reauth.""" mock_integration(hass, MockModule("test")) mock_entity_platform(hass, "config_flow.test", None) - await async_setup_component(hass, "persistent_notification", {}) with patch.dict(config_entries.HANDLERS): @@ -794,7 +792,6 @@ async def test_discovery_notification_not_created(hass): """Test that we not create a notification when discovery is aborted.""" mock_integration(hass, MockModule("test")) mock_entity_platform(hass, "config_flow.test", None) - await async_setup_component(hass, "persistent_notification", {}) class TestFlow(config_entries.ConfigFlow): """Test flow.""" @@ -1002,7 +999,6 @@ async def test_create_entry_options(hass): ), ) mock_entity_platform(hass, "config_flow.comp", None) - await async_setup_component(hass, "persistent_notification", {}) class TestFlow(config_entries.ConfigFlow): """Test flow.""" @@ -2199,7 +2195,6 @@ async def test_partial_flows_hidden(hass, manager): async_setup_entry = AsyncMock(return_value=True) mock_integration(hass, MockModule("comp", async_setup_entry=async_setup_entry)) mock_entity_platform(hass, "config_flow.comp", None) - await async_setup_component(hass, "persistent_notification", {}) # A flag to test our assertion that `async_step_discovery` was called and is in its blocked state # This simulates if the step was e.g. doing network i/o @@ -2275,7 +2270,6 @@ async def test_async_setup_init_entry(hass): ), ) mock_entity_platform(hass, "config_flow.comp", None) - await async_setup_component(hass, "persistent_notification", {}) class TestFlow(config_entries.ConfigFlow): """Test flow.""" @@ -2328,7 +2322,6 @@ async def test_async_setup_update_entry(hass): ), ) mock_entity_platform(hass, "config_flow.comp", None) - await async_setup_component(hass, "persistent_notification", {}) class TestFlow(config_entries.ConfigFlow): """Test flow.""" @@ -2917,8 +2910,6 @@ async def test__async_abort_entries_match(hass, manager, matchers, reason): data={"ip": "7.7.7.7", "host": "4.5.6.7", "port": 23}, ).add_to_hass(hass) - await async_setup_component(hass, "persistent_notification", {}) - mock_setup_entry = AsyncMock(return_value=True) mock_integration(hass, MockModule("comp", async_setup_entry=mock_setup_entry)) diff --git a/tests/test_loader.py b/tests/test_loader.py index 892e2da9c51..c51e805f400 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -7,7 +7,7 @@ from homeassistant import core, loader from homeassistant.components import http, hue from homeassistant.components.hue import light as hue_light -from tests.common import MockModule, async_mock_service, mock_integration +from tests.common import MockModule, mock_integration async def test_component_dependencies(hass): @@ -69,13 +69,10 @@ def test_component_loader_non_existing(hass): async def test_component_wrapper(hass): """Test component wrapper.""" - calls = async_mock_service(hass, "persistent_notification", "create") - components = loader.Components(hass) components.persistent_notification.async_create("message") - await hass.async_block_till_done() - assert len(calls) == 1 + assert len(hass.states.async_entity_ids("persistent_notification")) == 1 async def test_helpers_wrapper(hass): From 7223c59e79536032af06a9b923acadc4f9d63532 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 7 Oct 2021 13:54:34 +0200 Subject: [PATCH 0157/1038] Allow resetting an MQTT number (#57161) * Allow resetting an MQTT number * Add abbreviation --- homeassistant/components/mqtt/abbreviations.py | 1 + homeassistant/components/mqtt/number.py | 11 +++++++++-- tests/components/mqtt/test_number.py | 15 +++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index dd2f631848e..f3dcca9cfd5 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -134,6 +134,7 @@ ABBREVIATIONS = { "pl_osc_off": "payload_oscillation_off", "pl_osc_on": "payload_oscillation_on", "pl_paus": "payload_pause", + "pl_rst": "payload_reset", "pl_rst_hum": "payload_reset_humidity", "pl_rst_mode": "payload_reset_mode", "pl_rst_pct": "payload_reset_percentage", diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 866f93fd674..d7bce567976 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -35,10 +35,12 @@ _LOGGER = logging.getLogger(__name__) CONF_MIN = "min" CONF_MAX = "max" +CONF_PAYLOAD_RESET = "payload_reset" CONF_STEP = "step" DEFAULT_NAME = "MQTT Number" DEFAULT_OPTIMISTIC = False +DEFAULT_PAYLOAD_RESET = "None" MQTT_NUMBER_ATTRIBUTES_BLOCKED = frozenset( { @@ -64,6 +66,7 @@ PLATFORM_SCHEMA = vol.All( vol.Optional(CONF_MIN, default=DEFAULT_MIN_VALUE): vol.Coerce(float), 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, vol.Optional(CONF_STEP, default=DEFAULT_STEP): vol.All( vol.Coerce(float), vol.Range(min=1e-3) ), @@ -138,7 +141,9 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): if value_template is not None: payload = value_template.async_render_with_possible_json_value(payload) try: - if payload.isnumeric(): + if payload == self._config[CONF_PAYLOAD_RESET]: + num_value = None + elif payload.isnumeric(): num_value = int(payload) else: num_value = float(payload) @@ -146,7 +151,9 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): _LOGGER.warning("Payload '%s' is not a Number", msg.payload) return - if num_value < self.min_value or num_value > self.max_value: + if num_value is not None and ( + num_value < self.min_value or num_value > self.max_value + ): _LOGGER.error( "Invalid value for %s: %s (range %s - %s)", self.entity_id, diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index 37693340308..b0989c59ca2 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -66,6 +66,7 @@ async def test_run_number_setup(hass, mqtt_mock): "state_topic": topic, "command_topic": topic, "name": "Test Number", + "payload_reset": "reset!", } }, ) @@ -85,6 +86,13 @@ async def test_run_number_setup(hass, mqtt_mock): state = hass.states.get("number.test_number") assert state.state == "20.5" + async_fire_mqtt_message(hass, topic, "reset!") + + await hass.async_block_till_done() + + state = hass.states.get("number.test_number") + assert state.state == "unknown" + async def test_value_template(hass, mqtt_mock): """Test that it fetches the given payload with a template.""" @@ -118,6 +126,13 @@ async def test_value_template(hass, mqtt_mock): state = hass.states.get("number.test_number") assert state.state == "20.5" + async_fire_mqtt_message(hass, topic, '{"val":null}') + + await hass.async_block_till_done() + + state = hass.states.get("number.test_number") + assert state.state == "unknown" + async def test_run_number_service_optimistic(hass, mqtt_mock): """Test that set_value service works in optimistic mode.""" From e5b93cdcafbd6ead735e41c91cb1eaf8c0f6cf8c Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 7 Oct 2021 14:07:17 +0200 Subject: [PATCH 0158/1038] Use EntityDescription - darksky (#57083) Co-authored-by: Franck Nijhof --- homeassistant/components/darksky/sensor.py | 859 +++++++++++---------- 1 file changed, 447 insertions(+), 412 deletions(-) diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index beb277d76fa..228370be16a 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -1,9 +1,10 @@ """Support for Dark Sky weather service.""" from __future__ import annotations +from dataclasses import dataclass, field from datetime import timedelta import logging -from typing import NamedTuple +from typing import Literal, NamedTuple import forecastio from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout @@ -13,6 +14,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_TEMPERATURE, PLATFORM_SCHEMA, SensorEntity, + SensorEntityDescription, ) from homeassistant.const import ( ATTR_ATTRIBUTION, @@ -23,9 +25,15 @@ from homeassistant.const import ( CONF_NAME, CONF_SCAN_INTERVAL, DEGREE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_OZONE, + DEVICE_CLASS_PRESSURE, LENGTH_CENTIMETERS, + LENGTH_INCHES, LENGTH_KILOMETERS, + LENGTH_MILES, PERCENTAGE, + PRECIPITATION_INCHES, PRECIPITATION_MILLIMETERS_PER_HOUR, PRESSURE_MBAR, SPEED_KILOMETERS_PER_HOUR, @@ -58,333 +66,372 @@ DEPRECATED_SENSOR_TYPES = { "temperature_min", } -# Sensor types are defined like so: -# Name, si unit, us unit, ca unit, uk unit, uk2 unit -SENSOR_TYPES = { - "summary": [ - "Summary", - None, - None, - None, - None, - None, - None, - ["currently", "hourly", "daily"], - ], - "minutely_summary": ["Minutely Summary", None, None, None, None, None, None, []], - "hourly_summary": ["Hourly Summary", None, None, None, None, None, None, []], - "daily_summary": ["Daily Summary", None, None, None, None, None, None, []], - "icon": [ - "Icon", - None, - None, - None, - None, - None, - None, - ["currently", "hourly", "daily"], - ], - "nearest_storm_distance": [ - "Nearest Storm Distance", - LENGTH_KILOMETERS, - "mi", - LENGTH_KILOMETERS, - LENGTH_KILOMETERS, - "mi", - "mdi:weather-lightning", - ["currently"], - ], - "nearest_storm_bearing": [ - "Nearest Storm Bearing", - DEGREE, - DEGREE, - DEGREE, - DEGREE, - DEGREE, - "mdi:weather-lightning", - ["currently"], - ], - "precip_type": [ - "Precip", - None, - None, - None, - None, - None, - "mdi:weather-pouring", - ["currently", "minutely", "hourly", "daily"], - ], - "precip_intensity": [ - "Precip Intensity", - PRECIPITATION_MILLIMETERS_PER_HOUR, - "in", - PRECIPITATION_MILLIMETERS_PER_HOUR, - PRECIPITATION_MILLIMETERS_PER_HOUR, - PRECIPITATION_MILLIMETERS_PER_HOUR, - "mdi:weather-rainy", - ["currently", "minutely", "hourly", "daily"], - ], - "precip_probability": [ - "Precip Probability", - PERCENTAGE, - PERCENTAGE, - PERCENTAGE, - PERCENTAGE, - PERCENTAGE, - "mdi:water-percent", - ["currently", "minutely", "hourly", "daily"], - ], - "precip_accumulation": [ - "Precip Accumulation", - LENGTH_CENTIMETERS, - "in", - LENGTH_CENTIMETERS, - LENGTH_CENTIMETERS, - LENGTH_CENTIMETERS, - "mdi:weather-snowy", - ["hourly", "daily"], - ], - "temperature": [ - "Temperature", - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, - TEMP_CELSIUS, - TEMP_CELSIUS, - "mdi:thermometer", - ["currently", "hourly"], - ], - "apparent_temperature": [ - "Apparent Temperature", - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, - TEMP_CELSIUS, - TEMP_CELSIUS, - "mdi:thermometer", - ["currently", "hourly"], - ], - "dew_point": [ - "Dew Point", - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, - TEMP_CELSIUS, - TEMP_CELSIUS, - "mdi:thermometer", - ["currently", "hourly", "daily"], - ], - "wind_speed": [ - "Wind Speed", - SPEED_METERS_PER_SECOND, - SPEED_MILES_PER_HOUR, - SPEED_KILOMETERS_PER_HOUR, - SPEED_MILES_PER_HOUR, - SPEED_MILES_PER_HOUR, - "mdi:weather-windy", - ["currently", "hourly", "daily"], - ], - "wind_bearing": [ - "Wind Bearing", - DEGREE, - DEGREE, - DEGREE, - DEGREE, - DEGREE, - "mdi:compass", - ["currently", "hourly", "daily"], - ], - "wind_gust": [ - "Wind Gust", - SPEED_METERS_PER_SECOND, - SPEED_MILES_PER_HOUR, - SPEED_KILOMETERS_PER_HOUR, - SPEED_MILES_PER_HOUR, - SPEED_MILES_PER_HOUR, - "mdi:weather-windy-variant", - ["currently", "hourly", "daily"], - ], - "cloud_cover": [ - "Cloud Coverage", - PERCENTAGE, - PERCENTAGE, - PERCENTAGE, - PERCENTAGE, - PERCENTAGE, - "mdi:weather-partly-cloudy", - ["currently", "hourly", "daily"], - ], - "humidity": [ - "Humidity", - PERCENTAGE, - PERCENTAGE, - PERCENTAGE, - PERCENTAGE, - PERCENTAGE, - "mdi:water-percent", - ["currently", "hourly", "daily"], - ], - "pressure": [ - "Pressure", - PRESSURE_MBAR, - PRESSURE_MBAR, - PRESSURE_MBAR, - PRESSURE_MBAR, - PRESSURE_MBAR, - "mdi:gauge", - ["currently", "hourly", "daily"], - ], - "visibility": [ - "Visibility", - LENGTH_KILOMETERS, - "mi", - LENGTH_KILOMETERS, - LENGTH_KILOMETERS, - "mi", - "mdi:eye", - ["currently", "hourly", "daily"], - ], - "ozone": [ - "Ozone", - "DU", - "DU", - "DU", - "DU", - "DU", - "mdi:eye", - ["currently", "hourly", "daily"], - ], - "apparent_temperature_max": [ - "Daily High Apparent Temperature", - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, - TEMP_CELSIUS, - TEMP_CELSIUS, - "mdi:thermometer", - ["daily"], - ], - "apparent_temperature_high": [ - "Daytime High Apparent Temperature", - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, - TEMP_CELSIUS, - TEMP_CELSIUS, - "mdi:thermometer", - ["daily"], - ], - "apparent_temperature_min": [ - "Daily Low Apparent Temperature", - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, - TEMP_CELSIUS, - TEMP_CELSIUS, - "mdi:thermometer", - ["daily"], - ], - "apparent_temperature_low": [ - "Overnight Low Apparent Temperature", - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, - TEMP_CELSIUS, - TEMP_CELSIUS, - "mdi:thermometer", - ["daily"], - ], - "temperature_max": [ - "Daily High Temperature", - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, - TEMP_CELSIUS, - TEMP_CELSIUS, - "mdi:thermometer", - ["daily"], - ], - "temperature_high": [ - "Daytime High Temperature", - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, - TEMP_CELSIUS, - TEMP_CELSIUS, - "mdi:thermometer", - ["daily"], - ], - "temperature_min": [ - "Daily Low Temperature", - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, - TEMP_CELSIUS, - TEMP_CELSIUS, - "mdi:thermometer", - ["daily"], - ], - "temperature_low": [ - "Overnight Low Temperature", - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, - TEMP_CELSIUS, - TEMP_CELSIUS, - "mdi:thermometer", - ["daily"], - ], - "precip_intensity_max": [ - "Daily Max Precip Intensity", - PRECIPITATION_MILLIMETERS_PER_HOUR, - "in", - PRECIPITATION_MILLIMETERS_PER_HOUR, - PRECIPITATION_MILLIMETERS_PER_HOUR, - PRECIPITATION_MILLIMETERS_PER_HOUR, - "mdi:thermometer", - ["daily"], - ], - "uv_index": [ - "UV Index", - UV_INDEX, - UV_INDEX, - UV_INDEX, - UV_INDEX, - UV_INDEX, - "mdi:weather-sunny", - ["currently", "hourly", "daily"], - ], - "moon_phase": [ - "Moon Phase", - None, - None, - None, - None, - None, - "mdi:weather-night", - ["daily"], - ], - "sunrise_time": [ - "Sunrise", - None, - None, - None, - None, - None, - "mdi:white-balance-sunny", - ["daily"], - ], - "sunset_time": [ - "Sunset", - None, - None, - None, - None, - None, - "mdi:weather-night", - ["daily"], - ], - "alerts": ["Alerts", None, None, None, None, None, "mdi:alert-circle-outline", []], +MAP_UNIT_SYSTEM: dict[ + Literal["si", "us", "ca", "uk", "uk2"], + Literal["si_unit", "us_unit", "ca_unit", "uk_unit", "uk2_unit"], +] = { + "si": "si_unit", + "us": "us_unit", + "ca": "ca_unit", + "uk": "uk_unit", + "uk2": "uk2_unit", +} + + +@dataclass +class DarkskySensorEntityDescription(SensorEntityDescription): + """Describes Darksky sensor entity.""" + + si_unit: str | None = None + us_unit: str | None = None + ca_unit: str | None = None + uk_unit: str | None = None + uk2_unit: str | None = None + forecast_mode: list[str] = field(default_factory=list) + + +SENSOR_TYPES: dict[str, DarkskySensorEntityDescription] = { + "summary": DarkskySensorEntityDescription( + key="summary", + name="Summary", + forecast_mode=["currently", "hourly", "daily"], + ), + "minutely_summary": DarkskySensorEntityDescription( + key="minutely_summary", + name="Minutely Summary", + forecast_mode=[], + ), + "hourly_summary": DarkskySensorEntityDescription( + key="hourly_summary", + name="Hourly Summary", + forecast_mode=[], + ), + "daily_summary": DarkskySensorEntityDescription( + key="daily_summary", + name="Daily Summary", + forecast_mode=[], + ), + "icon": DarkskySensorEntityDescription( + key="icon", + name="Icon", + forecast_mode=["currently", "hourly", "daily"], + ), + "nearest_storm_distance": DarkskySensorEntityDescription( + key="nearest_storm_distance", + name="Nearest Storm Distance", + si_unit=LENGTH_KILOMETERS, + us_unit=LENGTH_MILES, + ca_unit=LENGTH_KILOMETERS, + uk_unit=LENGTH_KILOMETERS, + uk2_unit=LENGTH_MILES, + icon="mdi:weather-lightning", + forecast_mode=["currently"], + ), + "nearest_storm_bearing": DarkskySensorEntityDescription( + key="nearest_storm_bearing", + name="Nearest Storm Bearing", + si_unit=DEGREE, + us_unit=DEGREE, + ca_unit=DEGREE, + uk_unit=DEGREE, + uk2_unit=DEGREE, + icon="mdi:weather-lightning", + forecast_mode=["currently"], + ), + "precip_type": DarkskySensorEntityDescription( + key="precip_type", + name="Precip", + icon="mdi:weather-pouring", + forecast_mode=["currently", "minutely", "hourly", "daily"], + ), + "precip_intensity": DarkskySensorEntityDescription( + key="precip_intensity", + name="Precip Intensity", + si_unit=PRECIPITATION_MILLIMETERS_PER_HOUR, + us_unit=PRECIPITATION_INCHES, + ca_unit=PRECIPITATION_MILLIMETERS_PER_HOUR, + uk_unit=PRECIPITATION_MILLIMETERS_PER_HOUR, + uk2_unit=PRECIPITATION_MILLIMETERS_PER_HOUR, + icon="mdi:weather-rainy", + forecast_mode=["currently", "minutely", "hourly", "daily"], + ), + "precip_probability": DarkskySensorEntityDescription( + key="precip_probability", + name="Precip Probability", + si_unit=PERCENTAGE, + us_unit=PERCENTAGE, + ca_unit=PERCENTAGE, + uk_unit=PERCENTAGE, + uk2_unit=PERCENTAGE, + icon="mdi:water-percent", + forecast_mode=["currently", "minutely", "hourly", "daily"], + ), + "precip_accumulation": DarkskySensorEntityDescription( + key="precip_accumulation", + name="Precip Accumulation", + si_unit=LENGTH_CENTIMETERS, + us_unit=LENGTH_INCHES, + ca_unit=LENGTH_CENTIMETERS, + uk_unit=LENGTH_CENTIMETERS, + uk2_unit=LENGTH_CENTIMETERS, + icon="mdi:weather-snowy", + forecast_mode=["hourly", "daily"], + ), + "temperature": DarkskySensorEntityDescription( + key="temperature", + name="Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + si_unit=TEMP_CELSIUS, + us_unit=TEMP_FAHRENHEIT, + ca_unit=TEMP_CELSIUS, + uk_unit=TEMP_CELSIUS, + uk2_unit=TEMP_CELSIUS, + forecast_mode=["currently", "hourly"], + ), + "apparent_temperature": DarkskySensorEntityDescription( + key="apparent_temperature", + name="Apparent Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + si_unit=TEMP_CELSIUS, + us_unit=TEMP_FAHRENHEIT, + ca_unit=TEMP_CELSIUS, + uk_unit=TEMP_CELSIUS, + uk2_unit=TEMP_CELSIUS, + forecast_mode=["currently", "hourly"], + ), + "dew_point": DarkskySensorEntityDescription( + key="dew_point", + name="Dew Point", + device_class=DEVICE_CLASS_TEMPERATURE, + si_unit=TEMP_CELSIUS, + us_unit=TEMP_FAHRENHEIT, + ca_unit=TEMP_CELSIUS, + uk_unit=TEMP_CELSIUS, + uk2_unit=TEMP_CELSIUS, + forecast_mode=["currently", "hourly", "daily"], + ), + "wind_speed": DarkskySensorEntityDescription( + key="wind_speed", + name="Wind Speed", + si_unit=SPEED_METERS_PER_SECOND, + us_unit=SPEED_MILES_PER_HOUR, + ca_unit=SPEED_KILOMETERS_PER_HOUR, + uk_unit=SPEED_MILES_PER_HOUR, + uk2_unit=SPEED_MILES_PER_HOUR, + icon="mdi:weather-windy", + forecast_mode=["currently", "hourly", "daily"], + ), + "wind_bearing": DarkskySensorEntityDescription( + key="wind_bearing", + name="Wind Bearing", + si_unit=DEGREE, + us_unit=DEGREE, + ca_unit=DEGREE, + uk_unit=DEGREE, + uk2_unit=DEGREE, + icon="mdi:compass", + forecast_mode=["currently", "hourly", "daily"], + ), + "wind_gust": DarkskySensorEntityDescription( + key="wind_gust", + name="Wind Gust", + si_unit=SPEED_METERS_PER_SECOND, + us_unit=SPEED_MILES_PER_HOUR, + ca_unit=SPEED_KILOMETERS_PER_HOUR, + uk_unit=SPEED_MILES_PER_HOUR, + uk2_unit=SPEED_MILES_PER_HOUR, + icon="mdi:weather-windy-variant", + forecast_mode=["currently", "hourly", "daily"], + ), + "cloud_cover": DarkskySensorEntityDescription( + key="cloud_cover", + name="Cloud Coverage", + si_unit=PERCENTAGE, + us_unit=PERCENTAGE, + ca_unit=PERCENTAGE, + uk_unit=PERCENTAGE, + uk2_unit=PERCENTAGE, + icon="mdi:weather-partly-cloudy", + forecast_mode=["currently", "hourly", "daily"], + ), + "humidity": DarkskySensorEntityDescription( + key="humidity", + name="Humidity", + device_class=DEVICE_CLASS_HUMIDITY, + si_unit=PERCENTAGE, + us_unit=PERCENTAGE, + ca_unit=PERCENTAGE, + uk_unit=PERCENTAGE, + uk2_unit=PERCENTAGE, + forecast_mode=["currently", "hourly", "daily"], + ), + "pressure": DarkskySensorEntityDescription( + key="pressure", + name="Pressure", + device_class=DEVICE_CLASS_PRESSURE, + si_unit=PRESSURE_MBAR, + us_unit=PRESSURE_MBAR, + ca_unit=PRESSURE_MBAR, + uk_unit=PRESSURE_MBAR, + uk2_unit=PRESSURE_MBAR, + forecast_mode=["currently", "hourly", "daily"], + ), + "visibility": DarkskySensorEntityDescription( + key="visibility", + name="Visibility", + si_unit=LENGTH_KILOMETERS, + us_unit=LENGTH_MILES, + ca_unit=LENGTH_KILOMETERS, + uk_unit=LENGTH_KILOMETERS, + uk2_unit=LENGTH_MILES, + icon="mdi:eye", + forecast_mode=["currently", "hourly", "daily"], + ), + "ozone": DarkskySensorEntityDescription( + key="ozone", + name="Ozone", + device_class=DEVICE_CLASS_OZONE, + si_unit="DU", + us_unit="DU", + ca_unit="DU", + uk_unit="DU", + uk2_unit="DU", + forecast_mode=["currently", "hourly", "daily"], + ), + "apparent_temperature_max": DarkskySensorEntityDescription( + key="apparent_temperature_max", + name="Daily High Apparent Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + si_unit=TEMP_CELSIUS, + us_unit=TEMP_FAHRENHEIT, + ca_unit=TEMP_CELSIUS, + uk_unit=TEMP_CELSIUS, + uk2_unit=TEMP_CELSIUS, + forecast_mode=["daily"], + ), + "apparent_temperature_high": DarkskySensorEntityDescription( + key="apparent_temperature_high", + name="Daytime High Apparent Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + si_unit=TEMP_CELSIUS, + us_unit=TEMP_FAHRENHEIT, + ca_unit=TEMP_CELSIUS, + uk_unit=TEMP_CELSIUS, + uk2_unit=TEMP_CELSIUS, + forecast_mode=["daily"], + ), + "apparent_temperature_min": DarkskySensorEntityDescription( + key="apparent_temperature_min", + name="Daily Low Apparent Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + si_unit=TEMP_CELSIUS, + us_unit=TEMP_FAHRENHEIT, + ca_unit=TEMP_CELSIUS, + uk_unit=TEMP_CELSIUS, + uk2_unit=TEMP_CELSIUS, + forecast_mode=["daily"], + ), + "apparent_temperature_low": DarkskySensorEntityDescription( + key="apparent_temperature_low", + name="Overnight Low Apparent Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + si_unit=TEMP_CELSIUS, + us_unit=TEMP_FAHRENHEIT, + ca_unit=TEMP_CELSIUS, + uk_unit=TEMP_CELSIUS, + uk2_unit=TEMP_CELSIUS, + forecast_mode=["daily"], + ), + "temperature_max": DarkskySensorEntityDescription( + key="temperature_max", + name="Daily High Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + si_unit=TEMP_CELSIUS, + us_unit=TEMP_FAHRENHEIT, + ca_unit=TEMP_CELSIUS, + uk_unit=TEMP_CELSIUS, + uk2_unit=TEMP_CELSIUS, + forecast_mode=["daily"], + ), + "temperature_high": DarkskySensorEntityDescription( + key="temperature_high", + name="Daytime High Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + si_unit=TEMP_CELSIUS, + us_unit=TEMP_FAHRENHEIT, + ca_unit=TEMP_CELSIUS, + uk_unit=TEMP_CELSIUS, + uk2_unit=TEMP_CELSIUS, + forecast_mode=["daily"], + ), + "temperature_min": DarkskySensorEntityDescription( + key="temperature_min", + name="Daily Low Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + si_unit=TEMP_CELSIUS, + us_unit=TEMP_FAHRENHEIT, + ca_unit=TEMP_CELSIUS, + uk_unit=TEMP_CELSIUS, + uk2_unit=TEMP_CELSIUS, + forecast_mode=["daily"], + ), + "temperature_low": DarkskySensorEntityDescription( + key="temperature_low", + name="Overnight Low Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + si_unit=TEMP_CELSIUS, + us_unit=TEMP_FAHRENHEIT, + ca_unit=TEMP_CELSIUS, + uk_unit=TEMP_CELSIUS, + uk2_unit=TEMP_CELSIUS, + forecast_mode=["daily"], + ), + "precip_intensity_max": DarkskySensorEntityDescription( + key="precip_intensity_max", + name="Daily Max Precip Intensity", + si_unit=PRECIPITATION_MILLIMETERS_PER_HOUR, + us_unit=PRECIPITATION_INCHES, + ca_unit=PRECIPITATION_MILLIMETERS_PER_HOUR, + uk_unit=PRECIPITATION_MILLIMETERS_PER_HOUR, + uk2_unit=PRECIPITATION_MILLIMETERS_PER_HOUR, + icon="mdi:thermometer", + forecast_mode=["daily"], + ), + "uv_index": DarkskySensorEntityDescription( + key="uv_index", + name="UV Index", + si_unit=UV_INDEX, + us_unit=UV_INDEX, + ca_unit=UV_INDEX, + uk_unit=UV_INDEX, + uk2_unit=UV_INDEX, + icon="mdi:weather-sunny", + forecast_mode=["currently", "hourly", "daily"], + ), + "moon_phase": DarkskySensorEntityDescription( + key="moon_phase", + name="Moon Phase", + icon="mdi:weather-night", + forecast_mode=["daily"], + ), + "sunrise_time": DarkskySensorEntityDescription( + key="sunrise_time", + name="Sunrise", + icon="mdi:white-balance-sunny", + forecast_mode=["daily"], + ), + "sunset_time": DarkskySensorEntityDescription( + key="sunset_time", + name="Sunset", + icon="mdi:weather-night", + forecast_mode=["daily"], + ), + "alerts": DarkskySensorEntityDescription( + key="alerts", + name="Alerts", + icon="mdi:alert-circle-outline", + forecast_mode=[], + ), } @@ -558,26 +605,31 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for variable in config[CONF_MONITORED_CONDITIONS]: if variable in DEPRECATED_SENSOR_TYPES: _LOGGER.warning("Monitored condition %s is deprecated", variable) - if not SENSOR_TYPES[variable][7] or "currently" in SENSOR_TYPES[variable][7]: + description = SENSOR_TYPES[variable] + if not description.forecast_mode or "currently" in description.forecast_mode: if variable == "alerts": - sensors.append(DarkSkyAlertSensor(forecast_data, variable, name)) + sensors.append(DarkSkyAlertSensor(forecast_data, description, name)) else: - sensors.append(DarkSkySensor(forecast_data, variable, name)) + sensors.append(DarkSkySensor(forecast_data, description, name)) - if forecast is not None and "daily" in SENSOR_TYPES[variable][7]: - for forecast_day in forecast: - sensors.append( + if forecast is not None and "daily" in description.forecast_mode: + sensors.extend( + [ DarkSkySensor( - forecast_data, variable, name, forecast_day=forecast_day + forecast_data, description, name, forecast_day=forecast_day ) - ) - if forecast_hour is not None and "hourly" in SENSOR_TYPES[variable][7]: - for forecast_h in forecast_hour: - sensors.append( + for forecast_day in forecast + ] + ) + if forecast_hour is not None and "hourly" in description.forecast_mode: + sensors.extend( + [ DarkSkySensor( - forecast_data, variable, name, forecast_hour=forecast_h + forecast_data, description, name, forecast_hour=forecast_h ) - ) + for forecast_h in forecast_hour + ] + ) add_entities(sensors, True) @@ -585,33 +637,31 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class DarkSkySensor(SensorEntity): """Implementation of a Dark Sky sensor.""" + entity_description: DarkskySensorEntityDescription + _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + def __init__( - self, forecast_data, sensor_type, name, forecast_day=None, forecast_hour=None + self, + forecast_data, + description: DarkskySensorEntityDescription, + name, + forecast_day=None, + forecast_hour=None, ): """Initialize the sensor.""" - self.client_name = name - self._name = SENSOR_TYPES[sensor_type][0] + self.entity_description = description self.forecast_data = forecast_data - self.type = sensor_type self.forecast_day = forecast_day self.forecast_hour = forecast_hour - self._state = None self._icon = None self._unit_of_measurement = None - @property - def name(self): - """Return the name of the sensor.""" - if self.forecast_day is not None: - return f"{self.client_name} {self._name} {self.forecast_day}d" - if self.forecast_hour is not None: - return f"{self.client_name} {self._name} {self.forecast_hour}h" - return f"{self.client_name} {self._name}" - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state + if forecast_day is not None: + self._attr_name = f"{name} {description.name} {forecast_day}d" + elif forecast_hour is not None: + self._attr_name = f"{name} {description.name} {forecast_hour}h" + else: + self._attr_name = f"{name} {description.name}" @property def native_unit_of_measurement(self): @@ -626,7 +676,7 @@ class DarkSkySensor(SensorEntity): @property def entity_picture(self): """Return the entity picture to use in the frontend, if any.""" - if self._icon is None or "summary" not in self.type: + if self._icon is None or "summary" not in self.entity_description.key: return None if self._icon in CONDITION_PICTURES: @@ -636,31 +686,19 @@ class DarkSkySensor(SensorEntity): def update_unit_of_measurement(self): """Update units based on unit system.""" - unit_index = {"si": 1, "us": 2, "ca": 3, "uk": 4, "uk2": 5}.get( - self.unit_system, 1 - ) - self._unit_of_measurement = SENSOR_TYPES[self.type][unit_index] + unit_key = MAP_UNIT_SYSTEM.get(self.unit_system, "si_unit") + self._unit_of_measurement = getattr(self.entity_description, unit_key) @property def icon(self): """Icon to use in the frontend, if any.""" - if "summary" in self.type and self._icon in CONDITION_PICTURES: + if ( + "summary" in self.entity_description.key + and self._icon in CONDITION_PICTURES + ): return CONDITION_PICTURES[self._icon].icon - return SENSOR_TYPES[self.type][6] - - @property - def device_class(self): - """Device class of the entity.""" - if SENSOR_TYPES[self.type][1] == TEMP_CELSIUS: - return DEVICE_CLASS_TEMPERATURE - - return None - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION} + return self.entity_description.icon def update(self): """Get the latest data from Dark Sky and updates the states.""" @@ -671,39 +709,42 @@ class DarkSkySensor(SensorEntity): self.forecast_data.update() self.update_unit_of_measurement() - if self.type == "minutely_summary": + sensor_type = self.entity_description.key + if sensor_type == "minutely_summary": self.forecast_data.update_minutely() minutely = self.forecast_data.data_minutely - self._state = getattr(minutely, "summary", "") + self._attr_native_value = getattr(minutely, "summary", "") self._icon = getattr(minutely, "icon", "") - elif self.type == "hourly_summary": + elif sensor_type == "hourly_summary": self.forecast_data.update_hourly() hourly = self.forecast_data.data_hourly - self._state = getattr(hourly, "summary", "") + self._attr_native_value = getattr(hourly, "summary", "") self._icon = getattr(hourly, "icon", "") elif self.forecast_hour is not None: self.forecast_data.update_hourly() hourly = self.forecast_data.data_hourly if hasattr(hourly, "data"): - self._state = self.get_state(hourly.data[self.forecast_hour]) + self._attr_native_value = self.get_state( + hourly.data[self.forecast_hour] + ) else: - self._state = 0 - elif self.type == "daily_summary": + self._attr_native_value = 0 + elif sensor_type == "daily_summary": self.forecast_data.update_daily() daily = self.forecast_data.data_daily - self._state = getattr(daily, "summary", "") + self._attr_native_value = getattr(daily, "summary", "") self._icon = getattr(daily, "icon", "") elif self.forecast_day is not None: self.forecast_data.update_daily() daily = self.forecast_data.data_daily if hasattr(daily, "data"): - self._state = self.get_state(daily.data[self.forecast_day]) + self._attr_native_value = self.get_state(daily.data[self.forecast_day]) else: - self._state = 0 + self._attr_native_value = 0 else: self.forecast_data.update_currently() currently = self.forecast_data.data_currently - self._state = self.get_state(currently) + self._attr_native_value = self.get_state(currently) def get_state(self, data): """ @@ -711,21 +752,22 @@ class DarkSkySensor(SensorEntity): If the sensor type is unknown, the current state is returned. """ - lookup_type = convert_to_camel(self.type) + sensor_type = self.entity_description.key + lookup_type = convert_to_camel(sensor_type) state = getattr(data, lookup_type, None) if state is None: return state - if "summary" in self.type: + if "summary" in sensor_type: self._icon = getattr(data, "icon", "") # Some state data needs to be rounded to whole values or converted to # percentages - if self.type in ["precip_probability", "cloud_cover", "humidity"]: + if sensor_type in {"precip_probability", "cloud_cover", "humidity"}: return round(state * 100, 1) - if self.type in [ + if sensor_type in { "dew_point", "temperature", "apparent_temperature", @@ -741,7 +783,7 @@ class DarkSkySensor(SensorEntity): "pressure", "ozone", "uvIndex", - ]: + }: return round(state, 1) return state @@ -749,30 +791,23 @@ class DarkSkySensor(SensorEntity): class DarkSkyAlertSensor(SensorEntity): """Implementation of a Dark Sky sensor.""" - def __init__(self, forecast_data, sensor_type, name): + entity_description: DarkskySensorEntityDescription + _attr_native_value: int | None + + def __init__( + self, forecast_data, description: DarkskySensorEntityDescription, name + ): """Initialize the sensor.""" - self.client_name = name - self._name = SENSOR_TYPES[sensor_type][0] + self.entity_description = description self.forecast_data = forecast_data - self.type = sensor_type - self._state = None - self._icon = None self._alerts = None - @property - def name(self): - """Return the name of the sensor.""" - return f"{self.client_name} {self._name}" - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state + self._attr_name = f"{name} {description.name}" @property def icon(self): """Icon to use in the frontend, if any.""" - if self._state is not None and self._state > 0: + if self._attr_native_value is not None and self._attr_native_value > 0: return "mdi:alert-circle" return "mdi:alert-circle-outline" @@ -790,7 +825,7 @@ class DarkSkyAlertSensor(SensorEntity): self.forecast_data.update() self.forecast_data.update_alerts() alerts = self.forecast_data.data_alerts - self._state = self.get_state(alerts) + self._attr_native_value = self.get_state(alerts) def get_state(self, data): """ From 82828b5a1b4364eb2b62d298b9f1c8008d95589c Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 7 Oct 2021 15:08:53 +0200 Subject: [PATCH 0159/1038] Fix netgear config flow import (#57253) --- homeassistant/components/netgear/device_tracker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/netgear/device_tracker.py b/homeassistant/components/netgear/device_tracker.py index f568a506552..0d3a1098f0c 100644 --- a/homeassistant/components/netgear/device_tracker.py +++ b/homeassistant/components/netgear/device_tracker.py @@ -4,6 +4,7 @@ import logging import voluptuous as vol from homeassistant.components.device_tracker import ( + DOMAIN as DEVICE_TRACKER_DOMAIN, PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, SOURCE_TYPE_ROUTER, ) @@ -50,7 +51,7 @@ async def async_get_scanner(hass, config): hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, - data=config, + data=config[DEVICE_TRACKER_DOMAIN], ) ) From 9dfb684002f1a408d77a8d53f277aa6213c8c181 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Thu, 7 Oct 2021 06:38:02 -0700 Subject: [PATCH 0160/1038] Enable template icons for template selects (#57092) Co-authored-by: Franck Nijhof --- homeassistant/components/template/select.py | 15 ++- tests/components/template/test_select.py | 138 +++++++++++++++++++- 2 files changed, 149 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/template/select.py b/homeassistant/components/template/select.py index 96e86e8caec..03136c0193d 100644 --- a/homeassistant/components/template/select.py +++ b/homeassistant/components/template/select.py @@ -13,7 +13,13 @@ from homeassistant.components.select.const import ( ATTR_OPTIONS, DOMAIN as SELECT_DOMAIN, ) -from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, CONF_UNIQUE_ID +from homeassistant.const import ( + CONF_ICON, + CONF_NAME, + CONF_OPTIMISTIC, + CONF_STATE, + CONF_UNIQUE_ID, +) from homeassistant.core import Config, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -41,6 +47,7 @@ SELECT_SCHEMA = vol.Schema( vol.Optional(CONF_AVAILABILITY): cv.template, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_ICON): cv.template, } ) @@ -64,6 +71,7 @@ async def _async_create_entities( definition[ATTR_OPTIONS], definition.get(CONF_OPTIMISTIC, DEFAULT_OPTIMISTIC), unique_id, + definition.get(CONF_ICON), ) ) return entities @@ -109,9 +117,12 @@ class TemplateSelect(TemplateEntity, SelectEntity): options_template: Template, optimistic: bool, unique_id: str | None, + icon_template: Template | None, ) -> None: """Initialize the select.""" - super().__init__(availability_template=availability_template) + super().__init__( + availability_template=availability_template, icon_template=icon_template + ) self._attr_name = DEFAULT_NAME name_template.hass = hass with contextlib.suppress(TemplateError): diff --git a/tests/components/template/test_select.py b/tests/components/template/test_select.py index 4deb02986b8..40ead0d637c 100644 --- a/tests/components/template/test_select.py +++ b/tests/components/template/test_select.py @@ -15,7 +15,7 @@ from homeassistant.components.select.const import ( DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION as SELECT_SERVICE_SELECT_OPTION, ) -from homeassistant.const import CONF_ENTITY_ID, STATE_UNKNOWN +from homeassistant.const import ATTR_ICON, CONF_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import Context from homeassistant.helpers.entity_registry import async_get @@ -144,7 +144,7 @@ async def test_missing_required_keys(hass, calls): async def test_templates_with_entities(hass, calls): - """Test tempalates with values from other entities.""" + """Test templates with values from other entities.""" with assert_setup_component(1, "input_select"): assert await setup.async_setup_component( hass, @@ -288,3 +288,137 @@ def _verify(hass, expected_current_option, expected_options, entity_name=_TEST_S attributes = state.attributes assert state.state == str(expected_current_option) assert attributes.get(SELECT_ATTR_OPTIONS) == expected_options + + +async def test_template_icon_with_entities(hass, calls): + """Test templates with values from other entities.""" + with assert_setup_component(1, "input_select"): + assert await setup.async_setup_component( + hass, + "input_select", + { + "input_select": { + "option": { + "options": ["a", "b"], + "initial": "a", + "name": "Option", + }, + } + }, + ) + + with assert_setup_component(1, "template"): + assert await setup.async_setup_component( + hass, + "template", + { + "template": { + "unique_id": "b", + "select": { + "state": f"{{{{ states('{_OPTION_INPUT_SELECT}') }}}}", + "options": f"{{{{ state_attr('{_OPTION_INPUT_SELECT}', '{INPUT_SELECT_ATTR_OPTIONS}') }}}}", + "select_option": { + "service": "input_select.select_option", + "data": { + "entity_id": _OPTION_INPUT_SELECT, + "option": "{{ option }}", + }, + }, + "optimistic": True, + "unique_id": "a", + "icon": f"{{% if (states('{_OPTION_INPUT_SELECT}') == 'a') %}}mdi:greater{{% else %}}mdi:less{{% endif %}}", + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get(_TEST_SELECT) + assert state.state == "a" + assert state.attributes[ATTR_ICON] == "mdi:greater" + + await hass.services.async_call( + INPUT_SELECT_DOMAIN, + INPUT_SELECT_SERVICE_SELECT_OPTION, + {CONF_ENTITY_ID: _OPTION_INPUT_SELECT, INPUT_SELECT_ATTR_OPTION: "b"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(_TEST_SELECT) + assert state.state == "b" + assert state.attributes[ATTR_ICON] == "mdi:less" + + +async def test_template_icon_with_trigger(hass): + """Test trigger based template select.""" + with assert_setup_component(1, "input_select"): + assert await setup.async_setup_component( + hass, + "input_select", + { + "input_select": { + "option": { + "options": ["a", "b"], + "initial": "a", + "name": "Option", + }, + } + }, + ) + + assert await setup.async_setup_component( + hass, + "template", + { + "template": { + "trigger": {"platform": "state", "entity_id": _OPTION_INPUT_SELECT}, + "select": { + "unique_id": "b", + "state": "{{ trigger.to_state.state }}", + "options": f"{{{{ state_attr('{_OPTION_INPUT_SELECT}', '{INPUT_SELECT_ATTR_OPTIONS}') }}}}", + "select_option": { + "service": "input_select.select_option", + "data": { + "entity_id": _OPTION_INPUT_SELECT, + "option": "{{ option }}", + }, + }, + "optimistic": True, + "icon": "{% if (trigger.to_state.state or '') == 'a' %}mdi:greater{% else %}mdi:less{% endif %}", + }, + }, + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + await hass.services.async_call( + INPUT_SELECT_DOMAIN, + INPUT_SELECT_SERVICE_SELECT_OPTION, + {CONF_ENTITY_ID: _OPTION_INPUT_SELECT, INPUT_SELECT_ATTR_OPTION: "b"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(_TEST_SELECT) + assert state is not None + assert state.state == "b" + assert state.attributes[ATTR_ICON] == "mdi:less" + + await hass.services.async_call( + INPUT_SELECT_DOMAIN, + INPUT_SELECT_SERVICE_SELECT_OPTION, + {CONF_ENTITY_ID: _OPTION_INPUT_SELECT, INPUT_SELECT_ATTR_OPTION: "a"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(_TEST_SELECT) + assert state.state == "a" + assert state.attributes[ATTR_ICON] == "mdi:greater" From 0e48985fc5aeec8870547cdecef4dec8959db533 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 7 Oct 2021 17:13:07 +0200 Subject: [PATCH 0161/1038] Validate initial value for input_datetime (#57256) --- .../components/input_datetime/__init__.py | 25 +++++++++++++++++ tests/components/input_datetime/test_init.py | 27 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index 84a7fb89fe1..2713eef17f8 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -81,6 +81,30 @@ def has_date_or_time(conf): raise vol.Invalid("Entity needs at least a date or a time") +def valid_initial(conf): + """Check the initial value is valid.""" + initial = conf.get(CONF_INITIAL) + if not initial: + return conf + + if conf[CONF_HAS_DATE] and conf[CONF_HAS_TIME]: + parsed_value = dt_util.parse_datetime(initial) + if parsed_value is not None: + return conf + raise vol.Invalid(f"Initial value '{initial}' can't be parsed as a datetime") + + if conf[CONF_HAS_DATE]: + parsed_value = dt_util.parse_date(initial) + if parsed_value is not None: + return conf + raise vol.Invalid(f"Initial value '{initial}' can't be parsed as a date") + + parsed_value = dt_util.parse_time(initial) + if parsed_value is not None: + return conf + raise vol.Invalid(f"Initial value '{initial}' can't be parsed as a time") + + CONFIG_SCHEMA = vol.Schema( { DOMAIN: cv.schema_with_slug_keys( @@ -93,6 +117,7 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_INITIAL): cv.string, }, has_date_or_time, + valid_initial, ) ) }, diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index 39497e1164c..0a968caf67f 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -744,3 +744,30 @@ async def test_timestamp(hass): finally: dt_util.set_default_time_zone(ORIG_TIMEZONE) + + +@pytest.mark.parametrize( + "config, error", + [ + ( + {"has_time": True, "has_date": True, "initial": "abc"}, + "'abc' can't be parsed as a datetime", + ), + ( + {"has_time": False, "has_date": True, "initial": "abc"}, + "'abc' can't be parsed as a date", + ), + ( + {"has_time": True, "has_date": False, "initial": "abc"}, + "'abc' can't be parsed as a time", + ), + ], +) +async def test_invalid_initial(hass, caplog, config, error): + """Test configuration is rejected if the initial value is invalid.""" + assert not await async_setup_component( + hass, + DOMAIN, + {DOMAIN: {"test_date": config}}, + ) + assert error in caplog.text From a238cce37c13e02842e1361ec79397e1f8c2d993 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 7 Oct 2021 17:44:25 +0200 Subject: [PATCH 0162/1038] Update led brightness select state only if valid data is available, Xiaomi Miio integration (#57197) * Update state if there is valid data * Add comment --- homeassistant/components/xiaomi_miio/select.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index daa721fef95..9e9cdcec1ae 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -146,10 +146,14 @@ class XiaomiAirHumidifierSelector(XiaomiSelector): @callback def _handle_coordinator_update(self): """Fetch state from the device.""" - self._current_led_brightness = self._extract_value_from_attribute( + led_brightness = self._extract_value_from_attribute( self.coordinator.data, self.entity_description.key ) - self.async_write_ha_state() + # 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: + self._current_led_brightness = led_brightness + self.async_write_ha_state() @property def current_option(self): From a2dcc0308b5051ed7076445891b87a7be0a7881c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 7 Oct 2021 05:52:24 -1000 Subject: [PATCH 0163/1038] Discover tplink devices periodically (#57221) - These devices sometimes do not respond on the first try or may be subject to transient broadcast failures, or overloads. We now try discovery periodically once the integration has been loaded. - We used to try this 4x at startup, but that solution seemed to aggressive as we want to be sure we pickup the devices after startup as well since the network will likely be more calm after startup. --- homeassistant/components/tplink/__init__.py | 18 +++++++++++++++- tests/components/tplink/test_init.py | 24 ++++++++++++++++----- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 5c40f61e2df..9f3658cf2cd 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from datetime import timedelta from typing import Any from kasa import SmartDevice, SmartDeviceException @@ -11,9 +12,15 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import network from homeassistant.config_entries import ConfigEntry, ConfigEntryNotReady -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME +from homeassistant.const import ( + CONF_HOST, + CONF_MAC, + CONF_NAME, + EVENT_HOMEASSISTANT_STARTED, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType from .const import ( @@ -32,6 +39,8 @@ from .migration import ( async_migrate_yaml_entries, ) +DISCOVERY_INTERVAL = timedelta(minutes=15) + TPLINK_HOST_SCHEMA = vol.Schema({vol.Required(CONF_HOST): cv.string}) CONFIG_SCHEMA = vol.Schema( @@ -118,6 +127,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if discovered_devices: async_trigger_discovery(hass, discovered_devices) + async def _async_discovery(*_: Any) -> None: + if discovered := await async_discover_devices(hass): + async_trigger_discovery(hass, discovered) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _async_discovery) + async_track_time_interval(hass, _async_discovery, DISCOVERY_INTERVAL) + return True diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index ee57a500a8d..c166fccc9b5 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -1,27 +1,41 @@ """Tests for the TP-Link component.""" from __future__ import annotations -from unittest.mock import patch +from datetime import timedelta +from unittest.mock import MagicMock, patch from homeassistant.components import tplink from homeassistant.components.tplink.const import DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STARTED from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from . import IP_ADDRESS, MAC_ADDRESS, _patch_discovery, _patch_single_discovery -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed async def test_configuring_tplink_causes_discovery(hass): """Test that specifying empty config does discovery.""" with patch("homeassistant.components.tplink.Discover.discover") as discover: - discover.return_value = {"host": 1234} + discover.return_value = {MagicMock(): MagicMock()} await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() + call_count = len(discover.mock_calls) + assert discover.mock_calls - assert discover.mock_calls + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert len(discover.mock_calls) == call_count * 2 + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=15)) + await hass.async_block_till_done() + assert len(discover.mock_calls) == call_count * 3 + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=30)) + await hass.async_block_till_done() + assert len(discover.mock_calls) == call_count * 4 async def test_config_entry_reload(hass): From c651cff6a07a05a12b613714206a63317e4a0fc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Thu, 7 Oct 2021 18:23:03 +0200 Subject: [PATCH 0164/1038] Bump Mill library to 0.6.1 (#57261) --- homeassistant/components/mill/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index bf332487a88..e5dbbdfc1e8 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -2,7 +2,7 @@ "domain": "mill", "name": "Mill", "documentation": "https://www.home-assistant.io/integrations/mill", - "requirements": ["millheater==0.6.0"], + "requirements": ["millheater==0.6.1"], "codeowners": ["@danielhiversen"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 28eaa8072fe..df09b777557 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1002,7 +1002,7 @@ micloud==0.3 miflora==0.7.0 # homeassistant.components.mill -millheater==0.6.0 +millheater==0.6.1 # homeassistant.components.minio minio==4.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4bd5e936133..96aaae1dfdd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -588,7 +588,7 @@ mficlient==0.3.0 micloud==0.3 # homeassistant.components.mill -millheater==0.6.0 +millheater==0.6.1 # homeassistant.components.minio minio==4.0.9 From dc5e4392ae7da417293700e8e39f425a903aeb92 Mon Sep 17 00:00:00 2001 From: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> Date: Thu, 7 Oct 2021 18:30:17 +0200 Subject: [PATCH 0165/1038] Refactor Xiaomi vacuum to sensors (#54990) * Refactor Xiaomi vacuum with sensors. This is the first step into refactoring xiaomi vacuum attributes into sensors. What is missing are some switches and binary sensors etc. https://github.com/home-assistant/core/issues/51361 Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> * Use generic sensor for Xiaomi vacuum sensors. By using HA coordinator, the generic Xiaomi sensor class can be used with these coordinators to get the status sensors from vacuum. This also means now that sensors are available as soon as HA starts, which is a nice plus. Now the only reason to create a subclass of the generic sensors is when custom value parsing needs to be done. https://github.com/home-assistant/core/issues/51361 Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> * Working vacuum sensors via 1 coordinator. Vacuum needs a custom coordinator to ensure that it fetches all the needed data and puts it in a dict. From this dict the sensors will then get their data accordingly. https://github.com/home-assistant/core/issues/51361 Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> * Remove vacuum setup method in sensor Sensor is generic enough that vacuum does not require its own setup method. https://github.com/home-assistant/core/issues/51361 Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> * Don't auto register generic sensors. Let the user decide which sensor is useful for them and enable them. https://github.com/home-assistant/core/issues/51361 Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> * Remove converted attributes from xiaomi vacuum. The attributes that have been converted so far should be removed from the vacuum attributes list. https://github.com/home-assistant/core/issues/51361 Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> * Fetch data from vacuum sequentially. It seems some vacuums do not like parallel requests. The requests that came before are ignored. https://github.com/home-assistant/core/issues/51361 Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> * Refactor vacuum sensors to its own container. By moving vacuum sensors to its own container, there is no more key collisions. This in turns means that there is need for the split hack to ensure key names are correct. https://github.com/home-assistant/core/issues/51361 Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> * fixup! fix bad rebase. Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> * Fix sensor naming and default registration. Use proper names for sensors, no need to include from which device status it came. Registration of the sensor by default has been parameterised. If the param is not set, the sensor is not registered. https://github.com/home-assistant/core/issues/51361 Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> * Make vacuum platform also use coordinator for its data. By using the coordinator for data in vacuum platfrom, removes the cases where request gets ignored during the case where the requests are done concurrently by separate platforms. https://github.com/home-assistant/core/issues/51361 Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> * Add binary sensor for vacuum Add binary sensor for waterbox, mop, and water shortage. https://github.com/home-assistant/core/issues/51361 Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> * Added proper icons to sensors. https://github.com/home-assistant/core/issues/51361 Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> * Refactor sensors to use dataclass. Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> * Make vacuum use coordinator for its data. This commit also insures that the binary sensors are only registered for devices that include a mop. Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Minor refactoring Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> * Update data from coordinator after running command. This is is to have a faster status change when executing a command like changing fan speeds. If a manual refresh is not triggered. Worst case scenario it will take 10s for the new fan speed to be reported in HA. Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> * Refresh within coroutine is ok. Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> * Move logging to _handle_coordinator_update Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> * Use internal state attribute. Signed-off-by: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> * Fix vacuum typing * Fix tests constants * Fix vacuum inconsistent return values * Fix vacuum state update * Fix vacuum tests * Remove impossible cases * Parametrize custom services test * Move goto test * Move clean segment test * Move clean single segment test * Test service pause * Include vacuum in coverage * Delint * Fix vacuum sensor dict collision. This also prevents collision for unique id. As the key is used to construct unique ids. * Use f strings as dict keys Co-authored-by: Martin Hjelmare --- .coveragerc | 1 - .../components/xiaomi_miio/__init__.py | 143 ++++++-- .../components/xiaomi_miio/binary_sensor.py | 89 ++++- homeassistant/components/xiaomi_miio/const.py | 11 +- .../components/xiaomi_miio/device.py | 52 +++ .../components/xiaomi_miio/sensor.py | 185 +++++++++- .../components/xiaomi_miio/vacuum.py | 219 ++++-------- tests/components/xiaomi_miio/__init__.py | 1 + .../xiaomi_miio/test_config_flow.py | 4 +- tests/components/xiaomi_miio/test_vacuum.py | 323 +++++++++--------- 10 files changed, 664 insertions(+), 364 deletions(-) diff --git a/.coveragerc b/.coveragerc index 8a53086d840..8ee3a3ceab6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1234,7 +1234,6 @@ omit = homeassistant/components/xiaomi_miio/select.py homeassistant/components/xiaomi_miio/sensor.py homeassistant/components/xiaomi_miio/switch.py - homeassistant/components/xiaomi_miio/vacuum.py homeassistant/components/xiaomi_tv/media_player.py homeassistant/components/xmpp/notify.py homeassistant/components/xs1/* diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 157199e977a..11fe4cc1337 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -1,4 +1,7 @@ """Support for Xiaomi Miio.""" +from __future__ import annotations + +from dataclasses import dataclass from datetime import timedelta import logging @@ -11,7 +14,11 @@ from miio import ( AirPurifier, AirPurifierMB4, AirPurifierMiot, + CleaningDetails, + CleaningSummary, + ConsumableStatus, DeviceException, + DNDStatus, Fan, Fan1C, FanP5, @@ -19,6 +26,9 @@ from miio import ( FanP10, FanP11, FanZA5, + Timer, + Vacuum, + VacuumStatus, ) from miio.gateway.gateway import GatewayException @@ -72,7 +82,7 @@ HUMIDIFIER_PLATFORMS = [ "switch", ] LIGHT_PLATFORMS = ["light"] -VACUUM_PLATFORMS = ["vacuum"] +VACUUM_PLATFORMS = ["binary_sensor", "sensor", "vacuum"] AIR_MONITOR_PLATFORMS = ["air_quality", "sensor"] MODEL_TO_CLASS_MAP = { @@ -133,6 +143,99 @@ def get_platforms(config_entry): return [] +def _async_update_data_default(hass, device): + async def update(): + """Fetch data from the device using async_add_executor_job.""" + try: + async with async_timeout.timeout(10): + state = await hass.async_add_executor_job(device.status) + _LOGGER.debug("Got new state: %s", state) + return state + + except DeviceException as ex: + raise UpdateFailed(ex) from ex + + return update + + +@dataclass(frozen=True) +class VacuumCoordinatorData: + """A class that holds the vacuum data retrieved by the coordinator.""" + + status: VacuumStatus + dnd_status: DNDStatus + last_clean_details: CleaningDetails + consumable_status: ConsumableStatus + clean_history_status: CleaningSummary + timers: list[Timer] + fan_speeds: dict[str, int] + fan_speeds_reverse: dict[int, str] + + +@dataclass(init=False, frozen=True) +class VacuumCoordinatorDataAttributes: + """ + A class that holds attribute names for VacuumCoordinatorData. + + These attributes can be used in methods like `getattr` when a generic solutions is + needed. + See homeassistant.components.xiaomi_miio.device.XiaomiCoordinatedMiioEntity + ._extract_value_from_attribute for + an example. + """ + + status: str = "status" + dnd_status: str = "dnd_status" + last_clean_details: str = "last_clean_details" + consumable_status: str = "consumable_status" + clean_history_status: str = "clean_history_status" + timer: str = "timer" + fan_speeds: str = "fan_speeds" + fan_speeds_reverse: str = "fan_speeds_reverse" + + +def _async_update_data_vacuum(hass, device: Vacuum): + def update() -> VacuumCoordinatorData: + timer = [] + + # See https://github.com/home-assistant/core/issues/38285 for reason on + # Why timers must be fetched separately. + try: + timer = device.timer() + except DeviceException as ex: + _LOGGER.debug( + "Unable to fetch timers, this may happen on some devices: %s", ex + ) + + fan_speeds = device.fan_speed_presets() + + data = VacuumCoordinatorData( + device.status(), + device.dnd_status(), + device.last_clean_details(), + device.consumable_status(), + device.clean_history(), + timer, + fan_speeds, + {v: k for k, v in fan_speeds.items()}, + ) + + return data + + async def update_async(): + """Fetch data from the device using async_add_executor_job.""" + try: + async with async_timeout.timeout(10): + state = await hass.async_add_executor_job(update) + _LOGGER.debug("Got new vacuum state: %s", state) + return state + + except DeviceException as ex: + raise UpdateFailed(ex) from ex + + return update_async + + async def async_create_miio_device_and_coordinator( hass: core.HomeAssistant, entry: config_entries.ConfigEntry ): @@ -143,8 +246,14 @@ async def async_create_miio_device_and_coordinator( name = entry.title device = None migrate = False + update_method = _async_update_data_default + coordinator_class = DataUpdateCoordinator - if model not in MODELS_HUMIDIFIER and model not in MODELS_FAN: + if ( + model not in MODELS_HUMIDIFIER + and model not in MODELS_FAN + and model not in MODELS_VACUUM + ): return _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) @@ -168,6 +277,10 @@ async def async_create_miio_device_and_coordinator( device = AirPurifier(host, token) elif model.startswith("zhimi.airfresh."): device = AirFresh(host, token) + elif model in MODELS_VACUUM: + device = Vacuum(host, token) + update_method = _async_update_data_vacuum + coordinator_class = DataUpdateCoordinator[VacuumCoordinatorData] # Pedestal fans elif model in MODEL_TO_CLASS_MAP: device = MODEL_TO_CLASS_MAP[model](host, token) @@ -192,34 +305,12 @@ async def async_create_miio_device_and_coordinator( hass.config_entries.async_update_entry(entry, title=migrate_entity_name) entity_registry.async_remove(entity_id) - async def async_update_data(): - """Fetch data from the device using async_add_executor_job.""" - - async def _async_fetch_data(): - """Fetch data from the device.""" - async with async_timeout.timeout(10): - state = await hass.async_add_executor_job(device.status) - _LOGGER.debug("Got new state: %s", state) - return state - - try: - return await _async_fetch_data() - except DeviceException as ex: - if getattr(ex, "code", None) != -9999: - raise UpdateFailed(ex) from ex - _LOGGER.info("Got exception while fetching the state, trying again: %s", ex) - # Try to fetch the data a second time after error code -9999 - try: - return await _async_fetch_data() - except DeviceException as ex: - raise UpdateFailed(ex) from ex - # Create update miio device and coordinator - coordinator = DataUpdateCoordinator( + coordinator = coordinator_class( hass, _LOGGER, name=name, - update_method=async_update_data, + update_method=update_method(hass, device), # Polling interval. Will only be polled if there are subscribers. update_interval=timedelta(seconds=60), ) diff --git a/homeassistant/components/xiaomi_miio/binary_sensor.py b/homeassistant/components/xiaomi_miio/binary_sensor.py index 61c3a4fde61..28553c159fe 100644 --- a/homeassistant/components/xiaomi_miio/binary_sensor.py +++ b/homeassistant/components/xiaomi_miio/binary_sensor.py @@ -1,17 +1,19 @@ """Support for Xiaomi Miio binary sensors.""" from __future__ import annotations -from collections.abc import Callable from dataclasses import dataclass -from enum import Enum +import logging +from typing import Callable from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_PLUG, + DEVICE_CLASS_PROBLEM, BinarySensorEntity, BinarySensorEntityDescription, ) +from . import VacuumCoordinatorDataAttributes from .const import ( CONF_DEVICE, CONF_FLOW_TYPE, @@ -23,12 +25,20 @@ from .const import ( MODELS_HUMIDIFIER_MIIO, MODELS_HUMIDIFIER_MIOT, MODELS_HUMIDIFIER_MJJSQ, + MODELS_VACUUM, + MODELS_VACUUM_WITH_MOP, ) from .device import XiaomiCoordinatedMiioEntity +_LOGGER = logging.getLogger(__name__) + + ATTR_NO_WATER = "no_water" ATTR_POWERSUPPLY_ATTACHED = "powersupply_attached" ATTR_WATER_TANK_DETACHED = "water_tank_detached" +ATTR_MOP_ATTACHED = "is_water_box_carriage_attached" +ATTR_WATER_BOX_ATTACHED = "is_water_box_attached" +ATTR_WATER_SHORTAGE = "is_water_shortage" @dataclass @@ -36,6 +46,7 @@ class XiaomiMiioBinarySensorDescription(BinarySensorEntityDescription): """A class that describes binary sensor entities.""" value: Callable | None = None + parent_key: str | None = None BINARY_SENSOR_TYPES = ( @@ -59,11 +70,63 @@ BINARY_SENSOR_TYPES = ( ) FAN_ZA5_BINARY_SENSORS = (ATTR_POWERSUPPLY_ATTACHED,) + +VACUUM_SENSORS = { + ATTR_MOP_ATTACHED: XiaomiMiioBinarySensorDescription( + key=ATTR_MOP_ATTACHED, + name="Mop Attached", + icon="mdi:square-rounded", + parent_key=VacuumCoordinatorDataAttributes.status, + entity_registry_enabled_default=True, + device_class=DEVICE_CLASS_CONNECTIVITY, + ), + ATTR_WATER_BOX_ATTACHED: XiaomiMiioBinarySensorDescription( + key=ATTR_WATER_BOX_ATTACHED, + name="Water Box Attached", + icon="mdi:water", + parent_key=VacuumCoordinatorDataAttributes.status, + entity_registry_enabled_default=True, + device_class=DEVICE_CLASS_CONNECTIVITY, + ), + ATTR_WATER_SHORTAGE: XiaomiMiioBinarySensorDescription( + key=ATTR_WATER_SHORTAGE, + name="Water Shortage", + icon="mdi:water", + parent_key=VacuumCoordinatorDataAttributes.status, + entity_registry_enabled_default=True, + device_class=DEVICE_CLASS_PROBLEM, + ), +} + HUMIDIFIER_MIIO_BINARY_SENSORS = (ATTR_WATER_TANK_DETACHED,) HUMIDIFIER_MIOT_BINARY_SENSORS = (ATTR_WATER_TANK_DETACHED,) HUMIDIFIER_MJJSQ_BINARY_SENSORS = (ATTR_NO_WATER, ATTR_WATER_TANK_DETACHED) +def _setup_vacuum_sensors(hass, config_entry, async_add_entities): + """Only vacuums with mop should have binary sensor registered.""" + + if config_entry.data[CONF_MODEL] not in MODELS_VACUUM_WITH_MOP: + return + + device = hass.data[DOMAIN][config_entry.entry_id].get(KEY_DEVICE) + entities = [] + + for sensor, description in VACUUM_SENSORS.items(): + entities.append( + XiaomiGenericBinarySensor( + f"{config_entry.title} {description.name}", + device, + config_entry, + f"{sensor}_{config_entry.unique_id}", + hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR], + description, + ) + ) + + async_add_entities(entities) + + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Xiaomi sensor from a config entry.""" entities = [] @@ -79,6 +142,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensors = HUMIDIFIER_MIOT_BINARY_SENSORS elif model in MODELS_HUMIDIFIER_MJJSQ: sensors = HUMIDIFIER_MJJSQ_BINARY_SENSORS + elif model in MODELS_VACUUM: + return _setup_vacuum_sensors(hass, config_entry, async_add_entities) + for description in BINARY_SENSOR_TYPES: if description.key not in sensors: continue @@ -103,11 +169,20 @@ class XiaomiGenericBinarySensor(XiaomiCoordinatedMiioEntity, BinarySensorEntity) """Initialize the entity.""" super().__init__(name, device, entry, unique_id, coordinator) - self.entity_description = description + self.entity_description: XiaomiMiioBinarySensorDescription = description + self._attr_entity_registry_enabled_default = ( + description.entity_registry_enabled_default + ) @property def is_on(self): """Return true if the binary sensor is on.""" + if self.entity_description.parent_key is not None: + return self._extract_value_from_attribute( + getattr(self.coordinator.data, self.entity_description.parent_key), + self.entity_description.key, + ) + state = self._extract_value_from_attribute( self.coordinator.data, self.entity_description.key ) @@ -115,11 +190,3 @@ class XiaomiGenericBinarySensor(XiaomiCoordinatedMiioEntity, BinarySensorEntity) return self.entity_description.value(state) return state - - @staticmethod - def _extract_value_from_attribute(state, attribute): - value = getattr(state, attribute) - if isinstance(value, Enum): - return value.value - - return value diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index cda65bdf0aa..4adc2d287dd 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -1,4 +1,12 @@ """Constants for the Xiaomi Miio component.""" +from miio.vacuum import ( + ROCKROBO_S5, + ROCKROBO_S6, + ROCKROBO_S6_MAXV, + ROCKROBO_S7, + ROCKROBO_V1, +) + DOMAIN = "xiaomi_miio" # Config flow @@ -177,7 +185,8 @@ MODELS_LIGHT = ( + MODELS_LIGHT_BULB + MODELS_LIGHT_MONO ) -MODELS_VACUUM = ["roborock.vacuum", "rockrobo.vacuum"] +MODELS_VACUUM = [ROCKROBO_V1, ROCKROBO_S5, ROCKROBO_S6, ROCKROBO_S6_MAXV, ROCKROBO_S7] +MODELS_VACUUM_WITH_MOP = [ROCKROBO_S5, ROCKROBO_S6, ROCKROBO_S6_MAXV, ROCKROBO_S7] MODELS_AIR_MONITOR = [ MODEL_AIRQUALITYMONITOR_V1, MODEL_AIRQUALITYMONITOR_B1, diff --git a/homeassistant/components/xiaomi_miio/device.py b/homeassistant/components/xiaomi_miio/device.py index f8402138f21..aa81d8c23b6 100644 --- a/homeassistant/components/xiaomi_miio/device.py +++ b/homeassistant/components/xiaomi_miio/device.py @@ -1,4 +1,6 @@ """Code to handle a Xiaomi Device.""" +import datetime +from enum import Enum from functools import partial import logging @@ -157,3 +159,53 @@ class XiaomiCoordinatedMiioEntity(CoordinatorEntity): _LOGGER.error(mask_error, exc) return False + + @classmethod + def _extract_value_from_attribute(cls, state, attribute): + value = getattr(state, attribute) + if isinstance(value, Enum): + return value.value + if isinstance(value, datetime.timedelta): + return cls._parse_time_delta(value) + if isinstance(value, datetime.time): + return cls._parse_datetime_time(value) + if isinstance(value, datetime.datetime): + return cls._parse_datetime_datetime(value) + if isinstance(value, datetime.timedelta): + return cls._parse_time_delta(value) + if isinstance(value, float): + return value + if isinstance(value, int): + return value + + _LOGGER.warning( + "Could not determine how to parse state value of type %s for state %s and attribute %s", + type(value), + type(state), + attribute, + ) + + return value + + @staticmethod + def _parse_time_delta(timedelta: datetime.timedelta) -> int: + return timedelta.seconds + + @staticmethod + def _parse_datetime_time(time: datetime.time) -> str: + time = datetime.datetime.now().replace( + hour=time.hour, minute=time.minute, second=0, microsecond=0 + ) + + if time < datetime.datetime.now(): + time += datetime.timedelta(days=1) + + return time.isoformat() + + @staticmethod + def _parse_datetime_datetime(time: datetime.datetime) -> str: + return time.isoformat() + + @staticmethod + def _parse_datetime_timedelta(time: datetime.timedelta) -> int: + return time.seconds diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index d199c051eae..e7516bbca65 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations from dataclasses import dataclass -from enum import Enum import logging from miio import AirQualityMonitor, DeviceException @@ -22,6 +21,7 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.const import ( + AREA_SQUARE_METERS, ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, @@ -37,15 +37,18 @@ from homeassistant.const import ( DEVICE_CLASS_POWER, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, LIGHT_LUX, PERCENTAGE, POWER_WATT, PRESSURE_HPA, TEMP_CELSIUS, TIME_HOURS, + TIME_SECONDS, VOLUME_CUBIC_METERS, ) +from . import VacuumCoordinatorDataAttributes from .const import ( CONF_DEVICE, CONF_FLOW_TYPE, @@ -75,6 +78,7 @@ from .const import ( MODELS_HUMIDIFIER_MJJSQ, MODELS_PURIFIER_MIIO, MODELS_PURIFIER_MIOT, + MODELS_VACUUM, ) from .device import XiaomiCoordinatedMiioEntity, XiaomiMiioEntity from .gateway import XiaomiGatewayDevice @@ -109,6 +113,20 @@ ATTR_PRESSURE = "pressure" ATTR_PURIFY_VOLUME = "purify_volume" ATTR_SENSOR_STATE = "sensor_state" ATTR_WATER_LEVEL = "water_level" +ATTR_DND_START = "start" +ATTR_DND_END = "end" +ATTR_LAST_CLEAN_TIME = "duration" +ATTR_LAST_CLEAN_AREA = "area" +ATTR_LAST_CLEAN_START = "start" +ATTR_LAST_CLEAN_END = "end" +ATTR_CLEAN_HISTORY_TOTAL_DURATION = "total_duration" +ATTR_CLEAN_HISTORY_TOTAL_AREA = "total_area" +ATTR_CLEAN_HISTORY_COUNT = "count" +ATTR_CLEAN_HISTORY_DUST_COLLECTION_COUNT = "dust_collection_count" +ATTR_CONSUMABLE_STATUS_MAIN_BRUSH_LEFT = "main_brush_left" +ATTR_CONSUMABLE_STATUS_SIDE_BRUSH_LEFT = "side_brush_left" +ATTR_CONSUMABLE_STATUS_FILTER_LEFT = "filter_left" +ATTR_CONSUMABLE_STATUS_SENSOR_DIRTY_LEFT = "sensor_dirty_left" @dataclass @@ -116,6 +134,7 @@ class XiaomiMiioSensorDescription(SensorEntityDescription): """Class that holds device specific info for a xiaomi aqara or humidifier sensor.""" attributes: tuple = () + parent_key: str | None = None SENSOR_TYPES = { @@ -348,6 +367,138 @@ MODEL_TO_SENSORS_MAP = { MODEL_FAN_ZA5: FAN_ZA5_SENSORS, } +VACUUM_SENSORS = { + f"dnd_{ATTR_DND_START}": XiaomiMiioSensorDescription( + key=ATTR_DND_START, + icon="mdi:minus-circle-off", + name="DnD Start", + device_class=DEVICE_CLASS_TIMESTAMP, + parent_key=VacuumCoordinatorDataAttributes.dnd_status, + entity_registry_enabled_default=False, + ), + f"dnd_{ATTR_DND_END}": XiaomiMiioSensorDescription( + key=ATTR_DND_END, + icon="mdi:minus-circle-off", + name="DnD End", + device_class=DEVICE_CLASS_TIMESTAMP, + parent_key=VacuumCoordinatorDataAttributes.dnd_status, + entity_registry_enabled_default=False, + ), + f"last_clean_{ATTR_LAST_CLEAN_START}": XiaomiMiioSensorDescription( + key=ATTR_LAST_CLEAN_START, + icon="mdi:clock-time-twelve", + name="Last Clean Start", + device_class=DEVICE_CLASS_TIMESTAMP, + parent_key=VacuumCoordinatorDataAttributes.last_clean_details, + ), + f"last_clean_{ATTR_LAST_CLEAN_END}": XiaomiMiioSensorDescription( + key=ATTR_LAST_CLEAN_END, + icon="mdi:clock-time-twelve", + device_class=DEVICE_CLASS_TIMESTAMP, + parent_key=VacuumCoordinatorDataAttributes.last_clean_details, + name="Last Clean End", + ), + f"last_clean_{ATTR_LAST_CLEAN_TIME}": XiaomiMiioSensorDescription( + native_unit_of_measurement=TIME_SECONDS, + icon="mdi:timer-sand", + key=ATTR_LAST_CLEAN_TIME, + parent_key=VacuumCoordinatorDataAttributes.last_clean_details, + name="Last Clean Duration", + ), + f"last_clean_{ATTR_LAST_CLEAN_AREA}": XiaomiMiioSensorDescription( + native_unit_of_measurement=AREA_SQUARE_METERS, + icon="mdi:texture-box", + key=ATTR_LAST_CLEAN_AREA, + parent_key=VacuumCoordinatorDataAttributes.last_clean_details, + name="Last Clean Area", + ), + f"clean_history_{ATTR_CLEAN_HISTORY_TOTAL_DURATION}": XiaomiMiioSensorDescription( + native_unit_of_measurement=TIME_SECONDS, + icon="mdi:timer-sand", + key=ATTR_CLEAN_HISTORY_TOTAL_DURATION, + parent_key=VacuumCoordinatorDataAttributes.clean_history_status, + name="Total duration", + entity_registry_enabled_default=False, + ), + f"clean_history_{ATTR_CLEAN_HISTORY_TOTAL_AREA}": XiaomiMiioSensorDescription( + native_unit_of_measurement=AREA_SQUARE_METERS, + icon="mdi:texture-box", + key=ATTR_CLEAN_HISTORY_TOTAL_AREA, + parent_key=VacuumCoordinatorDataAttributes.clean_history_status, + name="Total Clean Area", + entity_registry_enabled_default=False, + ), + f"clean_history_{ATTR_CLEAN_HISTORY_COUNT}": XiaomiMiioSensorDescription( + native_unit_of_measurement="", + icon="mdi:counter", + state_class=STATE_CLASS_TOTAL_INCREASING, + key=ATTR_CLEAN_HISTORY_COUNT, + parent_key=VacuumCoordinatorDataAttributes.clean_history_status, + name="Total Clean Count", + entity_registry_enabled_default=False, + ), + f"clean_history_{ATTR_CLEAN_HISTORY_DUST_COLLECTION_COUNT}": XiaomiMiioSensorDescription( + native_unit_of_measurement="", + icon="mdi:counter", + state_class="total_increasing", + key=ATTR_CLEAN_HISTORY_DUST_COLLECTION_COUNT, + parent_key=VacuumCoordinatorDataAttributes.clean_history_status, + name="Total Dust Collection Count", + entity_registry_enabled_default=False, + ), + f"consumable_{ATTR_CONSUMABLE_STATUS_MAIN_BRUSH_LEFT}": XiaomiMiioSensorDescription( + native_unit_of_measurement=TIME_SECONDS, + icon="mdi:brush", + key=ATTR_CONSUMABLE_STATUS_MAIN_BRUSH_LEFT, + parent_key=VacuumCoordinatorDataAttributes.consumable_status, + name="Main Brush Left", + entity_registry_enabled_default=False, + ), + f"consumable_{ATTR_CONSUMABLE_STATUS_SIDE_BRUSH_LEFT}": XiaomiMiioSensorDescription( + native_unit_of_measurement=TIME_SECONDS, + icon="mdi:brush", + key=ATTR_CONSUMABLE_STATUS_SIDE_BRUSH_LEFT, + parent_key=VacuumCoordinatorDataAttributes.consumable_status, + name="Side Brush Left", + entity_registry_enabled_default=False, + ), + f"consumable_{ATTR_CONSUMABLE_STATUS_FILTER_LEFT}": XiaomiMiioSensorDescription( + native_unit_of_measurement=TIME_SECONDS, + icon="mdi:air-filter", + key=ATTR_CONSUMABLE_STATUS_FILTER_LEFT, + parent_key=VacuumCoordinatorDataAttributes.consumable_status, + name="Filter Left", + entity_registry_enabled_default=False, + ), + f"consumable_{ATTR_CONSUMABLE_STATUS_SENSOR_DIRTY_LEFT}": XiaomiMiioSensorDescription( + native_unit_of_measurement=TIME_SECONDS, + icon="mdi:eye-outline", + key=ATTR_CONSUMABLE_STATUS_SENSOR_DIRTY_LEFT, + parent_key=VacuumCoordinatorDataAttributes.consumable_status, + name="Sensor Dirty Left", + entity_registry_enabled_default=False, + ), +} + + +def _setup_vacuum_sensors(hass, config_entry, async_add_entities): + device = hass.data[DOMAIN][config_entry.entry_id].get(KEY_DEVICE) + entities = [] + + for sensor, description in VACUUM_SENSORS.items(): + entities.append( + XiaomiGenericSensor( + f"{config_entry.title} {description.name}", + device, + config_entry, + f"{sensor}_{config_entry.unique_id}", + hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR], + description, + ) + ) + + async_add_entities(entities) + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Xiaomi sensor from a config entry.""" @@ -416,6 +567,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensors = PURIFIER_MIIO_SENSORS elif model in MODELS_PURIFIER_MIOT: sensors = PURIFIER_MIOT_SENSORS + elif model in MODELS_VACUUM: + return _setup_vacuum_sensors(hass, config_entry, async_add_entities) for sensor, description in SENSOR_TYPES.items(): if sensor not in sensors: @@ -435,19 +588,31 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class XiaomiGenericSensor(XiaomiCoordinatedMiioEntity, SensorEntity): - """Representation of a Xiaomi Humidifier sensor.""" + """Representation of a Xiaomi generic sensor.""" - def __init__(self, name, device, entry, unique_id, coordinator, description): + def __init__( + self, + name, + device, + entry, + unique_id, + coordinator, + description: XiaomiMiioSensorDescription, + ): """Initialize the entity.""" super().__init__(name, device, entry, unique_id, coordinator) - - self._attr_name = name self._attr_unique_id = unique_id - self.entity_description = description + self.entity_description: XiaomiMiioSensorDescription = description @property def native_value(self): """Return the state of the device.""" + if self.entity_description.parent_key is not None: + return self._extract_value_from_attribute( + getattr(self.coordinator.data, self.entity_description.parent_key), + self.entity_description.key, + ) + return self._extract_value_from_attribute( self.coordinator.data, self.entity_description.key ) @@ -461,14 +626,6 @@ class XiaomiGenericSensor(XiaomiCoordinatedMiioEntity, SensorEntity): if hasattr(self.coordinator.data, attr) } - @staticmethod - def _extract_value_from_attribute(state, attribute): - value = getattr(state, attribute) - if isinstance(value, Enum): - return value.value - - return value - class XiaomiAirQualityMonitor(XiaomiMiioEntity, SensorEntity): """Representation of a Xiaomi Air Quality Monitor.""" diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 94d93d77f2f..60d557837fb 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -1,12 +1,13 @@ """Support for the Xiaomi vacuum cleaner robot.""" +from __future__ import annotations + from functools import partial import logging -from miio import DeviceException, Vacuum +from miio import DeviceException import voluptuous as vol from homeassistant.components.vacuum import ( - ATTR_CLEANED_AREA, STATE_CLEANING, STATE_DOCKED, STATE_ERROR, @@ -25,13 +26,18 @@ from homeassistant.components.vacuum import ( SUPPORT_STOP, StateVacuumEntity, ) -from homeassistant.const import CONF_HOST, CONF_TOKEN, STATE_OFF, STATE_ON +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.util.dt import as_utc +from . import VacuumCoordinatorData +from ...helpers.update_coordinator import DataUpdateCoordinator from .const import ( CONF_DEVICE, CONF_FLOW_TYPE, + DOMAIN, + KEY_COORDINATOR, + KEY_DEVICE, SERVICE_CLEAN_SEGMENT, SERVICE_CLEAN_ZONE, SERVICE_GOTO, @@ -40,25 +46,10 @@ from .const import ( SERVICE_START_REMOTE_CONTROL, SERVICE_STOP_REMOTE_CONTROL, ) -from .device import XiaomiMiioEntity +from .device import XiaomiCoordinatedMiioEntity _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = "Xiaomi Vacuum cleaner" - -ATTR_CLEAN_START = "clean_start" -ATTR_CLEAN_STOP = "clean_stop" -ATTR_CLEANING_TIME = "cleaning_time" -ATTR_DO_NOT_DISTURB = "do_not_disturb" -ATTR_DO_NOT_DISTURB_START = "do_not_disturb_start" -ATTR_DO_NOT_DISTURB_END = "do_not_disturb_end" -ATTR_MAIN_BRUSH_LEFT = "main_brush_left" -ATTR_SIDE_BRUSH_LEFT = "side_brush_left" -ATTR_FILTER_LEFT = "filter_left" -ATTR_SENSOR_DIRTY_LEFT = "sensor_dirty_left" -ATTR_CLEANING_COUNT = "cleaning_count" -ATTR_CLEANED_TOTAL_AREA = "total_cleaned_area" -ATTR_CLEANING_TOTAL_TIME = "total_cleaning_time" ATTR_ERROR = "error" ATTR_RC_DURATION = "duration" ATTR_RC_ROTATION = "rotation" @@ -67,7 +58,6 @@ ATTR_STATUS = "status" ATTR_ZONE_ARRAY = "zone" ATTR_ZONE_REPEATER = "repeats" ATTR_TIMERS = "timers" -ATTR_MOP_ATTACHED = "mop_attached" SUPPORT_XIAOMI = ( SUPPORT_STATE @@ -112,16 +102,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: - host = config_entry.data[CONF_HOST] - token = config_entry.data[CONF_TOKEN] name = config_entry.title unique_id = config_entry.unique_id - # Create handler - _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) - vacuum = Vacuum(host, token) - - mirobo = MiroboVacuum(name, vacuum, config_entry, unique_id) + mirobo = MiroboVacuum( + name, + hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE], + config_entry, + unique_id, + hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR], + ) entities.append(mirobo) platform = entity_platform.async_get_current_platform() @@ -206,65 +196,57 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, update_before_add=True) -class MiroboVacuum(XiaomiMiioEntity, StateVacuumEntity): +class MiroboVacuum(XiaomiCoordinatedMiioEntity, StateVacuumEntity): """Representation of a Xiaomi Vacuum cleaner robot.""" - def __init__(self, name, device, entry, unique_id): + coordinator: DataUpdateCoordinator[VacuumCoordinatorData] + + def __init__( + self, name, device, entry, unique_id, coordinator: DataUpdateCoordinator + ): """Initialize the Xiaomi vacuum cleaner robot handler.""" - super().__init__(name, device, entry, unique_id) + super().__init__(name, device, entry, unique_id, coordinator) + self._state: str | None = None - self.vacuum_state = None - self._available = False - - self.consumable_state = None - self.clean_history = None - self.dnd_state = None - self.last_clean = None - self._fan_speeds = None - self._fan_speeds_reverse = None - - self._timers = None + async def async_added_to_hass(self) -> None: + """Run when entity is about to be added to hass.""" + await super().async_added_to_hass() + self._handle_coordinator_update() @property def state(self): """Return the status of the vacuum cleaner.""" - if self.vacuum_state is not None: - # The vacuum reverts back to an idle state after erroring out. - # We want to keep returning an error until it has been cleared. - if self.vacuum_state.got_error: - return STATE_ERROR - try: - return STATE_CODE_TO_STATE[int(self.vacuum_state.state_code)] - except KeyError: - _LOGGER.error( - "STATE not supported: %s, state_code: %s", - self.vacuum_state.state, - self.vacuum_state.state_code, - ) - return None + # The vacuum reverts back to an idle state after erroring out. + # We want to keep returning an error until it has been cleared. + if self.coordinator.data.status.got_error: + return STATE_ERROR + + return self._state @property def battery_level(self): """Return the battery level of the vacuum cleaner.""" - if self.vacuum_state is not None: - return self.vacuum_state.battery + return self.coordinator.data.status.battery @property def fan_speed(self): """Return the fan speed of the vacuum cleaner.""" - if self.vacuum_state is not None: - speed = self.vacuum_state.fanspeed - if speed in self._fan_speeds_reverse: - return self._fan_speeds_reverse[speed] + speed = self.coordinator.data.status.fanspeed + if speed in self.coordinator.data.fan_speeds_reverse: + return self.coordinator.data.fan_speeds_reverse[speed] - _LOGGER.debug("Unable to find reverse for %s", speed) + _LOGGER.debug("Unable to find reverse for %s", speed) - return speed + return speed @property def fan_speed_list(self): """Get the list of available fan speed steps of the vacuum cleaner.""" - return list(self._fan_speeds) if self._fan_speeds else [] + return ( + list(self.coordinator.data.fan_speeds) + if self.coordinator.data.fan_speeds + else [] + ) @property def timers(self): @@ -275,65 +257,22 @@ class MiroboVacuum(XiaomiMiioEntity, StateVacuumEntity): "cron": timer.cron, "next_schedule": as_utc(timer.next_schedule), } - for timer in self._timers + for timer in self.coordinator.data.timers ] @property def extra_state_attributes(self): """Return the specific state attributes of this vacuum cleaner.""" attrs = {} - if self.vacuum_state is not None: - attrs.update( - { - ATTR_DO_NOT_DISTURB: STATE_ON - if self.dnd_state.enabled - else STATE_OFF, - ATTR_DO_NOT_DISTURB_START: str(self.dnd_state.start), - ATTR_DO_NOT_DISTURB_END: str(self.dnd_state.end), - # Not working --> 'Cleaning mode': - # STATE_ON if self.vacuum_state.in_cleaning else STATE_OFF, - ATTR_CLEANING_TIME: int( - self.vacuum_state.clean_time.total_seconds() / 60 - ), - ATTR_CLEANED_AREA: int(self.vacuum_state.clean_area), - ATTR_CLEANING_COUNT: int(self.clean_history.count), - ATTR_CLEANED_TOTAL_AREA: int(self.clean_history.total_area), - ATTR_CLEANING_TOTAL_TIME: int( - self.clean_history.total_duration.total_seconds() / 60 - ), - ATTR_MAIN_BRUSH_LEFT: int( - self.consumable_state.main_brush_left.total_seconds() / 3600 - ), - ATTR_SIDE_BRUSH_LEFT: int( - self.consumable_state.side_brush_left.total_seconds() / 3600 - ), - ATTR_FILTER_LEFT: int( - self.consumable_state.filter_left.total_seconds() / 3600 - ), - ATTR_SENSOR_DIRTY_LEFT: int( - self.consumable_state.sensor_dirty_left.total_seconds() / 3600 - ), - ATTR_STATUS: str(self.vacuum_state.state), - ATTR_MOP_ATTACHED: self.vacuum_state.is_water_box_attached, - } - ) + attrs[ATTR_STATUS] = str(self.coordinator.data.status.state) - if self.last_clean: - attrs[ATTR_CLEAN_START] = self.last_clean.start - attrs[ATTR_CLEAN_STOP] = self.last_clean.end + if self.coordinator.data.status.got_error: + attrs[ATTR_ERROR] = self.coordinator.data.status.error - if self.vacuum_state.got_error: - attrs[ATTR_ERROR] = self.vacuum_state.error - - if self.timers: - attrs[ATTR_TIMERS] = self.timers + if self.timers: + attrs[ATTR_TIMERS] = self.timers return attrs - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._available - @property def supported_features(self): """Flag vacuum cleaner robot features that are supported.""" @@ -343,6 +282,7 @@ class MiroboVacuum(XiaomiMiioEntity, StateVacuumEntity): """Call a vacuum command handling error messages.""" try: await self.hass.async_add_executor_job(partial(func, *args, **kwargs)) + await self.coordinator.async_refresh() return True except DeviceException as exc: _LOGGER.error(mask_error, exc) @@ -364,8 +304,8 @@ class MiroboVacuum(XiaomiMiioEntity, StateVacuumEntity): async def async_set_fan_speed(self, fan_speed, **kwargs): """Set fan speed.""" - if fan_speed in self._fan_speeds: - fan_speed = self._fan_speeds[fan_speed] + if fan_speed in self.coordinator.data.fan_speeds: + fan_speed = self.coordinator.data.fan_speeds[fan_speed] else: try: fan_speed = int(fan_speed) @@ -459,39 +399,6 @@ class MiroboVacuum(XiaomiMiioEntity, StateVacuumEntity): segments=segments, ) - def update(self): - """Fetch state from the device.""" - try: - state = self._device.status() - self.vacuum_state = state - - self._fan_speeds = self._device.fan_speed_presets() - self._fan_speeds_reverse = {v: k for k, v in self._fan_speeds.items()} - - self.consumable_state = self._device.consumable_status() - self.clean_history = self._device.clean_history() - self.last_clean = self._device.last_clean_details() - self.dnd_state = self._device.dnd_status() - - self._available = True - except (OSError, DeviceException) as exc: - if self._available: - self._available = False - _LOGGER.warning("Got exception while fetching the state: %s", exc) - - # Fetch timers separately, see #38285 - try: - # Do not try this if the first fetch timed out. - # Two timeouts take longer than 10 seconds and trigger a warning. - # See #52353 - if self._available: - self._timers = self._device.timer() - except DeviceException as exc: - _LOGGER.debug( - "Unable to fetch timers, this may happen on some devices: %s", exc - ) - self._timers = [] - async def async_clean_zone(self, zone, repeats=1): """Clean selected area for the number of repeats indicated.""" for _zone in zone: @@ -499,5 +406,21 @@ class MiroboVacuum(XiaomiMiioEntity, StateVacuumEntity): _LOGGER.debug("Zone with repeats: %s", zone) try: await self.hass.async_add_executor_job(self._device.zoned_clean, zone) + await self.coordinator.async_refresh() except (OSError, DeviceException) as exc: _LOGGER.error("Unable to send zoned_clean command to the vacuum: %s", exc) + + @callback + def _handle_coordinator_update(self) -> None: + state_code = int(self.coordinator.data.status.state_code) + if state_code not in STATE_CODE_TO_STATE: + _LOGGER.error( + "STATE not supported: %s, state_code: %s", + self.coordinator.data.status.state, + self.coordinator.data.status.state_code, + ) + self._state = None + else: + self._state = STATE_CODE_TO_STATE[state_code] + + super()._handle_coordinator_update() diff --git a/tests/components/xiaomi_miio/__init__.py b/tests/components/xiaomi_miio/__init__.py index 9f162e02f28..24e66e16b08 100644 --- a/tests/components/xiaomi_miio/__init__.py +++ b/tests/components/xiaomi_miio/__init__.py @@ -1 +1,2 @@ """Tests for the Xiaomi Miio integration.""" +TEST_MAC = "ab:cd:ef:gh:ij:kl" diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index 454940f1356..5e7c0351c14 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -8,6 +8,8 @@ from homeassistant import config_entries, data_entry_flow from homeassistant.components.xiaomi_miio import const from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN +from . import TEST_MAC + from tests.common import MockConfigEntry ZEROCONF_NAME = "name" @@ -23,7 +25,6 @@ TEST_TOKEN = "12345678901234567890123456789012" TEST_NAME = "Test_Gateway" TEST_NAME2 = "Test_Gateway_2" TEST_MODEL = const.MODELS_GATEWAY[0] -TEST_MAC = "ab:cd:ef:gh:ij:kl" TEST_MAC2 = "mn:op:qr:st:uv:wx" TEST_MAC_DEVICE = "abcdefghijkl" TEST_MAC_DEVICE2 = "mnopqrstuvwx" @@ -31,7 +32,6 @@ TEST_GATEWAY_ID = TEST_MAC TEST_HARDWARE_VERSION = "AB123" TEST_FIRMWARE_VERSION = "1.2.3_456" TEST_ZEROCONF_NAME = "lumi-gateway-v3_miio12345678._miio._udp.local." -TEST_SUB_DEVICE_LIST = [] TEST_CLOUD_DEVICES_1 = [ { "parent_id": None, diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index 0eb806c0a64..10f1dd649c8 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -13,6 +13,7 @@ from homeassistant.components.vacuum import ( DOMAIN, SERVICE_CLEAN_SPOT, SERVICE_LOCATE, + SERVICE_PAUSE, SERVICE_RETURN_TO_BASE, SERVICE_SEND_COMMAND, SERVICE_SET_FAN_SPEED, @@ -21,24 +22,17 @@ from homeassistant.components.vacuum import ( STATE_CLEANING, STATE_ERROR, ) -from homeassistant.components.xiaomi_miio import const -from homeassistant.components.xiaomi_miio.const import DOMAIN as XIAOMI_DOMAIN +from homeassistant.components.xiaomi_miio.const import ( + CONF_DEVICE, + CONF_FLOW_TYPE, + CONF_MAC, + CONF_MODEL, + DOMAIN as XIAOMI_DOMAIN, + MODELS_VACUUM, +) from homeassistant.components.xiaomi_miio.vacuum import ( - ATTR_CLEANED_AREA, - ATTR_CLEANED_TOTAL_AREA, - ATTR_CLEANING_COUNT, - ATTR_CLEANING_TIME, - ATTR_CLEANING_TOTAL_TIME, - ATTR_DO_NOT_DISTURB, - ATTR_DO_NOT_DISTURB_END, - ATTR_DO_NOT_DISTURB_START, ATTR_ERROR, - ATTR_FILTER_LEFT, - ATTR_MAIN_BRUSH_LEFT, - ATTR_SIDE_BRUSH_LEFT, ATTR_TIMERS, - CONF_HOST, - CONF_TOKEN, SERVICE_CLEAN_SEGMENT, SERVICE_CLEAN_ZONE, SERVICE_GOTO, @@ -50,17 +44,17 @@ from homeassistant.components.xiaomi_miio.vacuum import ( from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, - STATE_OFF, - STATE_ON, + CONF_HOST, + CONF_TOKEN, STATE_UNAVAILABLE, ) from homeassistant.util import dt as dt_util -from .test_config_flow import TEST_MAC +from . import TEST_MAC -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed -PLATFORM = "xiaomi_miio" +# pylint: disable=consider-using-tuple # calls made when device status is requested STATUS_CALLS = [ @@ -115,7 +109,7 @@ def mirobo_is_got_error_fixture(): mock_vacuum.timer.return_value = [mock_timer_1, mock_timer_2] - with patch("homeassistant.components.xiaomi_miio.vacuum.Vacuum") as mock_vacuum_cls: + with patch("homeassistant.components.xiaomi_miio.Vacuum") as mock_vacuum_cls: mock_vacuum_cls.return_value = mock_vacuum yield mock_vacuum @@ -143,7 +137,7 @@ def mirobo_old_speeds_fixture(request): mock_vacuum.fan_speed_presets.return_value = request.param mock_vacuum.status().fanspeed = list(request.param.values())[0] - with patch("homeassistant.components.xiaomi_miio.vacuum.Vacuum") as mock_vacuum_cls: + with patch("homeassistant.components.xiaomi_miio.Vacuum") as mock_vacuum_cls: mock_vacuum_cls.return_value = mock_vacuum yield mock_vacuum @@ -154,7 +148,8 @@ def mirobo_is_on_fixture(): mock_vacuum = MagicMock() mock_vacuum.status().data = {"test": "raw"} mock_vacuum.status().is_on = True - mock_vacuum.status().fanspeed = 99 + mock_vacuum.fan_speed_presets.return_value = new_fanspeeds + mock_vacuum.status().fanspeed = list(new_fanspeeds.values())[0] mock_vacuum.status().got_error = False mock_vacuum.status().battery = 32 mock_vacuum.status().clean_area = 133.43218 @@ -176,6 +171,19 @@ def mirobo_is_on_fixture(): mock_vacuum.status().state = "Test Xiaomi Cleaning" mock_vacuum.status().state_code = 5 mock_vacuum.dnd_status().enabled = False + mock_vacuum.last_clean_details().start = datetime( + 2020, 4, 1, 13, 21, 10, tzinfo=dt_util.UTC + ) + mock_vacuum.last_clean_details().end = datetime( + 2020, 4, 1, 13, 21, 10, tzinfo=dt_util.UTC + ) + mock_vacuum.last_clean_details().duration = timedelta( + hours=11, minutes=15, seconds=34 + ) + mock_vacuum.last_clean_details().area = 133.43218 + mock_vacuum.last_clean_details().error_code = 1 + mock_vacuum.last_clean_details().error = "test_error_code" + mock_vacuum.last_clean_details().complete = True mock_timer_1 = MagicMock() mock_timer_1.enabled = True @@ -189,12 +197,12 @@ def mirobo_is_on_fixture(): mock_vacuum.timer.return_value = [mock_timer_1, mock_timer_2] - with patch("homeassistant.components.xiaomi_miio.vacuum.Vacuum") as mock_vacuum_cls: + with patch("homeassistant.components.xiaomi_miio.Vacuum") as mock_vacuum_cls: mock_vacuum_cls.return_value = mock_vacuum yield mock_vacuum -async def test_xiaomi_exceptions(hass, caplog, mock_mirobo_is_on): +async def test_xiaomi_exceptions(hass, mock_mirobo_is_on): """Test error logging on exceptions.""" entity_name = "test_vacuum_cleaner_error" entity_id = await setup_component(hass, entity_name) @@ -204,53 +212,39 @@ async def test_xiaomi_exceptions(hass, caplog, mock_mirobo_is_on): return state.state != STATE_UNAVAILABLE # The initial setup has to be done successfully - assert "Initializing with host 192.168.1.100 (token 12345...)" in caplog.text - assert "WARNING" not in caplog.text assert is_available() # Second update causes an exception, which should be logged mock_mirobo_is_on.status.side_effect = DeviceException("dummy exception") - await hass.helpers.entity_component.async_update_entity(entity_id) - assert "WARNING" in caplog.text - assert "Got exception while fetching the state" in caplog.text + future = dt_util.utcnow() + timedelta(seconds=60) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + assert not is_available() # Third update does not get logged as the device is already unavailable, # so we clear the log and reset the status to test that - caplog.clear() mock_mirobo_is_on.status.reset_mock() + future += timedelta(seconds=60) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - await hass.helpers.entity_component.async_update_entity(entity_id) - assert "Got exception while fetching the state" not in caplog.text assert not is_available() assert mock_mirobo_is_on.status.call_count == 1 -async def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): +async def test_xiaomi_vacuum_services(hass, mock_mirobo_is_got_error): """Test vacuum supported features.""" entity_name = "test_vacuum_cleaner_1" entity_id = await setup_component(hass, entity_name) - assert "Initializing with host 192.168.1.100 (token 12345...)" in caplog.text - # Check state attributes state = hass.states.get(entity_id) assert state.state == STATE_ERROR assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 14204 - assert state.attributes.get(ATTR_DO_NOT_DISTURB) == STATE_ON - assert state.attributes.get(ATTR_DO_NOT_DISTURB_START) == "22:00:00" - assert state.attributes.get(ATTR_DO_NOT_DISTURB_END) == "06:00:00" assert state.attributes.get(ATTR_ERROR) == "Error message" assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-80" - assert state.attributes.get(ATTR_CLEANING_TIME) == 155 - assert state.attributes.get(ATTR_CLEANED_AREA) == 123 - assert state.attributes.get(ATTR_MAIN_BRUSH_LEFT) == 12 - assert state.attributes.get(ATTR_SIDE_BRUSH_LEFT) == 12 - assert state.attributes.get(ATTR_FILTER_LEFT) == 12 - assert state.attributes.get(ATTR_CLEANING_COUNT) == 35 - assert state.attributes.get(ATTR_CLEANED_TOTAL_AREA) == 123 - assert state.attributes.get(ATTR_CLEANING_TOTAL_TIME) == 695 assert state.attributes.get(ATTR_TIMERS) == [ { "enabled": True, @@ -274,6 +268,13 @@ async def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) mock_mirobo_is_got_error.reset_mock() + await hass.services.async_call( + DOMAIN, SERVICE_PAUSE, {"entity_id": entity_id}, blocking=True + ) + mock_mirobo_is_got_error.assert_has_calls([mock.call.pause()], any_order=True) + mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) + mock_mirobo_is_got_error.reset_mock() + await hass.services.async_call( DOMAIN, SERVICE_STOP, {"entity_id": entity_id}, blocking=True ) @@ -327,28 +328,121 @@ async def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): mock_mirobo_is_got_error.reset_mock() -async def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): +@pytest.mark.parametrize( + "error, status_calls", + [(None, STATUS_CALLS), (DeviceException("dummy exception"), [])], +) +@pytest.mark.parametrize( + "service, service_data, device_method, device_method_call", + [ + ( + SERVICE_START_REMOTE_CONTROL, + {ATTR_ENTITY_ID: "vacuum.test_vacuum_cleaner_2"}, + "manual_start", + mock.call(), + ), + ( + SERVICE_MOVE_REMOTE_CONTROL, + { + ATTR_ENTITY_ID: "vacuum.test_vacuum_cleaner_2", + "duration": 1000, + "rotation": -40, + "velocity": -0.1, + }, + "manual_control", + mock.call( + **{ + "duration": 1000, + "rotation": -40, + "velocity": -0.1, + } + ), + ), + ( + SERVICE_STOP_REMOTE_CONTROL, + { + ATTR_ENTITY_ID: "vacuum.test_vacuum_cleaner_2", + }, + "manual_stop", + mock.call(), + ), + ( + SERVICE_MOVE_REMOTE_CONTROL_STEP, + { + ATTR_ENTITY_ID: "vacuum.test_vacuum_cleaner_2", + "duration": 2000, + "rotation": 120, + "velocity": 0.1, + }, + "manual_control_once", + mock.call( + **{ + "duration": 2000, + "rotation": 120, + "velocity": 0.1, + } + ), + ), + ( + SERVICE_CLEAN_ZONE, + { + ATTR_ENTITY_ID: "vacuum.test_vacuum_cleaner_2", + "zone": [[123, 123, 123, 123]], + "repeats": 2, + }, + "zoned_clean", + mock.call([[123, 123, 123, 123, 2]]), + ), + ( + SERVICE_GOTO, + { + ATTR_ENTITY_ID: "vacuum.test_vacuum_cleaner_2", + "x_coord": 25500, + "y_coord": 26500, + }, + "goto", + mock.call(x_coord=25500, y_coord=26500), + ), + ( + SERVICE_CLEAN_SEGMENT, + { + ATTR_ENTITY_ID: "vacuum.test_vacuum_cleaner_2", + "segments": ["1", "2"], + }, + "segment_clean", + mock.call(segments=[int(i) for i in ["1", "2"]]), + ), + ( + SERVICE_CLEAN_SEGMENT, + { + ATTR_ENTITY_ID: "vacuum.test_vacuum_cleaner_2", + "segments": 1, + }, + "segment_clean", + mock.call(segments=[1]), + ), + ], +) +async def test_xiaomi_specific_services( + hass, + mock_mirobo_is_on, + service, + service_data, + device_method, + device_method_call, + error, + status_calls, +): """Test vacuum supported features.""" entity_name = "test_vacuum_cleaner_2" entity_id = await setup_component(hass, entity_name) - assert "Initializing with host 192.168.1.100 (token 12345" in caplog.text - # Check state attributes state = hass.states.get(entity_id) assert state.state == STATE_CLEANING assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 14204 - assert state.attributes.get(ATTR_DO_NOT_DISTURB) == STATE_OFF assert state.attributes.get(ATTR_ERROR) is None assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-30" - assert state.attributes.get(ATTR_CLEANING_TIME) == 175 - assert state.attributes.get(ATTR_CLEANED_AREA) == 133 - assert state.attributes.get(ATTR_MAIN_BRUSH_LEFT) == 11 - assert state.attributes.get(ATTR_SIDE_BRUSH_LEFT) == 11 - assert state.attributes.get(ATTR_FILTER_LEFT) == 11 - assert state.attributes.get(ATTR_CLEANING_COUNT) == 41 - assert state.attributes.get(ATTR_CLEANED_TOTAL_AREA) == 323 - assert state.attributes.get(ATTR_CLEANING_TOTAL_TIME) == 675 assert state.attributes.get(ATTR_TIMERS) == [ { "enabled": True, @@ -363,64 +457,18 @@ async def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): ] # Xiaomi vacuum specific services: - await hass.services.async_call( - XIAOMI_DOMAIN, - SERVICE_START_REMOTE_CONTROL, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - - mock_mirobo_is_on.assert_has_calls([mock.call.manual_start()], any_order=True) - mock_mirobo_is_on.assert_has_calls(STATUS_CALLS, any_order=True) - mock_mirobo_is_on.reset_mock() - - control = {"duration": 1000, "rotation": -40, "velocity": -0.1} - await hass.services.async_call( - XIAOMI_DOMAIN, - SERVICE_MOVE_REMOTE_CONTROL, - {**control, ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - mock_mirobo_is_on.manual_control.assert_has_calls( - [mock.call(**control)], any_order=True - ) - mock_mirobo_is_on.assert_has_calls(STATUS_CALLS, any_order=True) - mock_mirobo_is_on.reset_mock() + device_method_attr = getattr(mock_mirobo_is_on, device_method) + device_method_attr.side_effect = error await hass.services.async_call( XIAOMI_DOMAIN, - SERVICE_STOP_REMOTE_CONTROL, - {ATTR_ENTITY_ID: entity_id}, + service, + service_data, blocking=True, ) - mock_mirobo_is_on.assert_has_calls([mock.call.manual_stop()], any_order=True) - mock_mirobo_is_on.assert_has_calls(STATUS_CALLS, any_order=True) - mock_mirobo_is_on.reset_mock() - control_once = {"duration": 2000, "rotation": 120, "velocity": 0.1} - await hass.services.async_call( - XIAOMI_DOMAIN, - SERVICE_MOVE_REMOTE_CONTROL_STEP, - {**control_once, ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - mock_mirobo_is_on.manual_control_once.assert_has_calls( - [mock.call(**control_once)], any_order=True - ) - mock_mirobo_is_on.assert_has_calls(STATUS_CALLS, any_order=True) - mock_mirobo_is_on.reset_mock() - - control = {"zone": [[123, 123, 123, 123]], "repeats": 2} - await hass.services.async_call( - XIAOMI_DOMAIN, - SERVICE_CLEAN_ZONE, - {**control, ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - mock_mirobo_is_on.zoned_clean.assert_has_calls( - [mock.call([[123, 123, 123, 123, 2]])], any_order=True - ) - mock_mirobo_is_on.assert_has_calls(STATUS_CALLS, any_order=True) + device_method_attr.assert_has_calls([device_method_call], any_order=True) + mock_mirobo_is_on.assert_has_calls(status_calls, any_order=True) mock_mirobo_is_on.reset_mock() @@ -429,8 +477,6 @@ async def test_xiaomi_vacuum_fanspeeds(hass, caplog, mock_mirobo_fanspeeds): entity_name = "test_vacuum_cleaner_2" entity_id = await setup_component(hass, entity_name) - assert "Initializing with host 192.168.1.100 (token 12345" in caplog.text - state = hass.states.get(entity_id) assert state.attributes.get(ATTR_FAN_SPEED) == "Silent" fanspeeds = state.attributes.get(ATTR_FAN_SPEED_LIST) @@ -474,51 +520,6 @@ async def test_xiaomi_vacuum_fanspeeds(hass, caplog, mock_mirobo_fanspeeds): assert "Fan speed step not recognized" in caplog.text -async def test_xiaomi_vacuum_goto_service(hass, caplog, mock_mirobo_is_on): - """Test vacuum supported features.""" - entity_name = "test_vacuum_cleaner_2" - entity_id = await setup_component(hass, entity_name) - - data = {"entity_id": entity_id, "x_coord": 25500, "y_coord": 25500} - await hass.services.async_call(XIAOMI_DOMAIN, SERVICE_GOTO, data, blocking=True) - mock_mirobo_is_on.goto.assert_has_calls( - [mock.call(x_coord=data["x_coord"], y_coord=data["y_coord"])], any_order=True - ) - mock_mirobo_is_on.assert_has_calls(STATUS_CALLS, any_order=True) - - -async def test_xiaomi_vacuum_clean_segment_service(hass, caplog, mock_mirobo_is_on): - """Test vacuum supported features.""" - entity_name = "test_vacuum_cleaner_2" - entity_id = await setup_component(hass, entity_name) - - data = {"entity_id": entity_id, "segments": ["1", "2"]} - await hass.services.async_call( - XIAOMI_DOMAIN, SERVICE_CLEAN_SEGMENT, data, blocking=True - ) - mock_mirobo_is_on.segment_clean.assert_has_calls( - [mock.call(segments=[int(i) for i in data["segments"]])], any_order=True - ) - mock_mirobo_is_on.assert_has_calls(STATUS_CALLS, any_order=True) - - -async def test_xiaomi_vacuum_clean_segment_service_single_segment( - hass, caplog, mock_mirobo_is_on -): - """Test vacuum supported features.""" - entity_name = "test_vacuum_cleaner_2" - entity_id = await setup_component(hass, entity_name) - - data = {"entity_id": entity_id, "segments": 1} - await hass.services.async_call( - XIAOMI_DOMAIN, SERVICE_CLEAN_SEGMENT, data, blocking=True - ) - mock_mirobo_is_on.segment_clean.assert_has_calls( - [mock.call(segments=[data["segments"]])], any_order=True - ) - mock_mirobo_is_on.assert_has_calls(STATUS_CALLS, any_order=True) - - async def setup_component(hass, entity_name): """Set up vacuum component.""" entity_id = f"{DOMAIN}.{entity_name}" @@ -528,11 +529,11 @@ async def setup_component(hass, entity_name): unique_id="123456", title=entity_name, data={ - const.CONF_FLOW_TYPE: const.CONF_DEVICE, + CONF_FLOW_TYPE: CONF_DEVICE, CONF_HOST: "192.168.1.100", CONF_TOKEN: "12345678901234567890123456789012", - const.CONF_MODEL: const.MODELS_VACUUM[0], - const.CONF_MAC: TEST_MAC, + CONF_MODEL: MODELS_VACUUM[0], + CONF_MAC: TEST_MAC, }, ) From f142d0a945a5f58cdc48fde5de570ae4739dac7e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Oct 2021 21:11:38 +0200 Subject: [PATCH 0166/1038] Upgrade ambee to 0.4.0 (#57264) --- homeassistant/components/ambee/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ambee/manifest.json b/homeassistant/components/ambee/manifest.json index e546f5009e8..3226e9de3a3 100644 --- a/homeassistant/components/ambee/manifest.json +++ b/homeassistant/components/ambee/manifest.json @@ -3,7 +3,7 @@ "name": "Ambee", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ambee", - "requirements": ["ambee==0.3.0"], + "requirements": ["ambee==0.4.0"], "codeowners": ["@frenck"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index df09b777557..dfb80c82ad6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -276,7 +276,7 @@ aladdin_connect==0.3 alpha_vantage==2.3.1 # homeassistant.components.ambee -ambee==0.3.0 +ambee==0.4.0 # homeassistant.components.amberelectric amberelectric==1.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 96aaae1dfdd..161a6fa633d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -197,7 +197,7 @@ airthings_cloud==0.0.1 airtouch4pyapi==1.0.5 # homeassistant.components.ambee -ambee==0.3.0 +ambee==0.4.0 # homeassistant.components.amberelectric amberelectric==1.0.3 From b980dc7e3389d547b950d024256435fc88ba15d6 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 7 Oct 2021 13:19:03 -0600 Subject: [PATCH 0167/1038] Use current config entry standards for Ambient PWS (#57133) --- .../components/ambient_station/__init__.py | 44 +++++++++---------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 482f526430a..fc1f7d71d74 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -57,28 +57,28 @@ def async_hydrate_station_data(data: dict[str, Any]) -> dict[str, Any]: return data -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the Ambient PWS as config entry.""" hass.data.setdefault(DOMAIN, {DATA_CLIENT: {}}) - if not config_entry.unique_id: + if not entry.unique_id: hass.config_entries.async_update_entry( - config_entry, unique_id=config_entry.data[CONF_APP_KEY] + entry, unique_id=entry.data[CONF_APP_KEY] ) session = aiohttp_client.async_get_clientsession(hass) try: ambient = AmbientStation( hass, - config_entry, + entry, Client( - config_entry.data[CONF_API_KEY], - config_entry.data[CONF_APP_KEY], + entry.data[CONF_API_KEY], + entry.data[CONF_APP_KEY], session=session, ), ) hass.loop.create_task(ambient.ws_connect()) - hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = ambient + hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] = ambient except WebsocketError as err: LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady from err @@ -86,7 +86,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def _async_disconnect_websocket(_: Event) -> None: await ambient.client.websocket.disconnect() - config_entry.async_on_unload( + entry.async_on_unload( hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, _async_disconnect_websocket ) @@ -95,30 +95,30 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b return True -async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload an Ambient PWS config entry.""" - ambient = hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) + ambient = hass.data[DOMAIN][DATA_CLIENT].pop(entry.entry_id) hass.async_create_task(ambient.ws_disconnect()) - return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) -async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Migrate old entry.""" - version = config_entry.version + version = entry.version LOGGER.debug("Migrating from version %s", version) # 1 -> 2: Unique ID format changed, so delete and re-import: if version == 1: dev_reg = await hass.helpers.device_registry.async_get_registry() - dev_reg.async_clear_config_entry(config_entry) + dev_reg.async_clear_config_entry(entry) en_reg = await hass.helpers.entity_registry.async_get_registry() - en_reg.async_clear_config_entry(config_entry) + en_reg.async_clear_config_entry(entry) - version = config_entry.version = 2 - hass.config_entries.async_update_entry(config_entry) + version = entry.version = 2 + hass.config_entries.async_update_entry(entry) LOGGER.info("Migration to version %s successful", version) return True @@ -127,11 +127,9 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> class AmbientStation: """Define a class to handle the Ambient websocket.""" - def __init__( - self, hass: HomeAssistant, config_entry: ConfigEntry, client: Client - ) -> None: + def __init__(self, hass: HomeAssistant, entry: ConfigEntry, client: Client) -> None: """Initialize.""" - self._config_entry = config_entry + self._entry = entry self._entry_setup_complete = False self._hass = hass self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY @@ -193,9 +191,7 @@ class AmbientStation: # attempt forward setup of the config entry (because it will have # already been done): if not self._entry_setup_complete: - self._hass.config_entries.async_setup_platforms( - self._config_entry, PLATFORMS - ) + self._hass.config_entries.async_setup_platforms(self._entry, PLATFORMS) self._entry_setup_complete = True self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY From 667e730946be802b45f713b220e6efda0de12e0c Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Fri, 8 Oct 2021 07:14:00 +1100 Subject: [PATCH 0168/1038] Improve dlna_dmr code quality (#56886) * Listen for config updates from DlnaDmrEntity.async_added_to_hass Use `Entity.async_on_remove` for dealing with callback cancellation, instead of re-inventing the wheel with `_remove_ssdp_callbacks`. * Use async_write_ha_state within async methods * Import YAML config from async_setup_platform * Import flow prompts user when device is uncontactable during migration When config flow is able to contact a device, or when it has information from SSDP, it will create config entries without error. If the device is uncontactable at this point then it will appear as unavailable in HA until it is turned on again. When import flow cannot migrate an entry because it needs to contact the device and can't, it will notify the user with a config flow form. * Don't del unused parameters, HA pylint doesn't care * Remove unused imports from tests * Abort config flow at earliest opportunity * Return async_abort instead of raising AbortFlow * Consolidate config entry test cleanup into a single function * fixup! Consolidate config entry test cleanup into a single function Revert "Consolidate config entry test cleanup into a single function" This reverts commit 8220da7263e346a37c3e1a219dc374c4a43c7df5. * Check resource acquisition/release in specific tests * fixup! Check resource acquisition/release in specific tests * Remove unused network dependency from manifest * _on_event runs in async context * Call async_write_ha_state directly (not via shedule_update) Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/dlna_dmr/__init__.py | 28 +- .../components/dlna_dmr/config_flow.py | 289 ++++++++++-------- homeassistant/components/dlna_dmr/data.py | 6 +- .../components/dlna_dmr/manifest.json | 2 +- .../components/dlna_dmr/media_player.py | 59 ++-- .../components/dlna_dmr/strings.json | 3 + .../components/dlna_dmr/translations/en.json | 3 + tests/components/dlna_dmr/conftest.py | 23 +- tests/components/dlna_dmr/test_config_flow.py | 272 +++++------------ tests/components/dlna_dmr/test_init.py | 93 +++--- .../components/dlna_dmr/test_media_player.py | 94 +++++- 11 files changed, 436 insertions(+), 436 deletions(-) diff --git a/homeassistant/components/dlna_dmr/__init__.py b/homeassistant/components/dlna_dmr/__init__.py index 536567336fd..6a53490819f 100644 --- a/homeassistant/components/dlna_dmr/__init__.py +++ b/homeassistant/components/dlna_dmr/__init__.py @@ -3,39 +3,13 @@ from __future__ import annotations from homeassistant import config_entries from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN -from homeassistant.const import CONF_PLATFORM, CONF_URL from homeassistant.core import HomeAssistant -from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN, LOGGER +from .const import LOGGER PLATFORMS = [MEDIA_PLAYER_DOMAIN] -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up DLNA component.""" - if MEDIA_PLAYER_DOMAIN not in config: - return True - - for entry_config in config[MEDIA_PLAYER_DOMAIN]: - if entry_config.get(CONF_PLATFORM) != DOMAIN: - continue - LOGGER.warning( - "Configuring dlna_dmr via yaml is deprecated; the configuration for" - " %s has been migrated to a config entry and can be safely removed", - entry_config.get(CONF_URL), - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=entry_config, - ) - ) - - return True - - async def async_setup_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry ) -> bool: diff --git a/homeassistant/components/dlna_dmr/config_flow.py b/homeassistant/components/dlna_dmr/config_flow.py index 53513d593f5..551faf2815f 100644 --- a/homeassistant/components/dlna_dmr/config_flow.py +++ b/homeassistant/components/dlna_dmr/config_flow.py @@ -25,6 +25,7 @@ from .const import ( CONF_CALLBACK_URL_OVERRIDE, CONF_LISTEN_PORT, CONF_POLL_AVAILABILITY, + DEFAULT_NAME, DOMAIN, ) from .data import get_domain_data @@ -51,6 +52,11 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize flow.""" self._discoveries: list[Mapping[str, str]] = [] + self._location: str | None = None + self._udn: str | None = None + self._device_type: str | None = None + self._name: str | None = None + self._options: dict[str, Any] = {} @staticmethod @callback @@ -68,22 +74,18 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """ LOGGER.debug("async_step_user: user_input: %s", user_input) + # Device setup manually, assume we don't get SSDP broadcast notifications + self._options[CONF_POLL_AVAILABILITY] = True + errors = {} if user_input is not None: + self._location = user_input[CONF_URL] try: - discovery = await self._async_connect(user_input[CONF_URL]) + await self._async_connect() except ConnectError as err: errors["base"] = err.args[0] else: - # If unmigrated config was imported earlier then use it - import_data = get_domain_data(self.hass).unmigrated_config.get( - user_input[CONF_URL] - ) - if import_data is not None: - return await self.async_step_import(import_data) - # Device setup manually, assume we don't get SSDP broadcast notifications - options = {CONF_POLL_AVAILABILITY: True} - return await self._async_create_entry_from_discovery(discovery, options) + return self._create_entry() data_schema = vol.Schema({CONF_URL: str}) return self.async_show_form( @@ -93,9 +95,10 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, import_data: FlowInput = None) -> FlowResult: """Import a new DLNA DMR device from a config entry. - This flow is triggered by `async_setup`. If no device has been - configured before, find any matching device and create a config_entry - for it. Otherwise, do nothing. + This flow is triggered by `async_setup_platform`. If the device has not + been migrated, and can be connected to, automatically import it. If it + cannot be connected to, prompt the user to turn it on. If it has already + been migrated, do nothing. """ LOGGER.debug("async_step_import: import_data: %s", import_data) @@ -103,123 +106,183 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): LOGGER.debug("Entry not imported: incomplete_config") return self.async_abort(reason="incomplete_config") - self._async_abort_entries_match({CONF_URL: import_data[CONF_URL]}) + self._location = import_data[CONF_URL] + self._async_abort_entries_match({CONF_URL: self._location}) - location = import_data[CONF_URL] - self._discoveries = await self._async_get_discoveries() - - poll_availability = True - - # Find the device in the list of unconfigured devices - for discovery in self._discoveries: - if discovery[ssdp.ATTR_SSDP_LOCATION] == location: - # Device found via SSDP, it shouldn't need polling - poll_availability = False - LOGGER.debug( - "Entry %s found via SSDP, with UDN %s", - import_data[CONF_URL], - discovery[ssdp.ATTR_SSDP_UDN], - ) - break - else: - # Not in discoveries. Try connecting directly. - try: - discovery = await self._async_connect(location) - except ConnectError as err: - LOGGER.debug( - "Entry %s not imported: %s", import_data[CONF_URL], err.args[0] - ) - # Store the config to apply if the device is added later - get_domain_data(self.hass).unmigrated_config[location] = import_data - return self.async_abort(reason=err.args[0]) + # Use the location as this config flow's unique ID until UDN is known + await self.async_set_unique_id(self._location) # Set options from the import_data, except listen_ip which is no longer used - options = { - CONF_LISTEN_PORT: import_data.get(CONF_LISTEN_PORT), - CONF_CALLBACK_URL_OVERRIDE: import_data.get(CONF_CALLBACK_URL_OVERRIDE), - CONF_POLL_AVAILABILITY: poll_availability, - } + self._options[CONF_LISTEN_PORT] = import_data.get(CONF_LISTEN_PORT) + self._options[CONF_CALLBACK_URL_OVERRIDE] = import_data.get( + CONF_CALLBACK_URL_OVERRIDE + ) # Override device name if it's set in the YAML - if CONF_NAME in import_data: - discovery = dict(discovery) - discovery[ssdp.ATTR_UPNP_FRIENDLY_NAME] = import_data[CONF_NAME] + self._name = import_data.get(CONF_NAME) - LOGGER.debug("Entry %s ready for import", import_data[CONF_URL]) - return await self._async_create_entry_from_discovery(discovery, options) + discoveries = await self._async_get_discoveries() + + # Find the device in the list of unconfigured devices + for discovery in discoveries: + if discovery[ssdp.ATTR_SSDP_LOCATION] == self._location: + # Device found via SSDP, it shouldn't need polling + self._options[CONF_POLL_AVAILABILITY] = False + # Discovery info has everything required to create config entry + await self._async_set_info_from_discovery(discovery) + LOGGER.debug( + "Entry %s found via SSDP, with UDN %s", + self._location, + self._udn, + ) + return self._create_entry() + + # This device will need to be polled + self._options[CONF_POLL_AVAILABILITY] = True + + # Device was not found via SSDP, connect directly for configuration + try: + await self._async_connect() + except ConnectError as err: + # This will require user action + LOGGER.debug("Entry %s not imported yet: %s", self._location, err.args[0]) + return await self.async_step_import_turn_on() + + LOGGER.debug("Entry %s ready for import", self._location) + return self._create_entry() + + async def async_step_import_turn_on( + self, user_input: FlowInput = None + ) -> FlowResult: + """Request the user to turn on the device so that import can finish.""" + LOGGER.debug("async_step_import_turn_on: %s", user_input) + + self.context["title_placeholders"] = {"name": self._name or self._location} + + errors = {} + if user_input is not None: + try: + await self._async_connect() + except ConnectError as err: + errors["base"] = err.args[0] + else: + return self._create_entry() + + self._set_confirm_only() + return self.async_show_form(step_id="import_turn_on", errors=errors) async def async_step_ssdp(self, discovery_info: DiscoveryInfoType) -> FlowResult: """Handle a flow initialized by SSDP discovery.""" LOGGER.debug("async_step_ssdp: discovery_info %s", pformat(discovery_info)) - self._discoveries = [discovery_info] + await self._async_set_info_from_discovery(discovery_info) - udn = discovery_info[ssdp.ATTR_SSDP_UDN] - location = discovery_info[ssdp.ATTR_SSDP_LOCATION] + # Abort if a migration flow for the device's location is in progress + for progress in self._async_in_progress(include_uninitialized=True): + if progress["context"].get("unique_id") == self._location: + LOGGER.debug( + "Aborting SSDP setup because migration for %s is in progress", + self._location, + ) + return self.async_abort(reason="already_in_progress") - # Abort if already configured, but update the last-known location - await self.async_set_unique_id(udn) - self._abort_if_unique_id_configured( - updates={CONF_URL: location}, reload_on_update=False - ) - - # If the device needs migration because it wasn't turned on when HA - # started, silently migrate it now. - import_data = get_domain_data(self.hass).unmigrated_config.get(location) - if import_data is not None: - return await self.async_step_import(import_data) - - parsed_url = urlparse(location) - name = discovery_info.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) or parsed_url.hostname - self.context["title_placeholders"] = {"name": name} + self.context["title_placeholders"] = {"name": self._name} return await self.async_step_confirm() async def async_step_confirm(self, user_input: FlowInput = None) -> FlowResult: - """Allow the user to confirm adding the device. - - Also check that the device is still available, otherwise when it is - added to HA it won't report the correct DeviceInfo. - """ + """Allow the user to confirm adding the device.""" LOGGER.debug("async_step_confirm: %s", user_input) - errors = {} if user_input is not None: - discovery = self._discoveries[0] - try: - await self._async_connect(discovery[ssdp.ATTR_SSDP_LOCATION]) - except ConnectError as err: - errors["base"] = err.args[0] - else: - return await self._async_create_entry_from_discovery(discovery) + return self._create_entry() self._set_confirm_only() - return self.async_show_form(step_id="confirm", errors=errors) + return self.async_show_form(step_id="confirm") - async def _async_create_entry_from_discovery( - self, - discovery: Mapping[str, Any], - options: Mapping[str, Any] | None = None, - ) -> FlowResult: - """Create an entry from discovery.""" - LOGGER.debug("_async_create_entry_from_discovery: discovery: %s", discovery) + async def _async_connect(self) -> None: + """Connect to a device to confirm it works and gather extra information. - location = discovery[ssdp.ATTR_SSDP_LOCATION] - udn = discovery[ssdp.ATTR_SSDP_UDN] + Updates this flow's unique ID to the device UDN if not already done. + Raises ConnectError if something goes wrong. + """ + LOGGER.debug("_async_connect: location: %s", self._location) + assert self._location, "self._location has not been set before connect" + + domain_data = get_domain_data(self.hass) + try: + device = await domain_data.upnp_factory.async_create_device(self._location) + except UpnpError as err: + raise ConnectError("could_not_connect") from err + + try: + device = find_device_of_type(device, DmrDevice.DEVICE_TYPES) + except UpnpError as err: + raise ConnectError("not_dmr") from err + + if not self._udn: + self._udn = device.udn + await self.async_set_unique_id(self._udn) # Abort if already configured, but update the last-known location - await self.async_set_unique_id(udn) - self._abort_if_unique_id_configured(updates={CONF_URL: location}) + self._abort_if_unique_id_configured( + updates={CONF_URL: self._location}, reload_on_update=False + ) - parsed_url = urlparse(location) - title = discovery.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) or parsed_url.hostname + if not self._device_type: + self._device_type = device.device_type + if not self._name: + self._name = device.name + + def _create_entry(self) -> FlowResult: + """Create a config entry, assuming all required information is now known.""" + LOGGER.debug( + "_async_create_entry: location: %s, UDN: %s", self._location, self._udn + ) + assert self._location + assert self._udn + assert self._device_type + + title = self._name or urlparse(self._location).hostname or DEFAULT_NAME data = { - CONF_URL: discovery[ssdp.ATTR_SSDP_LOCATION], - CONF_DEVICE_ID: discovery[ssdp.ATTR_SSDP_UDN], - CONF_TYPE: discovery.get(ssdp.ATTR_SSDP_NT) or discovery[ssdp.ATTR_SSDP_ST], + CONF_URL: self._location, + CONF_DEVICE_ID: self._udn, + CONF_TYPE: self._device_type, } - return self.async_create_entry(title=title, data=data, options=options) + return self.async_create_entry(title=title, data=data, options=self._options) + + async def _async_set_info_from_discovery( + self, discovery_info: Mapping[str, Any], abort_if_configured: bool = True + ) -> None: + """Set information required for a config entry from the SSDP discovery.""" + LOGGER.debug( + "_async_set_info_from_discovery: location: %s, UDN: %s", + discovery_info[ssdp.ATTR_SSDP_LOCATION], + discovery_info[ssdp.ATTR_SSDP_UDN], + ) + + if not self._location: + self._location = discovery_info[ssdp.ATTR_SSDP_LOCATION] + assert isinstance(self._location, str) + + self._udn = discovery_info[ssdp.ATTR_SSDP_UDN] + await self.async_set_unique_id(self._udn) + + if abort_if_configured: + # Abort if already configured, but update the last-known location + self._abort_if_unique_id_configured( + updates={CONF_URL: self._location}, reload_on_update=False + ) + + self._device_type = ( + discovery_info.get(ssdp.ATTR_SSDP_NT) or discovery_info[ssdp.ATTR_SSDP_ST] + ) + self._name = ( + discovery_info.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) + or urlparse(self._location).hostname + or DEFAULT_NAME + ) async def _async_get_discoveries(self) -> list[Mapping[str, str]]: """Get list of unconfigured DLNA devices discovered by SSDP.""" @@ -245,32 +308,6 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return discoveries - async def _async_connect(self, location: str) -> dict[str, str]: - """Connect to a device to confirm it works and get discovery information. - - Raises ConnectError if something goes wrong. - """ - LOGGER.debug("_async_connect(location=%s)", location) - domain_data = get_domain_data(self.hass) - try: - device = await domain_data.upnp_factory.async_create_device(location) - except UpnpError as err: - raise ConnectError("could_not_connect") from err - - try: - device = find_device_of_type(device, DmrDevice.DEVICE_TYPES) - except UpnpError as err: - raise ConnectError("not_dmr") from err - - discovery = { - ssdp.ATTR_SSDP_LOCATION: location, - ssdp.ATTR_SSDP_UDN: device.udn, - ssdp.ATTR_SSDP_ST: device.device_type, - ssdp.ATTR_UPNP_FRIENDLY_NAME: device.name, - } - - return discovery - class DlnaDmrOptionsFlowHandler(config_entries.OptionsFlow): """Handle a DLNA DMR options flow. diff --git a/homeassistant/components/dlna_dmr/data.py b/homeassistant/components/dlna_dmr/data.py index 8d4693dd435..d7b330f0fe8 100644 --- a/homeassistant/components/dlna_dmr/data.py +++ b/homeassistant/components/dlna_dmr/data.py @@ -3,8 +3,7 @@ from __future__ import annotations import asyncio from collections import defaultdict -from collections.abc import Mapping -from typing import Any, NamedTuple, cast +from typing import NamedTuple, cast from async_upnp_client import UpnpEventHandler, UpnpFactory, UpnpRequester from async_upnp_client.aiohttp import AiohttpNotifyServer, AiohttpSessionRequester @@ -33,7 +32,6 @@ class DlnaDmrData: event_notifiers: dict[EventListenAddr, AiohttpNotifyServer] event_notifier_refs: defaultdict[EventListenAddr, int] stop_listener_remove: CALLBACK_TYPE | None = None - unmigrated_config: dict[str, Mapping[str, Any]] def __init__(self, hass: HomeAssistant) -> None: """Initialize global data.""" @@ -43,11 +41,9 @@ class DlnaDmrData: self.upnp_factory = UpnpFactory(self.requester, non_strict=True) self.event_notifiers = {} self.event_notifier_refs = defaultdict(int) - self.unmigrated_config = {} async def async_cleanup_event_notifiers(self, event: Event) -> None: """Clean up resources when Home Assistant is stopped.""" - del event # unused LOGGER.debug("Cleaning resources in DlnaDmrData") async with self.lock: tasks = (server.stop_server() for server in self.event_notifiers.values()) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 53bee3d8519..7c47d9329bb 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", "requirements": ["async-upnp-client==0.22.5"], - "dependencies": ["network", "ssdp"], + "dependencies": ["ssdp"], "codeowners": ["@StevenLooman", "@chishm"], "iot_class": "local_push" } diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 8542464e41e..839b58b6b5a 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -42,12 +42,12 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry, 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 ( CONF_CALLBACK_URL_OVERRIDE, CONF_LISTEN_PORT, CONF_POLL_AVAILABILITY, - DEFAULT_NAME, DOMAIN, LOGGER as _LOGGER, MEDIA_TYPE_MAP, @@ -69,7 +69,7 @@ PLATFORM_SCHEMA = vol.All( vol.Required(CONF_URL): cv.string, vol.Optional(CONF_LISTEN_IP): cv.string, vol.Optional(CONF_LISTEN_PORT): cv.port, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_CALLBACK_URL_OVERRIDE): cv.url, } ), @@ -98,13 +98,35 @@ def catch_request_errors(func: Func) -> Func: return cast(Func, wrapper) +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up DLNA media_player platform.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=config, + ) + ) + + _LOGGER.warning( + "Configuring dlna_dmr via yaml is deprecated; the configuration for" + " %s will be migrated to a config entry and can be safely removed when" + "migration is complete", + config.get(CONF_URL), + ) + + async def async_setup_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the DlnaDmrEntity from a config entry.""" - del hass # Unused _LOGGER.debug("media_player.async_setup_entry %s (%s)", entry.entry_id, entry.title) # Create our own device-wrapping entity @@ -118,10 +140,6 @@ async def async_setup_entry( location=entry.data[CONF_URL], ) - entry.async_on_unload( - entry.add_update_listener(entity.async_config_update_listener) - ) - async_add_entities([entity]) @@ -139,7 +157,6 @@ class DlnaDmrEntity(MediaPlayerEntity): _device_lock: asyncio.Lock # Held when connecting or disconnecting the device _device: DmrDevice | None = None - _remove_ssdp_callbacks: list[Callable] check_available: bool = False # Track BOOTID in SSDP advertisements for device changes @@ -167,10 +184,19 @@ class DlnaDmrEntity(MediaPlayerEntity): self.poll_availability = poll_availability self.location = location self._device_lock = asyncio.Lock() - self._remove_ssdp_callbacks = [] async def async_added_to_hass(self) -> None: """Handle addition.""" + # Update this entity when the associated config entry is modified + if self.registry_entry and self.registry_entry.config_entry_id: + config_entry = self.hass.config_entries.async_get_entry( + self.registry_entry.config_entry_id + ) + assert config_entry is not None + self.async_on_remove( + config_entry.add_update_listener(self.async_config_update_listener) + ) + # Try to connect to the last known location, but don't worry if not available if not self._device: try: @@ -179,7 +205,7 @@ class DlnaDmrEntity(MediaPlayerEntity): _LOGGER.debug("Couldn't connect immediately: %r", err) # Get SSDP notifications for only this device - self._remove_ssdp_callbacks.append( + self.async_on_remove( await ssdp.async_register_callback( self.hass, self.async_ssdp_callback, {"USN": self.usn} ) @@ -189,7 +215,7 @@ class DlnaDmrEntity(MediaPlayerEntity): # (device name) which often is not the USN (service within the device) # that we're interested in. So also listen for byebye advertisements for # the UDN, which is reported in the _udn field of the combined_headers. - self._remove_ssdp_callbacks.append( + self.async_on_remove( await ssdp.async_register_callback( self.hass, self.async_ssdp_callback, @@ -199,9 +225,6 @@ class DlnaDmrEntity(MediaPlayerEntity): async def async_will_remove_from_hass(self) -> None: """Handle removal.""" - for callback in self._remove_ssdp_callbacks: - callback() - self._remove_ssdp_callbacks.clear() await self._device_disconnect() async def async_ssdp_callback( @@ -255,13 +278,12 @@ class DlnaDmrEntity(MediaPlayerEntity): ) # Device could have been de/re-connected, state probably changed - self.schedule_update_ha_state() + self.async_write_ha_state() async def async_config_update_listener( self, hass: HomeAssistant, entry: config_entries.ConfigEntry ) -> None: """Handle options update by modifying self in-place.""" - del hass # Unused _LOGGER.debug( "Updating: %s with data=%s and options=%s", self.name, @@ -292,7 +314,7 @@ class DlnaDmrEntity(MediaPlayerEntity): _LOGGER.warning("Couldn't (re)connect after config change: %r", err) # Device was de/re-connected, state might have changed - self.schedule_update_ha_state() + self.async_write_ha_state() async def _device_connect(self, location: str) -> None: """Connect to the device now that it's available.""" @@ -415,11 +437,10 @@ class DlnaDmrEntity(MediaPlayerEntity): self, service: UpnpService, state_variables: Sequence[UpnpStateVariable] ) -> None: """State variable(s) changed, let home-assistant know.""" - del service # Unused if not state_variables: # Indicates a failure to resubscribe, check if device is still available self.check_available = True - self.schedule_update_ha_state() + self.async_write_ha_state() @property def available(self) -> bool: diff --git a/homeassistant/components/dlna_dmr/strings.json b/homeassistant/components/dlna_dmr/strings.json index 27e96b465db..c418305d2e6 100644 --- a/homeassistant/components/dlna_dmr/strings.json +++ b/homeassistant/components/dlna_dmr/strings.json @@ -9,6 +9,9 @@ "url": "[%key:common::config_flow::data::url%]" } }, + "import_turn_on": { + "description": "Please turn on the device and click submit to continue migration" + }, "confirm": { "description": "[%key:common::config_flow::description::confirm_setup%]" } diff --git a/homeassistant/components/dlna_dmr/translations/en.json b/homeassistant/components/dlna_dmr/translations/en.json index 94bbd365e18..c307d6b3571 100644 --- a/homeassistant/components/dlna_dmr/translations/en.json +++ b/homeassistant/components/dlna_dmr/translations/en.json @@ -17,6 +17,9 @@ "confirm": { "description": "Do you want to start set up?" }, + "import_turn_on": { + "description": "Please turn on the device and click confirm to continue migration" + }, "user": { "data": { "url": "URL" diff --git a/tests/components/dlna_dmr/conftest.py b/tests/components/dlna_dmr/conftest.py index 60116d949ae..f352349205e 100644 --- a/tests/components/dlna_dmr/conftest.py +++ b/tests/components/dlna_dmr/conftest.py @@ -52,20 +52,12 @@ def domain_data_mock(hass: HomeAssistant) -> Iterable[Mock]: seal(upnp_device) domain_data.upnp_factory.async_create_device.return_value = upnp_device - domain_data.unmigrated_config = {} - with patch.dict(hass.data, {DLNA_DOMAIN: domain_data}): yield domain_data - # Make sure the event notifiers are released - assert ( - domain_data.async_get_event_notifier.await_count - == domain_data.async_release_event_notifier.await_count - ) - @pytest.fixture -def config_entry_mock() -> Iterable[MockConfigEntry]: +def config_entry_mock() -> MockConfigEntry: """Mock a config entry for this platform.""" mock_entry = MockConfigEntry( unique_id=MOCK_DEVICE_UDN, @@ -78,7 +70,7 @@ def config_entry_mock() -> Iterable[MockConfigEntry]: title=MOCK_DEVICE_NAME, options={}, ) - yield mock_entry + return mock_entry @pytest.fixture @@ -100,14 +92,6 @@ def dmr_device_mock(domain_data_mock: Mock) -> Iterable[Mock]: yield device - # Make sure the device is disconnected - assert ( - device.async_subscribe_services.await_count - == device.async_unsubscribe_services.await_count - ) - - assert device.on_event is None - @pytest.fixture(name="skip_notifications", autouse=True) def skip_notifications_fixture() -> Iterable[None]: @@ -125,9 +109,6 @@ def ssdp_scanner_mock() -> Iterable[Mock]: reg_callback = mock_scanner.return_value.async_register_callback reg_callback.return_value = Mock(return_value=None) yield mock_scanner.return_value - assert ( - reg_callback.call_count == reg_callback.return_value.call_count - ), "Not all callbacks unregistered" @pytest.fixture(autouse=True) diff --git a/tests/components/dlna_dmr/test_config_flow.py b/tests/components/dlna_dmr/test_config_flow.py index 1bf93781be1..0586a43422a 100644 --- a/tests/components/dlna_dmr/test_config_flow.py +++ b/tests/components/dlna_dmr/test_config_flow.py @@ -86,12 +86,6 @@ async def test_user_flow(hass: HomeAssistant) -> None: # Wait for platform to be fully setup await hass.async_block_till_done() - # Remove the device to clean up all resources, completing its life cycle - entry_id = result["result"].entry_id - assert await hass.config_entries.async_remove(entry_id) == { - "require_restart": False - } - async def test_user_flow_uncontactable( hass: HomeAssistant, domain_data_mock: Mock @@ -154,12 +148,6 @@ async def test_user_flow_embedded_st( # Wait for platform to be fully setup await hass.async_block_till_done() - # Remove the device to clean up all resources, completing its life cycle - entry_id = result["result"].entry_id - assert await hass.config_entries.async_remove(entry_id) == { - "require_restart": False - } - async def test_user_flow_wrong_st(hass: HomeAssistant, domain_data_mock: Mock) -> None: """Test user-init'd config flow with user entering a URL for the wrong device.""" @@ -194,30 +182,6 @@ async def test_import_flow_invalid(hass: HomeAssistant, domain_data_mock: Mock) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "incomplete_config" - # Device is not contactable - domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpError - - result = await hass.config_entries.flow.async_init( - DLNA_DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_PLATFORM: DLNA_DOMAIN, CONF_URL: MOCK_DEVICE_LOCATION}, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "could_not_connect" - - # Device is the wrong type - domain_data_mock.upnp_factory.async_create_device.side_effect = None - upnp_device = domain_data_mock.upnp_factory.async_create_device.return_value - upnp_device.device_type = WRONG_DEVICE_TYPE - - result = await hass.config_entries.flow.async_init( - DLNA_DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_PLATFORM: DLNA_DOMAIN, CONF_URL: MOCK_DEVICE_LOCATION}, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "not_dmr" - async def test_import_flow_ssdp_discovered( hass: HomeAssistant, ssdp_scanner_mock: Mock @@ -248,7 +212,6 @@ async def test_import_flow_ssdp_discovered( CONF_CALLBACK_URL_OVERRIDE: None, CONF_POLL_AVAILABILITY: False, } - entry_id = result["result"].entry_id # The config entry should not be duplicated when dlna_dmr is restarted ssdp_scanner_mock.async_get_discovery_info_by_st.side_effect = [ @@ -267,11 +230,6 @@ async def test_import_flow_ssdp_discovered( # Wait for platform to be fully setup await hass.async_block_till_done() - # Remove the device to clean up all resources, completing its life cycle - assert await hass.config_entries.async_remove(entry_id) == { - "require_restart": False - } - async def test_import_flow_direct_connect( hass: HomeAssistant, ssdp_scanner_mock: Mock @@ -299,7 +257,6 @@ async def test_import_flow_direct_connect( CONF_CALLBACK_URL_OVERRIDE: None, CONF_POLL_AVAILABILITY: True, } - entry_id = result["result"].entry_id # The config entry should not be duplicated when dlna_dmr is restarted result = await hass.config_entries.flow.async_init( @@ -310,10 +267,78 @@ async def test_import_flow_direct_connect( assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" - # Remove the device to clean up all resources, completing its life cycle - assert await hass.config_entries.async_remove(entry_id) == { - "require_restart": False + +async def test_import_flow_offline( + hass: HomeAssistant, domain_data_mock: Mock, ssdp_scanner_mock: Mock +) -> None: + """Test import flow of offline device.""" + # Device is not yet contactable + domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpError + + result = await hass.config_entries.flow.async_init( + DLNA_DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_PLATFORM: DLNA_DOMAIN, + CONF_URL: MOCK_DEVICE_LOCATION, + CONF_NAME: IMPORTED_DEVICE_NAME, + CONF_CALLBACK_URL_OVERRIDE: "http://override/callback", + CONF_LISTEN_PORT: 2222, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + assert result["step_id"] == "import_turn_on" + + import_flow_id = result["flow_id"] + + # User clicks submit, same form is displayed with an error + result = await hass.config_entries.flow.async_configure( + import_flow_id, user_input={} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "could_not_connect"} + assert result["step_id"] == "import_turn_on" + + # Device is discovered via SSDP, new flow should not be initialized + ssdp_scanner_mock.async_get_discovery_info_by_st.side_effect = [ + [MOCK_DISCOVERY], + [], + [], + ] + domain_data_mock.upnp_factory.async_create_device.side_effect = None + + result = await hass.config_entries.flow.async_init( + DLNA_DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=MOCK_DISCOVERY, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_in_progress" + + # User clicks submit, config entry should be created + result = await hass.config_entries.flow.async_configure( + import_flow_id, user_input={} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == IMPORTED_DEVICE_NAME + assert result["data"] == { + CONF_URL: MOCK_DEVICE_LOCATION, + CONF_DEVICE_ID: MOCK_DEVICE_UDN, + CONF_TYPE: MOCK_DEVICE_TYPE, } + # Options should be retained + assert result["options"] == { + CONF_LISTEN_PORT: 2222, + CONF_CALLBACK_URL_OVERRIDE: "http://override/callback", + CONF_POLL_AVAILABILITY: True, + } + + # Wait for platform to be fully setup + await hass.async_block_till_done() async def test_import_flow_options( @@ -351,134 +376,6 @@ async def test_import_flow_options( # Wait for platform to be fully setup await hass.async_block_till_done() - # Remove the device to clean up all resources, completing its life cycle - entry_id = result["result"].entry_id - assert await hass.config_entries.async_remove(entry_id) == { - "require_restart": False - } - - -async def test_import_flow_deferred_ssdp( - hass: HomeAssistant, domain_data_mock: Mock, ssdp_scanner_mock: Mock -) -> None: - """Test YAML import of unavailable device later found via SSDP.""" - # Attempted import at hass start fails because device is unavailable - ssdp_scanner_mock.async_get_discovery_info_by_st.side_effect = [ - [], - [], - [], - ] - domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpError - result = await hass.config_entries.flow.async_init( - DLNA_DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_PLATFORM: DLNA_DOMAIN, - CONF_URL: MOCK_DEVICE_LOCATION, - CONF_NAME: IMPORTED_DEVICE_NAME, - CONF_LISTEN_PORT: 2222, - CONF_CALLBACK_URL_OVERRIDE: "http://override/callback", - }, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "could_not_connect" - - # Device becomes available then discovered via SSDP, import now occurs automatically - ssdp_scanner_mock.async_get_discovery_info_by_st.side_effect = [ - [MOCK_DISCOVERY], - [], - [], - ] - domain_data_mock.upnp_factory.async_create_device.side_effect = None - - result = await hass.config_entries.flow.async_init( - DLNA_DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data=MOCK_DISCOVERY, - ) - await hass.async_block_till_done() - - assert hass.config_entries.flow.async_progress(include_uninitialized=True) == [] - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == IMPORTED_DEVICE_NAME - assert result["data"] == { - CONF_URL: MOCK_DEVICE_LOCATION, - CONF_DEVICE_ID: MOCK_DEVICE_UDN, - CONF_TYPE: MOCK_DEVICE_TYPE, - } - assert result["options"] == { - CONF_LISTEN_PORT: 2222, - CONF_CALLBACK_URL_OVERRIDE: "http://override/callback", - CONF_POLL_AVAILABILITY: False, - } - - # Remove the device to clean up all resources, completing its life cycle - entry_id = result["result"].entry_id - assert await hass.config_entries.async_remove(entry_id) == { - "require_restart": False - } - - -async def test_import_flow_deferred_user( - hass: HomeAssistant, domain_data_mock: Mock, ssdp_scanner_mock: Mock -) -> None: - """Test YAML import of unavailable device later added by user.""" - # Attempted import at hass start fails because device is unavailable - ssdp_scanner_mock.async_get_discovery_info_by_st.return_value = [] - domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpError - result = await hass.config_entries.flow.async_init( - DLNA_DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_PLATFORM: DLNA_DOMAIN, - CONF_URL: MOCK_DEVICE_LOCATION, - CONF_NAME: IMPORTED_DEVICE_NAME, - CONF_LISTEN_PORT: 2222, - CONF_CALLBACK_URL_OVERRIDE: "http://override/callback", - }, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "could_not_connect" - - # Device becomes available then added by user, use all imported settings - domain_data_mock.upnp_factory.async_create_device.side_effect = None - - result = await hass.config_entries.flow.async_init( - DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {} - assert result["step_id"] == "user" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} - ) - await hass.async_block_till_done() - - assert hass.config_entries.flow.async_progress(include_uninitialized=True) == [] - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == IMPORTED_DEVICE_NAME - assert result["data"] == { - CONF_URL: MOCK_DEVICE_LOCATION, - CONF_DEVICE_ID: MOCK_DEVICE_UDN, - CONF_TYPE: MOCK_DEVICE_TYPE, - } - assert result["options"] == { - CONF_LISTEN_PORT: 2222, - CONF_CALLBACK_URL_OVERRIDE: "http://override/callback", - CONF_POLL_AVAILABILITY: True, - } - - # Remove the device to clean up all resources, completing its life cycle - entry_id = result["result"].entry_id - assert await hass.config_entries.async_remove(entry_id) == { - "require_restart": False - } - async def test_ssdp_flow_success(hass: HomeAssistant) -> None: """Test that SSDP discovery with an available device works.""" @@ -488,7 +385,6 @@ async def test_ssdp_flow_success(hass: HomeAssistant) -> None: data=MOCK_DISCOVERY, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {} assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -505,20 +401,14 @@ async def test_ssdp_flow_success(hass: HomeAssistant) -> None: } assert result["options"] == {} - # Remove the device to clean up all resources, completing its life cycle - entry_id = result["result"].entry_id - assert await hass.config_entries.async_remove(entry_id) == { - "require_restart": False - } - async def test_ssdp_flow_unavailable( hass: HomeAssistant, domain_data_mock: Mock ) -> None: - """Test that SSDP discovery with an unavailable device gives an error message. + """Test that SSDP discovery with an unavailable device still succeeds. - This may occur if the device is turned on, discovered, then turned off - before the user attempts to add it. + All the required information for configuration is obtained from the SSDP + message, there's no need to connect to the device to configure it. """ result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, @@ -526,7 +416,6 @@ async def test_ssdp_flow_unavailable( data=MOCK_DISCOVERY, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {} assert result["step_id"] == "confirm" domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpError @@ -534,9 +423,16 @@ async def test_ssdp_flow_unavailable( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "could_not_connect"} - assert result["step_id"] == "confirm" + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == MOCK_DEVICE_NAME + assert result["data"] == { + CONF_URL: MOCK_DEVICE_LOCATION, + CONF_DEVICE_ID: MOCK_DEVICE_UDN, + CONF_TYPE: MOCK_DEVICE_TYPE, + } + assert result["options"] == {} async def test_ssdp_flow_existing( diff --git a/tests/components/dlna_dmr/test_init.py b/tests/components/dlna_dmr/test_init.py index 91aec7310ab..be793d67c5e 100644 --- a/tests/components/dlna_dmr/test_init.py +++ b/tests/components/dlna_dmr/test_init.py @@ -1,59 +1,60 @@ -"""Tests for the DLNA DMR __init__ module.""" +"""Test the DLNA DMR component setup and cleanup.""" from unittest.mock import Mock -from async_upnp_client import UpnpError - -from homeassistant.components.dlna_dmr.const import ( - CONF_LISTEN_PORT, - DOMAIN as DLNA_DOMAIN, -) -from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN -from homeassistant.const import CONF_NAME, CONF_PLATFORM, CONF_URL +from homeassistant.components import media_player +from homeassistant.components.dlna_dmr.const import DOMAIN as DLNA_DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component -from .conftest import MOCK_DEVICE_LOCATION +from tests.common import MockConfigEntry -async def test_import_flow_started(hass: HomeAssistant, domain_data_mock: Mock) -> None: - """Test import flow of YAML config is started if there's config data.""" - mock_config: ConfigType = { - MEDIA_PLAYER_DOMAIN: [ - { - CONF_PLATFORM: DLNA_DOMAIN, - CONF_URL: MOCK_DEVICE_LOCATION, - CONF_LISTEN_PORT: 1234, - }, - { - CONF_PLATFORM: "other_domain", - CONF_URL: MOCK_DEVICE_LOCATION, - CONF_NAME: "another device", - }, - ] - } - - # Device is not available yet - domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpError - - # Run the setup - await async_setup_component(hass, DLNA_DOMAIN, mock_config) +async def test_resource_lifecycle( + hass: HomeAssistant, + domain_data_mock: Mock, + config_entry_mock: MockConfigEntry, + ssdp_scanner_mock: Mock, + dmr_device_mock: Mock, +) -> None: + """Test that resources are acquired/released as the entity is setup/unloaded.""" + # Set up the config entry + config_entry_mock.add_to_hass(hass) + assert await async_setup_component(hass, DLNA_DOMAIN, {}) is True await hass.async_block_till_done() - # Check config_flow has completed - assert hass.config_entries.flow.async_progress(include_uninitialized=True) == [] - - # Check device contact attempt was made - domain_data_mock.upnp_factory.async_create_device.assert_awaited_once_with( - MOCK_DEVICE_LOCATION + # Check the entity is created and working + entries = entity_registry.async_entries_for_config_entry( + entity_registry.async_get(hass), config_entry_mock.entry_id ) + assert len(entries) == 1 + entity_id = entries[0].entity_id + mock_state = hass.states.get(entity_id) + assert mock_state is not None + assert mock_state.state == media_player.STATE_IDLE - # Check the device is added to the unmigrated configs - assert domain_data_mock.unmigrated_config == { - MOCK_DEVICE_LOCATION: { - CONF_PLATFORM: DLNA_DOMAIN, - CONF_URL: MOCK_DEVICE_LOCATION, - CONF_LISTEN_PORT: 1234, - } + # Check update listeners and event notifiers are subscribed + assert len(config_entry_mock.update_listeners) == 1 + assert domain_data_mock.async_get_event_notifier.await_count == 1 + assert domain_data_mock.async_release_event_notifier.await_count == 0 + assert ssdp_scanner_mock.async_register_callback.await_count == 2 + assert ssdp_scanner_mock.async_register_callback.return_value.call_count == 0 + assert dmr_device_mock.async_subscribe_services.await_count == 1 + assert dmr_device_mock.async_unsubscribe_services.await_count == 0 + assert dmr_device_mock.on_event is not None + + # Unload the config entry + assert await hass.config_entries.async_remove(config_entry_mock.entry_id) == { + "require_restart": False } + + # Check update listeners and event notifiers are released + assert not config_entry_mock.update_listeners + assert domain_data_mock.async_get_event_notifier.await_count == 1 + assert domain_data_mock.async_release_event_notifier.await_count == 1 + assert ssdp_scanner_mock.async_register_callback.await_count == 2 + assert ssdp_scanner_mock.async_register_callback.return_value.call_count == 2 + assert dmr_device_mock.async_subscribe_services.await_count == 1 + assert dmr_device_mock.async_unsubscribe_services.await_count == 1 + assert dmr_device_mock.on_event is None diff --git a/tests/components/dlna_dmr/test_media_player.py b/tests/components/dlna_dmr/test_media_player.py index 4c27de1be67..2d02c8f1a8f 100644 --- a/tests/components/dlna_dmr/test_media_player.py +++ b/tests/components/dlna_dmr/test_media_player.py @@ -24,7 +24,7 @@ from homeassistant.components.dlna_dmr.const import ( from homeassistant.components.dlna_dmr.data import EventListenAddr from homeassistant.components.media_player import ATTR_TO_PROPERTY, const as mp_const from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM, CONF_URL from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import async_get as async_get_dr from homeassistant.helpers.entity_component import async_update_entity @@ -32,6 +32,7 @@ from homeassistant.helpers.entity_registry import ( async_entries_for_config_entry, async_get as async_get_er, ) +from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component from .conftest import ( @@ -65,7 +66,9 @@ async def setup_mock_component(hass: HomeAssistant, mock_entry: MockConfigEntry) @pytest.fixture async def mock_entity_id( hass: HomeAssistant, + domain_data_mock: Mock, config_entry_mock: MockConfigEntry, + ssdp_scanner_mock: Mock, dmr_device_mock: Mock, ) -> AsyncIterable[str]: """Fixture to set up a mock DlnaDmrEntity in a connected state. @@ -74,8 +77,17 @@ async def mock_entity_id( """ entity_id = await setup_mock_component(hass, config_entry_mock) + # Check the entity has registered all needed listeners + assert len(config_entry_mock.update_listeners) == 1 + assert domain_data_mock.async_get_event_notifier.await_count == 1 + assert domain_data_mock.async_release_event_notifier.await_count == 0 + assert ssdp_scanner_mock.async_register_callback.await_count == 2 + assert ssdp_scanner_mock.async_register_callback.return_value.call_count == 0 assert dmr_device_mock.async_subscribe_services.await_count == 1 + assert dmr_device_mock.async_unsubscribe_services.await_count == 0 + assert dmr_device_mock.on_event is not None + # Run the test yield entity_id # Unload config entry to clean up @@ -83,12 +95,29 @@ async def mock_entity_id( "require_restart": False } + # Check entity has cleaned up its resources + assert not config_entry_mock.update_listeners + assert ( + domain_data_mock.async_get_event_notifier.await_count + == domain_data_mock.async_release_event_notifier.await_count + ) + assert ( + ssdp_scanner_mock.async_register_callback.await_count + == ssdp_scanner_mock.async_register_callback.return_value.call_count + ) + assert ( + dmr_device_mock.async_subscribe_services.await_count + == dmr_device_mock.async_unsubscribe_services.await_count + ) + assert dmr_device_mock.on_event is None + @pytest.fixture async def mock_disconnected_entity_id( hass: HomeAssistant, domain_data_mock: Mock, config_entry_mock: MockConfigEntry, + ssdp_scanner_mock: Mock, dmr_device_mock: Mock, ) -> AsyncIterable[str]: """Fixture to set up a mock DlnaDmrEntity in a disconnected state. @@ -100,8 +129,19 @@ async def mock_disconnected_entity_id( entity_id = await setup_mock_component(hass, config_entry_mock) - assert dmr_device_mock.async_subscribe_services.await_count == 0 + # Check the entity has registered all needed listeners + assert len(config_entry_mock.update_listeners) == 1 + assert ssdp_scanner_mock.async_register_callback.await_count == 2 + assert ssdp_scanner_mock.async_register_callback.return_value.call_count == 0 + # The DmrDevice hasn't been instantiated yet + assert domain_data_mock.async_get_event_notifier.await_count == 0 + assert domain_data_mock.async_release_event_notifier.await_count == 0 + assert dmr_device_mock.async_subscribe_services.await_count == 0 + assert dmr_device_mock.async_unsubscribe_services.await_count == 0 + assert dmr_device_mock.on_event is None + + # Run the test yield entity_id # Unload config entry to clean up @@ -109,6 +149,54 @@ async def mock_disconnected_entity_id( "require_restart": False } + # Check entity has cleaned up its resources + assert not config_entry_mock.update_listeners + assert ( + domain_data_mock.async_get_event_notifier.await_count + == domain_data_mock.async_release_event_notifier.await_count + ) + assert ( + ssdp_scanner_mock.async_register_callback.await_count + == ssdp_scanner_mock.async_register_callback.return_value.call_count + ) + assert ( + dmr_device_mock.async_subscribe_services.await_count + == dmr_device_mock.async_unsubscribe_services.await_count + ) + assert dmr_device_mock.on_event is None + + +async def test_setup_platform_import_flow_started( + hass: HomeAssistant, domain_data_mock: Mock +) -> None: + """Test import flow of YAML config is started if there's config data.""" + # Cause connection attempts to fail + domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpConnectionError + + # Run the setup + mock_config: ConfigType = { + MP_DOMAIN: [ + { + CONF_PLATFORM: DLNA_DOMAIN, + CONF_URL: MOCK_DEVICE_LOCATION, + CONF_LISTEN_PORT: 1234, + } + ] + } + + await async_setup_component(hass, MP_DOMAIN, mock_config) + await hass.async_block_till_done() + + # Check config_flow has started + flows = hass.config_entries.flow.async_progress(include_uninitialized=True) + assert len(flows) == 1 + + # It should be paused, waiting for the user to turn on the device + flow = flows[0] + assert flow["handler"] == "dlna_dmr" + assert flow["step_id"] == "import_turn_on" + assert flow["context"].get("unique_id") == MOCK_DEVICE_LOCATION + async def test_setup_entry_no_options( hass: HomeAssistant, @@ -799,7 +887,7 @@ async def test_ssdp_byebye( # Device should be gone mock_state = hass.states.get(mock_entity_id) assert mock_state is not None - assert mock_state.state == media_player.STATE_IDLE + assert mock_state.state == ha_const.STATE_UNAVAILABLE # Second byebye will do nothing await ssdp_callback( From ac3741df418880e86477b88e69632c6e10a92ed1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 7 Oct 2021 10:14:14 -1000 Subject: [PATCH 0169/1038] Fix RGB only (no color temp) devices with tplink (#57267) --- homeassistant/components/tplink/light.py | 2 +- tests/components/tplink/test_light.py | 57 ++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index f1d936ecdfe..f0e911ea412 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -145,7 +145,7 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): def color_mode(self) -> str | None: """Return the active color mode.""" if self.device.is_color: - if self.device.color_temp: + if self.device.is_variable_color_temp and self.device.color_temp: return COLOR_MODE_COLOR_TEMP return COLOR_MODE_HS if self.device.is_variable_color_temp: diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index 19116005c37..501221f5a6f 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -1,5 +1,7 @@ """Tests for light platform.""" +from unittest.mock import PropertyMock + import pytest from homeassistant.components import tplink @@ -117,6 +119,61 @@ async def test_color_light(hass: HomeAssistant) -> None: bulb.set_hsv.reset_mock() +async def test_color_light_no_temp(hass: HomeAssistant) -> None: + """Test a light.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.is_variable_color_temp = False + type(bulb).color_temp = PropertyMock(side_effect=Exception) + with _patch_discovery(device=bulb), _patch_single_discovery(device=bulb): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 128 + assert attributes[ATTR_COLOR_MODE] == "hs" + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["brightness", "hs"] + assert attributes[ATTR_HS_COLOR] == (10, 30) + assert attributes[ATTR_RGB_COLOR] == (255, 191, 178) + assert attributes[ATTR_XY_COLOR] == (0.42, 0.336) + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.turn_off.assert_called_once() + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.turn_on.assert_called_once() + bulb.turn_on.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + bulb.set_brightness.assert_called_with(39, transition=None) + bulb.set_brightness.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, + blocking=True, + ) + bulb.set_hsv.assert_called_with(10, 30, None, transition=None) + bulb.set_hsv.reset_mock() + + @pytest.mark.parametrize("is_color", [True, False]) async def test_color_temp_light(hass: HomeAssistant, is_color: bool) -> None: """Test a light.""" From 33b8130002f548a7150c41da32b44a0d3cd17cb6 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 7 Oct 2021 22:18:12 +0200 Subject: [PATCH 0170/1038] Update frontend to 20211007.0 (#57268) --- 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 121cf6ea754..f36d72a967c 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20211006.0" + "home-assistant-frontend==20211007.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9bcaec32d7c..dfab58665ec 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==3.4.8 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211006.0 +home-assistant-frontend==20211007.0 httpx==0.19.0 ifaddr==0.1.7 jinja2==3.0.2 diff --git a/requirements_all.txt b/requirements_all.txt index dfb80c82ad6..92991fcad2c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -807,7 +807,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211006.0 +home-assistant-frontend==20211007.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 161a6fa633d..05c380480ae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -488,7 +488,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211006.0 +home-assistant-frontend==20211007.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 3476b430dbe8ae0bf0d8de532042449619584e61 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 7 Oct 2021 16:22:33 -0400 Subject: [PATCH 0171/1038] Convert val to str when needed while calling zwave_js.set_value (#57216) --- homeassistant/components/zwave_js/services.py | 63 +++++++++++---- tests/components/zwave_js/test_services.py | 81 +++++++++++++++++++ 2 files changed, 130 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 08841465321..ac3f233ba49 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -408,7 +408,7 @@ class ZWaveServices: async def async_set_value(self, service: ServiceCall) -> None: """Set a value on a node.""" # pylint: disable=no-self-use - nodes = service.data[const.ATTR_NODES] + nodes: set[ZwaveNode] = service.data[const.ATTR_NODES] command_class = service.data[const.ATTR_COMMAND_CLASS] property_ = service.data[const.ATTR_PROPERTY] property_key = service.data.get(const.ATTR_PROPERTY_KEY) @@ -418,15 +418,27 @@ class ZWaveServices: options = service.data.get(const.ATTR_OPTIONS) for node in nodes: + value_id = get_value_id( + node, + command_class, + property_, + endpoint=endpoint, + property_key=property_key, + ) + # If value has a string type but the new value is not a string, we need to + # convert it to one. We use new variable `new_value_` to convert the data + # so we can preserve the original `new_value` for every node. + if ( + value_id in node.values + and node.values[value_id].metadata.type == "string" + and not isinstance(new_value, str) + ): + new_value_ = str(new_value) + else: + new_value_ = new_value success = await node.async_set_value( - get_value_id( - node, - command_class, - property_, - endpoint=endpoint, - property_key=property_key, - ), - new_value, + value_id, + new_value_, options=options, wait_for_result=wait_for_result, ) @@ -452,11 +464,16 @@ class ZWaveServices: await self.async_set_value(service) return + command_class = service.data[const.ATTR_COMMAND_CLASS] + property_ = service.data[const.ATTR_PROPERTY] + property_key = service.data.get(const.ATTR_PROPERTY_KEY) + endpoint = service.data.get(const.ATTR_ENDPOINT) + value = { - "commandClass": service.data[const.ATTR_COMMAND_CLASS], - "property": service.data[const.ATTR_PROPERTY], - "propertyKey": service.data.get(const.ATTR_PROPERTY_KEY), - "endpoint": service.data.get(const.ATTR_ENDPOINT), + "commandClass": command_class, + "property": property_, + "propertyKey": property_key, + "endpoint": endpoint, } new_value = service.data[const.ATTR_VALUE] @@ -464,12 +481,30 @@ class ZWaveServices: # schema validation and can use that to get the client, otherwise we can just # get the client from the node. client: ZwaveClient = None - first_node = next((node for node in nodes), None) + first_node: ZwaveNode = next((node for node in nodes), None) if first_node: client = first_node.client else: entry_id = self._hass.config_entries.async_entries(const.DOMAIN)[0].entry_id client = self._hass.data[const.DOMAIN][entry_id][const.DATA_CLIENT] + first_node = next( + node + for node in client.driver.controller.nodes.values() + if get_value_id(node, command_class, property_, endpoint, property_key) + in node.values + ) + + # If value has a string type but the new value is not a string, we need to + # convert it to one + value_id = get_value_id( + first_node, command_class, property_, endpoint, property_key + ) + if ( + value_id in first_node.values + and first_node.values[value_id].metadata.type == "string" + and not isinstance(new_value, str) + ): + new_value = str(new_value) success = await async_multicast_set_value( client=client, diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index 0831d08b216..571190bd35c 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -43,6 +43,7 @@ from .common import ( CLIMATE_DANFOSS_LC13_ENTITY, CLIMATE_EUROTRONICS_SPIRIT_Z_ENTITY, CLIMATE_RADIO_THERMOSTAT_ENTITY, + SCHLAGE_BE469_LOCK_ENTITY, ) from tests.common import MockConfigEntry @@ -1021,6 +1022,51 @@ async def test_set_value(hass, client, climate_danfoss_lc_13, integration): ) +async def test_set_value_string( + hass, client, climate_danfoss_lc_13, lock_schlage_be469, integration +): + """Test set_value service converts number to string when needed.""" + client.async_send_command.return_value = {"success": True} + + # Test that number gets converted to a string when needed + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: SCHLAGE_BE469_LOCK_ENTITY, + ATTR_COMMAND_CLASS: 99, + ATTR_PROPERTY: "userCode", + ATTR_PROPERTY_KEY: 1, + ATTR_VALUE: 12345, + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == lock_schlage_be469.node_id + assert args["valueId"] == { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyName": "userCode", + "propertyKey": 1, + "propertyKeyName": "1", + "metadata": { + "type": "string", + "readable": True, + "writeable": True, + "minLength": 4, + "maxLength": 10, + "label": "User Code (1)", + }, + "value": "**********", + } + assert args["value"] == "12345" + + async def test_set_value_options(hass, client, aeon_smart_switch_6, integration): """Test set_value service with options.""" await hass.services.async_call( @@ -1381,6 +1427,41 @@ async def test_multicast_set_value_options( client.async_send_command.reset_mock() +async def test_multicast_set_value_string( + hass, + client, + lock_id_lock_as_id150, + lock_schlage_be469, + integration, +): + """Test multicast_set_value service converts number to string when needed.""" + client.async_send_command.return_value = {"success": True} + + # Test that number gets converted to a string when needed + await hass.services.async_call( + DOMAIN, + SERVICE_MULTICAST_SET_VALUE, + { + ATTR_BROADCAST: True, + ATTR_COMMAND_CLASS: 99, + ATTR_PROPERTY: "userCode", + ATTR_PROPERTY_KEY: 1, + ATTR_VALUE: 12345, + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "broadcast_node.set_value" + assert args["valueId"] == { + "commandClass": 99, + "property": "userCode", + "propertyKey": 1, + } + assert args["value"] == "12345" + + async def test_ping( hass, client, From be61009030a93281e133cbfb2bcd83f7913f60a9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 7 Oct 2021 22:23:23 +0200 Subject: [PATCH 0172/1038] Correct SQL query generated by get_metadata_with_session (#57225) Co-authored-by: Franck Nijhof --- homeassistant/components/recorder/statistics.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 7b7e349b843..d253d1e2275 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -13,6 +13,7 @@ from sqlalchemy import bindparam, func from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext import baked from sqlalchemy.orm.scoping import scoped_session +from sqlalchemy.sql.expression import true from homeassistant.const import ( PRESSURE_PA, @@ -396,9 +397,9 @@ def get_metadata_with_session( StatisticsMeta.statistic_id.in_(bindparam("statistic_ids")) ) if statistic_type == "mean": - baked_query += lambda q: q.filter(StatisticsMeta.has_mean.isnot(False)) + baked_query += lambda q: q.filter(StatisticsMeta.has_mean == true()) elif statistic_type == "sum": - baked_query += lambda q: q.filter(StatisticsMeta.has_sum.isnot(False)) + baked_query += lambda q: q.filter(StatisticsMeta.has_sum == true()) result = execute(baked_query(session).params(statistic_ids=statistic_ids)) if not result: return {} From cd1a71b0706294f7e30ce8fdc435d68b48772de1 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 7 Oct 2021 22:27:48 +0200 Subject: [PATCH 0173/1038] Motion_blinds fix up button not available for unidirection blinds (#57266) --- homeassistant/components/motion_blinds/cover.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index c96dff93e67..16e5255240c 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -171,6 +171,8 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): @property def is_closed(self): """Return if the cover is closed or not.""" + if self._blind.position is None: + return None return self._blind.position == 100 async def async_added_to_hass(self): From 36a22400e55cdad1f953547cf6af74249cba407c Mon Sep 17 00:00:00 2001 From: Teemu R Date: Fri, 8 Oct 2021 00:20:26 +0200 Subject: [PATCH 0174/1038] Fix transition handling for tplink lights (#57272) * Fix transition handling for tplink light * Apply suggestions from code review * Test that all transitions are passed correctly * Fix linting Co-authored-by: Paulus Schoutsen --- homeassistant/components/tplink/light.py | 8 +++-- tests/components/tplink/test_light.py | 42 +++++++++++++++--------- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index f0e911ea412..49e8adbf08e 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -63,7 +63,9 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): @async_refresh_after async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" - transition = kwargs.get(ATTR_TRANSITION) + if (transition := kwargs.get(ATTR_TRANSITION)) is not None: + transition = int(transition * 1_000) + if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is not None: brightness = round((brightness * 100.0) / 255.0) @@ -92,7 +94,9 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): @async_refresh_after async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" - await self.device.turn_off(transition=kwargs.get(ATTR_TRANSITION)) + if (transition := kwargs.get(ATTR_TRANSITION)) is not None: + transition = int(transition * 1_000) + await self.device.turn_off(transition=transition) @property def min_mireds(self) -> int: diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index 501221f5a6f..93d8cdbf07d 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -1,5 +1,6 @@ """Tests for light platform.""" +from typing import Optional from unittest.mock import PropertyMock import pytest @@ -14,6 +15,7 @@ from homeassistant.components.light import ( ATTR_MIN_MIREDS, ATTR_RGB_COLOR, ATTR_SUPPORTED_COLOR_MODES, + ATTR_TRANSITION, ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN, ) @@ -45,8 +47,9 @@ async def test_light_unique_id(hass: HomeAssistant) -> None: assert entity_registry.async_get(entity_id).unique_id == "AABBCCDDEEFF" -async def test_color_light(hass: HomeAssistant) -> None: - """Test a light.""" +@pytest.mark.parametrize("transition", [2.0, None]) +async def test_color_light(hass: HomeAssistant, transition: Optional[float]) -> None: + """Test a color light and that all transitions are correctly passed.""" already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={}, unique_id=MAC_ADDRESS ) @@ -58,6 +61,11 @@ async def test_color_light(hass: HomeAssistant) -> None: await hass.async_block_till_done() entity_id = "light.my_bulb" + KASA_TRANSITION_VALUE = transition * 1_000 if transition is not None else None + + BASE_PAYLOAD = {ATTR_ENTITY_ID: entity_id} + if transition: + BASE_PAYLOAD[ATTR_TRANSITION] = transition state = hass.states.get(entity_id) assert state.state == "on" @@ -72,50 +80,52 @@ async def test_color_light(hass: HomeAssistant) -> None: assert attributes[ATTR_XY_COLOR] == (0.42, 0.336) await hass.services.async_call( - LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + LIGHT_DOMAIN, "turn_off", BASE_PAYLOAD, blocking=True ) - bulb.turn_off.assert_called_once() + bulb.turn_off.assert_called_once_with(transition=KASA_TRANSITION_VALUE) - await hass.services.async_call( - LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True - ) - bulb.turn_on.assert_called_once() + await hass.services.async_call(LIGHT_DOMAIN, "turn_on", BASE_PAYLOAD, blocking=True) + bulb.turn_on.assert_called_once_with(transition=KASA_TRANSITION_VALUE) bulb.turn_on.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, "turn_on", - {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + {**BASE_PAYLOAD, ATTR_BRIGHTNESS: 100}, blocking=True, ) - bulb.set_brightness.assert_called_with(39, transition=None) + bulb.set_brightness.assert_called_with(39, transition=KASA_TRANSITION_VALUE) bulb.set_brightness.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, "turn_on", - {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 150}, + {**BASE_PAYLOAD, ATTR_COLOR_TEMP: 150}, blocking=True, ) - bulb.set_color_temp.assert_called_with(6666, brightness=None, transition=None) + bulb.set_color_temp.assert_called_with( + 6666, brightness=None, transition=KASA_TRANSITION_VALUE + ) bulb.set_color_temp.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, "turn_on", - {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 150}, + {**BASE_PAYLOAD, ATTR_COLOR_TEMP: 150}, blocking=True, ) - bulb.set_color_temp.assert_called_with(6666, brightness=None, transition=None) + bulb.set_color_temp.assert_called_with( + 6666, brightness=None, transition=KASA_TRANSITION_VALUE + ) bulb.set_color_temp.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, "turn_on", - {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, + {**BASE_PAYLOAD, ATTR_HS_COLOR: (10, 30)}, blocking=True, ) - bulb.set_hsv.assert_called_with(10, 30, None, transition=None) + bulb.set_hsv.assert_called_with(10, 30, None, transition=KASA_TRANSITION_VALUE) bulb.set_hsv.reset_mock() From 01d883d7c92534ac27b6495a35b8b00dcbc1506b Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 8 Oct 2021 00:13:12 +0000 Subject: [PATCH 0175/1038] [ci skip] Translation update --- homeassistant/components/daikin/translations/nl.json | 1 + .../components/daikin/translations/zh-Hant.json | 1 + .../components/dlna_dmr/translations/ca.json | 3 +++ .../components/dlna_dmr/translations/de.json | 3 +++ .../components/dlna_dmr/translations/en.json | 2 +- .../components/flux_led/translations/fr.json | 11 +++++++++++ .../components/samsungtv/translations/fr.json | 1 + homeassistant/components/tuya/translations/fr.json | 9 +++++++++ homeassistant/components/tuya/translations/nl.json | 2 +- .../components/zwave_js/translations/fr.json | 5 +++++ 10 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/flux_led/translations/fr.json diff --git a/homeassistant/components/daikin/translations/nl.json b/homeassistant/components/daikin/translations/nl.json index 706a81b5f7f..33659797e7a 100644 --- a/homeassistant/components/daikin/translations/nl.json +++ b/homeassistant/components/daikin/translations/nl.json @@ -5,6 +5,7 @@ "cannot_connect": "Kon niet verbinden" }, "error": { + "api_password": "Ongeldige authenticatie, gebruik API-sleutel of wachtwoord.", "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" diff --git a/homeassistant/components/daikin/translations/zh-Hant.json b/homeassistant/components/daikin/translations/zh-Hant.json index a6d4b4598b1..8072d135c20 100644 --- a/homeassistant/components/daikin/translations/zh-Hant.json +++ b/homeassistant/components/daikin/translations/zh-Hant.json @@ -5,6 +5,7 @@ "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "error": { + "api_password": "\u9a57\u8b49\u78bc\u7121\u6548\u3001\u8acb\u4f7f\u7528 API \u5bc6\u9470\u6216\u5bc6\u78bc\u3002", "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" diff --git a/homeassistant/components/dlna_dmr/translations/ca.json b/homeassistant/components/dlna_dmr/translations/ca.json index cf3adc94405..686d20fbaab 100644 --- a/homeassistant/components/dlna_dmr/translations/ca.json +++ b/homeassistant/components/dlna_dmr/translations/ca.json @@ -17,6 +17,9 @@ "confirm": { "description": "Vols comen\u00e7ar la configuraci\u00f3?" }, + "import_turn_on": { + "description": "Engega el dispositiu i fes clic a Envia per continuar la migraci\u00f3" + }, "user": { "data": { "url": "URL" diff --git a/homeassistant/components/dlna_dmr/translations/de.json b/homeassistant/components/dlna_dmr/translations/de.json index 50f66761748..58848ea69e8 100644 --- a/homeassistant/components/dlna_dmr/translations/de.json +++ b/homeassistant/components/dlna_dmr/translations/de.json @@ -17,6 +17,9 @@ "confirm": { "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" + }, "user": { "data": { "url": "URL" diff --git a/homeassistant/components/dlna_dmr/translations/en.json b/homeassistant/components/dlna_dmr/translations/en.json index c307d6b3571..470328d0c27 100644 --- a/homeassistant/components/dlna_dmr/translations/en.json +++ b/homeassistant/components/dlna_dmr/translations/en.json @@ -18,7 +18,7 @@ "description": "Do you want to start set up?" }, "import_turn_on": { - "description": "Please turn on the device and click confirm to continue migration" + "description": "Please turn on the device and click submit to continue migration" }, "user": { "data": { diff --git a/homeassistant/components/flux_led/translations/fr.json b/homeassistant/components/flux_led/translations/fr.json new file mode 100644 index 00000000000..9cb1d7dfd16 --- /dev/null +++ b/homeassistant/components/flux_led/translations/fr.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "H\u00f4te" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/fr.json b/homeassistant/components/samsungtv/translations/fr.json index 9ee94b5edc2..75b6bab6676 100644 --- a/homeassistant/components/samsungtv/translations/fr.json +++ b/homeassistant/components/samsungtv/translations/fr.json @@ -6,6 +6,7 @@ "auth_missing": "Home Assistant n'est pas autoris\u00e9 \u00e0 se connecter \u00e0 ce t\u00e9l\u00e9viseur Samsung. Veuillez v\u00e9rifier les param\u00e8tres de votre t\u00e9l\u00e9viseur pour autoriser Home Assistant.", "cannot_connect": "\u00c9chec de connexion", "id_missing": "Cet appareil Samsung n'a pas de num\u00e9ro de s\u00e9rie.", + "missing_config_entry": "Cet appareil Samsung n'a pas d'entr\u00e9e de configuration.", "not_supported": "Ce t\u00e9l\u00e9viseur Samsung n'est actuellement pas pris en charge.", "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", "unknown": "Erreur inattendue" diff --git a/homeassistant/components/tuya/translations/fr.json b/homeassistant/components/tuya/translations/fr.json index b741d3f9377..2dea3145558 100644 --- a/homeassistant/components/tuya/translations/fr.json +++ b/homeassistant/components/tuya/translations/fr.json @@ -10,11 +10,20 @@ }, "flow_title": "Configuration Tuya", "step": { + "login": { + "data": { + "tuya_app_type": "Application mobile", + "username": "Compte" + }, + "description": "Entrez votre identifiant Tuya", + "title": "Tuya" + }, "user": { "data": { "country_code": "Le code de pays de votre compte (par exemple, 1 pour les \u00c9tats-Unis ou 86 pour la Chine)", "password": "Mot de passe", "platform": "L'application dans laquelle votre compte est enregistr\u00e9", + "region": "R\u00e9gion", "username": "Nom d'utilisateur" }, "description": "Saisissez vos informations d'identification Tuya.", diff --git a/homeassistant/components/tuya/translations/nl.json b/homeassistant/components/tuya/translations/nl.json index 3519ac126f6..88078f8e6a5 100644 --- a/homeassistant/components/tuya/translations/nl.json +++ b/homeassistant/components/tuya/translations/nl.json @@ -28,7 +28,7 @@ "data": { "access_id": "Tuya IoT-toegangs-ID", "access_secret": "Tuya IoT Access Secret", - "country_code": "De landcode van uw account (bijvoorbeeld 1 voor de VS of 86 voor China)", + "country_code": "Land", "password": "Wachtwoord", "platform": "De app waar uw account is geregistreerd", "region": "Regio", diff --git a/homeassistant/components/zwave_js/translations/fr.json b/homeassistant/components/zwave_js/translations/fr.json index bca57b9da68..1a8aef755b5 100644 --- a/homeassistant/components/zwave_js/translations/fr.json +++ b/homeassistant/components/zwave_js/translations/fr.json @@ -58,6 +58,10 @@ } }, "device_automation": { + "action_type": { + "clear_lock_usercode": "Effacer le code utilisateur sur {entity_name}", + "ping": "Pinger l'appareil" + }, "condition_type": { "config_parameter": "Valeur du param\u00e8tre de configuration {subtype}", "node_status": "\u00c9tat du n\u0153ud", @@ -100,6 +104,7 @@ "emulate_hardware": "\u00c9muler le mat\u00e9riel", "log_level": "Niveau du journal", "network_key": "Cl\u00e9 r\u00e9seau", + "s2_authenticated_key": "Cl\u00e9 d'authentification S2", "usb_path": "Chemin du p\u00e9riph\u00e9rique USB" }, "title": "Entrer dans la configuration du module compl\u00e9mentaire Z-Wave JS" From 49e6f84b1c0bbd31b98cc5d2bb73a3e814a74c2a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 7 Oct 2021 14:48:08 -1000 Subject: [PATCH 0176/1038] Bump yeelight to 0.7.7 (#57290) --- homeassistant/components/yeelight/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 561606f5509..1b8c959cb52 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,7 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.7.6", "async-upnp-client==0.22.5"], + "requirements": ["yeelight==0.7.7", "async-upnp-client==0.22.5"], "codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"], "config_flow": true, "dependencies": ["network"], diff --git a/requirements_all.txt b/requirements_all.txt index 92991fcad2c..5a7b14e9ab1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2456,7 +2456,7 @@ yalesmartalarmclient==0.3.4 yalexs==1.1.13 # homeassistant.components.yeelight -yeelight==0.7.6 +yeelight==0.7.7 # homeassistant.components.yeelightsunflower yeelightsunflower==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 05c380480ae..e32d8510fcc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1406,7 +1406,7 @@ yalesmartalarmclient==0.3.4 yalexs==1.1.13 # homeassistant.components.yeelight -yeelight==0.7.6 +yeelight==0.7.7 # homeassistant.components.youless youless-api==0.13 From ddab7f3024245e0a67b6aa5470754736e86e22a3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 7 Oct 2021 17:06:27 -1000 Subject: [PATCH 0177/1038] Bump HAP-python to 4.30 (#57284) --- homeassistant/components/homekit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homekit/test_type_fans.py | 6 +++--- tests/components/homekit/test_type_media_players.py | 7 +++++-- tests/components/homekit/test_type_remote.py | 7 +++++-- tests/components/homekit/test_type_thermostats.py | 6 +----- 7 files changed, 17 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 2589a1ac6ec..d23aa11b4ea 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit", "documentation": "https://www.home-assistant.io/integrations/homekit", "requirements": [ - "HAP-python==4.2.1", + "HAP-python==4.3.0", "fnvhash==0.1.0", "PyQRCode==1.2.1", "base36==0.1.1" diff --git a/requirements_all.txt b/requirements_all.txt index 5a7b14e9ab1..d2e39a25663 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -14,7 +14,7 @@ Adafruit-SHT31==1.0.2 # Adafruit_BBIO==1.1.1 # homeassistant.components.homekit -HAP-python==4.2.1 +HAP-python==4.3.0 # homeassistant.components.mastodon Mastodon.py==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e32d8510fcc..b9e8363683c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.homekit -HAP-python==4.2.1 +HAP-python==4.3.0 # homeassistant.components.flick_electric PyFlick==0.0.2 diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index 15e2366a883..85d00dcb287 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -297,7 +297,7 @@ async def test_fan_speed(hass, hk_driver, events): ) await hass.async_add_executor_job(acc.char_speed.client_update_value, 42) await hass.async_block_till_done() - assert acc.char_speed.value == 42 + assert acc.char_speed.value == 50 assert acc.char_active.value == 1 assert call_set_percentage[0] @@ -309,7 +309,7 @@ async def test_fan_speed(hass, hk_driver, events): # Verify speed is preserved from off to on hass.states.async_set(entity_id, STATE_OFF, {ATTR_PERCENTAGE: 42}) await hass.async_block_till_done() - assert acc.char_speed.value == 42 + assert acc.char_speed.value == 50 assert acc.char_active.value == 0 hk_driver.set_characteristics( @@ -325,7 +325,7 @@ async def test_fan_speed(hass, hk_driver, events): "mock_addr", ) await hass.async_block_till_done() - assert acc.char_speed.value == 42 + assert acc.char_speed.value == 50 assert acc.char_active.value == 1 diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index 33cac7bcf8a..c0184667e2c 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -1,5 +1,7 @@ """Test different accessory types: Media Players.""" +import pytest + from homeassistant.components.homekit.const import ( ATTR_KEY_NAME, ATTR_VALUE, @@ -353,8 +355,9 @@ async def test_media_player_television(hass, hk_driver, events, caplog): hass.bus.async_listen(EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED, listener) - await hass.async_add_executor_job(acc.char_remote_key.client_update_value, 20) - await hass.async_block_till_done() + with pytest.raises(ValueError): + await hass.async_add_executor_job(acc.char_remote_key.client_update_value, 20) + await hass.async_block_till_done() await hass.async_add_executor_job(acc.char_remote_key.client_update_value, 7) await hass.async_block_till_done() diff --git a/tests/components/homekit/test_type_remote.py b/tests/components/homekit/test_type_remote.py index ee71d7f4e3c..5c5b5ee6cd9 100644 --- a/tests/components/homekit/test_type_remote.py +++ b/tests/components/homekit/test_type_remote.py @@ -1,5 +1,7 @@ """Test different accessory types: Remotes.""" +import pytest + from homeassistant.components.homekit.const import ( ATTR_KEY_NAME, ATTR_VALUE, @@ -140,8 +142,9 @@ async def test_activity_remote(hass, hk_driver, events, caplog): hass.bus.async_listen(EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED, listener) - acc.char_remote_key.client_update_value(20) - await hass.async_block_till_done() + with pytest.raises(ValueError): + acc.char_remote_key.client_update_value(20) + await hass.async_block_till_done() acc.char_remote_key.client_update_value(7) await hass.async_block_till_done() diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index e73465b0ab0..ef517f4ab96 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -1746,11 +1746,7 @@ async def test_water_heater(hass, hk_driver, events): assert len(events) == 1 assert events[-1].data[ATTR_VALUE] == f"52.0{TEMP_CELSIUS}" - await hass.async_add_executor_job(acc.char_target_heat_cool.client_update_value, 0) - await hass.async_block_till_done() - assert acc.char_target_heat_cool.value == 1 - - await hass.async_add_executor_job(acc.char_target_heat_cool.client_update_value, 2) + await hass.async_add_executor_job(acc.char_target_heat_cool.client_update_value, 1) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 1 From 8fb0da77209bd2ef773067e2cf042b7995ba0cd0 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Fri, 8 Oct 2021 05:15:13 +0200 Subject: [PATCH 0178/1038] Stopgap fix for inconsistent upstream API of tplink dimmers (#57285) --- homeassistant/components/tplink/light.py | 8 ++++++++ tests/components/tplink/__init__.py | 1 + tests/components/tplink/test_light.py | 26 ++++++++++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 49e8adbf08e..3f4b130a5cc 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -69,6 +69,14 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is not None: brightness = round((brightness * 100.0) / 255.0) + if self.device.is_dimmer and transition is None: + # This is a stopgap solution for inconsistent set_brightness handling + # in the upstream library, see #57265. + # This should be removed when the upstream has fixed the issue. + # The device logic is to change the settings without turning it on + # except when transition is defined, so we leverage that here for now. + transition = 1 + # Handle turning to temp mode if ATTR_COLOR_TEMP in kwargs: color_tmp = mired_to_kelvin(int(kwargs[ATTR_COLOR_TEMP])) diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index 870e05e970b..f25fc13784a 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -33,6 +33,7 @@ def _mocked_bulb() -> SmartBulb: bulb.is_color = True bulb.is_strip = False bulb.is_plug = False + bulb.is_dimmer = False bulb.hsv = (10, 30, 5) bulb.device_id = MAC_ADDRESS bulb.valid_temperature_range.min = 4000 diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index 93d8cdbf07d..1017ad38eae 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -349,3 +349,29 @@ async def test_off_at_start_light(hass: HomeAssistant) -> None: assert state.state == "off" attributes = state.attributes assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["onoff"] + + +async def test_dimmer_turn_on_fix(hass: HomeAssistant) -> None: + """Test a light.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={}, unique_id=MAC_ADDRESS + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.is_dimmer = True + bulb.is_on = False + + with _patch_discovery(device=bulb), _patch_single_discovery(device=bulb): + await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == "off" + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.turn_on.assert_called_once_with(transition=1) + bulb.turn_on.reset_mock() From 56d6173d705fdcfd2c0fe09f4b70ccdb34791cef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 7 Oct 2021 17:15:29 -1000 Subject: [PATCH 0179/1038] Recreate the powerwall session/object when attempting relogin (#56935) --- .../components/powerwall/__init__.py | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index 6c9de5585b9..6220f81e9a3 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -91,11 +91,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry_id = entry.entry_id hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN].setdefault(entry_id, {}) http_session = requests.Session() + ip_address = entry.data[CONF_IP_ADDRESS] password = entry.data.get(CONF_PASSWORD) - power_wall = Powerwall(entry.data[CONF_IP_ADDRESS], http_session=http_session) + power_wall = Powerwall(ip_address, http_session=http_session) try: powerwall_data = await hass.async_add_executor_job( _login_and_fetch_base_info, power_wall, password @@ -115,13 +115,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await _migrate_old_unique_ids(hass, entry_id, powerwall_data) login_failed_count = 0 + runtime_data = hass.data[DOMAIN][entry.entry_id] = { + POWERWALL_API_CHANGED: False, + POWERWALL_HTTP_SESSION: http_session, + } + + def _recreate_powerwall_login(): + nonlocal http_session + nonlocal power_wall + http_session.close() + http_session = requests.Session() + power_wall = Powerwall(ip_address, http_session=http_session) + runtime_data[POWERWALL_OBJECT] = power_wall + runtime_data[POWERWALL_HTTP_SESSION] = http_session + power_wall.login("", password) + async def async_update_data(): """Fetch data from API endpoint.""" # Check if we had an error before nonlocal login_failed_count _LOGGER.debug("Checking if update failed") - if hass.data[DOMAIN][entry.entry_id][POWERWALL_API_CHANGED]: - return hass.data[DOMAIN][entry.entry_id][POWERWALL_COORDINATOR].data + if runtime_data[POWERWALL_API_CHANGED]: + return runtime_data[POWERWALL_COORDINATOR].data _LOGGER.debug("Updating data") try: @@ -130,9 +145,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if password is None: raise ConfigEntryAuthFailed from err - # If the session expired, relogin, and try again + # If the session expired, recreate, relogin, and try again try: - await hass.async_add_executor_job(power_wall.login, "", password) + await hass.async_add_executor_job(_recreate_powerwall_login) return await _async_update_powerwall_data(hass, entry, power_wall) except AccessDeniedError as ex: login_failed_count += 1 @@ -153,13 +168,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_interval=timedelta(seconds=UPDATE_INTERVAL), ) - hass.data[DOMAIN][entry.entry_id] = powerwall_data - hass.data[DOMAIN][entry.entry_id].update( + runtime_data.update( { + **powerwall_data, POWERWALL_OBJECT: power_wall, POWERWALL_COORDINATOR: coordinator, - POWERWALL_HTTP_SESSION: http_session, - POWERWALL_API_CHANGED: False, } ) From 7d4dd94da81f9bf3c74b2bafca4f673554e30590 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 7 Oct 2021 22:13:14 -0700 Subject: [PATCH 0180/1038] Add WebSocket API for intiting a WebRTC stream (#57034) * Add WebSocket API for intiting a WebRTC stream See https://github.com/home-assistant/architecture/discussions/640 * Increase test coverage for webrtc camera stream * Apply suggestions from code review Co-authored-by: Paulus Schoutsen Co-authored-by: Paulus Schoutsen --- homeassistant/components/camera/__init__.py | 75 ++++++++- homeassistant/components/camera/const.py | 9 + tests/components/camera/test_init.py | 172 +++++++++++++++++++- 3 files changed, 253 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index bfa68fe67e6..56f7c56008b 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -63,6 +63,8 @@ from .const import ( DATA_CAMERA_PREFS, DOMAIN, SERVICE_RECORD, + STREAM_TYPE_HLS, + STREAM_TYPE_WEB_RTC, ) from .img_util import scale_jpeg_camera_image from .prefs import CameraPreferences @@ -207,7 +209,6 @@ async def async_get_image( async def async_get_stream_source(hass: HomeAssistant, entity_id: str) -> str | None: """Fetch the stream source for a camera entity.""" camera = _get_camera_from_entity_id(hass, entity_id) - return await camera.stream_source() @@ -303,6 +304,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: WS_TYPE_CAMERA_THUMBNAIL, websocket_camera_thumbnail, SCHEMA_WS_CAMERA_THUMBNAIL ) hass.components.websocket_api.async_register_command(ws_camera_stream) + hass.components.websocket_api.async_register_command(ws_camera_web_rtc_offer) hass.components.websocket_api.async_register_command(websocket_get_prefs) hass.components.websocket_api.async_register_command(websocket_update_prefs) @@ -421,6 +423,18 @@ class Camera(Entity): """Return the interval between frames of the mjpeg stream.""" return MIN_STREAM_INTERVAL + @property + def stream_type(self) -> str | None: + """Return the type of stream supported by this camera. + + A camera may have a single stream type which is used to inform the + frontend which camera attributes and player to use. The default type + is to use HLS, and components can override to change the type. + """ + if not self.supported_features & SUPPORT_STREAM: + return None + return STREAM_TYPE_HLS + async def create_stream(self) -> Stream | None: """Create a Stream for stream_source.""" # There is at most one stream (a decode worker) per camera @@ -433,10 +447,20 @@ class Camera(Entity): return self.stream async def stream_source(self) -> str | None: - """Return the source of the stream.""" + """Return the source of the stream. + + This is used by cameras with SUPPORT_STREAM and STREAM_TYPE_HLS. + """ # pylint: disable=no-self-use return None + async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str | None: + """Handle the WebRTC offer and return an answer. + + This is used by cameras with SUPPORT_STREAM and STREAM_TYPE_WEB_RTC. + """ + raise NotImplementedError() + def camera_image( self, width: int | None = None, height: int | None = None ) -> bytes | None: @@ -548,6 +572,9 @@ class Camera(Entity): if self.motion_detection_enabled: attrs["motion_detection"] = self.motion_detection_enabled + if self.stream_type: + attrs["stream_type"] = self.stream_type + return attrs @callback @@ -699,6 +726,50 @@ async def ws_camera_stream( ) +@websocket_api.websocket_command( + { + vol.Required("type"): "camera/web_rtc_offer", + vol.Required("entity_id"): cv.entity_id, + vol.Required("offer"): str, + } +) +@websocket_api.async_response +async def ws_camera_web_rtc_offer( + hass: HomeAssistant, connection: ActiveConnection, msg: dict +) -> None: + """Handle the signal path for a WebRTC stream. + + This signal path is used to route the offer created by the client to the + camera device through the integration for negitioation on initial setup, + which returns an answer. The actual streaming is handled entirely between + the client and camera device. + + Async friendly. + """ + entity_id = msg["entity_id"] + offer = msg["offer"] + camera = _get_camera_from_entity_id(hass, entity_id) + if camera.stream_type != STREAM_TYPE_WEB_RTC: + connection.send_error( + msg["id"], + "web_rtc_offer_failed", + f"Camera does not support WebRTC, stream_type={camera.stream_type}", + ) + return + try: + answer = await camera.async_handle_web_rtc_offer(offer) + except (HomeAssistantError, ValueError) as ex: + _LOGGER.error("Error handling WebRTC offer: %s", ex) + connection.send_error(msg["id"], "web_rtc_offer_failed", str(ex)) + except asyncio.TimeoutError: + _LOGGER.error("Timeout handling WebRTC offer") + connection.send_error( + msg["id"], "web_rtc_offer_failed", "Timeout handling WebRTC offer" + ) + else: + connection.send_result(msg["id"], {"answer": answer}) + + @websocket_api.websocket_command( {vol.Required("type"): "camera/get_prefs", vol.Required("entity_id"): cv.entity_id} ) diff --git a/homeassistant/components/camera/const.py b/homeassistant/components/camera/const.py index 2cb01f44aa9..3eb131200e6 100644 --- a/homeassistant/components/camera/const.py +++ b/homeassistant/components/camera/const.py @@ -14,3 +14,12 @@ CONF_DURATION: Final = "duration" CAMERA_STREAM_SOURCE_TIMEOUT: Final = 10 CAMERA_IMAGE_TIMEOUT: Final = 10 + +# A camera that supports CAMERA_SUPPORT_STREAM may have a single stream +# type which is used to inform the frontend which player to use. +# Streams with RTSP sources typically use the stream component which uses +# HLS for display. WebRTC streams use the home assistant core for a signal +# path to initiate a stream, but the stream itself is between the client and +# device. +STREAM_TYPE_HLS = "hls" +STREAM_TYPE_WEB_RTC = "web_rtc" diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index df4b64e4310..122fe13e2f1 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -7,7 +7,11 @@ from unittest.mock import Mock, PropertyMock, mock_open, patch import pytest from homeassistant.components import camera -from homeassistant.components.camera.const import DOMAIN, PREF_PRELOAD_STREAM +from homeassistant.components.camera.const import ( + DOMAIN, + PREF_PRELOAD_STREAM, + STREAM_TYPE_WEB_RTC, +) from homeassistant.components.camera.prefs import CameraEntityPreferences from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.config import async_process_ha_core_config @@ -40,6 +44,24 @@ async def mock_camera_fixture(hass): yield +@pytest.fixture(name="mock_camera_web_rtc") +async def mock_camera_web_rtc_fixture(hass): + """Initialize a demo camera platform.""" + assert await async_setup_component( + hass, "camera", {camera.DOMAIN: {"platform": "demo"}} + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.camera.Camera.stream_type", + new_callable=PropertyMock(return_value=STREAM_TYPE_WEB_RTC), + ), patch( + "homeassistant.components.camera.Camera.async_handle_web_rtc_offer", + return_value="a=sendonly", + ): + yield + + @pytest.fixture(name="mock_stream") def mock_stream_fixture(hass): """Initialize a demo camera platform with streaming.""" @@ -467,3 +489,151 @@ async def test_camera_proxy_stream(hass, mock_camera, hass_client): ): response = await client.get("/api/camera_proxy_stream/camera.demo_camera") assert response.status == HTTP_BAD_GATEWAY + + +async def test_websocket_web_rtc_offer( + hass, + hass_ws_client, + mock_camera_web_rtc, +): + """Test initiating a WebRTC stream with offer and answer.""" + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 9, + "type": "camera/web_rtc_offer", + "entity_id": "camera.demo_camera", + "offer": "v=0\r\n", + } + ) + response = await client.receive_json() + + assert response["id"] == 9 + assert response["type"] == TYPE_RESULT + assert response["success"] + assert response["result"]["answer"] == "a=sendonly" + + +async def test_websocket_web_rtc_offer_invalid_entity( + hass, + hass_ws_client, + mock_camera_web_rtc, +): + """Test WebRTC with a camera entity that does not exist.""" + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 9, + "type": "camera/web_rtc_offer", + "entity_id": "camera.does_not_exist", + "offer": "v=0\r\n", + } + ) + response = await client.receive_json() + + assert response["id"] == 9 + assert response["type"] == TYPE_RESULT + assert not response["success"] + + +async def test_websocket_web_rtc_offer_missing_offer( + hass, + hass_ws_client, + mock_camera_web_rtc, +): + """Test WebRTC stream with missing required fields.""" + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 9, + "type": "camera/web_rtc_offer", + "entity_id": "camera.demo_camera", + } + ) + response = await client.receive_json() + + assert response["id"] == 9 + assert response["type"] == TYPE_RESULT + assert not response["success"] + assert response["error"]["code"] == "invalid_format" + + +async def test_websocket_web_rtc_offer_failure( + hass, + hass_ws_client, + mock_camera_web_rtc, +): + """Test WebRTC stream that fails handling the offer.""" + client = await hass_ws_client(hass) + + with patch( + "homeassistant.components.camera.Camera.async_handle_web_rtc_offer", + side_effect=HomeAssistantError("offer failed"), + ): + await client.send_json( + { + "id": 9, + "type": "camera/web_rtc_offer", + "entity_id": "camera.demo_camera", + "offer": "v=0\r\n", + } + ) + response = await client.receive_json() + + assert response["id"] == 9 + assert response["type"] == TYPE_RESULT + assert not response["success"] + assert response["error"]["code"] == "web_rtc_offer_failed" + assert response["error"]["message"] == "offer failed" + + +async def test_websocket_web_rtc_offer_timeout( + hass, + hass_ws_client, + mock_camera_web_rtc, +): + """Test WebRTC stream with timeout handling the offer.""" + client = await hass_ws_client(hass) + + with patch( + "homeassistant.components.camera.Camera.async_handle_web_rtc_offer", + side_effect=asyncio.TimeoutError(), + ): + await client.send_json( + { + "id": 9, + "type": "camera/web_rtc_offer", + "entity_id": "camera.demo_camera", + "offer": "v=0\r\n", + } + ) + response = await client.receive_json() + + assert response["id"] == 9 + assert response["type"] == TYPE_RESULT + assert not response["success"] + assert response["error"]["code"] == "web_rtc_offer_failed" + assert response["error"]["message"] == "Timeout handling WebRTC offer" + + +async def test_websocket_web_rtc_offer_invalid_stream_type( + hass, + hass_ws_client, + mock_camera, +): + """Test WebRTC initiating for a camera with a different stream_type.""" + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 9, + "type": "camera/web_rtc_offer", + "entity_id": "camera.demo_camera", + "offer": "v=0\r\n", + } + ) + response = await client.receive_json() + + assert response["id"] == 9 + assert response["type"] == TYPE_RESULT + assert not response["success"] + assert response["error"]["code"] == "web_rtc_offer_failed" From db7d2de8bc066b84042df8d66847e2cb9f06c9aa Mon Sep 17 00:00:00 2001 From: Yuval Aboulafia Date: Fri, 8 Oct 2021 10:03:09 +0300 Subject: [PATCH 0181/1038] Use _attr in 17track (#57187) * use _attr * fix name * attributes * set fixed attr before init * access parameters directly --- .../components/seventeentrack/sensor.py | 62 +++++-------------- 1 file changed, 16 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py index 44720db2fcb..9087cff8a97 100644 --- a/homeassistant/components/seventeentrack/sensor.py +++ b/homeassistant/components/seventeentrack/sensor.py @@ -37,7 +37,7 @@ CONF_SHOW_DELIVERED = "show_delivered" DATA_PACKAGES = "package_data" DATA_SUMMARY = "summary_data" -DEFAULT_ATTRIBUTION = "Data provided by 17track.net" +ATTRIBUTION = "Data provided by 17track.net" DEFAULT_SCAN_INTERVAL = timedelta(minutes=10) UNIQUE_ID_TEMPLATE = "package_{0}_{1}" @@ -97,48 +97,28 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class SeventeenTrackSummarySensor(SensorEntity): """Define a summary sensor.""" + _attr_icon = "mdi:package" + _attr_native_unit_of_measurement = "packages" + def __init__(self, data, status, initial_state): """Initialize.""" - self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} self._data = data self._state = initial_state self._status = status + self._attr_name = f"Seventeentrack Packages {status}" + self._attr_unique_id = f"summary_{data.account_id}_{slugify(status)}" @property def available(self): """Return whether the entity is available.""" return self._state is not None - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - return self._attrs - - @property - def icon(self): - """Return the icon.""" - return "mdi:package" - - @property - def name(self): - """Return the name.""" - return f"Seventeentrack Packages {self._status}" - @property def native_value(self): """Return the state.""" return self._state - @property - def unique_id(self): - """Return a unique, Home Assistant friendly identifier for this entity.""" - return f"summary_{self._data.account_id}_{slugify(self._status)}" - - @property - def native_unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return "packages" - async def async_update(self): """Update the sensor.""" await self._data.async_update() @@ -160,7 +140,7 @@ class SeventeenTrackSummarySensor(SensorEntity): ) if package_data: - self._attrs[ATTR_PACKAGES] = package_data + self._attr_extra_state_attributes[ATTR_PACKAGES] = package_data self._state = self._data.summary.get(self._status) @@ -168,10 +148,12 @@ class SeventeenTrackSummarySensor(SensorEntity): class SeventeenTrackPackageSensor(SensorEntity): """Define an individual package sensor.""" + _attr_icon = "mdi:package" + def __init__(self, data, package): """Initialize.""" - self._attrs = { - ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION, + self._attr_extra_state_attributes = { + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_DESTINATION_COUNTRY: package.destination_country, ATTR_INFO_TEXT: package.info_text, ATTR_TIMESTAMP: package.timestamp, @@ -186,22 +168,15 @@ class SeventeenTrackPackageSensor(SensorEntity): self._state = package.status self._tracking_number = package.tracking_number self.entity_id = ENTITY_ID_TEMPLATE.format(self._tracking_number) + self._attr_unique_id = UNIQUE_ID_TEMPLATE.format( + data.account_id, self._tracking_number + ) @property def available(self): """Return whether the entity is available.""" return self._data.packages.get(self._tracking_number) is not None - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - return self._attrs - - @property - def icon(self): - """Return the icon.""" - return "mdi:package" - @property def name(self): """Return the name.""" @@ -215,11 +190,6 @@ class SeventeenTrackPackageSensor(SensorEntity): """Return the state.""" return self._state - @property - def unique_id(self): - """Return a unique, Home Assistant friendly identifier for this entity.""" - return UNIQUE_ID_TEMPLATE.format(self._data.account_id, self._tracking_number) - async def async_update(self): """Update the sensor.""" await self._data.async_update() @@ -239,7 +209,7 @@ class SeventeenTrackPackageSensor(SensorEntity): async_call_later(self.hass, 1, self._remove) return - self._attrs.update( + self._attr_extra_state_attributes.update( { ATTR_INFO_TEXT: package.info_text, ATTR_TIMESTAMP: package.timestamp, From 82160fa350359ccf6b7fff08ff4b4771d819d76a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 8 Oct 2021 11:34:22 +0200 Subject: [PATCH 0182/1038] Add config flow to Stookalert (#57119) --- .coveragerc | 3 +- .strict-typing | 1 + CODEOWNERS | 2 +- .../components/stookalert/__init__.py | 29 +++- .../components/stookalert/binary_sensor.py | 131 ++++++++++-------- .../components/stookalert/config_flow.py | 37 +++++ homeassistant/components/stookalert/const.py | 26 ++++ .../components/stookalert/manifest.json | 3 +- .../components/stookalert/strings.json | 14 ++ .../stookalert/translations/en.json | 14 ++ homeassistant/generated/config_flows.py | 1 + mypy.ini | 11 ++ requirements_test_all.txt | 3 + tests/components/stookalert/__init__.py | 1 + .../components/stookalert/test_config_flow.py | 78 +++++++++++ 15 files changed, 294 insertions(+), 60 deletions(-) create mode 100644 homeassistant/components/stookalert/config_flow.py create mode 100644 homeassistant/components/stookalert/const.py create mode 100644 homeassistant/components/stookalert/strings.json create mode 100644 homeassistant/components/stookalert/translations/en.json create mode 100644 tests/components/stookalert/__init__.py create mode 100644 tests/components/stookalert/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 8ee3a3ceab6..70f2b8cee34 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1000,7 +1000,8 @@ omit = homeassistant/components/starlingbank/sensor.py homeassistant/components/steam_online/sensor.py homeassistant/components/stiebel_eltron/* - homeassistant/components/stookalert/* + homeassistant/components/stookalert/__init__.py + homeassistant/components/stookalert/binary_sensor.py homeassistant/components/stream/* homeassistant/components/streamlabswater/* homeassistant/components/suez_water/* diff --git a/.strict-typing b/.strict-typing index b5ae496fcf0..7710f637090 100644 --- a/.strict-typing +++ b/.strict-typing @@ -101,6 +101,7 @@ homeassistant.components.simplisafe.* homeassistant.components.slack.* homeassistant.components.sonos.media_player homeassistant.components.ssdp.* +homeassistant.components.stookalert.* homeassistant.components.stream.* homeassistant.components.sun.* homeassistant.components.surepetcare.* diff --git a/CODEOWNERS b/CODEOWNERS index 73c6e63eb02..a4756f961be 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -498,7 +498,7 @@ homeassistant/components/srp_energy/* @briglx homeassistant/components/starline/* @anonym-tsk homeassistant/components/statistics/* @fabaff homeassistant/components/stiebel_eltron/* @fucm -homeassistant/components/stookalert/* @fwestenberg +homeassistant/components/stookalert/* @fwestenberg @frenck homeassistant/components/stream/* @hunterjm @uvjustin @allenporter homeassistant/components/stt/* @pvizeli homeassistant/components/subaru/* @G-Two diff --git a/homeassistant/components/stookalert/__init__.py b/homeassistant/components/stookalert/__init__.py index c9f86228515..8dfc208d945 100644 --- a/homeassistant/components/stookalert/__init__.py +++ b/homeassistant/components/stookalert/__init__.py @@ -1 +1,28 @@ -"""The Stookalert component.""" +"""The Stookalert integration.""" +from __future__ import annotations + +import stookalert + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import CONF_PROVINCE, DOMAIN + +PLATFORMS = (BINARY_SENSOR_DOMAIN,) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Stookalert from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = stookalert.stookalert(entry.data[CONF_PROVINCE]) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Stookalert config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + del hass.data[DOMAIN][entry.entry_id] + return unload_ok diff --git a/homeassistant/components/stookalert/binary_sensor.py b/homeassistant/components/stookalert/binary_sensor.py index 033af78560c..91634bfffa0 100644 --- a/homeassistant/components/stookalert/binary_sensor.py +++ b/homeassistant/components/stookalert/binary_sensor.py @@ -1,4 +1,6 @@ -"""This component provides support for Stookalert Binary Sensor.""" +"""This integration provides support for Stookalert Binary Sensor.""" +from __future__ import annotations + from datetime import timedelta import stookalert @@ -9,28 +11,32 @@ from homeassistant.components.binary_sensor import ( PLATFORM_SCHEMA, BinarySensorEntity, ) -from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + 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.typing import ConfigType, DiscoveryInfoType + +from .const import ( + ATTR_ENTRY_TYPE, + CONF_PROVINCE, + DOMAIN, + ENTRY_TYPE_SERVICE, + LOGGER, + PROVINCES, +) -SCAN_INTERVAL = timedelta(minutes=60) -CONF_PROVINCE = "province" -DEFAULT_DEVICE_CLASS = DEVICE_CLASS_SAFETY DEFAULT_NAME = "Stookalert" ATTRIBUTION = "Data provided by rivm.nl" -PROVINCES = [ - "Drenthe", - "Flevoland", - "Friesland", - "Gelderland", - "Groningen", - "Limburg", - "Noord-Brabant", - "Noord-Holland", - "Overijssel", - "Utrecht", - "Zeeland", - "Zuid-Holland", -] +SCAN_INTERVAL = timedelta(minutes=60) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -40,47 +46,60 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Stookalert binary sensor platform.""" - province = config[CONF_PROVINCE] - name = config[CONF_NAME] - api_handler = stookalert.stookalert(province) - add_entities([StookalertBinarySensor(name, api_handler)], update_before_add=True) +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Import the Stookalert platform into a config entry.""" + LOGGER.warning( + "Configuration of the Stookalert platform in YAML is deprecated and will be " + "removed in Home Assistant 2022.1; Your existing configuration " + "has been imported into the UI automatically and can be safely removed " + "from your configuration.yaml file" + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_PROVINCE: config[CONF_PROVINCE], + }, + ) + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Stookalert binary sensor from a config entry.""" + client = hass.data[DOMAIN][entry.entry_id] + async_add_entities([StookalertBinarySensor(client, entry)], update_before_add=True) class StookalertBinarySensor(BinarySensorEntity): - """An implementation of RIVM Stookalert.""" + """Defines a Stookalert binary sensor.""" - def __init__(self, name, api_handler): + _attr_device_class = DEVICE_CLASS_SAFETY + + def __init__(self, client: stookalert.stookalert, entry: ConfigEntry) -> None: """Initialize a Stookalert device.""" - self._name = name - self._api_handler = api_handler + self._client = client + self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + self._attr_name = f"Stookalert {entry.data[CONF_PROVINCE]}" + self._attr_unique_id = entry.unique_id + self._attr_device_info = { + ATTR_IDENTIFIERS: {(DOMAIN, f"{entry.entry_id}")}, + ATTR_NAME: entry.data[CONF_PROVINCE], + ATTR_MANUFACTURER: "RIVM", + ATTR_MODEL: "Stookalert", + ATTR_ENTRY_TYPE: ENTRY_TYPE_SERVICE, + } - @property - def extra_state_attributes(self): - """Return the attribute(s) of the sensor.""" - state_attr = {ATTR_ATTRIBUTION: ATTRIBUTION} - - if self._api_handler.last_updated is not None: - state_attr["last_updated"] = self._api_handler.last_updated.isoformat() - - return state_attr - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def is_on(self): - """Return True if the Alert is active.""" - return self._api_handler.state == 1 - - @property - def device_class(self): - """Return the device class of this binary sensor.""" - return DEFAULT_DEVICE_CLASS - - def update(self): + def update(self) -> None: """Update the data from the Stookalert handler.""" - self._api_handler.get_alerts() + self._client.get_alerts() + self._attr_is_on = self._client.state == 1 diff --git a/homeassistant/components/stookalert/config_flow.py b/homeassistant/components/stookalert/config_flow.py new file mode 100644 index 00000000000..4f625ec2d1a --- /dev/null +++ b/homeassistant/components/stookalert/config_flow.py @@ -0,0 +1,37 @@ +"""Config flow to configure the Stookalert integration.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow +from homeassistant.data_entry_flow import FlowResult + +from .const import CONF_PROVINCE, DOMAIN, PROVINCES + + +class StookalertFlowHandler(ConfigFlow, domain=DOMAIN): + """Config flow for Stookalert.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + if user_input is not None: + await self.async_set_unique_id(user_input[CONF_PROVINCE]) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=user_input[CONF_PROVINCE], data=user_input + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_PROVINCE): vol.In(PROVINCES)}), + ) + + async def async_step_import(self, config: dict[str, Any]) -> FlowResult: + """Handle a flow initialized by importing a config.""" + return await self.async_step_user(config) diff --git a/homeassistant/components/stookalert/const.py b/homeassistant/components/stookalert/const.py new file mode 100644 index 00000000000..bbd5922b82a --- /dev/null +++ b/homeassistant/components/stookalert/const.py @@ -0,0 +1,26 @@ +"""Constants for the Stookalert integration.""" +import logging +from typing import Final + +DOMAIN: Final = "stookalert" +LOGGER = logging.getLogger(__package__) + +CONF_PROVINCE: Final = "province" + +PROVINCES: Final = ( + "Drenthe", + "Flevoland", + "Friesland", + "Gelderland", + "Groningen", + "Limburg", + "Noord-Brabant", + "Noord-Holland", + "Overijssel", + "Utrecht", + "Zeeland", + "Zuid-Holland", +) + +ATTR_ENTRY_TYPE: Final = "entry_type" +ENTRY_TYPE_SERVICE: Final = "service" diff --git a/homeassistant/components/stookalert/manifest.json b/homeassistant/components/stookalert/manifest.json index 094f4c45670..401ed5a27e5 100644 --- a/homeassistant/components/stookalert/manifest.json +++ b/homeassistant/components/stookalert/manifest.json @@ -1,8 +1,9 @@ { "domain": "stookalert", "name": "RIVM Stookalert", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/stookalert", - "codeowners": ["@fwestenberg"], + "codeowners": ["@fwestenberg", "@frenck"], "requirements": ["stookalert==0.1.4"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/stookalert/strings.json b/homeassistant/components/stookalert/strings.json new file mode 100644 index 00000000000..a05ae4e61e7 --- /dev/null +++ b/homeassistant/components/stookalert/strings.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "province": "Province" + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + } + } +} diff --git a/homeassistant/components/stookalert/translations/en.json b/homeassistant/components/stookalert/translations/en.json new file mode 100644 index 00000000000..3c3480b85ae --- /dev/null +++ b/homeassistant/components/stookalert/translations/en.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured" + }, + "step": { + "user": { + "data": { + "province": "Province" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 2a04ec39478..30617373dbf 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -271,6 +271,7 @@ FLOWS = [ "squeezebox", "srp_energy", "starline", + "stookalert", "subaru", "surepetcare", "switchbot", diff --git a/mypy.ini b/mypy.ini index 0d4ca87ac64..440f410d0ab 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1122,6 +1122,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.stookalert.*] +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.stream.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b9e8363683c..cadc30b0a18 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1284,6 +1284,9 @@ starline==0.1.5 # homeassistant.components.statsd statsd==3.2.1 +# homeassistant.components.stookalert +stookalert==0.1.4 + # homeassistant.components.huawei_lte # homeassistant.components.solaredge # homeassistant.components.thermoworks_smoke diff --git a/tests/components/stookalert/__init__.py b/tests/components/stookalert/__init__.py new file mode 100644 index 00000000000..3785c76639a --- /dev/null +++ b/tests/components/stookalert/__init__.py @@ -0,0 +1 @@ +"""Tests for the Stookalert integration.""" diff --git a/tests/components/stookalert/test_config_flow.py b/tests/components/stookalert/test_config_flow.py new file mode 100644 index 00000000000..ceee26fa8e2 --- /dev/null +++ b/tests/components/stookalert/test_config_flow.py @@ -0,0 +1,78 @@ +"""Tests for the Stookalert config flow.""" +from unittest.mock import patch + +from homeassistant.components.stookalert.const import CONF_PROVINCE, DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from tests.common import MockConfigEntry + + +async def test_full_user_flow(hass: HomeAssistant) -> None: + """Test the full user configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + with patch( + "homeassistant.components.stookalert.async_setup_entry", return_value=True + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PROVINCE: "Overijssel", + }, + ) + + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == "Overijssel" + assert result2.get("data") == { + CONF_PROVINCE: "Overijssel", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_already_configured(hass: HomeAssistant) -> None: + """Test we abort if the Stookalert province is already configured.""" + MockConfigEntry( + domain=DOMAIN, data={CONF_PROVINCE: "Overijssel"}, unique_id="Overijssel" + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert "flow_id" in result + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PROVINCE: "Overijssel", + }, + ) + + assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("reason") == "already_configured" + + +async def test_import_flow(hass: HomeAssistant) -> None: + """Test the import configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data={"province": "Overijssel"} + ) + + assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("title") == "Overijssel" + assert result.get("data") == { + CONF_PROVINCE: "Overijssel", + } From b0f24b65d64787dffe333c163e57642430248eb7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 8 Oct 2021 07:45:05 -0700 Subject: [PATCH 0183/1038] Drop more persistent notification patches (#57295) --- tests/components/climacell/conftest.py | 9 --------- tests/components/dlna_dmr/conftest.py | 9 --------- tests/components/google_travel_time/conftest.py | 9 --------- tests/components/vizio/conftest.py | 9 --------- tests/components/waze_travel_time/conftest.py | 9 --------- 5 files changed, 45 deletions(-) diff --git a/tests/components/climacell/conftest.py b/tests/components/climacell/conftest.py index d4c77c58879..88640c69c14 100644 --- a/tests/components/climacell/conftest.py +++ b/tests/components/climacell/conftest.py @@ -7,15 +7,6 @@ import pytest from tests.common import load_fixture -@pytest.fixture(name="skip_notifications", autouse=True) -def skip_notifications_fixture(): - """Skip notification calls.""" - with patch("homeassistant.components.persistent_notification.async_create"), patch( - "homeassistant.components.persistent_notification.async_dismiss" - ): - yield - - @pytest.fixture(name="climacell_config_flow_connect", autouse=True) def climacell_config_flow_connect(): """Mock valid climacell config flow setup.""" diff --git a/tests/components/dlna_dmr/conftest.py b/tests/components/dlna_dmr/conftest.py index f352349205e..71910ec1cd8 100644 --- a/tests/components/dlna_dmr/conftest.py +++ b/tests/components/dlna_dmr/conftest.py @@ -93,15 +93,6 @@ def dmr_device_mock(domain_data_mock: Mock) -> Iterable[Mock]: yield device -@pytest.fixture(name="skip_notifications", autouse=True) -def skip_notifications_fixture() -> Iterable[None]: - """Skip notification calls.""" - with patch("homeassistant.components.persistent_notification.async_create"), patch( - "homeassistant.components.persistent_notification.async_dismiss" - ): - yield - - @pytest.fixture(autouse=True) def ssdp_scanner_mock() -> Iterable[Mock]: """Mock the SSDP module.""" diff --git a/tests/components/google_travel_time/conftest.py b/tests/components/google_travel_time/conftest.py index 87d922ac22c..7f668383c4b 100644 --- a/tests/components/google_travel_time/conftest.py +++ b/tests/components/google_travel_time/conftest.py @@ -5,15 +5,6 @@ from googlemaps.exceptions import ApiError import pytest -@pytest.fixture(name="skip_notifications", autouse=True) -def skip_notifications_fixture(): - """Skip notification calls.""" - with patch("homeassistant.components.persistent_notification.async_create"), patch( - "homeassistant.components.persistent_notification.async_dismiss" - ): - yield - - @pytest.fixture(name="validate_config_entry") def validate_config_entry_fixture(): """Return valid config entry.""" diff --git a/tests/components/vizio/conftest.py b/tests/components/vizio/conftest.py index 2c32b07cc1a..ffe79cb4ecf 100644 --- a/tests/components/vizio/conftest.py +++ b/tests/components/vizio/conftest.py @@ -39,15 +39,6 @@ def get_mock_inputs(input_list): return [MockInput(input) for input in input_list] -@pytest.fixture(name="skip_notifications", autouse=True) -def skip_notifications_fixture(): - """Skip notification calls.""" - with patch("homeassistant.components.persistent_notification.async_create"), patch( - "homeassistant.components.persistent_notification.async_dismiss" - ): - yield - - @pytest.fixture(name="vizio_get_unique_id", autouse=True) def vizio_get_unique_id_fixture(): """Mock get vizio unique ID.""" diff --git a/tests/components/waze_travel_time/conftest.py b/tests/components/waze_travel_time/conftest.py index 6954522dc85..04675666356 100644 --- a/tests/components/waze_travel_time/conftest.py +++ b/tests/components/waze_travel_time/conftest.py @@ -12,15 +12,6 @@ def mock_wrc(): yield -@pytest.fixture(name="skip_notifications", autouse=True) -def skip_notifications_fixture(): - """Skip notification calls.""" - with patch("homeassistant.components.persistent_notification.async_create"), patch( - "homeassistant.components.persistent_notification.async_dismiss" - ): - yield - - @pytest.fixture(name="validate_config_entry") def validate_config_entry_fixture(): """Return valid config entry.""" From ba83433c6453430f9d056105585904688ddde5fc Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Fri, 8 Oct 2021 17:57:49 +0200 Subject: [PATCH 0184/1038] Fix multiple upnp/ssdp issues (#57314) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ssdp/test_init.py | 10 +++++----- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 7c47d9329bb..1a54bd6d4ec 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Renderer", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.22.5"], + "requirements": ["async-upnp-client==0.22.8"], "dependencies": ["ssdp"], "codeowners": ["@StevenLooman", "@chishm"], "iot_class": "local_push" diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 3e99a77e8bb..85e489a72dd 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,7 +2,7 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", - "requirements": ["async-upnp-client==0.22.5"], + "requirements": ["async-upnp-client==0.22.8"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 9a1875777a6..4029ff5c3bb 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -3,7 +3,7 @@ "name": "UPnP/IGD", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": ["async-upnp-client==0.22.5"], + "requirements": ["async-upnp-client==0.22.8"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman","@ehendrix23"], "ssdp": [ diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 1b8c959cb52..632fdf426f2 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,7 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.7.7", "async-upnp-client==0.22.5"], + "requirements": ["yeelight==0.7.7", "async-upnp-client==0.22.8"], "codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index dfab58665ec..47f12448430 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.4 aiohttp==3.7.4.post0 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.22.5 +async-upnp-client==0.22.8 async_timeout==3.0.1 attrs==21.2.0 awesomeversion==21.8.1 diff --git a/requirements_all.txt b/requirements_all.txt index d2e39a25663..e57ebbfd6be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -327,7 +327,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.22.5 +async-upnp-client==0.22.8 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cadc30b0a18..9fde6c54717 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -224,7 +224,7 @@ arcam-fmj==0.7.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.22.5 +async-upnp-client==0.22.8 # homeassistant.components.aurora auroranoaa==0.0.2 diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index f3ddab39c39..64edd9e8341 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -23,9 +23,9 @@ from tests.common import async_fire_time_changed def _ssdp_headers(headers): - return CaseInsensitiveDict( - headers, _timestamp=datetime(2021, 1, 1, 12, 00), _udn=udn_from_headers(headers) - ) + ssdp_headers = CaseInsensitiveDict(headers, _timestamp=datetime(2021, 1, 1, 12, 00)) + ssdp_headers["_udn"] = udn_from_headers(ssdp_headers) + return ssdp_headers async def init_ssdp_component(hass: homeassistant) -> SsdpListener: @@ -45,7 +45,7 @@ async def test_ssdp_flow_dispatched_on_st(mock_get_ssdp, hass, caplog, mock_flow mock_ssdp_search_response = _ssdp_headers( { "st": "mock-st", - "location": None, + "location": "http://1.1.1.1", "usn": "uuid:mock-udn::mock-st", "server": "mock-server", "ext": "", @@ -64,7 +64,7 @@ async def test_ssdp_flow_dispatched_on_st(mock_get_ssdp, hass, caplog, mock_flow } assert mock_flow_init.mock_calls[0][2]["data"] == { ssdp.ATTR_SSDP_ST: "mock-st", - ssdp.ATTR_SSDP_LOCATION: None, + ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1", ssdp.ATTR_SSDP_USN: "uuid:mock-udn::mock-st", ssdp.ATTR_SSDP_SERVER: "mock-server", ssdp.ATTR_SSDP_EXT: "", From e77fae56d9f78164b786bbb37bae132f44590a0f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 8 Oct 2021 05:58:18 -1000 Subject: [PATCH 0185/1038] Migrate tplink hosts that were previously imported from yaml (#57308) --- homeassistant/components/tplink/__init__.py | 2 ++ homeassistant/components/tplink/migration.py | 6 +++++- tests/components/tplink/test_migration.py | 22 ++++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 9f3658cf2cd..ff0526490f5 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -120,6 +120,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async_migrate_legacy_entries( hass, hosts_by_mac, config_entries_by_mac, legacy_entry ) + # Migrate the yaml entry that was previously imported + async_migrate_yaml_entries(hass, legacy_entry.data) if conf is not None: async_migrate_yaml_entries(hass, conf) diff --git a/homeassistant/components/tplink/migration.py b/homeassistant/components/tplink/migration.py index af81323d39f..9344dd1532f 100644 --- a/homeassistant/components/tplink/migration.py +++ b/homeassistant/components/tplink/migration.py @@ -2,6 +2,8 @@ from __future__ import annotations from datetime import datetime +from types import MappingProxyType +from typing import Any from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry @@ -65,7 +67,9 @@ def async_migrate_legacy_entries( @callback -def async_migrate_yaml_entries(hass: HomeAssistant, conf: ConfigType) -> None: +def async_migrate_yaml_entries( + hass: HomeAssistant, conf: ConfigType | MappingProxyType[str, Any] +) -> None: """Migrate yaml to config entries.""" for device_type in (CONF_LIGHT, CONF_SWITCH, CONF_STRIP, CONF_DIMMER): for device in conf.get(device_type, []): diff --git a/tests/components/tplink/test_migration.py b/tests/components/tplink/test_migration.py index 6cd82448ca2..a1cd581e211 100644 --- a/tests/components/tplink/test_migration.py +++ b/tests/components/tplink/test_migration.py @@ -239,3 +239,25 @@ async def test_migrate_from_yaml(hass: HomeAssistant): assert migrated_entry is not None assert migrated_entry.data[CONF_HOST] == IP_ADDRESS + + +async def test_migrate_from_legacy_entry(hass: HomeAssistant): + """Test migrate from legacy entry that was already imported from yaml.""" + data = { + CONF_DISCOVERY: False, + CONF_SWITCH: [{CONF_HOST: IP_ADDRESS}], + } + config_entry = MockConfigEntry(domain=DOMAIN, data=data, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + with _patch_discovery(), _patch_single_discovery(): + await setup.async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + migrated_entry = None + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.unique_id == MAC_ADDRESS: + migrated_entry = entry + break + + assert migrated_entry is not None + assert migrated_entry.data[CONF_HOST] == IP_ADDRESS From c04be4f5d0ed1740ef0c1cd8fad610d6d53ad868 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Fri, 8 Oct 2021 17:59:19 +0200 Subject: [PATCH 0186/1038] Upgrade aionanoleaf to 0.0.3 to fix deadlock (#57312) --- homeassistant/components/nanoleaf/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nanoleaf/manifest.json b/homeassistant/components/nanoleaf/manifest.json index c527127f6e8..b5e57ac842d 100644 --- a/homeassistant/components/nanoleaf/manifest.json +++ b/homeassistant/components/nanoleaf/manifest.json @@ -3,7 +3,7 @@ "name": "Nanoleaf", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/nanoleaf", - "requirements": ["aionanoleaf==0.0.2"], + "requirements": ["aionanoleaf==0.0.3"], "zeroconf": ["_nanoleafms._tcp.local.", "_nanoleafapi._tcp.local."], "homekit" : { "models": [ diff --git a/requirements_all.txt b/requirements_all.txt index e57ebbfd6be..45d33f81ad8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -216,7 +216,7 @@ aiomodernforms==0.1.8 aiomusiccast==0.9.2 # homeassistant.components.nanoleaf -aionanoleaf==0.0.2 +aionanoleaf==0.0.3 # homeassistant.components.keyboard_remote aionotify==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9fde6c54717..b494582ffc9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -146,7 +146,7 @@ aiomodernforms==0.1.8 aiomusiccast==0.9.2 # homeassistant.components.nanoleaf -aionanoleaf==0.0.2 +aionanoleaf==0.0.3 # homeassistant.components.notion aionotion==3.0.2 From 4dafd42154d76a2abd56087c9c36c9e723d66989 Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Fri, 8 Oct 2021 18:02:01 +0200 Subject: [PATCH 0187/1038] Fix Nanoleaf light turn_off transition (#57305) --- homeassistant/components/nanoleaf/light.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py index b1d206bd4dc..af9faf21b79 100644 --- a/homeassistant/components/nanoleaf/light.py +++ b/homeassistant/components/nanoleaf/light.py @@ -182,8 +182,8 @@ class NanoleafLight(LightEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" - transition = kwargs.get(ATTR_TRANSITION) - await self._nanoleaf.turn_off(transition) + transition: float | None = kwargs.get(ATTR_TRANSITION) + await self._nanoleaf.turn_off(None if transition is None else int(transition)) async def async_update(self) -> None: """Fetch new state data for this light.""" From 830e2bc47a4f68e0bb252e0a59570c5c5cb739ab Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 8 Oct 2021 18:03:21 +0200 Subject: [PATCH 0188/1038] Netgear fix port and device model beeing overwritten (#57277) Co-authored-by: Martin Hjelmare --- homeassistant/components/netgear/config_flow.py | 11 +++++++++-- homeassistant/components/netgear/router.py | 4 ++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/netgear/config_flow.py b/homeassistant/components/netgear/config_flow.py index 18813ac27cd..62985c7104c 100644 --- a/homeassistant/components/netgear/config_flow.py +++ b/homeassistant/components/netgear/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure the Netgear integration.""" +import logging from urllib.parse import urlparse from pynetgear import DEFAULT_HOST, DEFAULT_PORT, DEFAULT_USER @@ -20,6 +21,8 @@ from .const import CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME, DEFAULT_NAME, DOMA from .errors import CannotLoginException from .router import get_api +_LOGGER = logging.getLogger(__name__) + def _discovery_schema_with_defaults(discovery_info): return vol.Schema(_ordered_shared_schema(discovery_info)) @@ -120,15 +123,19 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): device_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) if device_url.hostname: updated_data[CONF_HOST] = device_url.hostname - if device_url.port: - updated_data[CONF_PORT] = device_url.port if device_url.scheme == "https": updated_data[CONF_SSL] = True else: updated_data[CONF_SSL] = False + _LOGGER.debug("Netgear ssdp discovery info: %s", discovery_info) + await self.async_set_unique_id(discovery_info[ssdp.ATTR_UPNP_SERIAL]) self._abort_if_unique_id_configured(updates=updated_data) + + if device_url.port: + updated_data[CONF_PORT] = device_url.port + self.placeholders.update(updated_data) self.discovered = True diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index 53cc4f32728..cc508f043ff 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -274,8 +274,8 @@ class NetgearDeviceEntity(Entity): """Return the device information.""" return { "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, - "name": self._device_name, - "model": self._device["device_model"], + "default_name": self._device_name, + "default_model": self._device["device_model"], "via_device": (DOMAIN, self._router.unique_id), } From 7f966613bdf372706d40c51565b89bc5820b6f7e Mon Sep 17 00:00:00 2001 From: David Boslee Date: Fri, 8 Oct 2021 10:38:22 -0600 Subject: [PATCH 0189/1038] Disconnect websockets after token is revoked (#57091) Co-authored-by: Paulus Schoutsen --- homeassistant/auth/__init__.py | 25 ++++++++++++- .../components/websocket_api/auth.py | 13 +++++-- .../components/websocket_api/http.py | 4 ++- tests/auth/test_init.py | 36 +++++++++++++++++++ tests/components/auth/test_init.py | 10 ++++-- tests/components/websocket_api/test_auth.py | 13 +++++++ 6 files changed, 94 insertions(+), 7 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index c528aff221f..f47228ee506 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -9,7 +9,7 @@ from typing import Any, Dict, Mapping, Optional, Tuple, cast import jwt from homeassistant import data_entry_flow -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.util import dt as dt_util @@ -155,6 +155,7 @@ class AuthManager: self._providers = providers self._mfa_modules = mfa_modules self.login_flow = AuthManagerFlowManager(hass, self) + self._revoke_callbacks: dict[str, list[CALLBACK_TYPE]] = {} @property def auth_providers(self) -> list[AuthProvider]: @@ -446,6 +447,28 @@ class AuthManager: """Delete a refresh token.""" await self._store.async_remove_refresh_token(refresh_token) + callbacks = self._revoke_callbacks.pop(refresh_token.id, []) + for revoke_callback in callbacks: + revoke_callback() + + @callback + def async_register_revoke_token_callback( + self, refresh_token_id: str, revoke_callback: CALLBACK_TYPE + ) -> CALLBACK_TYPE: + """Register a callback to be called when the refresh token id is revoked.""" + if refresh_token_id not in self._revoke_callbacks: + self._revoke_callbacks[refresh_token_id] = [] + + callbacks = self._revoke_callbacks[refresh_token_id] + callbacks.append(revoke_callback) + + @callback + def unregister() -> None: + if revoke_callback in callbacks: + callbacks.remove(revoke_callback) + + return unregister + @callback def async_create_access_token( self, refresh_token: models.RefreshToken, remote_ip: str | None = None diff --git a/homeassistant/components/websocket_api/auth.py b/homeassistant/components/websocket_api/auth.py index 130ffe82840..794dae77153 100644 --- a/homeassistant/components/websocket_api/auth.py +++ b/homeassistant/components/websocket_api/auth.py @@ -11,7 +11,7 @@ from voluptuous.humanize import humanize_error from homeassistant.auth.models import RefreshToken, User from homeassistant.components.http.ban import process_success_login, process_wrong_login from homeassistant.const import __version__ -from homeassistant.core import HomeAssistant +from homeassistant.core import CALLBACK_TYPE, HomeAssistant from .connection import ActiveConnection from .error import Disconnect @@ -57,11 +57,13 @@ class AuthPhase: logger: WebSocketAdapter, hass: HomeAssistant, send_message: Callable[[str | dict[str, Any]], None], + cancel_ws: CALLBACK_TYPE, request: Request, ) -> None: """Initialize the authentiated connection.""" self._hass = hass self._send_message = send_message + self._cancel_ws = cancel_ws self._logger = logger self._request = request @@ -83,7 +85,14 @@ class AuthPhase: msg["access_token"] ) if refresh_token is not None: - return await self._async_finish_auth(refresh_token.user, refresh_token) + conn = await self._async_finish_auth(refresh_token.user, refresh_token) + conn.subscriptions[ + "auth" + ] = self._hass.auth.async_register_revoke_token_callback( + refresh_token.id, self._cancel_ws + ) + + return conn self._send_message(auth_invalid_message("Invalid access token or password")) await process_wrong_login(self._request) diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index aa6a74b27ec..bce6713403a 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -161,7 +161,9 @@ class WebSocketHandler: # event we do not want to block for websocket responses self._writer_task = asyncio.create_task(self._writer()) - auth = AuthPhase(self._logger, self.hass, self._send_message, request) + auth = AuthPhase( + self._logger, self.hass, self._send_message, self._cancel, request + ) connection = None disconnect_warn = None diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index 4c3d93ede15..fa8c86536ca 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -529,6 +529,42 @@ async def test_remove_refresh_token(mock_hass): assert await manager.async_validate_access_token(access_token) is None +async def test_register_revoke_token_callback(mock_hass): + """Test that a registered revoke token callback is called.""" + manager = await auth.auth_manager_from_config(mock_hass, [], []) + user = MockUser().add_to_auth_manager(manager) + refresh_token = await manager.async_create_refresh_token(user, CLIENT_ID) + + called = False + + def cb(): + nonlocal called + called = True + + manager.async_register_revoke_token_callback(refresh_token.id, cb) + await manager.async_remove_refresh_token(refresh_token) + assert called + + +async def test_unregister_revoke_token_callback(mock_hass): + """Test that a revoke token callback can be unregistered.""" + manager = await auth.auth_manager_from_config(mock_hass, [], []) + user = MockUser().add_to_auth_manager(manager) + refresh_token = await manager.async_create_refresh_token(user, CLIENT_ID) + + called = False + + def cb(): + nonlocal called + called = True + + unregister = manager.async_register_revoke_token_callback(refresh_token.id, cb) + unregister() + + await manager.async_remove_refresh_token(refresh_token) + assert not called + + async def test_create_access_token(mock_hass): """Test normal refresh_token's jwt_key keep same after used.""" manager = await auth.auth_manager_from_config(mock_hass, [], []) diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index 207667fc26d..b615ba4156c 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -363,11 +363,15 @@ async def test_ws_refresh_tokens(hass, hass_ws_client, hass_access_token): assert token["last_used_ip"] == refresh_token.last_used_ip -async def test_ws_delete_refresh_token(hass, hass_ws_client, hass_access_token): +async def test_ws_delete_refresh_token( + hass, hass_admin_user, hass_admin_credential, hass_ws_client, hass_access_token +): """Test deleting a refresh token.""" assert await async_setup_component(hass, "auth", {"http": {}}) - refresh_token = await hass.auth.async_validate_access_token(hass_access_token) + refresh_token = await hass.auth.async_create_refresh_token( + hass_admin_user, CLIENT_ID, credential=hass_admin_credential + ) ws_client = await hass_ws_client(hass, hass_access_token) @@ -382,7 +386,7 @@ async def test_ws_delete_refresh_token(hass, hass_ws_client, hass_access_token): result = await ws_client.receive_json() assert result["success"], result - refresh_token = await hass.auth.async_validate_access_token(hass_access_token) + refresh_token = await hass.auth.async_get_refresh_token(refresh_token.id) assert refresh_token is None diff --git a/tests/components/websocket_api/test_auth.py b/tests/components/websocket_api/test_auth.py index a57faf4a895..7834474470c 100644 --- a/tests/components/websocket_api/test_auth.py +++ b/tests/components/websocket_api/test_auth.py @@ -1,6 +1,7 @@ """Test auth of websocket API.""" from unittest.mock import patch +import aiohttp import pytest from homeassistant.components.websocket_api.auth import ( @@ -191,3 +192,15 @@ async def test_auth_with_invalid_token(hass, hass_client_no_auth): auth_msg = await ws.receive_json() assert auth_msg["type"] == TYPE_AUTH_INVALID + + +async def test_auth_close_after_revoke(hass, websocket_client, hass_access_token): + """Test that a websocket is closed after the refresh token is revoked.""" + assert not websocket_client.closed + + refresh_token = await hass.auth.async_validate_access_token(hass_access_token) + await hass.auth.async_remove_refresh_token(refresh_token) + + msg = await websocket_client.receive() + assert msg.type == aiohttp.WSMsgType.CLOSE + assert websocket_client.closed From 7e348606159c7a3815a7a64ddd070615ca40a061 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 8 Oct 2021 18:52:58 +0200 Subject: [PATCH 0190/1038] Improve state of cover groups (#57313) Co-authored-by: Paulus Schoutsen --- homeassistant/components/group/cover.py | 10 ++- tests/components/group/test_cover.py | 105 +++++++++++++++++++++++- 2 files changed, 111 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index 45d88a07f88..68ad61c33fc 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -259,25 +259,31 @@ class CoverGroup(GroupEntity, CoverEntity): """Update state and attributes.""" self._attr_assumed_state = False - self._attr_is_closed = None + self._attr_is_closed = True self._attr_is_closing = False self._attr_is_opening = False + has_valid_state = False for entity_id in self._entities: state = self.hass.states.get(entity_id) if not state: continue if state.state == STATE_OPEN: self._attr_is_closed = False + has_valid_state = True continue if state.state == STATE_CLOSED: - self._attr_is_closed = True + has_valid_state = True continue if state.state == STATE_CLOSING: self._attr_is_closing = True + has_valid_state = True continue if state.state == STATE_OPENING: self._attr_is_opening = True + has_valid_state = True continue + if not has_valid_state: + self._attr_is_closed = None position_covers = self._covers[KEY_POSITION] all_position_states = [self.hass.states.get(x) for x in position_covers] diff --git a/tests/components/group/test_cover.py b/tests/components/group/test_cover.py index 9d16be9150b..cf1fba992e7 100644 --- a/tests/components/group/test_cover.py +++ b/tests/components/group/test_cover.py @@ -96,6 +96,106 @@ async def setup_comp(hass, config_count): await hass.async_block_till_done() +@pytest.mark.parametrize("config_count", [(CONFIG_ATTRIBUTES, 1)]) +async def test_state(hass, setup_comp): + """Test handling of state.""" + state = hass.states.get(COVER_GROUP) + # No entity has a valid state -> group state unknown + assert state.state == STATE_UNKNOWN + assert state.attributes[ATTR_FRIENDLY_NAME] == DEFAULT_NAME + assert state.attributes[ATTR_ENTITY_ID] == [ + DEMO_COVER, + DEMO_COVER_POS, + DEMO_COVER_TILT, + DEMO_TILT, + ] + assert ATTR_ASSUMED_STATE not in state.attributes + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + assert ATTR_CURRENT_POSITION not in state.attributes + assert ATTR_CURRENT_TILT_POSITION not in state.attributes + + # Set all entities as closed -> group state closed + hass.states.async_set(DEMO_COVER, STATE_CLOSED, {}) + hass.states.async_set(DEMO_COVER_POS, STATE_CLOSED, {}) + hass.states.async_set(DEMO_COVER_TILT, STATE_CLOSED, {}) + hass.states.async_set(DEMO_TILT, STATE_CLOSED, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_CLOSED + + # Set all entities as open -> group state open + hass.states.async_set(DEMO_COVER, STATE_OPEN, {}) + hass.states.async_set(DEMO_COVER_POS, STATE_OPEN, {}) + hass.states.async_set(DEMO_COVER_TILT, STATE_OPEN, {}) + hass.states.async_set(DEMO_TILT, STATE_OPEN, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + + # Set first entity as open -> group state open + hass.states.async_set(DEMO_COVER, STATE_OPEN, {}) + hass.states.async_set(DEMO_COVER_POS, STATE_CLOSED, {}) + hass.states.async_set(DEMO_COVER_TILT, STATE_CLOSED, {}) + hass.states.async_set(DEMO_TILT, STATE_CLOSED, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + + # Set last entity as open -> group state open + hass.states.async_set(DEMO_COVER, STATE_OPEN, {}) + hass.states.async_set(DEMO_COVER_POS, STATE_CLOSED, {}) + hass.states.async_set(DEMO_COVER_TILT, STATE_CLOSED, {}) + hass.states.async_set(DEMO_TILT, STATE_CLOSED, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + + # Set conflicting valid states -> opening state has priority + hass.states.async_set(DEMO_COVER, STATE_OPEN, {}) + hass.states.async_set(DEMO_COVER_POS, STATE_OPENING, {}) + hass.states.async_set(DEMO_COVER_TILT, STATE_CLOSING, {}) + hass.states.async_set(DEMO_TILT, STATE_CLOSED, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPENING + + # Set all entities to unknown state -> group state unknown + hass.states.async_set(DEMO_COVER, STATE_UNKNOWN, {}) + hass.states.async_set(DEMO_COVER_POS, STATE_UNKNOWN, {}) + hass.states.async_set(DEMO_COVER_TILT, STATE_UNKNOWN, {}) + hass.states.async_set(DEMO_TILT, STATE_UNKNOWN, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_UNKNOWN + + # Set one entity to unknown state -> open state has priority + hass.states.async_set(DEMO_COVER, STATE_OPEN, {}) + hass.states.async_set(DEMO_COVER_POS, STATE_UNKNOWN, {}) + hass.states.async_set(DEMO_COVER_TILT, STATE_CLOSED, {}) + hass.states.async_set(DEMO_TILT, STATE_OPEN, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + + # Set one entity to unknown state -> opening state has priority + hass.states.async_set(DEMO_COVER, STATE_OPEN, {}) + hass.states.async_set(DEMO_COVER_POS, STATE_OPENING, {}) + hass.states.async_set(DEMO_COVER_TILT, STATE_UNKNOWN, {}) + hass.states.async_set(DEMO_TILT, STATE_CLOSED, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPENING + + # Set one entity to unknown state -> closing state has priority + hass.states.async_set(DEMO_COVER, STATE_OPEN, {}) + hass.states.async_set(DEMO_COVER_POS, STATE_UNKNOWN, {}) + hass.states.async_set(DEMO_COVER_TILT, STATE_CLOSING, {}) + hass.states.async_set(DEMO_TILT, STATE_CLOSED, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_CLOSING + + @pytest.mark.parametrize("config_count", [(CONFIG_ATTRIBUTES, 1)]) async def test_attributes(hass, setup_comp): """Test handling of state attributes.""" @@ -196,7 +296,7 @@ async def test_attributes(hass, setup_comp): # ### Test assumed state ### # ########################## - # For covers + # For covers - assumed state set true if position differ hass.states.async_set( DEMO_COVER, STATE_OPEN, {ATTR_SUPPORTED_FEATURES: 4, ATTR_CURRENT_POSITION: 100} ) @@ -220,7 +320,7 @@ async def test_attributes(hass, setup_comp): assert ATTR_CURRENT_POSITION not in state.attributes assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 60 - # For tilts + # For tilts - assumed state set true if tilt position differ hass.states.async_set( DEMO_TILT, STATE_OPEN, @@ -252,6 +352,7 @@ async def test_attributes(hass, setup_comp): state = hass.states.get(COVER_GROUP) assert state.attributes[ATTR_ASSUMED_STATE] is True + # Test entity registry integration entity_registry = er.async_get(hass) entry = entity_registry.async_get(COVER_GROUP) assert entry From eba1d7d16a1e5e7495fc9abe75a320c44dd707a7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 8 Oct 2021 10:48:52 -0700 Subject: [PATCH 0191/1038] Guard for bad last reset (#57344) --- homeassistant/components/sensor/recorder.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index c485622af80..30b5a4605ef 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -7,6 +7,7 @@ import datetime import itertools import logging import math +from typing import Any from sqlalchemy.orm.session import Session @@ -362,13 +363,14 @@ def _wanted_statistics(sensor_states: list[State]) -> dict[str, set[str]]: return wanted_statistics -def _last_reset_as_utc_isoformat( - last_reset_s: str | None, entity_id: str -) -> str | None: +def _last_reset_as_utc_isoformat(last_reset_s: Any, entity_id: str) -> str | None: """Parse last_reset and convert it to UTC.""" if last_reset_s is None: return None - last_reset = dt_util.parse_datetime(last_reset_s) + if isinstance(last_reset_s, str): + last_reset = dt_util.parse_datetime(last_reset_s) + else: + last_reset = None if last_reset is None: _LOGGER.warning( "Ignoring invalid last reset '%s' for %s", last_reset_s, entity_id From 03644055957219c01608f6c89af87980694bed9e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 8 Oct 2021 12:03:47 -0600 Subject: [PATCH 0192/1038] Reorganize RainMachine services (#57145) * Reorganize RainMachine services * Code review * Ensure integration services aren't tied to a particular config entry * Cleanup * linting * Code review * Code review * Code review * Code review --- .../components/rainmachine/__init__.py | 81 +++++++++- .../components/rainmachine/services.yaml | 152 ++++++------------ .../components/rainmachine/switch.py | 146 ++++++++--------- 3 files changed, 191 insertions(+), 188 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index d5eceab05fc..0d25b6c41c7 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -9,16 +9,18 @@ from typing import Any from regenmaschine import Client from regenmaschine.controller import Controller from regenmaschine.errors import RainMachineError +import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, + CONF_DEVICE_ID, CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv import homeassistant.helpers.device_registry as dr @@ -44,6 +46,8 @@ from .const import ( LOGGER, ) +CONF_SECONDS = "seconds" + DEFAULT_ATTRIBUTION = "Data provided by Green Electronics LLC" DEFAULT_ICON = "mdi:water" DEFAULT_SSL = True @@ -53,6 +57,39 @@ CONFIG_SCHEMA = cv.deprecated(DOMAIN) PLATFORMS = ["binary_sensor", "sensor", "switch"] +SERVICE_NAME_PAUSE_WATERING = "pause_watering" +SERVICE_NAME_STOP_ALL = "stop_all" +SERVICE_NAME_UNPAUSE_WATERING = "unpause_watering" + +SERVICE_SCHEMA = vol.Schema( + { + vol.Required(CONF_DEVICE_ID): cv.string, + } +) + +SERVICE_PAUSE_WATERING_SCHEMA = SERVICE_SCHEMA.extend( + { + vol.Required(CONF_SECONDS): cv.positive_int, + } +) + + +@callback +def async_get_controller_for_service_call( + hass: HomeAssistant, call: ServiceCall +) -> Controller: + """Get the controller related to a service call (by device ID).""" + controllers: dict[str, Controller] = hass.data[DOMAIN][DATA_CONTROLLER] + device_id = call.data[CONF_DEVICE_ID] + device_registry = dr.async_get(hass) + + if device_entry := device_registry.async_get(device_id): + for entry_id in device_entry.config_entries: + if entry_id in controllers: + return controllers[entry_id] + + raise ValueError(f"No controller for device ID: {device_id}") + async def async_update_programs_and_zones( hass: HomeAssistant, entry: ConfigEntry @@ -158,6 +195,37 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(entry.add_update_listener(async_reload_entry)) + async def async_pause_watering(call: ServiceCall) -> None: + """Pause watering for a set number of seconds.""" + controller = async_get_controller_for_service_call(hass, call) + await controller.watering.pause_all(call.data[CONF_SECONDS]) + await async_update_programs_and_zones(hass, entry) + + async def async_stop_all(call: ServiceCall) -> None: + """Stop all watering.""" + controller = async_get_controller_for_service_call(hass, call) + await controller.watering.stop_all() + await async_update_programs_and_zones(hass, entry) + + async def async_unpause_watering(call: ServiceCall) -> None: + """Unpause watering.""" + controller = async_get_controller_for_service_call(hass, call) + await controller.watering.unpause_all() + await async_update_programs_and_zones(hass, entry) + + for service_name, schema, method in ( + ( + SERVICE_NAME_PAUSE_WATERING, + SERVICE_PAUSE_WATERING_SCHEMA, + async_pause_watering, + ), + (SERVICE_NAME_STOP_ALL, SERVICE_SCHEMA, async_stop_all), + (SERVICE_NAME_UNPAUSE_WATERING, SERVICE_SCHEMA, async_unpause_watering), + ): + if hass.services.has_service(DOMAIN, service_name): + continue + hass.services.async_register(DOMAIN, service_name, method, schema=schema) + return True @@ -166,6 +234,17 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) + + if len(hass.config_entries.async_entries(DOMAIN)) == 1: + # If this is the last instance of RainMachine, deregister any services defined + # during integration setup: + for service_name in ( + SERVICE_NAME_PAUSE_WATERING, + SERVICE_NAME_STOP_ALL, + SERVICE_NAME_UNPAUSE_WATERING, + ): + hass.services.async_remove(DOMAIN, service_name) + return unload_ok diff --git a/homeassistant/components/rainmachine/services.yaml b/homeassistant/components/rainmachine/services.yaml index c12e0938059..fc6a71f8842 100644 --- a/homeassistant/components/rainmachine/services.yaml +++ b/homeassistant/components/rainmachine/services.yaml @@ -1,79 +1,46 @@ # Describes the format for available RainMachine services disable_program: - name: Disable program - description: Disable a program. + name: Disable Program + description: Disable a program target: entity: integration: rainmachine domain: switch - fields: - program_id: - name: Program ID - description: The program to disable. - required: true - selector: - number: - min: 1 - max: 255 disable_zone: - name: Disable zone - description: Disable a zone. + name: Disable Zone + description: Disable a zone target: entity: integration: rainmachine domain: switch - fields: - zone_id: - name: Zone ID - description: The zone to disable. - required: true - selector: - number: - min: 1 - max: 255 enable_program: - name: Enable program - description: Enable a program. + name: Enable Program + description: Enable a program target: entity: integration: rainmachine domain: switch - fields: - program_id: - name: Program ID - description: The program to enable. - required: true - selector: - number: - min: 1 - max: 255 enable_zone: - name: Enable zone - description: Enable a zone. + name: Enable Zone + description: Enable a zone target: entity: integration: rainmachine domain: switch +pause_watering: + name: Pause All Watering + description: Pause all watering activities for a number of seconds fields: - zone_id: - name: Zone ID - description: The zone to enable. + device_id: + name: Controller + description: The controller whose watering activities should be paused required: true selector: - number: - min: 1 - max: 255 -pause_watering: - name: Pause watering - description: Pause all watering for a number of seconds. - target: - entity: - integration: rainmachine - domain: switch - fields: + device: + integration: rainmachine seconds: - name: Seconds - description: The time to pause. + name: Duration + description: The amount of time (in seconds) to pause watering required: true selector: number: @@ -81,40 +48,23 @@ pause_watering: max: 86400 unit_of_measurement: seconds start_program: - name: Start program - description: Start a program. + name: Start Program + description: Start a program target: entity: integration: rainmachine domain: switch - fields: - program_id: - name: Program ID - description: The program to start. - required: true - selector: - number: - min: 1 - max: 255 start_zone: - name: Start zone - description: Start a zone for a set number of seconds. + name: Start Zone + description: Start a zone target: entity: integration: rainmachine domain: switch fields: - zone_id: - name: Zone ID - description: The zone to start. - required: true - selector: - number: - min: 1 - max: 255 zone_run_time: - name: Zone run time - description: The number of seconds to run the zone. + name: Run Time + description: The amount of time (in seconds) to run the zone default: 600 selector: number: @@ -122,48 +72,38 @@ start_zone: max: 86400 mode: box stop_all: - name: Stop all - description: Stop all watering activities. - target: - entity: - integration: rainmachine - domain: switch + name: Stop All Watering + description: Stop all watering activities + fields: + device_id: + name: Controller + description: The controller whose watering activities should be stopped + required: true + selector: + device: + integration: rainmachine stop_program: - name: Stop program - description: Stop a program. + name: Stop Program + description: Stop a program target: entity: integration: rainmachine domain: switch - fields: - program_id: - name: Program ID - description: The program to stop. - required: true - selector: - number: - min: 1 - max: 255 stop_zone: - name: Stop zone - description: Stop a zone. + name: Stop Zone + description: Stop a zone target: entity: integration: rainmachine domain: switch +unpause_watering: + name: Unpause All Watering + description: Unpause all paused watering activities fields: - zone_id: - name: Zone ID - description: The zone to stop. + device_id: + name: Controller + description: The controller whose watering activities should be unpaused required: true selector: - number: - min: 1 - max: 255 -unpause_watering: - name: Unpause watering - description: Unpause all watering. - target: - entity: - integration: rainmachine - domain: switch + device: + integration: rainmachine diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index a4d4bce2383..bf84bc1f360 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -51,10 +51,6 @@ ATTR_TIME_REMAINING = "time_remaining" ATTR_VEGETATION_TYPE = "vegetation_type" ATTR_ZONES = "zones" -CONF_PROGRAM_ID = "program_id" -CONF_SECONDS = "seconds" -CONF_ZONE_ID = "zone_id" - DEFAULT_ICON = "mdi:water" DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] @@ -112,9 +108,6 @@ VEGETATION_MAP = { 99: "Other", } -SWITCH_TYPE_PROGRAM = "program" -SWITCH_TYPE_ZONE = "zone" - @dataclass class RainMachineSwitchDescriptionMixin: @@ -136,42 +129,23 @@ async def async_setup_entry( """Set up RainMachine switches based on a config entry.""" platform = entity_platform.async_get_current_platform() - alter_program_schema = {vol.Required(CONF_PROGRAM_ID): cv.positive_int} - alter_zone_schema = {vol.Required(CONF_ZONE_ID): cv.positive_int} - for service_name, schema, method in ( - ("disable_program", alter_program_schema, "async_disable_program"), - ("disable_zone", alter_zone_schema, "async_disable_zone"), - ("enable_program", alter_program_schema, "async_enable_program"), - ("enable_zone", alter_zone_schema, "async_enable_zone"), - ( - "pause_watering", - {vol.Required(CONF_SECONDS): cv.positive_int}, - "async_pause_watering", - ), - ( - "start_program", - {vol.Required(CONF_PROGRAM_ID): cv.positive_int}, - "async_start_program", - ), + ("disable_program", {}, "async_disable_program"), + ("disable_zone", {}, "async_disable_zone"), + ("enable_program", {}, "async_enable_program"), + ("enable_zone", {}, "async_enable_zone"), + ("start_program", {}, "async_start_program"), ( "start_zone", { - vol.Required(CONF_ZONE_ID): cv.positive_int, vol.Optional( CONF_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN - ): cv.positive_int, + ): cv.positive_int }, "async_start_zone", ), - ("stop_all", {}, "async_stop_all"), - ( - "stop_program", - {vol.Required(CONF_PROGRAM_ID): cv.positive_int}, - "async_stop_program", - ), - ("stop_zone", {vol.Required(CONF_ZONE_ID): cv.positive_int}, "async_stop_zone"), - ("unpause_watering", {}, "async_unpause_watering"), + ("stop_program", {}, "async_stop_program"), + ("stop_zone", {}, "async_stop_zone"), ): platform.async_register_entity_service(service_name, schema, method) @@ -187,9 +161,7 @@ async def async_setup_entry( controller, entry, RainMachineSwitchDescription( - key=f"RainMachineProgram_{uid}", - name=program["name"], - uid=uid, + key=f"RainMachineProgram_{uid}", name=program["name"], uid=uid ), ) for uid, program in programs_coordinator.data.items() @@ -201,9 +173,7 @@ async def async_setup_entry( controller, entry, RainMachineSwitchDescription( - key=f"RainMachineZone_{uid}", - name=zone["name"], - uid=uid, + key=f"RainMachineZone_{uid}", name=zone["name"], uid=uid ), ) for uid, zone in zones_coordinator.data.items() @@ -267,60 +237,37 @@ class RainMachineSwitch(RainMachineEntity, SwitchEntity): async_update_programs_and_zones(self.hass, self._entry) ) - async def async_disable_program(self, *, program_id: int) -> None: + async def async_disable_program(self) -> None: """Disable a program.""" - await self._controller.programs.disable(program_id) - await async_update_programs_and_zones(self.hass, self._entry) + raise NotImplementedError("Service not implemented for this entity") - async def async_disable_zone(self, *, zone_id: int) -> None: + async def async_disable_zone(self) -> None: """Disable a zone.""" - await self._controller.zones.disable(zone_id) - await async_update_programs_and_zones(self.hass, self._entry) + raise NotImplementedError("Service not implemented for this entity") - async def async_enable_program(self, *, program_id: int) -> None: + async def async_enable_program(self) -> None: """Enable a program.""" - await self._controller.programs.enable(program_id) - await async_update_programs_and_zones(self.hass, self._entry) + raise NotImplementedError("Service not implemented for this entity") - async def async_enable_zone(self, *, zone_id: int) -> None: + async def async_enable_zone(self) -> None: """Enable a zone.""" - await self._controller.zones.enable(zone_id) - await async_update_programs_and_zones(self.hass, self._entry) + raise NotImplementedError("Service not implemented for this entity") - async def async_pause_watering(self, *, seconds: int) -> None: - """Pause watering for a set number of seconds.""" - await self._controller.watering.pause_all(seconds) - await async_update_programs_and_zones(self.hass, self._entry) + async def async_start_program(self) -> None: + """Start a program.""" + raise NotImplementedError("Service not implemented for this entity") - async def async_start_program(self, *, program_id: int) -> None: - """Start a particular program.""" - await self._controller.programs.start(program_id) - await async_update_programs_and_zones(self.hass, self._entry) + async def async_start_zone(self, *, zone_run_time: int) -> None: + """Start a zone.""" + raise NotImplementedError("Service not implemented for this entity") - async def async_start_zone(self, *, zone_id: int, zone_run_time: int) -> None: - """Start a particular zone for a certain amount of time.""" - await self._controller.zones.start(zone_id, zone_run_time) - await async_update_programs_and_zones(self.hass, self._entry) - - async def async_stop_all(self) -> None: - """Stop all watering.""" - await self._controller.watering.stop_all() - await async_update_programs_and_zones(self.hass, self._entry) - - async def async_stop_program(self, *, program_id: int) -> None: + async def async_stop_program(self) -> None: """Stop a program.""" - await self._controller.programs.stop(program_id) - await async_update_programs_and_zones(self.hass, self._entry) + raise NotImplementedError("Service not implemented for this entity") - async def async_stop_zone(self, *, zone_id: int) -> None: + async def async_stop_zone(self) -> None: """Stop a zone.""" - await self._controller.zones.stop(zone_id) - await async_update_programs_and_zones(self.hass, self._entry) - - async def async_unpause_watering(self) -> None: - """Unpause watering.""" - await self._controller.watering.unpause_all() - await async_update_programs_and_zones(self.hass, self._entry) + raise NotImplementedError("Service not implemented for this entity") @callback def update_from_latest_data(self) -> None: @@ -337,6 +284,24 @@ class RainMachineProgram(RainMachineSwitch): """Return a list of active zones associated with this program.""" return [z for z in self._data["wateringTimes"] if z["active"]] + async def async_disable_program(self) -> None: + """Disable a program.""" + await self._controller.programs.disable(self.entity_description.uid) + await async_update_programs_and_zones(self.hass, self._entry) + + async def async_enable_program(self) -> None: + """Enable a program.""" + await self._controller.programs.enable(self.entity_description.uid) + await async_update_programs_and_zones(self.hass, self._entry) + + async def async_start_program(self) -> None: + """Start a program.""" + await self.async_turn_on() + + async def async_stop_program(self) -> None: + """Stop a program.""" + await self.async_turn_off() + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the program off.""" await self._async_run_switch_coroutine( @@ -381,6 +346,25 @@ class RainMachineProgram(RainMachineSwitch): class RainMachineZone(RainMachineSwitch): """A RainMachine zone.""" + async def async_disable_zone(self) -> None: + """Disable a zone.""" + await self._controller.zones.disable(self.entity_description.uid) + await async_update_programs_and_zones(self.hass, self._entry) + + async def async_enable_zone(self) -> None: + """Enable a zone.""" + await self._controller.zones.enable(self.entity_description.uid) + await async_update_programs_and_zones(self.hass, self._entry) + + async def async_start_zone(self, *, zone_run_time: int) -> None: + """Start a particular zone for a certain amount of time.""" + await self._controller.zones.start(self.entity_description.uid, zone_run_time) + await async_update_programs_and_zones(self.hass, self._entry) + + async def async_stop_zone(self) -> None: + """Stop a zone.""" + await self.async_turn_off() + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the zone off.""" await self._async_run_switch_coroutine( From fb063928ce477fa08e3ad4cdd769e799e1ef0dc0 Mon Sep 17 00:00:00 2001 From: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> Date: Fri, 8 Oct 2021 15:15:45 -0400 Subject: [PATCH 0193/1038] Add device info to nws (#57153) * Add base entity * Use function for device_info Multiple inheritance makes this tricky with a base class * Device info in sensor * Device info weather * parantheses * isort --- homeassistant/components/nws/__init__.py | 10 ++++++++++ homeassistant/components/nws/sensor.py | 8 +++++++- homeassistant/components/nws/weather.py | 8 +++++++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py index b78961911d5..0f0886c5f41 100644 --- a/homeassistant/components/nws/__init__.py +++ b/homeassistant/components/nws/__init__.py @@ -166,3 +166,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if len(hass.data[DOMAIN]) == 0: hass.data.pop(DOMAIN) return unload_ok + + +def device_info(latitude, longitude): + """Return device registry information.""" + return { + "identifiers": {(DOMAIN, base_unique_id(latitude, longitude))}, + "name": f"NWS: {latitude}, {longitude}", + "manufacturer": "National Weather Service", + "entry_type": "service", + } diff --git a/homeassistant/components/nws/sensor.py b/homeassistant/components/nws/sensor.py index 4be99f95c19..49387896962 100644 --- a/homeassistant/components/nws/sensor.py +++ b/homeassistant/components/nws/sensor.py @@ -14,12 +14,13 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.distance import convert as convert_distance from homeassistant.util.dt import utcnow from homeassistant.util.pressure import convert as convert_pressure -from . import base_unique_id +from . import base_unique_id, device_info from .const import ( ATTRIBUTION, CONF_STATION, @@ -117,3 +118,8 @@ class NWSSensor(CoordinatorEntity, SensorEntity): def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" return False + + @property + def device_info(self) -> DeviceInfo: + """Return device info.""" + return device_info(self._latitude, self._longitude) diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index a8f3e55c270..dc76ebc25e5 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -23,13 +23,14 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import ConfigType from homeassistant.util.distance import convert as convert_distance from homeassistant.util.dt import utcnow from homeassistant.util.pressure import convert as convert_pressure from homeassistant.util.temperature import convert as convert_temperature -from . import base_unique_id +from . import base_unique_id, device_info from .const import ( ATTR_FORECAST_DAYTIME, ATTR_FORECAST_DETAILED_DESCRIPTION, @@ -317,3 +318,8 @@ class NWSWeather(WeatherEntity): def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" return self.mode == DAYNIGHT + + @property + def device_info(self) -> DeviceInfo: + """Return device info.""" + return device_info(self.latitude, self.longitude) From 4104a3dee64af29117cdb7b7050a60005106426f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 8 Oct 2021 13:20:57 -0600 Subject: [PATCH 0194/1038] Use built-in logic for options handler in AirVisual (#57131) --- .../components/airvisual/__init__.py | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 0419e43cd81..cda0eb52868 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Mapping from datetime import timedelta from math import ceil -from typing import Any +from typing import Any, Dict, cast from pyairvisual import CloudAPI, NodeSamba from pyairvisual.errors import ( @@ -54,8 +54,6 @@ from .const import ( PLATFORMS = ["sensor"] -DATA_LISTENER = "listener" - DEFAULT_ATTRIBUTION = "Data provided by AirVisual" DEFAULT_NODE_PRO_UPDATE_INTERVAL = timedelta(minutes=1) @@ -194,7 +192,7 @@ def _standardize_node_pro_config_entry( async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up AirVisual as config entry.""" - hass.data.setdefault(DOMAIN, {DATA_COORDINATOR: {}, DATA_LISTENER: {}}) + hass.data.setdefault(DOMAIN, {DATA_COORDINATOR: {}}) if CONF_API_KEY in config_entry.data: _standardize_geography_config_entry(hass, config_entry) @@ -217,7 +215,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ) try: - return await api_coro + data = await api_coro + return cast(Dict[str, Any], data) except (InvalidKeyError, KeyExpiredError) as ex: raise ConfigEntryAuthFailed from ex except AirVisualError as err: @@ -236,9 +235,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ) # Only geography-based entries have options: - hass.data[DOMAIN][DATA_LISTENER][ - config_entry.entry_id - ] = config_entry.add_update_listener(async_reload_entry) + config_entry.async_on_unload( + config_entry.add_update_listener(async_reload_entry) + ) else: # Remove outdated air_quality entities from the entity registry if they exist: ent_reg = entity_registry.async_get(hass) @@ -261,7 +260,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async with NodeSamba( config_entry.data[CONF_IP_ADDRESS], config_entry.data[CONF_PASSWORD] ) as node: - return await node.async_get_latest_measurements() + data = await node.async_get_latest_measurements() + return cast(Dict[str, Any], data) except NodeProError as err: raise UpdateFailed(f"Error while retrieving data: {err}") from err @@ -338,8 +338,6 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> if unload_ok: hass.data[DOMAIN][DATA_COORDINATOR].pop(config_entry.entry_id) - remove_listener = hass.data[DOMAIN][DATA_LISTENER].pop(config_entry.entry_id) - remove_listener() if CONF_API_KEY in config_entry.data: # Re-calculate the update interval period for any remaining consumers of From fe3b5e88049466e0a4279dc9158bed2ab395d798 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 8 Oct 2021 13:22:29 -0600 Subject: [PATCH 0195/1038] Use current config entry standards for SimpliSafe (#57141) * Use current config entry standards for SimpliSafe * Include tests --- .../components/simplisafe/__init__.py | 60 +++++++++---------- .../simplisafe/alarm_control_panel.py | 6 +- .../components/simplisafe/test_config_flow.py | 10 ++-- 3 files changed, 36 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 4ba26f0adc7..fce989bc953 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -130,12 +130,12 @@ async def async_get_client_id(hass: HomeAssistant) -> str: async def async_register_base_station( - hass: HomeAssistant, system: SystemV2 | SystemV3, config_entry_id: str + hass: HomeAssistant, entry: ConfigEntry, system: SystemV2 | SystemV3 ) -> None: """Register a new bridge.""" device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( - config_entry_id=config_entry_id, + config_entry_id=entry.entry_id, identifiers={(DOMAIN, system.serial)}, manufacturer="SimpliSafe", model=system.version, @@ -144,36 +144,34 @@ async def async_register_base_station( @callback -def _async_standardize_config_entry( - hass: HomeAssistant, config_entry: ConfigEntry -) -> None: +def _async_standardize_config_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Bring a config entry up to current standards.""" - if CONF_PASSWORD not in config_entry.data: + if CONF_PASSWORD not in entry.data: raise ConfigEntryAuthFailed("Config schema change requires re-authentication") entry_updates = {} - if not config_entry.unique_id: + if not entry.unique_id: # If the config entry doesn't already have a unique ID, set one: - entry_updates["unique_id"] = config_entry.data[CONF_USERNAME] - if CONF_CODE in config_entry.data: + entry_updates["unique_id"] = entry.data[CONF_USERNAME] + if CONF_CODE in entry.data: # If an alarm code was provided as part of configuration.yaml, pop it out of # the config entry's data and move it to options: - data = {**config_entry.data} + data = {**entry.data} entry_updates["data"] = data entry_updates["options"] = { - **config_entry.options, + **entry.options, CONF_CODE: data.pop(CONF_CODE), } if entry_updates: - hass.config_entries.async_update_entry(config_entry, **entry_updates) + hass.config_entries.async_update_entry(entry, **entry_updates) -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SimpliSafe as config entry.""" hass.data.setdefault(DOMAIN, {DATA_CLIENT: {}}) - hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = [] + hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] = [] - _async_standardize_config_entry(hass, config_entry) + _async_standardize_config_entry(hass, entry) _verify_domain_control = verify_domain_control(hass, DOMAIN) @@ -182,8 +180,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b try: api = await get_api( - config_entry.data[CONF_USERNAME], - config_entry.data[CONF_PASSWORD], + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], client_id=client_id, session=websession, ) @@ -193,15 +191,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady from err - simplisafe = SimpliSafe(hass, config_entry, api) + simplisafe = SimpliSafe(hass, entry, api) try: await simplisafe.async_init() except SimplipyError as err: raise ConfigEntryNotReady from err - hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = simplisafe - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] = simplisafe + hass.config_entries.async_setup_platforms(entry, PLATFORMS) @callback def verify_system_exists( @@ -292,7 +290,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ): async_register_admin_service(hass, DOMAIN, service, method, schema=schema) - config_entry.async_on_unload(config_entry.add_update_listener(async_reload_entry)) + entry.async_on_unload(entry.add_update_listener(async_reload_entry)) return True @@ -306,25 +304,25 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None: +async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle an options update.""" - await hass.config_entries.async_reload(config_entry.entry_id) + await hass.config_entries.async_reload(entry.entry_id) class SimpliSafe: """Define a SimpliSafe data object.""" - def __init__( - self, hass: HomeAssistant, config_entry: ConfigEntry, api: API - ) -> None: + def __init__(self, hass: HomeAssistant, entry: ConfigEntry, api: API) -> None: """Initialize.""" self._api = api self._hass = hass self._system_notifications: dict[int, set[SystemNotification]] = {} - self.config_entry = config_entry - self.coordinator: DataUpdateCoordinator | None = None + self.entry = entry self.systems: dict[int, SystemV2 | SystemV3] = {} + # This will get filled in by async_init: + self.coordinator: DataUpdateCoordinator | None = None + @callback def _async_process_new_notifications(self, system: SystemV2 | SystemV3) -> None: """Act on any new system notifications.""" @@ -369,15 +367,13 @@ class SimpliSafe: self._system_notifications[system.system_id] = set() self._hass.async_create_task( - async_register_base_station( - self._hass, system, self.config_entry.entry_id - ) + async_register_base_station(self._hass, self.entry, system) ) self.coordinator = DataUpdateCoordinator( self._hass, LOGGER, - name=self.config_entry.data[CONF_USERNAME], + name=self.entry.data[CONF_USERNAME], update_interval=DEFAULT_SCAN_INTERVAL, update_method=self.async_update, ) diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 8520cd2b50f..fb74aa1d26d 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -70,7 +70,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): """Initialize the SimpliSafe alarm.""" super().__init__(simplisafe, system, "Alarm Control Panel") - if code := self._simplisafe.config_entry.options.get(CONF_CODE): + if code := self._simplisafe.entry.options.get(CONF_CODE): if code.isdigit(): self._attr_code_format = FORMAT_NUMBER else: @@ -98,10 +98,10 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): @callback def _is_code_valid(self, code: str | None, state: str) -> bool: """Validate that a code matches the required one.""" - if not self._simplisafe.config_entry.options.get(CONF_CODE): + if not self._simplisafe.entry.options.get(CONF_CODE): return True - if not code or code != self._simplisafe.config_entry.options[CONF_CODE]: + if not code or code != self._simplisafe.entry.options[CONF_CODE]: LOGGER.warning( "Incorrect alarm code entered (target state: %s): %s", state, code ) diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index 4d438965806..395b2c98962 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -59,19 +59,19 @@ async def test_options_flow(hass): """Test config flow options.""" conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} - config_entry = MockConfigEntry( + entry = MockConfigEntry( domain=DOMAIN, unique_id="abcde12345", data=conf, options={CONF_CODE: "1234"}, ) - config_entry.add_to_hass(hass) + entry.add_to_hass(hass) with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True ): - await hass.config_entries.async_setup(config_entry.entry_id) - result = await hass.config_entries.options.async_init(config_entry.entry_id) + await hass.config_entries.async_setup(entry.entry_id) + result = await hass.config_entries.options.async_init(entry.entry_id) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "init" @@ -81,7 +81,7 @@ async def test_options_flow(hass): ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert config_entry.options == {CONF_CODE: "4321"} + assert entry.options == {CONF_CODE: "4321"} async def test_show_form(hass): From 722d3862dbd91334a284e7448184b8b57d51424f Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 9 Oct 2021 02:22:14 +0300 Subject: [PATCH 0196/1038] Fix Shelly RGB/W supported color mode detection (#57359) --- homeassistant/components/shelly/const.py | 5 +++++ homeassistant/components/shelly/light.py | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 3c9c24b1f7f..a6f3ab12c6b 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -21,6 +21,11 @@ LIGHT_TRANSITION_MIN_FIRMWARE_DATE: Final = 20210226 # max light transition time in milliseconds MAX_TRANSITION_TIME: Final = 5000 +RGBW_MODELS: Final = ( + "SHBLB-1", + "SHRGBW2", +) + MODELS_SUPPORTING_LIGHT_TRANSITION: Final = ( "SHBDUO-1", "SHCB-1", diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index cd034c1e7e5..3e0fce43681 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -46,6 +46,7 @@ from .const import ( LIGHT_TRANSITION_MIN_FIRMWARE_DATE, MAX_TRANSITION_TIME, MODELS_SUPPORTING_LIGHT_TRANSITION, + RGBW_MODELS, RPC, SHBLB_1_RGB_EFFECTS, STANDARD_RGB_EFFECTS, @@ -143,7 +144,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): if hasattr(block, "red") and hasattr(block, "green") and hasattr(block, "blue"): self._min_kelvin = KELVIN_MIN_VALUE_COLOR - if hasattr(block, "white"): + if wrapper.model in RGBW_MODELS: self._supported_color_modes.add(COLOR_MODE_RGBW) else: self._supported_color_modes.add(COLOR_MODE_RGB) From 204b9014648394db380210c5499b71821510f85f Mon Sep 17 00:00:00 2001 From: Clifford Roche Date: Fri, 8 Oct 2021 20:08:52 -0400 Subject: [PATCH 0197/1038] Bump greeclimate to 0.11.9 (#57358) --- homeassistant/components/gree/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index e62bf402523..4dc763411bb 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -3,7 +3,7 @@ "name": "Gree Climate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gree", - "requirements": ["greeclimate==0.11.8"], + "requirements": ["greeclimate==0.11.9"], "codeowners": ["@cmroche"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 45d33f81ad8..03dd9a41209 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -744,7 +744,7 @@ gpiozero==1.5.1 gps3==0.33.3 # homeassistant.components.gree -greeclimate==0.11.8 +greeclimate==0.11.9 # homeassistant.components.greeneye_monitor greeneye_monitor==2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b494582ffc9..3f77c2e2114 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -446,7 +446,7 @@ google-nest-sdm==0.3.7 googlemaps==2.5.1 # homeassistant.components.gree -greeclimate==0.11.8 +greeclimate==0.11.9 # homeassistant.components.growatt_server growattServer==1.1.0 From d55a7e5cc750cb02657ead0fb9e313bb9d89526e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 9 Oct 2021 00:11:46 +0000 Subject: [PATCH 0198/1038] [ci skip] Translation update --- .../translations/sensor.zh-Hans.json | 20 +++++++++++ .../translations/zh-Hans.json | 1 + .../binary_sensor/translations/zh-Hans.json | 4 +++ .../components/daikin/translations/is.json | 7 ++++ .../components/daikin/translations/it.json | 1 + .../components/demo/translations/zh-Hans.json | 4 ++- .../components/dlna_dmr/translations/et.json | 3 ++ .../components/dlna_dmr/translations/hu.json | 3 ++ .../components/dlna_dmr/translations/is.json | 9 +++++ .../components/dlna_dmr/translations/it.json | 3 ++ .../components/dlna_dmr/translations/ru.json | 3 ++ .../dlna_dmr/translations/zh-Hant.json | 3 ++ .../components/flux_led/translations/is.json | 35 +++++++++++++++++++ .../homekit/translations/zh-Hans.json | 13 +++---- .../translations/zh-Hans.json | 2 +- .../stookalert/translations/ca.json | 14 ++++++++ .../stookalert/translations/de.json | 14 ++++++++ .../stookalert/translations/et.json | 14 ++++++++ .../stookalert/translations/hu.json | 14 ++++++++ .../stookalert/translations/it.json | 14 ++++++++ .../stookalert/translations/ru.json | 14 ++++++++ .../components/tuya/translations/is.json | 28 +++++++++++++++ .../components/tuya/translations/it.json | 16 ++++----- .../components/twinkly/translations/ru.json | 2 +- .../water_heater/translations/zh-Hans.json | 11 ++++++ 25 files changed, 235 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/airvisual/translations/sensor.zh-Hans.json create mode 100644 homeassistant/components/daikin/translations/is.json create mode 100644 homeassistant/components/dlna_dmr/translations/is.json create mode 100644 homeassistant/components/flux_led/translations/is.json create mode 100644 homeassistant/components/stookalert/translations/ca.json create mode 100644 homeassistant/components/stookalert/translations/de.json create mode 100644 homeassistant/components/stookalert/translations/et.json create mode 100644 homeassistant/components/stookalert/translations/hu.json create mode 100644 homeassistant/components/stookalert/translations/it.json create mode 100644 homeassistant/components/stookalert/translations/ru.json create mode 100644 homeassistant/components/tuya/translations/is.json diff --git a/homeassistant/components/airvisual/translations/sensor.zh-Hans.json b/homeassistant/components/airvisual/translations/sensor.zh-Hans.json new file mode 100644 index 00000000000..8c56f25246e --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.zh-Hans.json @@ -0,0 +1,20 @@ +{ + "state": { + "airvisual__pollutant_label": { + "co": "\u4e00\u6c27\u5316\u78b3", + "n2": "\u4e8c\u6c27\u5316\u6c2e", + "o3": "\u81ed\u6c27", + "p1": "PM10", + "p2": "PM2.5", + "s2": "\u4e8c\u6c27\u5316\u786b" + }, + "airvisual__pollutant_level": { + "good": "\u826f\u597d", + "hazardous": "\u5371\u5bb3\u5065\u5eb7", + "moderate": "\u4e2d\u7b49", + "unhealthy": "\u4e0d\u5229\u4e8e\u5065\u5eb7", + "unhealthy_sensitive": "\u4e0d\u5229\u4e8e\u654f\u611f\u4eba\u7fa4", + "very_unhealthy": "\u975e\u5e38\u4e0d\u5229\u4e8e\u5065\u5eb7" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/translations/zh-Hans.json b/homeassistant/components/alarm_control_panel/translations/zh-Hans.json index fa819e71b49..e955d21afdb 100644 --- a/homeassistant/components/alarm_control_panel/translations/zh-Hans.json +++ b/homeassistant/components/alarm_control_panel/translations/zh-Hans.json @@ -29,6 +29,7 @@ "armed_custom_bypass": "\u81ea\u5b9a\u4e49\u533a\u57df\u8b66\u6212", "armed_home": "\u5728\u5bb6\u8b66\u6212", "armed_night": "\u591c\u95f4\u8b66\u6212", + "armed_vacation": "\u5ea6\u5047\u8b66\u6212", "arming": "\u8b66\u6212\u4e2d", "disarmed": "\u8b66\u6212\u89e3\u9664", "disarming": "\u8b66\u6212\u89e3\u9664", diff --git a/homeassistant/components/binary_sensor/translations/zh-Hans.json b/homeassistant/components/binary_sensor/translations/zh-Hans.json index 82cd0d3ccfe..0c556e7a9c0 100644 --- a/homeassistant/components/binary_sensor/translations/zh-Hans.json +++ b/homeassistant/components/binary_sensor/translations/zh-Hans.json @@ -178,6 +178,10 @@ "off": "\u6b63\u5e38", "on": "\u89e6\u53d1" }, + "update": { + "off": "\u5df2\u662f\u6700\u65b0", + "on": "\u6709\u66f4\u65b0" + }, "vibration": { "off": "\u6b63\u5e38", "on": "\u89e6\u53d1" diff --git a/homeassistant/components/daikin/translations/is.json b/homeassistant/components/daikin/translations/is.json new file mode 100644 index 00000000000..c0d8b4164da --- /dev/null +++ b/homeassistant/components/daikin/translations/is.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "api_password": "\u00d3gild au\u00f0kenning, nota\u00f0u anna\u00f0hvort API lykil e\u00f0a lykilor\u00f0." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/translations/it.json b/homeassistant/components/daikin/translations/it.json index c85f4961e57..203335c16eb 100644 --- a/homeassistant/components/daikin/translations/it.json +++ b/homeassistant/components/daikin/translations/it.json @@ -5,6 +5,7 @@ "cannot_connect": "Impossibile connettersi" }, "error": { + "api_password": "Autenticazione non valida, utilizzare la chiave API o la password.", "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" diff --git a/homeassistant/components/demo/translations/zh-Hans.json b/homeassistant/components/demo/translations/zh-Hans.json index 9155b5066c5..1c40afabb6e 100644 --- a/homeassistant/components/demo/translations/zh-Hans.json +++ b/homeassistant/components/demo/translations/zh-Hans.json @@ -4,6 +4,7 @@ "options_1": { "data": { "bool": "\u5e03\u5c14\u9009\u9879", + "constant": "\u5e38\u91cf", "int": "\u6570\u503c\u8f93\u5165" } }, @@ -15,5 +16,6 @@ } } } - } + }, + "title": "\u6f14\u793a" } \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/et.json b/homeassistant/components/dlna_dmr/translations/et.json index e32101ab251..033ff3e42c5 100644 --- a/homeassistant/components/dlna_dmr/translations/et.json +++ b/homeassistant/components/dlna_dmr/translations/et.json @@ -17,6 +17,9 @@ "confirm": { "description": "Kas alustada seadistamist?" }, + "import_turn_on": { + "description": "L\u00fclita seade sisse ja kl\u00f5psa migreerimise j\u00e4tkamiseks nuppu Edasta" + }, "user": { "data": { "url": "URL" diff --git a/homeassistant/components/dlna_dmr/translations/hu.json b/homeassistant/components/dlna_dmr/translations/hu.json index faa7e73eb76..af27382f511 100644 --- a/homeassistant/components/dlna_dmr/translations/hu.json +++ b/homeassistant/components/dlna_dmr/translations/hu.json @@ -17,6 +17,9 @@ "confirm": { "description": "Kezd\u0151dhet a be\u00e1ll\u00edt\u00e1s?" }, + "import_turn_on": { + "description": "Kapcsolja be az eszk\u00f6zt, \u00e9s kattintson a K\u00fcld\u00e9s gombra a migr\u00e1ci\u00f3 folytat\u00e1s\u00e1hoz" + }, "user": { "data": { "url": "URL" diff --git a/homeassistant/components/dlna_dmr/translations/is.json b/homeassistant/components/dlna_dmr/translations/is.json new file mode 100644 index 00000000000..9ca9e2791b6 --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/is.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "import_turn_on": { + "description": "Kveiktu \u00e1 t\u00e6kinu og smelltu \u00e1 senda til a\u00f0 halda \u00e1fram flutningi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/it.json b/homeassistant/components/dlna_dmr/translations/it.json index 5defd82a8be..0ab40e3c804 100644 --- a/homeassistant/components/dlna_dmr/translations/it.json +++ b/homeassistant/components/dlna_dmr/translations/it.json @@ -17,6 +17,9 @@ "confirm": { "description": "Vuoi iniziare la configurazione?" }, + "import_turn_on": { + "description": "Accendi il dispositivo e fai clic su Invia per continuare la migrazione" + }, "user": { "data": { "url": "URL" diff --git a/homeassistant/components/dlna_dmr/translations/ru.json b/homeassistant/components/dlna_dmr/translations/ru.json index bf1be8f6c3d..91d0d0dbff0 100644 --- a/homeassistant/components/dlna_dmr/translations/ru.json +++ b/homeassistant/components/dlna_dmr/translations/ru.json @@ -17,6 +17,9 @@ "confirm": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" }, + "import_turn_on": { + "description": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 '\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c' \u0434\u043b\u044f \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0435\u043d\u0438\u044f \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438" + }, "user": { "data": { "url": "URL-\u0430\u0434\u0440\u0435\u0441" diff --git a/homeassistant/components/dlna_dmr/translations/zh-Hant.json b/homeassistant/components/dlna_dmr/translations/zh-Hant.json index b7eab93d76d..04c48c833e8 100644 --- a/homeassistant/components/dlna_dmr/translations/zh-Hant.json +++ b/homeassistant/components/dlna_dmr/translations/zh-Hant.json @@ -17,6 +17,9 @@ "confirm": { "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" }, + "import_turn_on": { + "description": "\u8acb\u958b\u555f\u88dd\u7f6e\u4e26\u9ede\u9078\u50b3\u9001\u4ee5\u7e7c\u7e8c\u9077\u79fb" + }, "user": { "data": { "url": "\u7db2\u5740" diff --git a/homeassistant/components/flux_led/translations/is.json b/homeassistant/components/flux_led/translations/is.json new file mode 100644 index 00000000000..89f72dd4362 --- /dev/null +++ b/homeassistant/components/flux_led/translations/is.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "T\u00e6ki n\u00fa \u00feegar stillt", + "no_devices_found": "Engin t\u00e6ki fundust \u00e1 netinu" + }, + "error": { + "cannot_connect": "Tenging mist\u00f3kst" + }, + "flow_title": "{model} {id} ( {ipaddr} )", + "step": { + "discovery_confirm": { + "description": "Viltu setja upp {model} {id} ( {ipaddr} )?" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Ef \u00fe\u00fa skilur host eftir autt ver\u00f0ur leit notu\u00f0 til a\u00f0 finna t\u00e6ki" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "custom_effect_colors": "S\u00e9rsni\u00f0in \u00e1hrif: Listi yfir 1 til 16 [R, G, B] liti. D\u00e6mi: [255,0,255], [60,128,0]", + "custom_effect_speed_pct": "S\u00e9rsni\u00f0in \u00e1hrif: Hra\u00f0i \u00ed pr\u00f3sentum fyrir \u00e1hrifin sem skipta um liti.", + "custom_effect_transition": "S\u00e9rsni\u00f0in \u00e1hrif: Ger\u00f0 bl\u00f6ndunar \u00e1 milli litanna.", + "mode": "Valinn birtuhamur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/zh-Hans.json b/homeassistant/components/homekit/translations/zh-Hans.json index 4a1486735ff..73875ea0423 100644 --- a/homeassistant/components/homekit/translations/zh-Hans.json +++ b/homeassistant/components/homekit/translations/zh-Hans.json @@ -13,7 +13,7 @@ "include_domains": "\u8981\u5305\u542b\u7684\u57df" }, "description": "HomeKit \u96c6\u6210\u53ef\u4ee5\u8ba9\u60a8\u901a\u8fc7 HomeKit \u8bbf\u95ee Home Assistant \u4e2d\u7684\u5b9e\u4f53\u3002\u5728\u6865\u63a5\u6a21\u5f0f\u4e2d\uff0c\u6bcf\u4e2a\u6865\u63a5\u5668\u5b9e\u4f8b\u6700\u591a\u53ef\u6a21\u62df 150 \u4e2a\u914d\u4ef6\uff0c\u5305\u62ec\u6865\u63a5\u5668\u672c\u8eab\u3002\u5982\u679c\u60a8\u5e0c\u671b\u6865\u63a5\u7684\u914d\u4ef6\u591a\u4e8e\u6b64\u6570\u91cf\uff0c\u5efa\u8bae\u4e3a\u4e0d\u540c\u7684\u57df\u4f7f\u7528\u591a\u4e2a HomeKit \u6865\u63a5\u5668\u3002\u8be6\u7ec6\u7684\u5b9e\u4f53\u914d\u7f6e\u4ec5\u53ef\u7528\u4e8e\u4e3b\u6865\u63a5\u5668\uff0c\u4e14\u987b\u901a\u8fc7 YAML \u914d\u7f6e\u3002", - "title": "\u6fc0\u6d3b HomeKit" + "title": "\u9009\u62e9\u8981\u5305\u542b\u7684\u57df" } } }, @@ -21,18 +21,19 @@ "step": { "advanced": { "data": { - "auto_start": "[%key_id:43661779%]", + "auto_start": "\u81ea\u52a8\u542f\u52a8\uff08\u5982\u679c\u60a8\u624b\u52a8\u8c03\u7528 homekit.start \u670d\u52a1\uff0c\u8bf7\u7981\u7528\u6b64\u9879\uff09", "devices": "\u8bbe\u5907 (\u89e6\u53d1\u5668)" }, - "description": "\u8fd9\u4e9b\u8bbe\u7f6e\u53ea\u6709\u5f53 HomeKit \u529f\u80fd\u4e0d\u6b63\u5e38\u65f6\u624d\u9700\u8981\u8c03\u6574\u3002", + "description": "\u5c06\u4e3a\u6bcf\u4e2a\u9009\u62e9\u7684\u8bbe\u5907\u521b\u5efa\u4e00\u4e2a\u53ef\u7f16\u7a0b\u5f00\u5173\u914d\u4ef6\u3002\u53ef\u4ee5\u5728 HomeKit \u4e2d\u914d\u7f6e\u8fd9\u4e9b\u914d\u4ef6\uff0c\u5f53\u8bbe\u5907\u89e6\u53d1\u65f6\uff0c\u6267\u884c\u6307\u5b9a\u7684\u81ea\u52a8\u5316\u6216\u573a\u666f\u3002", "title": "\u9ad8\u7ea7\u914d\u7f6e" }, "cameras": { "data": { + "camera_audio": "\u652f\u6301\u97f3\u9891\u7684\u6444\u50cf\u673a", "camera_copy": "\u652f\u6301\u539f\u751f H.264 \u63a8\u6d41\u7684\u6444\u50cf\u673a" }, "description": "\u67e5\u627e\u6240\u6709\u652f\u6301\u539f\u751f H.264 \u63a8\u6d41\u7684\u6444\u50cf\u673a\u3002\u5982\u679c\u6444\u50cf\u673a\u8f93\u51fa\u7684\u4e0d\u662f H.264 \u6d41\uff0c\u7cfb\u7edf\u4f1a\u5c06\u89c6\u9891\u8f6c\u7801\u4e3a H.264 \u4ee5\u4f9b HomeKit \u4f7f\u7528\u3002\u8f6c\u7801\u9700\u8981\u9ad8\u6027\u80fd\u7684 CPU\uff0c\u56e0\u6b64\u5728\u5f00\u53d1\u677f\u8ba1\u7b97\u673a\u4e0a\u5f88\u96be\u5b8c\u6210\u3002", - "title": "\u8bf7\u9009\u62e9\u6444\u50cf\u673a\u7684\u89c6\u9891\u7f16\u7801\u3002" + "title": "\u6444\u50cf\u673a\u914d\u7f6e" }, "include_exclude": { "data": { @@ -40,7 +41,7 @@ "mode": "\u6a21\u5f0f" }, "description": "\u9009\u62e9\u8981\u5f00\u653e\u7684\u5b9e\u4f53\u3002\u5728\u9644\u4ef6\u6a21\u5f0f\u4e2d\uff0c\u53ea\u80fd\u5f00\u653e\u4e00\u4e2a\u5b9e\u4f53\u3002\u5728\u6865\u63a5\u5305\u542b\u6a21\u5f0f\u4e2d\uff0c\u5982\u679c\u4e0d\u9009\u62e9\u5305\u542b\u7684\u5b9e\u4f53\uff0c\u57df\u4e2d\u6240\u6709\u5b9e\u4f53\u90fd\u4f1a\u5f00\u653e\u3002\u5728\u6865\u63a5\u6392\u9664\u6a21\u5f0f\u4e2d\uff0c\u5982\u679c\u4e0d\u9009\u62e9\u6392\u9664\u7684\u5b9e\u4f53\uff0c\u57df\u4e2d\u6240\u6709\u5b9e\u4f53\u4e5f\u90fd\u4f1a\u5f00\u653e\u3002", - "title": "\u9009\u62e9\u8981\u5f00\u653e\u7684\u5b9e\u4f53" + "title": "\u9009\u62e9\u8981\u5305\u542b\u7684\u5b9e\u4f53" }, "init": { "data": { @@ -48,7 +49,7 @@ "mode": "\u6a21\u5f0f" }, "description": "HomeKit \u53ef\u4ee5\u88ab\u914d\u7f6e\u4e3a\u5bf9\u5916\u5c55\u793a\u4e00\u4e2a\u6865\u63a5\u5668\u6216\u5355\u4e2a\u914d\u4ef6\u3002\u5728\u914d\u4ef6\u6a21\u5f0f\u4e2d\uff0c\u53ea\u80fd\u4f7f\u7528\u4e00\u4e2a\u5b9e\u4f53\u3002\u8bbe\u5907\u7c7b\u578b\u4e3a\u201c\u7535\u89c6\u201d\u7684\u5a92\u4f53\u64ad\u653e\u5668\u5fc5\u987b\u4f7f\u7528\u914d\u4ef6\u6a21\u5f0f\u624d\u80fd\u6b63\u5e38\u5de5\u4f5c\u3002\u201c\u8981\u5305\u542b\u7684\u57df\u201d\u4e2d\u7684\u5b9e\u4f53\u5c06\u5411 HomeKit \u5f00\u653e\u3002\u5728\u4e0b\u4e00\u9875\u53ef\u4ee5\u9009\u62e9\u8981\u5305\u542b\u6216\u6392\u9664\u5176\u4e2d\u7684\u54ea\u4e9b\u5b9e\u4f53\u3002", - "title": "\u9009\u62e9\u8981\u5f00\u653e\u7684\u57df\u3002" + "title": "\u9009\u62e9\u8981\u5305\u542b\u7684\u57df\u3002" }, "yaml": { "description": "\u8be5\u6761\u76ee\u662f\u901a\u8fc7 YAML \u63a7\u5236\u7684", diff --git a/homeassistant/components/homekit_controller/translations/zh-Hans.json b/homeassistant/components/homekit_controller/translations/zh-Hans.json index 7da392179f6..a5f57e2f576 100644 --- a/homeassistant/components/homekit_controller/translations/zh-Hans.json +++ b/homeassistant/components/homekit_controller/translations/zh-Hans.json @@ -44,7 +44,7 @@ "data": { "device": "\u8bbe\u5907" }, - "description": "\u9009\u62e9\u60a8\u8981\u914d\u5bf9\u7684\u8bbe\u5907", + "description": "HomeKit \u63a7\u5236\u5668\u4f7f\u7528\u5b89\u5168\u7684\u52a0\u5bc6\u8fde\u63a5\uff0c\u901a\u8fc7\u5c40\u57df\u7f51\u76f4\u63a5\u8fdb\u884c\u901a\u4fe1\uff0c\u65e0\u9700\u5355\u72ec\u7684 HomeKit \u63a7\u5236\u5668\u6216 iCloud\u3002\u8bf7\u9009\u62e9\u8981\u914d\u5bf9\u7684\u8bbe\u5907\uff1a", "title": "\u4e0e HomeKit \u914d\u4ef6\u914d\u5bf9" } } diff --git a/homeassistant/components/stookalert/translations/ca.json b/homeassistant/components/stookalert/translations/ca.json new file mode 100644 index 00000000000..7b2b969986b --- /dev/null +++ b/homeassistant/components/stookalert/translations/ca.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat" + }, + "step": { + "user": { + "data": { + "province": "Prov\u00edncia" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookalert/translations/de.json b/homeassistant/components/stookalert/translations/de.json new file mode 100644 index 00000000000..b22b3623cfc --- /dev/null +++ b/homeassistant/components/stookalert/translations/de.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, + "step": { + "user": { + "data": { + "province": "Provinz" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookalert/translations/et.json b/homeassistant/components/stookalert/translations/et.json new file mode 100644 index 00000000000..c75d828ecc9 --- /dev/null +++ b/homeassistant/components/stookalert/translations/et.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba h\u00e4\u00e4lestatud" + }, + "step": { + "user": { + "data": { + "province": "Maakond" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookalert/translations/hu.json b/homeassistant/components/stookalert/translations/hu.json new file mode 100644 index 00000000000..a0d1008e6d4 --- /dev/null +++ b/homeassistant/components/stookalert/translations/hu.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + }, + "step": { + "user": { + "data": { + "province": "Tartom\u00e1ny" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookalert/translations/it.json b/homeassistant/components/stookalert/translations/it.json new file mode 100644 index 00000000000..65f18cad1a6 --- /dev/null +++ b/homeassistant/components/stookalert/translations/it.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" + }, + "step": { + "user": { + "data": { + "province": "Provincia" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookalert/translations/ru.json b/homeassistant/components/stookalert/translations/ru.json new file mode 100644 index 00000000000..57f6aaf4f50 --- /dev/null +++ b/homeassistant/components/stookalert/translations/ru.json @@ -0,0 +1,14 @@ +{ + "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." + }, + "step": { + "user": { + "data": { + "province": "\u041f\u0440\u043e\u0432\u0438\u043d\u0446\u0438\u044f" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/is.json b/homeassistant/components/tuya/translations/is.json new file mode 100644 index 00000000000..fc7a4878a25 --- /dev/null +++ b/homeassistant/components/tuya/translations/is.json @@ -0,0 +1,28 @@ +{ + "config": { + "error": { + "login_error": "Innskr\u00e1ningarvilla ( {code} ): {msg}" + }, + "step": { + "login": { + "data": { + "access_id": "A\u00f0gangsau\u00f0kenni", + "access_secret": "A\u00f0gangsleyndarm\u00e1l", + "country_code": "Landsn\u00famer", + "endpoint": "Frambo\u00f0ssv\u00e6\u00f0i", + "password": "Lykilor\u00f0", + "tuya_app_type": "App", + "username": "Reikningur" + }, + "title": "Tuya" + }, + "user": { + "data": { + "access_id": "Tuya IoT a\u00f0gangsau\u00f0kenni", + "access_secret": "Tuya IoT a\u00f0gangsleyndarm\u00e1l", + "region": "Landsv\u00e6\u00f0i" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/it.json b/homeassistant/components/tuya/translations/it.json index 9f3b7d498e3..8ffb263be36 100644 --- a/homeassistant/components/tuya/translations/it.json +++ b/homeassistant/components/tuya/translations/it.json @@ -13,8 +13,8 @@ "step": { "login": { "data": { - "access_id": "ID di accesso", - "access_secret": "Accesso segreto", + "access_id": "Access ID", + "access_secret": "Access Secret", "country_code": "Prefisso internazionale", "endpoint": "Zona di disponibilit\u00e0", "password": "Password", @@ -26,16 +26,16 @@ }, "user": { "data": { - "access_id": "ID accesso IoT Tuya", - "access_secret": "Secret IoT Tuya", - "country_code": "Prefisso internazionale del tuo account (ad es. 1 per gli Stati Uniti o 86 per la Cina)", + "access_id": "Tuya IoT Access ID", + "access_secret": "Tuya IoT Access Secret", + "country_code": "Nazione", "password": "Password", "platform": "L'app in cui \u00e8 registrato il tuo account", "region": "Area geografica", "tuya_project_type": "Tipo di progetto Tuya cloud", - "username": "Nome utente" + "username": "Account" }, - "description": "Inserisci le tue credenziali Tuya.", + "description": "Inserisci le tue credenziali Tuya", "title": "Integrazione Tuya" } } @@ -45,7 +45,7 @@ "cannot_connect": "Impossibile connettersi" }, "error": { - "dev_multi_type": "Pi\u00f9 dispositivi selezionati da configurare devono essere dello stesso tipo", + "dev_multi_type": "I dispositivi multipli selezionati da configurare devono essere dello stesso tipo", "dev_not_config": "Tipo di dispositivo non configurabile", "dev_not_found": "Dispositivo non trovato" }, diff --git a/homeassistant/components/twinkly/translations/ru.json b/homeassistant/components/twinkly/translations/ru.json index c9ea2c6927b..a4cadb51f4f 100644 --- a/homeassistant/components/twinkly/translations/ru.json +++ b/homeassistant/components/twinkly/translations/ru.json @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "host": "\u0418\u043c\u044f \u0445\u043e\u0441\u0442\u0430 (\u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441) \u0412\u0430\u0448\u0435\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Twinkly" + "host": "\u0418\u043c\u044f \u0445\u043e\u0441\u0442\u0430 (\u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441) \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441\u0432\u0435\u0442\u043e\u0434\u0438\u043e\u0434\u043d\u043e\u0439 \u043b\u0435\u043d\u0442\u044b Twinkly", "title": "Twinkly" diff --git a/homeassistant/components/water_heater/translations/zh-Hans.json b/homeassistant/components/water_heater/translations/zh-Hans.json index 21a77facc93..f4b9af1d589 100644 --- a/homeassistant/components/water_heater/translations/zh-Hans.json +++ b/homeassistant/components/water_heater/translations/zh-Hans.json @@ -4,5 +4,16 @@ "turn_off": "\u5173\u95ed {entity_name}", "turn_on": "\u6253\u5f00 {entity_name}" } + }, + "state": { + "_": { + "eco": "\u8282\u80fd", + "electric": "\u7535\u70ed", + "gas": "\u71c3\u6c14", + "heat_pump": "\u70ed\u6cf5", + "high_demand": "\u6025\u9700", + "off": "\u5173", + "performance": "\u9ad8\u6027\u80fd" + } } } \ No newline at end of file From 6d0da631bf0052bc10d55c60ea22f2862c46339c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 8 Oct 2021 22:12:06 -0700 Subject: [PATCH 0199/1038] Handle prepare timeout in websocket API (#55989) --- homeassistant/components/websocket_api/http.py | 13 +++++++++---- tests/components/websocket_api/test_http.py | 14 +++++++++++++- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index bce6713403a..8d75d50e59a 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -61,7 +61,7 @@ class WebSocketHandler: """Initialize an active connection.""" self.hass = hass self.request = request - self.wsock: web.WebSocketResponse | None = None + self.wsock = web.WebSocketResponse(heartbeat=55) self._to_write: asyncio.Queue = asyncio.Queue(maxsize=MAX_PENDING_MSG) self._handle_task: asyncio.Task | None = None self._writer_task: asyncio.Task | None = None @@ -71,7 +71,6 @@ class WebSocketHandler: async def _writer(self) -> None: """Write outgoing messages.""" # Exceptions if Socket disconnected or cancelled by connection handler - assert self.wsock is not None with suppress(RuntimeError, ConnectionResetError, *CANCELLATION_ERRORS): while not self.wsock.closed: message = await self._to_write.get() @@ -143,8 +142,14 @@ class WebSocketHandler: async def async_handle(self) -> web.WebSocketResponse: """Handle a websocket response.""" request = self.request - wsock = self.wsock = web.WebSocketResponse(heartbeat=55) - await wsock.prepare(request) + wsock = self.wsock + try: + async with async_timeout.timeout(10): + await wsock.prepare(request) + except asyncio.TimeoutError: + self._logger.warning("Timeout preparing request from %s", request.remote) + return wsock + self._logger.debug("Connected from %s", request.remote) self._handle_task = asyncio.current_task() diff --git a/tests/components/websocket_api/test_http.py b/tests/components/websocket_api/test_http.py index f3952f1dc4b..336c79d22b8 100644 --- a/tests/components/websocket_api/test_http.py +++ b/tests/components/websocket_api/test_http.py @@ -1,8 +1,9 @@ """Test Websocket API http module.""" +import asyncio from datetime import timedelta from unittest.mock import patch -from aiohttp import WSMsgType +from aiohttp import ServerDisconnectedError, WSMsgType, web import pytest from homeassistant.components.websocket_api import const, http @@ -80,3 +81,14 @@ async def test_non_json_message(hass, websocket_client, caplog): f"Unable to serialize to JSON. Bad data found at $.result[0](State: test_domain.entity).attributes.bad={bad_data}(" in caplog.text ) + + +async def test_prepare_fail(hass, hass_ws_client, caplog): + """Test failing to prepare.""" + with patch( + "homeassistant.components.websocket_api.http.web.WebSocketResponse.prepare", + side_effect=(asyncio.TimeoutError, web.WebSocketResponse.prepare), + ), pytest.raises(ServerDisconnectedError): + await hass_ws_client(hass) + + assert "Timeout preparing request" in caplog.text From fe065b2de86b34378de8d66b76b43421cbcbdd18 Mon Sep 17 00:00:00 2001 From: Ben <512997+benleb@users.noreply.github.com> Date: Sat, 9 Oct 2021 08:33:41 +0200 Subject: [PATCH 0200/1038] Add Sure Petcare Felaqua device (#56823) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add a SurePetcareSensor * add the felaqua sensor * add felaqua battery test * fix felaqua product_id * actually add a felaqua sensor 😅 * remove superclass --- .../components/surepetcare/sensor.py | 38 ++++++++++++++++++- tests/components/surepetcare/__init__.py | 15 +++++++- tests/components/surepetcare/test_sensor.py | 3 +- 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/surepetcare/sensor.py b/homeassistant/components/surepetcare/sensor.py index 01d4e9f83aa..508ad030922 100644 --- a/homeassistant/components/surepetcare/sensor.py +++ b/homeassistant/components/surepetcare/sensor.py @@ -2,13 +2,20 @@ from __future__ import annotations import logging +from typing import cast from surepy.entities import SurepyEntity +from surepy.entities.devices import Felaqua as SurepyFelaqua from surepy.enums import EntityType from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_VOLTAGE, DEVICE_CLASS_BATTERY, PERCENTAGE +from homeassistant.const import ( + ATTR_VOLTAGE, + DEVICE_CLASS_BATTERY, + PERCENTAGE, + VOLUME_MILLILITERS, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -24,7 +31,7 @@ async def async_setup_entry( ) -> None: """Set up Sure PetCare Flaps sensors.""" - entities: list[SureBattery] = [] + entities: list[SurePetcareEntity] = [] coordinator: SurePetcareDataCoordinator = hass.data[DOMAIN][entry.entry_id] @@ -38,6 +45,9 @@ async def async_setup_entry( ]: entities.append(SureBattery(surepy_entity.id, coordinator)) + if surepy_entity.type == EntityType.FELAQUA: + entities.append(Felaqua(surepy_entity.id, coordinator)) + async_add_entities(entities) @@ -78,3 +88,27 @@ class SureBattery(SurePetcareEntity, SensorEntity): } else: self._attr_extra_state_attributes = {} + + +class Felaqua(SurePetcareEntity, SensorEntity): + """Sure Petcare Felaqua.""" + + _attr_native_unit_of_measurement = VOLUME_MILLILITERS + + def __init__( + self, surepetcare_id: int, coordinator: SurePetcareDataCoordinator + ) -> None: + """Initialize a Sure Petcare Felaqua sensor.""" + super().__init__(surepetcare_id, coordinator) + + surepy_entity: SurepyFelaqua = coordinator.data[surepetcare_id] + + self._attr_name = self._device_name + self._attr_unique_id = self._device_id + self._attr_entity_picture = surepy_entity.icon + + @callback + def _update_attr(self, surepy_entity: SurepyEntity) -> None: + """Update the state.""" + surepy_entity = cast(SurepyFelaqua, surepy_entity) + self._attr_native_value = surepy_entity.water_remaining diff --git a/tests/components/surepetcare/__init__.py b/tests/components/surepetcare/__init__.py index 854ac923ead..23a5830062e 100644 --- a/tests/components/surepetcare/__init__.py +++ b/tests/components/surepetcare/__init__.py @@ -27,6 +27,19 @@ MOCK_FEEDER = { }, } +MOCK_FELAQUA = { + "id": 31337, + "product_id": 8, + "household_id": HOUSEHOLD_ID, + "name": "Felaqua", + "parent": {"product_id": 1, "id": HUB_ID}, + "status": { + "battery": 6.4, + "signal": {"device_rssi": 70, "hub_rssi": 65}, + "online": True, + }, +} + MOCK_CAT_FLAP = { "id": 13579, "product_id": 6, @@ -66,7 +79,7 @@ MOCK_PET = { } MOCK_API_DATA = { - "devices": [MOCK_HUB, MOCK_CAT_FLAP, MOCK_PET_FLAP, MOCK_FEEDER], + "devices": [MOCK_HUB, MOCK_CAT_FLAP, MOCK_PET_FLAP, MOCK_FEEDER, MOCK_FELAQUA], "pets": [MOCK_PET], } diff --git a/tests/components/surepetcare/test_sensor.py b/tests/components/surepetcare/test_sensor.py index 9edc28dc6dc..dd8ade9b1aa 100644 --- a/tests/components/surepetcare/test_sensor.py +++ b/tests/components/surepetcare/test_sensor.py @@ -3,12 +3,13 @@ from homeassistant.components.surepetcare.const import DOMAIN from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -from . import HOUSEHOLD_ID, MOCK_CONFIG +from . import HOUSEHOLD_ID, MOCK_CONFIG, MOCK_FELAQUA EXPECTED_ENTITY_IDS = { "sensor.pet_flap_battery_level": f"{HOUSEHOLD_ID}-13576-battery", "sensor.cat_flap_battery_level": f"{HOUSEHOLD_ID}-13579-battery", "sensor.feeder_battery_level": f"{HOUSEHOLD_ID}-12345-battery", + "sensor.felaqua_battery_level": f"{HOUSEHOLD_ID}-{MOCK_FELAQUA['id']}-battery", } From b27bcf1b0001d0cbeae4d33ba27aa79ae4007b1c Mon Sep 17 00:00:00 2001 From: RenierM26 <66512715+RenierM26@users.noreply.github.com> Date: Sat, 9 Oct 2021 10:02:35 +0200 Subject: [PATCH 0201/1038] Bump Switchbot library (#57367) * Bump dependency to fix SB password problem. * Bump API version. --- 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 38743981ed5..6dd23e2eec5 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.11.0"], + "requirements": ["PySwitchbot==0.12.0"], "config_flow": true, "codeowners": ["@danielhiversen", "@RenierM26"], "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 03dd9a41209..84cbbac8d5b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -46,7 +46,7 @@ PyRMVtransport==0.3.2 PySocks==1.7.1 # homeassistant.components.switchbot -# PySwitchbot==0.11.0 +# PySwitchbot==0.12.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3f77c2e2114..7d9955090a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -24,7 +24,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.2 # homeassistant.components.switchbot -# PySwitchbot==0.11.0 +# PySwitchbot==0.12.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 6a5895222ec908acad3cf478897ca2455f88f730 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 9 Oct 2021 05:23:40 -0400 Subject: [PATCH 0202/1038] Catch errors for efergy (#57326) Co-authored-by: Martin Hjelmare --- homeassistant/components/efergy/sensor.py | 27 +++++++++-- tests/components/efergy/test_sensor.py | 56 +++++++++++++++++++++-- 2 files changed, 74 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/efergy/sensor.py b/homeassistant/components/efergy/sensor.py index 338609cf342..a11fe5f3ac6 100644 --- a/homeassistant/components/efergy/sensor.py +++ b/homeassistant/components/efergy/sensor.py @@ -1,7 +1,9 @@ """Support for Efergy sensors.""" from __future__ import annotations -from pyefergy import Efergy +import logging + +from pyefergy import Efergy, exceptions import voluptuous as vol from homeassistant.components.sensor import ( @@ -20,6 +22,7 @@ from homeassistant.const import ( POWER_WATT, ) 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 @@ -39,6 +42,8 @@ CONF_CURRENT_VALUES = "current_values" DEFAULT_PERIOD = "year" DEFAULT_UTC_OFFSET = "0" +_LOGGER = logging.getLogger(__name__) + SENSOR_TYPES: dict[str, SensorEntityDescription] = { CONF_INSTANT: SensorEntityDescription( key=CONF_INSTANT, @@ -102,7 +107,10 @@ async def async_setup_platform( ) dev = [] - sensors = await api.get_sids() + try: + sensors = await api.get_sids() + except (exceptions.DataError, exceptions.ConnectTimeout) as ex: + raise PlatformNotReady("Error getting data from Efergy:") from ex for variable in config[CONF_MONITORED_VARIABLES]: if variable[CONF_TYPE] == CONF_CURRENT_VALUES: for sensor in sensors: @@ -150,6 +158,15 @@ class EfergySensor(SensorEntity): async def async_update(self) -> None: """Get the Efergy monitor data from the web service.""" - self._attr_native_value = await self.api.async_get_reading( - self.entity_description.key, period=self.period, sid=self.sid - ) + try: + self._attr_native_value = await self.api.async_get_reading( + self.entity_description.key, period=self.period, sid=self.sid + ) + except (exceptions.DataError, exceptions.ConnectTimeout) as ex: + if self._attr_available: + self._attr_available = False + _LOGGER.error("Error getting data from Efergy: %s", ex) + return + if not self._attr_available: + self._attr_available = True + _LOGGER.info("Connection to Efergy has resumed") diff --git a/tests/components/efergy/test_sensor.py b/tests/components/efergy/test_sensor.py index 97478155483..94a381b9048 100644 --- a/tests/components/efergy/test_sensor.py +++ b/tests/components/efergy/test_sensor.py @@ -1,9 +1,15 @@ """The tests for Efergy sensor platform.""" +import asyncio +from datetime import timedelta + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util -from tests.common import load_fixture +from tests.common import async_fire_time_changed, load_fixture from tests.test_util.aiohttp import AiohttpClientMocker token = "9p6QGJ7dpZfO3fqPTBk1fyEmjV1cGoLT" @@ -30,9 +36,14 @@ MULTI_SENSOR_CONFIG = { } -def mock_responses(aioclient_mock: AiohttpClientMocker): +def mock_responses(aioclient_mock: AiohttpClientMocker, error: bool = False): """Mock responses for Efergy.""" base_url = "https://engage.efergy.com/mobile_proxy/" + if error: + aioclient_mock.get( + f"{base_url}getCurrentValuesSummary?token={token}", exc=asyncio.TimeoutError + ) + return aioclient_mock.get( f"{base_url}getInstant?token={token}", text=load_fixture("efergy/efergy_instant.json"), @@ -64,7 +75,9 @@ async def test_single_sensor_readings( ): """Test for successfully setting up the Efergy platform.""" mock_responses(aioclient_mock) - assert await async_setup_component(hass, "sensor", {"sensor": ONE_SENSOR_CONFIG}) + assert await async_setup_component( + hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: ONE_SENSOR_CONFIG} + ) await hass.async_block_till_done() assert hass.states.get("sensor.energy_consumed").state == "38.21" @@ -79,9 +92,44 @@ async def test_multi_sensor_readings( ): """Test for multiple sensors in one household.""" mock_responses(aioclient_mock) - assert await async_setup_component(hass, "sensor", {"sensor": MULTI_SENSOR_CONFIG}) + assert await async_setup_component( + hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: MULTI_SENSOR_CONFIG} + ) await hass.async_block_till_done() assert hass.states.get("sensor.efergy_728386").state == "218" assert hass.states.get("sensor.efergy_0").state == "1808" assert hass.states.get("sensor.efergy_728387").state == "312" + + +async def test_failed_getting_sids( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +): + """Test failed gettings sids.""" + mock_responses(aioclient_mock, error=True) + assert await async_setup_component( + hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: ONE_SENSOR_CONFIG} + ) + assert not hass.states.async_all("sensor") + + +async def test_failed_update_and_reconnection( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +): + """Test failed update and reconnection.""" + mock_responses(aioclient_mock) + assert await async_setup_component( + hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: ONE_SENSOR_CONFIG} + ) + aioclient_mock.clear_requests() + mock_responses(aioclient_mock, error=True) + next_update = dt_util.utcnow() + timedelta(seconds=3) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + assert hass.states.get("sensor.efergy_728386").state == STATE_UNAVAILABLE + aioclient_mock.clear_requests() + mock_responses(aioclient_mock) + next_update = dt_util.utcnow() + timedelta(seconds=30) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + assert hass.states.get("sensor.efergy_728386").state == "1628" From 8de7966104911bca6f855a1755a6d71a07afb9de Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 9 Oct 2021 19:10:43 +0300 Subject: [PATCH 0203/1038] Add Shelly config entry reload on device config change (#57356) --- homeassistant/components/shelly/__init__.py | 74 ++++++++++++++++++++- homeassistant/components/shelly/const.py | 11 +++ 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index b0df4d4cb7f..87896f4380b 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -24,6 +24,7 @@ from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, device_registry, update_coordinator import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.typing import ConfigType from .const import ( @@ -39,6 +40,8 @@ from .const import ( DEFAULT_COAP_PORT, DEVICE, DOMAIN, + DUAL_MODE_LIGHT_MODELS, + ENTRY_RELOAD_COOLDOWN, EVENT_SHELLY_CLICK, INPUTS_EVENTS_DICT, POLLING_TIMEOUT_SEC, @@ -252,6 +255,18 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): self.entry = entry self.device = device + self._debounced_reload = Debouncer( + hass, + _LOGGER, + cooldown=ENTRY_RELOAD_COOLDOWN, + immediate=False, + function=self._async_reload_entry, + ) + entry.async_on_unload(self._debounced_reload.async_cancel) + self._last_cfg_changed: int | None = None + self._last_mode: str | None = None + self._last_effect: int | None = None + entry.async_on_unload( self.async_add_listener(self._async_device_updates_handler) ) @@ -261,6 +276,11 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop) ) + async def _async_reload_entry(self) -> None: + """Reload entry.""" + _LOGGER.debug("Reloading entry %s", self.name) + await self.hass.config_entries.async_reload(self.entry.entry_id) + @callback def _async_device_updates_handler(self) -> None: """Handle device updates.""" @@ -280,8 +300,24 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): break - # Check for input events + # Check for input events and config change + cfg_changed = 0 for block in self.device.blocks: + if block.type == "device": + cfg_changed = block.cfgChanged + + # For dual mode bulbs ignore change if it is due to mode/effect change + if self.model in DUAL_MODE_LIGHT_MODELS: + if "mode" in block.sensor_ids and self.model != "SHRGBW2": + if self._last_mode != block.mode: + self._last_cfg_changed = None + self._last_mode = block.mode + + if "effect" in block.sensor_ids: + if self._last_effect != block.effect: + self._last_cfg_changed = None + self._last_effect = block.effect + if ( "inputEvent" not in block.sensor_ids or "inputEventCnt" not in block.sensor_ids @@ -318,6 +354,15 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): self.name, ) + if self._last_cfg_changed is not None and cfg_changed > self._last_cfg_changed: + _LOGGER.info( + "Config for %s changed, reloading entry in %s seconds", + self.name, + ENTRY_RELOAD_COOLDOWN, + ) + self.hass.async_create_task(self._debounced_reload.async_call()) + self._last_cfg_changed = cfg_changed + async def _async_update_data(self) -> None: """Fetch data.""" if sleep_period := self.entry.data.get("sleep_period"): @@ -496,6 +541,15 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): self.entry = entry self.device = device + self._debounced_reload = Debouncer( + hass, + _LOGGER, + cooldown=ENTRY_RELOAD_COOLDOWN, + immediate=False, + function=self._async_reload_entry, + ) + entry.async_on_unload(self._debounced_reload.async_cancel) + entry.async_on_unload( self.async_add_listener(self._async_device_updates_handler) ) @@ -505,6 +559,11 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop) ) + async def _async_reload_entry(self) -> None: + """Reload entry.""" + _LOGGER.debug("Reloading entry %s", self.name) + await self.hass.config_entries.async_reload(self.entry.entry_id) + @callback def _async_device_updates_handler(self) -> None: """Handle device updates.""" @@ -518,7 +577,18 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): self._last_event = self.device.event for event in self.device.event["events"]: - if event.get("event") not in RPC_INPUTS_EVENTS_TYPES: + event_type = event.get("event") + if event_type is None: + continue + + if event_type == "config_changed": + _LOGGER.info( + "Config for %s changed, reloading entry in %s seconds", + self.name, + ENTRY_RELOAD_COOLDOWN, + ) + self.hass.async_create_task(self._debounced_reload.async_call()) + elif event_type not in RPC_INPUTS_EVENTS_TYPES: continue self.hass.bus.async_fire( diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index a6f3ab12c6b..50f81511062 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -35,6 +35,14 @@ MODELS_SUPPORTING_LIGHT_TRANSITION: Final = ( "SHVIN-1", ) +# Bulbs that support white & color modes +DUAL_MODE_LIGHT_MODELS: Final = ( + "SHBDUO-1", + "SHBLB-1", + "SHCB-1", + "SHRGBW2", +) + # Used in "_async_update_data" as timeout for polling data from devices. POLLING_TIMEOUT_SEC: Final = 18 @@ -137,3 +145,6 @@ UPTIME_DEVIATION: Final = 5 # Max RPC switch/input key instances MAX_RPC_KEY_INSTANCES = 4 + +# Time to wait before reloading entry upon device config change +ENTRY_RELOAD_COOLDOWN = 60 From 325d5e1c2204c00610661a45536ca3f06971d9e1 Mon Sep 17 00:00:00 2001 From: indykoning <15870933+indykoning@users.noreply.github.com> Date: Sat, 9 Oct 2021 20:36:38 +0200 Subject: [PATCH 0204/1038] Create devices for Growatt (#57068) --- .../components/growatt_server/sensor.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 804d4157543..599efcb6f42 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -15,6 +15,9 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.const import ( + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_NAME, CONF_NAME, CONF_PASSWORD, CONF_URL, @@ -37,7 +40,13 @@ from homeassistant.const import ( ) from homeassistant.util import Throttle, dt -from .const import CONF_PLANT_ID, DEFAULT_PLANT_ID, DEFAULT_URL, LOGIN_INVALID_AUTH_CODE +from .const import ( + CONF_PLANT_ID, + DEFAULT_PLANT_ID, + DEFAULT_URL, + DOMAIN, + LOGIN_INVALID_AUTH_CODE, +) _LOGGER = logging.getLogger(__name__) @@ -970,6 +979,12 @@ class GrowattInverter(SensorEntity): self._attr_unique_id = unique_id self._attr_icon = "mdi:solar-power" + self._attr_device_info = { + ATTR_IDENTIFIERS: {(DOMAIN, probe.device_id)}, + ATTR_NAME: name, + ATTR_MANUFACTURER: "Growatt", + } + @property def native_value(self): """Return the state of the sensor.""" From b2cfbb7d1e7f81783e88d950a802bc95c82c9b06 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 9 Oct 2021 22:57:13 +0200 Subject: [PATCH 0205/1038] Update frontend to 20211007.1 (#57385) --- 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 f36d72a967c..5b908efb2df 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20211007.0" + "home-assistant-frontend==20211007.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 47f12448430..455f5d2ddaf 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==3.4.8 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211007.0 +home-assistant-frontend==20211007.1 httpx==0.19.0 ifaddr==0.1.7 jinja2==3.0.2 diff --git a/requirements_all.txt b/requirements_all.txt index 84cbbac8d5b..3c34b9142d1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -807,7 +807,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211007.0 +home-assistant-frontend==20211007.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7d9955090a2..a656e6fa1dc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -488,7 +488,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211007.0 +home-assistant-frontend==20211007.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 5829f93b5329bc17ab1b16440f5743d26d95677a Mon Sep 17 00:00:00 2001 From: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> Date: Sat, 9 Oct 2021 17:38:45 -0400 Subject: [PATCH 0206/1038] Bump pynws to 1.3.2 (#57361) --- homeassistant/components/nws/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nws/manifest.json b/homeassistant/components/nws/manifest.json index 30b00fde15a..2e6f58028e0 100644 --- a/homeassistant/components/nws/manifest.json +++ b/homeassistant/components/nws/manifest.json @@ -3,7 +3,7 @@ "name": "National Weather Service (NWS)", "documentation": "https://www.home-assistant.io/integrations/nws", "codeowners": ["@MatthewFlamm"], - "requirements": ["pynws==1.3.1"], + "requirements": ["pynws==1.3.2"], "quality_scale": "platinum", "config_flow": true, "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 3c34b9142d1..7b10d4c8f6c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1667,7 +1667,7 @@ pynuki==1.4.1 pynut2==2.1.2 # homeassistant.components.nws -pynws==1.3.1 +pynws==1.3.2 # homeassistant.components.nx584 pynx584==0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a656e6fa1dc..04e97348fb1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -989,7 +989,7 @@ pynuki==1.4.1 pynut2==2.1.2 # homeassistant.components.nws -pynws==1.3.1 +pynws==1.3.2 # homeassistant.components.nx584 pynx584==0.5 From 6ef70c85ee03f9b0666368745d687f7f101e32cc Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 9 Oct 2021 23:42:20 +0200 Subject: [PATCH 0207/1038] Add -9999 error fix back in Xiaomi Miio (#57399) --- homeassistant/components/xiaomi_miio/__init__.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 11fe4cc1337..840212d3dd6 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -146,12 +146,23 @@ def get_platforms(config_entry): def _async_update_data_default(hass, device): async def update(): """Fetch data from the device using async_add_executor_job.""" - try: + + async def _async_fetch_data(): + """Fetch data from the device.""" async with async_timeout.timeout(10): state = await hass.async_add_executor_job(device.status) _LOGGER.debug("Got new state: %s", state) return state + try: + return await _async_fetch_data() + except DeviceException as ex: + if getattr(ex, "code", None) != -9999: + raise UpdateFailed(ex) from ex + _LOGGER.info("Got exception while fetching the state, trying again: %s", ex) + # Try to fetch the data a second time after error code -9999 + try: + return await _async_fetch_data() except DeviceException as ex: raise UpdateFailed(ex) from ex From d0101f67dae1734789c6699c5ac8a61f09b75e27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 9 Oct 2021 23:46:25 +0200 Subject: [PATCH 0208/1038] Bump pyhaversion to 21.10.0 (#57377) --- 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 de43a47d505..aa8a2659dcd 100644 --- a/homeassistant/components/version/manifest.json +++ b/homeassistant/components/version/manifest.json @@ -3,7 +3,7 @@ "name": "Version", "documentation": "https://www.home-assistant.io/integrations/version", "requirements": [ - "pyhaversion==21.7.0" + "pyhaversion==21.10.0" ], "codeowners": [ "@fabaff", diff --git a/requirements_all.txt b/requirements_all.txt index 7b10d4c8f6c..ced8f731d14 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1511,7 +1511,7 @@ pygtfs==0.1.6 pygti==0.9.2 # homeassistant.components.version -pyhaversion==21.7.0 +pyhaversion==21.10.0 # homeassistant.components.heos pyheos==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 04e97348fb1..26805da4ffc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -884,7 +884,7 @@ pygatt[GATTTOOL]==4.0.5 pygti==0.9.2 # homeassistant.components.version -pyhaversion==21.7.0 +pyhaversion==21.10.0 # homeassistant.components.heos pyheos==0.7.2 From 97187069a748e5fb82251eb6c6692327aa975e50 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Sun, 10 Oct 2021 00:09:01 +0200 Subject: [PATCH 0209/1038] Fix `opentherm_gw.set_clock` `time` parameter name (#57398) --- homeassistant/components/opentherm_gw/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/opentherm_gw/services.yaml b/homeassistant/components/opentherm_gw/services.yaml index 02f2e71053f..fc0b0011d7c 100644 --- a/homeassistant/components/opentherm_gw/services.yaml +++ b/homeassistant/components/opentherm_gw/services.yaml @@ -54,7 +54,7 @@ set_clock: selector: text: time: - name: Name + name: Time description: Optional time in 24h format which will be provided to the thermostat. Defaults to the current time. example: "19:34" selector: From 8b013b823b0871bdfb2d84589739f1efb0145ab9 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Sun, 10 Oct 2021 00:15:21 +0200 Subject: [PATCH 0210/1038] Fix default parameter values for service opentherm_gw.set_clock (#57397) --- homeassistant/components/opentherm_gw/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index e3ec9ddef13..f54a0e783cc 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -156,8 +156,9 @@ def register_services(hass): vol.Required(ATTR_GW_ID): vol.All( cv.string, vol.In(hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS]) ), - vol.Optional(ATTR_DATE, default=date.today()): cv.date, - vol.Optional(ATTR_TIME, default=datetime.now().time()): cv.time, + # pylint: disable=unnecessary-lambda + vol.Optional(ATTR_DATE, default=lambda: date.today()): cv.date, + vol.Optional(ATTR_TIME, default=lambda: datetime.now().time()): cv.time, } ) service_set_control_setpoint_schema = vol.Schema( From 9f34d010e67fe56f45b7bb1eda06506e33f7934a Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 10 Oct 2021 00:57:55 +0100 Subject: [PATCH 0211/1038] Add System Bridge display sensors (#57019) * System Bridge - Add Display Sensors * Omit default --- .../components/system_bridge/__init__.py | 1 + .../components/system_bridge/coordinator.py | 1 + .../components/system_bridge/manifest.json | 2 +- .../components/system_bridge/sensor.py | 55 +++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 60 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py index cba21ac0271..d1da463816f 100644 --- a/homeassistant/components/system_bridge/__init__.py +++ b/homeassistant/components/system_bridge/__init__.py @@ -84,6 +84,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: while ( coordinator.bridge.battery is None or coordinator.bridge.cpu is None + or coordinator.bridge.display is None or coordinator.bridge.filesystem is None or coordinator.bridge.graphics is None or coordinator.bridge.information is None diff --git a/homeassistant/components/system_bridge/coordinator.py b/homeassistant/components/system_bridge/coordinator.py index fb0b63c715a..7610e76b7bb 100644 --- a/homeassistant/components/system_bridge/coordinator.py +++ b/homeassistant/components/system_bridge/coordinator.py @@ -65,6 +65,7 @@ class SystemBridgeDataUpdateCoordinator(DataUpdateCoordinator[Bridge]): [ "battery", "cpu", + "display", "filesystem", "graphics", "memory", diff --git a/homeassistant/components/system_bridge/manifest.json b/homeassistant/components/system_bridge/manifest.json index 73d1d03618f..368262a767b 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": ["systembridge==2.1.0"], + "requirements": ["systembridge==2.1.3"], "codeowners": ["@timmo001"], "zeroconf": ["_system-bridge._udp.local."], "after_dependencies": ["zeroconf"], diff --git a/homeassistant/components/system_bridge/sensor.py b/homeassistant/components/system_bridge/sensor.py index 3e8d2aa7ff0..3787feecf13 100644 --- a/homeassistant/components/system_bridge/sensor.py +++ b/homeassistant/components/system_bridge/sensor.py @@ -23,6 +23,7 @@ from homeassistant.const import ( DEVICE_CLASS_VOLTAGE, ELECTRIC_POTENTIAL_VOLT, FREQUENCY_GIGAHERTZ, + FREQUENCY_HERTZ, FREQUENCY_MEGAHERTZ, PERCENTAGE, POWER_WATT, @@ -42,6 +43,8 @@ ATTR_SIZE: Final = "size" ATTR_TYPE: Final = "type" ATTR_USED: Final = "used" +PIXELS: Final = "px" + @dataclass class SystemBridgeSensorEntityDescription(SensorEntityDescription): @@ -84,6 +87,13 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, value=lambda bridge: bridge.cpu.cpu.voltage, ), + SystemBridgeSensorEntityDescription( + key="displays_connected", + name="Displays Connected", + state_class=STATE_CLASS_MEASUREMENT, + icon="mdi:monitor", + value=lambda bridge: len(bridge.display.displays), + ), SystemBridgeSensorEntityDescription( key="kernel", name="Kernel", @@ -225,6 +235,51 @@ async def async_setup_entry( for description in BATTERY_SENSOR_TYPES: entities.append(SystemBridgeSensor(coordinator, description)) + for index, _ in enumerate(coordinator.data.display.displays): + name = index + 1 + entities = [ + *entities, + SystemBridgeSensor( + coordinator, + SystemBridgeSensorEntityDescription( + key=f"display_{name}_resolution_x", + name=f"Display {name} Resolution X", + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=PIXELS, + icon="mdi:monitor", + value=lambda bridge, i=index: bridge.display.displays[ + i + ].resolutionX, + ), + ), + SystemBridgeSensor( + coordinator, + SystemBridgeSensorEntityDescription( + key=f"display_{name}_resolution_y", + name=f"Display {name} Resolution Y", + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=PIXELS, + icon="mdi:monitor", + value=lambda bridge, i=index: bridge.display.displays[ + i + ].resolutionY, + ), + ), + SystemBridgeSensor( + coordinator, + SystemBridgeSensorEntityDescription( + key=f"display_{name}_refresh_rate", + name=f"Display {name} Refresh Rate", + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=FREQUENCY_HERTZ, + icon="mdi:monitor", + value=lambda bridge, i=index: bridge.display.displays[ + i + ].currentRefreshRate, + ), + ), + ] + for index, _ in enumerate(coordinator.data.graphics.controllers): if coordinator.data.graphics.controllers[index].name is not None: # Remove vendor from name diff --git a/requirements_all.txt b/requirements_all.txt index ced8f731d14..481b8d5e220 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2266,7 +2266,7 @@ swisshydrodata==0.1.0 synology-srm==0.2.0 # homeassistant.components.system_bridge -systembridge==2.1.0 +systembridge==2.1.3 # homeassistant.components.tahoma tahoma-api==0.0.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 26805da4ffc..a40976d974d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1303,7 +1303,7 @@ sunwatcher==0.2.1 surepy==0.7.2 # homeassistant.components.system_bridge -systembridge==2.1.0 +systembridge==2.1.3 # homeassistant.components.tellduslive tellduslive==0.10.11 From 80ee5834180cfed9fbacd5726466a4271e1909b4 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 10 Oct 2021 00:12:54 +0000 Subject: [PATCH 0212/1038] [ci skip] Translation update --- .../stookalert/translations/zh-Hant.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 homeassistant/components/stookalert/translations/zh-Hant.json diff --git a/homeassistant/components/stookalert/translations/zh-Hant.json b/homeassistant/components/stookalert/translations/zh-Hant.json new file mode 100644 index 00000000000..4e570d2a943 --- /dev/null +++ b/homeassistant/components/stookalert/translations/zh-Hant.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "step": { + "user": { + "data": { + "province": "\u7701" + } + } + } + } +} \ No newline at end of file From 49d97e13de932d7dea08e3e9a741830289597233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Sun, 10 Oct 2021 07:49:02 +0100 Subject: [PATCH 0213/1038] Improve Whirlpool component code quality (#57357) * Improve Whirlpool component code This implements a few suggestions given in https://github.com/home-assistant/core/pull/48346#pullrequestreview-773552670 * Add return typing Co-authored-by: Martin Hjelmare * Add reason assertion to config_flow test Co-authored-by: Martin Hjelmare --- homeassistant/components/whirlpool/climate.py | 36 +++++++++++-------- .../components/whirlpool/config_flow.py | 10 ++++-- tests/components/whirlpool/test_climate.py | 31 ++++++++-------- .../components/whirlpool/test_config_flow.py | 35 ++++++++++++++++++ 4 files changed, 81 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/whirlpool/climate.py b/homeassistant/components/whirlpool/climate.py index 811b05435bd..bd8cb0505fa 100644 --- a/homeassistant/components/whirlpool/climate.py +++ b/homeassistant/components/whirlpool/climate.py @@ -48,6 +48,18 @@ AIRCON_FANSPEED_MAP = { FAN_MODE_TO_AIRCON_FANSPEED = {v: k for k, v in AIRCON_FANSPEED_MAP.items()} +SUPPORTED_FAN_MODES = [FAN_AUTO, FAN_HIGH, FAN_MEDIUM, FAN_LOW, FAN_OFF] +SUPPORTED_HVAC_MODES = [ + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_OFF, +] +SUPPORTED_MAX_TEMP = 30 +SUPPORTED_MIN_TEMP = 16 +SUPPORTED_SWING_MODES = [SWING_HORIZONTAL, SWING_OFF] +SUPPORTED_TARGET_TEMPERATURE_STEP = 1 + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up entry.""" @@ -66,20 +78,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class AirConEntity(ClimateEntity): """Representation of an air conditioner.""" - _attr_fan_modes = [FAN_AUTO, FAN_HIGH, FAN_MEDIUM, FAN_LOW, FAN_OFF] - _attr_hvac_modes = [ - HVAC_MODE_COOL, - HVAC_MODE_HEAT, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_OFF, - ] - _attr_max_temp = 30 - _attr_min_temp = 16 + _attr_fan_modes = SUPPORTED_FAN_MODES + _attr_hvac_modes = SUPPORTED_HVAC_MODES + _attr_max_temp = SUPPORTED_MAX_TEMP + _attr_min_temp = SUPPORTED_MIN_TEMP _attr_supported_features = ( SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | SUPPORT_SWING_MODE ) - _attr_swing_modes = [SWING_HORIZONTAL, SWING_OFF] - _attr_target_temperature_step = 1 + _attr_swing_modes = SUPPORTED_SWING_MODES + _attr_target_temperature_step = SUPPORTED_TARGET_TEMPERATURE_STEP _attr_temperature_unit = TEMP_CELSIUS _attr_should_poll = False @@ -141,7 +148,7 @@ class AirConEntity(ClimateEntity): return HVAC_MODE_OFF mode: AirconMode = self._aircon.get_mode() - return AIRCON_MODE_MAP.get(mode, None) + return AIRCON_MODE_MAP.get(mode) async def async_set_hvac_mode(self, hvac_mode): """Set HVAC mode.""" @@ -151,8 +158,7 @@ class AirConEntity(ClimateEntity): mode = HVAC_MODE_TO_AIRCON_MODE.get(hvac_mode) if not mode: - _LOGGER.warning("Unexpected hvac mode: %s", hvac_mode) - return + raise ValueError(f"Invalid hvac mode {hvac_mode}") await self._aircon.set_mode(mode) if not self._aircon.get_power_on(): @@ -168,7 +174,7 @@ class AirConEntity(ClimateEntity): """Set fan mode.""" fanspeed = FAN_MODE_TO_AIRCON_FANSPEED.get(fan_mode) if not fanspeed: - return + raise ValueError(f"Invalid fan mode {fan_mode}") await self._aircon.set_fanspeed(fanspeed) @property diff --git a/homeassistant/components/whirlpool/config_flow.py b/homeassistant/components/whirlpool/config_flow.py index c7ec37290cb..ac6cb3d568e 100644 --- a/homeassistant/components/whirlpool/config_flow.py +++ b/homeassistant/components/whirlpool/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Whirlpool Sixth Sense integration.""" +from __future__ import annotations + import asyncio import logging @@ -13,10 +15,14 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -STEP_USER_DATA_SCHEMA = vol.Schema({CONF_USERNAME: str, CONF_PASSWORD: str}) +STEP_USER_DATA_SCHEMA = vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} +) -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input( + hass: core.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. diff --git a/tests/components/whirlpool/test_climate.py b/tests/components/whirlpool/test_climate.py index 77009607947..314c6c2685b 100644 --- a/tests/components/whirlpool/test_climate.py +++ b/tests/components/whirlpool/test_climate.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock, MagicMock import aiohttp +import pytest import whirlpool from homeassistant.components.climate.const import ( @@ -271,13 +272,14 @@ async def test_service_calls(hass: HomeAssistant, mock_aircon_api: MagicMock): ) mock_aircon_api.return_value.set_mode.reset_mock() - # HVAC_MODE_DRY should be ignored - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: "climate.said1", ATTR_HVAC_MODE: HVAC_MODE_DRY}, - blocking=True, - ) + # HVAC_MODE_DRY is not supported + with pytest.raises(ValueError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: "climate.said1", ATTR_HVAC_MODE: HVAC_MODE_DRY}, + blocking=True, + ) mock_aircon_api.return_value.set_mode.assert_not_called() mock_aircon_api.return_value.set_mode.reset_mock() @@ -325,13 +327,14 @@ async def test_service_calls(hass: HomeAssistant, mock_aircon_api: MagicMock): ) mock_aircon_api.return_value.set_fanspeed.reset_mock() - # FAN_MIDDLE should be ignored - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: "climate.said1", ATTR_FAN_MODE: FAN_MIDDLE}, - blocking=True, - ) + # FAN_MIDDLE is not supported + with pytest.raises(ValueError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: "climate.said1", ATTR_FAN_MODE: FAN_MIDDLE}, + blocking=True, + ) mock_aircon_api.return_value.set_fanspeed.assert_not_called() mock_aircon_api.return_value.set_fanspeed.reset_mock() diff --git a/tests/components/whirlpool/test_config_flow.py b/tests/components/whirlpool/test_config_flow.py index 6746e406a85..721a4da9383 100644 --- a/tests/components/whirlpool/test_config_flow.py +++ b/tests/components/whirlpool/test_config_flow.py @@ -7,6 +7,8 @@ import aiohttp from homeassistant import config_entries from homeassistant.components.whirlpool.const import DOMAIN +from tests.common import MockConfigEntry + async def test_form(hass): """Test we get the form.""" @@ -120,3 +122,36 @@ async def test_form_generic_auth_exception(hass): ) assert result2["type"] == "form" assert result2["errors"] == {"base": "unknown"} + + +async def test_form_already_configured(hass): + """Test we handle cannot connect error.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={"username": "test-username", "password": "test-password"}, + unique_id="test-username", + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == config_entries.SOURCE_USER + + with patch("homeassistant.components.whirlpool.config_flow.Auth.do_auth"), patch( + "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", + return_value=True, + ): + 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"] == "abort" + assert result2["reason"] == "already_configured" From be8724a6f8e3d2612447f8ef394983961bcb59b9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 9 Oct 2021 21:00:28 -1000 Subject: [PATCH 0214/1038] Do all of dhcp scapy startup in the executor (#57392) --- homeassistant/components/dhcp/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index 61208ac6423..3e4fd8fec01 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -276,6 +276,10 @@ class DHCPWatcher(WatcherBase): self._sniffer.stop() async def async_start(self): + """Start watching for dhcp packets.""" + await self.hass.async_add_executor_job(self._start) + + def _start(self): """Start watching for dhcp packets.""" # Local import because importing from scapy has side effects such as opening # sockets @@ -319,7 +323,7 @@ class DHCPWatcher(WatcherBase): conf.sniff_promisc = 0 try: - await self.hass.async_add_executor_job(_verify_l2socket_setup, FILTER) + _verify_l2socket_setup(FILTER) except (Scapy_Exception, OSError) as ex: if os.geteuid() == 0: _LOGGER.error("Cannot watch for dhcp packets: %s", ex) @@ -330,7 +334,7 @@ class DHCPWatcher(WatcherBase): return try: - await self.hass.async_add_executor_job(_verify_working_pcap, FILTER) + _verify_working_pcap(FILTER) except (Scapy_Exception, ImportError) as ex: _LOGGER.error( "Cannot watch for dhcp packets without a functional packet filter: %s", From 45b60b83469c300833d486aadc3827a3a08df22f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 9 Oct 2021 21:01:18 -1000 Subject: [PATCH 0215/1038] Remove executor calls in isy994 as its fully async (#57394) --- homeassistant/components/isy994/__init__.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 02bbea29bdb..a5ceeaea1d8 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -205,21 +205,18 @@ async def async_setup_entry( # Load platforms for the devices in the ISY controller that we support. hass.config_entries.async_setup_platforms(entry, PLATFORMS) - def _start_auto_update() -> None: - """Start isy auto update.""" - _LOGGER.debug("ISY Starting Event Stream and automatic updates") - isy.websocket.start() - - def _stop_auto_update(event) -> None: + @callback + def _async_stop_auto_update(event) -> None: """Stop the isy auto update on Home Assistant Shutdown.""" _LOGGER.debug("ISY Stopping Event Stream and automatic updates") isy.websocket.stop() - await hass.async_add_executor_job(_start_auto_update) + _LOGGER.debug("ISY Starting Event Stream and automatic updates") + isy.websocket.start() entry.async_on_unload(entry.add_update_listener(_async_update_listener)) entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_auto_update) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_auto_update) ) # Register Integration-wide Services: From a58085639e78a5ba34ea063762e8e4dc2dc42c94 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 9 Oct 2021 21:01:45 -1000 Subject: [PATCH 0216/1038] Restore yeelight workaround for failing to update state after on/off (#57400) --- homeassistant/components/yeelight/__init__.py | 4 +- homeassistant/components/yeelight/light.py | 43 ++++++++++- tests/components/yeelight/test_light.py | 74 ++++++++++++++++++- 3 files changed, 116 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index a1dce44893b..fb908775d1b 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -36,8 +36,8 @@ from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) -STATE_CHANGE_TIME = 0.25 # seconds - +STATE_CHANGE_TIME = 0.40 # seconds +POWER_STATE_CHANGE_TIME = 1 # seconds DOMAIN = "yeelight" DATA_YEELIGHT = DOMAIN diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 69dde0e75b6..67c9dc2ba07 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -38,6 +38,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.event import async_call_later import homeassistant.util.color as color_util from homeassistant.util.color import ( color_temperature_kelvin_to_mired as kelvin_to_mired, @@ -62,6 +63,7 @@ from . import ( DATA_DEVICE, DATA_UPDATED, DOMAIN, + POWER_STATE_CHANGE_TIME, YEELIGHT_FLOW_TRANSITION_SCHEMA, YeelightEntity, ) @@ -247,7 +249,7 @@ def _async_cmd(func): except BULB_NETWORK_EXCEPTIONS as ex: # A network error happened, the bulb is likely offline now self.device.async_mark_unavailable() - self.async_write_ha_state() + self.async_state_changed() exc_message = str(ex) or type(ex) raise HomeAssistantError( f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {exc_message}" @@ -419,13 +421,22 @@ class YeelightGenericLight(YeelightEntity, LightEntity): else: self._custom_effects = {} + self._unexpected_state_check = None + + @callback + def async_state_changed(self): + """Call when the device changes state.""" + if not self._device.available: + self._async_cancel_pending_state_check() + self.async_write_ha_state() + async def async_added_to_hass(self): """Handle entity which will be added.""" self.async_on_remove( async_dispatcher_connect( self.hass, DATA_UPDATED.format(self._device.host), - self.async_write_ha_state, + self.async_state_changed, ) ) await super().async_added_to_hass() @@ -760,6 +771,33 @@ class YeelightGenericLight(YeelightEntity, LightEntity): if self.config[CONF_SAVE_ON_CHANGE] and (brightness or colortemp or rgb): await self.async_set_default() + self._async_schedule_state_check(True) + + @callback + def _async_cancel_pending_state_check(self): + """Cancel a pending state check.""" + if self._unexpected_state_check: + self._unexpected_state_check() + self._unexpected_state_check = None + + @callback + def _async_schedule_state_check(self, expected_power_state): + """Schedule a poll if the change failed to get pushed back to us. + + Some devices (mainly nightlights) will not send back the on state + so we need to force a refresh. + """ + self._async_cancel_pending_state_check() + + async def _async_update_if_state_unexpected(*_): + self._unexpected_state_check = None + if self.is_on != expected_power_state: + await self.device.async_update(True) + + self._unexpected_state_check = async_call_later( + self.hass, POWER_STATE_CHANGE_TIME, _async_update_if_state_unexpected + ) + @_async_cmd async def _async_turn_off(self, duration) -> None: """Turn off with a given transition duration wrapped with _async_cmd.""" @@ -775,6 +813,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s await self._async_turn_off(duration) + self._async_schedule_state_check(False) @_async_cmd async def async_set_mode(self, mode: str): diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index fd6e12f2635..9c5a76e4a4b 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -1,5 +1,6 @@ """Test the Yeelight light.""" import asyncio +from datetime import timedelta import logging import socket from unittest.mock import ANY, AsyncMock, MagicMock, call, patch @@ -98,6 +99,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from homeassistant.util.color import ( color_hs_to_RGB, color_hs_to_xy, @@ -121,7 +123,7 @@ from . import ( _patch_discovery_interval, ) -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed CONFIG_ENTRY_DATA = { CONF_HOST: IP_ADDRESS, @@ -1377,3 +1379,73 @@ async def test_ambilight_with_nightlight_disabled(hass: HomeAssistant): assert state.state == "on" # bg_power off should not set the brightness to 0 assert state.attributes[ATTR_BRIGHTNESS] == 128 + + +async def test_state_fails_to_update_triggers_update(hass: HomeAssistant): + """Ensure we call async_get_properties if the turn on/off fails to update the state.""" + mocked_bulb = _mocked_bulb() + properties = {**PROPERTIES} + properties.pop("active_mode") + properties["color_mode"] = "3" # HSV + mocked_bulb.last_properties = properties + mocked_bulb.bulb_type = BulbType.Color + config_entry = MockConfigEntry( + domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: False} + ) + config_entry.add_to_hass(hass) + with _patch_discovery(), _patch_discovery_interval(), patch( + f"{MODULE}.AsyncBulb", return_value=mocked_bulb + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + # We use asyncio.create_task now to avoid + # blocking starting so we need to block again + await hass.async_block_till_done() + + assert len(mocked_bulb.async_get_properties.mock_calls) == 1 + + mocked_bulb.last_properties["power"] = "off" + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ENTITY_LIGHT, + }, + blocking=True, + ) + assert len(mocked_bulb.async_turn_on.mock_calls) == 1 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1)) + await hass.async_block_till_done() + assert len(mocked_bulb.async_get_properties.mock_calls) == 2 + + mocked_bulb.last_properties["power"] = "on" + for _ in range(5): + await hass.services.async_call( + "light", + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: ENTITY_LIGHT, + }, + blocking=True, + ) + assert len(mocked_bulb.async_turn_off.mock_calls) == 5 + # Even with five calls we only do one state request + # since each successive call should cancel the unexpected + # state check + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=2)) + await hass.async_block_till_done() + assert len(mocked_bulb.async_get_properties.mock_calls) == 3 + + # But if the state is correct no calls + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ENTITY_LIGHT, + }, + blocking=True, + ) + assert len(mocked_bulb.async_turn_on.mock_calls) == 1 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=3)) + await hass.async_block_till_done() + assert len(mocked_bulb.async_get_properties.mock_calls) == 3 From 5b3711ed198f216f638aee3995eb16ebfc92c676 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 9 Oct 2021 21:02:33 -1000 Subject: [PATCH 0217/1038] Use switch format unique ids for tplink dimmers (#57346) --- homeassistant/components/tplink/__init__.py | 47 +++++++++++- homeassistant/components/tplink/light.py | 10 ++- tests/components/tplink/__init__.py | 29 +++++++- tests/components/tplink/test_init.py | 81 ++++++++++++++++++++- 4 files changed, 163 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index ff0526490f5..d17815d2344 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import network +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.config_entries import ConfigEntry, ConfigEntryNotReady from homeassistant.const import ( CONF_HOST, @@ -19,7 +20,11 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STARTED, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + entity_registry as er, +) from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType @@ -158,12 +163,52 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except SmartDeviceException as ex: raise ConfigEntryNotReady from ex + if device.is_dimmer: + async_fix_dimmer_unique_id(hass, entry, device) + hass.data[DOMAIN][entry.entry_id] = TPLinkDataUpdateCoordinator(hass, device) hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True +@callback +def async_fix_dimmer_unique_id( + hass: HomeAssistant, entry: ConfigEntry, device: SmartDevice +) -> None: + """Migrate the unique id of dimmers back to the legacy one. + + Dimmers used to use the switch format since pyHS100 treated them as SmartPlug but + the old code created them as lights + + https://github.com/home-assistant/core/blob/2021.9.7/homeassistant/components/tplink/common.py#L86 + """ + + # This is the unique id before 2021.0/2021.1 + original_unique_id = legacy_device_id(device) + + # This is the unique id that was used in 2021.0/2021.1 rollout + rollout_unique_id = device.mac.replace(":", "").upper() + + entity_registry = er.async_get(hass) + + rollout_entity_id = entity_registry.async_get_entity_id( + LIGHT_DOMAIN, DOMAIN, rollout_unique_id + ) + original_entry_id = entity_registry.async_get_entity_id( + LIGHT_DOMAIN, DOMAIN, original_unique_id + ) + + # If they are now using the 2021.0/2021.1 rollout entity id + # and have deleted the original entity id, we want to update that entity id + # so they don't end up with another _2 entity, but only if they deleted + # the original + if rollout_entity_id and not original_entry_id: + entity_registry.async_update_entity( + rollout_entity_id, new_unique_id=original_unique_id + ) + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" hass_data: dict[str, Any] = hass.data[DOMAIN] diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 3f4b130a5cc..ad423e84fa5 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -26,6 +26,7 @@ from homeassistant.util.color import ( color_temperature_mired_to_kelvin as mired_to_kelvin, ) +from . import legacy_device_id from .const import DOMAIN from .coordinator import TPLinkDataUpdateCoordinator from .entity import CoordinatedTPLinkEntity, async_refresh_after @@ -58,7 +59,14 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity): """Initialize the switch.""" super().__init__(device, coordinator) # For backwards compat with pyHS100 - self._attr_unique_id = self.device.mac.replace(":", "").upper() + if self.device.is_dimmer: + # Dimmers used to use the switch format since + # pyHS100 treated them as SmartPlug but the old code + # created them as lights + # https://github.com/home-assistant/core/blob/2021.9.7/homeassistant/components/tplink/common.py#L86 + self._attr_unique_id = legacy_device_id(device) + else: + self._attr_unique_id = self.device.mac.replace(":", "").upper() @async_refresh_after async def async_turn_on(self, **kwargs: Any) -> None: diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index f25fc13784a..4e6dbb9dae7 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, MagicMock, patch -from kasa import SmartBulb, SmartPlug, SmartStrip +from kasa import SmartBulb, SmartDimmer, SmartPlug, SmartStrip from kasa.exceptions import SmartDeviceException from kasa.protocol import TPLinkSmartHomeProtocol @@ -48,6 +48,33 @@ def _mocked_bulb() -> SmartBulb: return bulb +def _mocked_dimmer() -> SmartDimmer: + dimmer = MagicMock(auto_spec=SmartDimmer) + dimmer.update = AsyncMock() + dimmer.mac = MAC_ADDRESS + dimmer.alias = ALIAS + dimmer.model = MODEL + dimmer.host = IP_ADDRESS + dimmer.brightness = 50 + dimmer.color_temp = 4000 + dimmer.is_color = True + dimmer.is_strip = False + dimmer.is_plug = False + dimmer.is_dimmer = True + dimmer.hsv = (10, 30, 5) + dimmer.device_id = MAC_ADDRESS + dimmer.valid_temperature_range.min = 4000 + dimmer.valid_temperature_range.max = 9000 + dimmer.hw_info = {"sw_ver": "1.0.0"} + dimmer.turn_off = AsyncMock() + dimmer.turn_on = AsyncMock() + dimmer.set_brightness = AsyncMock() + dimmer.set_hsv = AsyncMock() + dimmer.set_color_temp = AsyncMock() + dimmer.protocol = _mock_protocol() + return dimmer + + def _mocked_plug() -> SmartPlug: plug = MagicMock(auto_spec=SmartPlug) plug.update = AsyncMock() diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index c166fccc9b5..73edc63e28c 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -4,14 +4,23 @@ from __future__ import annotations from datetime import timedelta from unittest.mock import MagicMock, patch +from homeassistant import setup from homeassistant.components import tplink from homeassistant.components.tplink.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_registry import EntityRegistry from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from . import IP_ADDRESS, MAC_ADDRESS, _patch_discovery, _patch_single_discovery +from . import ( + IP_ADDRESS, + MAC_ADDRESS, + _mocked_dimmer, + _patch_discovery, + _patch_single_discovery, +) from tests.common import MockConfigEntry, async_fire_time_changed @@ -63,3 +72,73 @@ async def test_config_entry_retry(hass): await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_dimmer_switch_unique_id_fix_original_entity_was_deleted( + hass: HomeAssistant, entity_reg: EntityRegistry +): + """Test that roll out unique id entity id changed to the original unique id.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=MAC_ADDRESS) + config_entry.add_to_hass(hass) + dimmer = _mocked_dimmer() + rollout_unique_id = MAC_ADDRESS.replace(":", "").upper() + original_unique_id = tplink.legacy_device_id(dimmer) + rollout_dimmer_entity_reg = entity_reg.async_get_or_create( + config_entry=config_entry, + platform=DOMAIN, + domain="light", + unique_id=rollout_unique_id, + original_name="Rollout dimmer", + ) + + with _patch_discovery(device=dimmer), _patch_single_discovery(device=dimmer): + await setup.async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + migrated_dimmer_entity_reg = entity_reg.async_get_or_create( + config_entry=config_entry, + platform=DOMAIN, + domain="light", + unique_id=original_unique_id, + original_name="Migrated dimmer", + ) + assert migrated_dimmer_entity_reg.entity_id == rollout_dimmer_entity_reg.entity_id + + +async def test_dimmer_switch_unique_id_fix_original_entity_still_exists( + hass: HomeAssistant, entity_reg: EntityRegistry +): + """Test no migration happens if the original entity id still exists.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=MAC_ADDRESS) + config_entry.add_to_hass(hass) + dimmer = _mocked_dimmer() + rollout_unique_id = MAC_ADDRESS.replace(":", "").upper() + original_unique_id = tplink.legacy_device_id(dimmer) + original_dimmer_entity_reg = entity_reg.async_get_or_create( + config_entry=config_entry, + platform=DOMAIN, + domain="light", + unique_id=original_unique_id, + original_name="Original dimmer", + ) + rollout_dimmer_entity_reg = entity_reg.async_get_or_create( + config_entry=config_entry, + platform=DOMAIN, + domain="light", + unique_id=rollout_unique_id, + original_name="Rollout dimmer", + ) + + with _patch_discovery(device=dimmer), _patch_single_discovery(device=dimmer): + await setup.async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + migrated_dimmer_entity_reg = entity_reg.async_get_or_create( + config_entry=config_entry, + platform=DOMAIN, + domain="light", + unique_id=original_unique_id, + original_name="Migrated dimmer", + ) + assert migrated_dimmer_entity_reg.entity_id == original_dimmer_entity_reg.entity_id + assert migrated_dimmer_entity_reg.entity_id != rollout_dimmer_entity_reg.entity_id From 3efbd6a1c90fec5ded04394ad3b16c7af6c29c84 Mon Sep 17 00:00:00 2001 From: icemanch Date: Sun, 10 Oct 2021 15:18:15 -0400 Subject: [PATCH 0218/1038] Flux led color support (#57353) Co-authored-by: J. Nick Koston --- homeassistant/components/flux_led/__init__.py | 2 +- .../components/flux_led/config_flow.py | 18 - homeassistant/components/flux_led/const.py | 1 - homeassistant/components/flux_led/light.py | 303 +++++++------ .../components/flux_led/manifest.json | 3 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/flux_led/__init__.py | 19 +- tests/components/flux_led/test_config_flow.py | 2 - tests/components/flux_led/test_light.py | 405 ++++++++++++++---- 10 files changed, 511 insertions(+), 246 deletions(-) diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index fa3ad23a41c..99d75e884b5 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -28,7 +28,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS: Final = ["light"] DISCOVERY_INTERVAL: Final = timedelta(minutes=15) -REQUEST_REFRESH_DELAY: Final = 0.65 +REQUEST_REFRESH_DELAY: Final = 1.5 async def async_wifi_bulb_for_host(hass: HomeAssistant, host: str) -> WifiLedBulb: diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index 206c8f91433..02ddd4a1530 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -13,7 +13,6 @@ from homeassistant.const import CONF_HOST, CONF_MAC, CONF_MODE, CONF_NAME, CONF_ from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import device_registry as dr -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import DiscoveryInfoType from . import async_discover_devices, async_wifi_bulb_for_host @@ -28,10 +27,6 @@ from .const import ( FLUX_LED_EXCEPTIONS, FLUX_MAC, FLUX_MODEL, - MODE_AUTO, - MODE_RGB, - MODE_RGBW, - MODE_WHITE, TRANSITION_GRADUAL, TRANSITION_JUMP, TRANSITION_STROBE, @@ -233,19 +228,6 @@ class OptionsFlow(config_entries.OptionsFlow): options = self._config_entry.options options_schema = vol.Schema( { - vol.Required( - CONF_MODE, default=options.get(CONF_MODE, MODE_AUTO) - ): vol.All( - cv.string, - vol.In( - [ - MODE_AUTO, - MODE_RGBW, - MODE_RGB, - MODE_WHITE, - ] - ), - ), vol.Optional( CONF_CUSTOM_EFFECT_COLORS, default=options.get(CONF_CUSTOM_EFFECT_COLORS, ""), diff --git a/homeassistant/components/flux_led/const.py b/homeassistant/components/flux_led/const.py index 6b2c4a8dace..4c8a924df98 100644 --- a/homeassistant/components/flux_led/const.py +++ b/homeassistant/components/flux_led/const.py @@ -28,7 +28,6 @@ MODE_AUTO: Final = "auto" MODE_RGB: Final = "rgb" MODE_RGBW: Final = "rgbw" - # This mode enables white value to be controlled by brightness. # RGB value is ignored when this mode is specified. MODE_WHITE: Final = "w" diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 8c14a1c22b6..a9b8bd32e32 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -8,6 +8,21 @@ import random from typing import Any, Final, cast from flux_led import WifiLedBulb +from flux_led.const import ( + COLOR_MODE_CCT as FLUX_COLOR_MODE_CCT, + COLOR_MODE_DIM as FLUX_COLOR_MODE_DIM, + COLOR_MODE_RGB as FLUX_COLOR_MODE_RGB, + COLOR_MODE_RGBW as FLUX_COLOR_MODE_RGBW, + COLOR_MODE_RGBWW as FLUX_COLOR_MODE_RGBWW, +) +from flux_led.device import MAX_TEMP, MIN_TEMP +from flux_led.utils import ( + color_temp_to_white_levels, + rgbcw_brightness, + rgbcw_to_rgbwc, + rgbw_brightness, + rgbww_brightness, +) import voluptuous as vol from homeassistant import config_entries @@ -15,16 +30,22 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, - ATTR_HS_COLOR, - ATTR_WHITE_VALUE, + ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, + ATTR_WHITE, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_ONOFF, + COLOR_MODE_RGB, + COLOR_MODE_RGBW, + COLOR_MODE_RGBWW, + COLOR_MODE_WHITE, EFFECT_COLORLOOP, EFFECT_RANDOM, PLATFORM_SCHEMA, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, - SUPPORT_WHITE_VALUE, + SUPPORT_TRANSITION, LightEntity, ) from homeassistant.const import ( @@ -46,7 +67,10 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity -import homeassistant.util.color as color_util +from homeassistant.util.color import ( + color_temperature_kelvin_to_mired, + color_temperature_mired_to_kelvin, +) from . import FluxLedUpdateCoordinator from .const import ( @@ -74,7 +98,16 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -SUPPORT_FLUX_LED: Final = SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | SUPPORT_COLOR +SUPPORT_FLUX_LED: Final = SUPPORT_EFFECT | SUPPORT_TRANSITION + + +FLUX_COLOR_MODE_TO_HASS: Final = { + FLUX_COLOR_MODE_RGB: COLOR_MODE_RGB, + FLUX_COLOR_MODE_RGBW: COLOR_MODE_RGBW, + FLUX_COLOR_MODE_RGBWW: COLOR_MODE_RGBWW, + FLUX_COLOR_MODE_CCT: COLOR_MODE_COLOR_TEMP, + FLUX_COLOR_MODE_DIM: COLOR_MODE_WHITE, +} # Constant color temp values for 2 flux_led special modes @@ -128,8 +161,6 @@ EFFECT_MAP: Final = { EFFECT_ID_NAME: Final = {v: k for k, v in EFFECT_MAP.items()} EFFECT_CUSTOM_CODE: Final = 0x60 -WHITE_MODES: Final = {MODE_RGBW} - FLUX_EFFECT_LIST: Final = sorted(EFFECT_MAP) + [EFFECT_RANDOM] SERVICE_CUSTOM_EFFECT: Final = "set_custom_effect" @@ -243,7 +274,6 @@ async def async_setup_entry( coordinator, entry.unique_id, entry.data[CONF_NAME], - options.get(CONF_MODE) or MODE_AUTO, list(custom_effect_colors), options.get(CONF_CUSTOM_EFFECT_SPEED_PCT, DEFAULT_EFFECT_SPEED), options.get(CONF_CUSTOM_EFFECT_TRANSITION, TRANSITION_GRADUAL), @@ -262,7 +292,6 @@ class FluxLight(CoordinatorEntity, LightEntity): coordinator: FluxLedUpdateCoordinator, unique_id: str | None, name: str, - mode: str, custom_effect_colors: list[tuple[int, int, int]], custom_effect_speed_pct: int, custom_effect_transition: str, @@ -272,18 +301,29 @@ class FluxLight(CoordinatorEntity, LightEntity): self._bulb: WifiLedBulb = coordinator.device self._attr_name = name self._attr_unique_id = unique_id - self._ip_address = coordinator.host - self._mode = mode + self._attr_supported_features = SUPPORT_FLUX_LED + self._attr_min_mireds = ( + color_temperature_kelvin_to_mired(MAX_TEMP) + 1 + ) # for rounding + self._attr_max_mireds = color_temperature_kelvin_to_mired(MIN_TEMP) + self._attr_supported_color_modes = { + FLUX_COLOR_MODE_TO_HASS.get(mode, COLOR_MODE_ONOFF) + for mode in self._bulb.color_modes + } + self._attr_effect_list = FLUX_EFFECT_LIST + if custom_effect_colors: + self._attr_effect_list = [*FLUX_EFFECT_LIST, EFFECT_CUSTOM] self._custom_effect_colors = custom_effect_colors self._custom_effect_speed_pct = custom_effect_speed_pct self._custom_effect_transition = custom_effect_transition - old_protocol = self._bulb.protocol == "LEDENET_ORIGINAL" if self.unique_id: + old_protocol = self._bulb.protocol == "LEDENET_ORIGINAL" + raw_state = self._bulb.raw_state self._attr_device_info = { "connections": {(dr.CONNECTION_NETWORK_MAC, self.unique_id)}, - ATTR_MODEL: f"0x{self._bulb.raw_state[1]:02X}", - ATTR_SW_VERSION: "1" if old_protocol else str(self._bulb.raw_state[10]), + ATTR_MODEL: f"0x{self._bulb.model_num:02X}", ATTR_NAME: self.name, + ATTR_SW_VERSION: "1" if old_protocol else str(raw_state.version_number), ATTR_MANUFACTURER: "FluxLED/Magic Home", } @@ -295,49 +335,53 @@ class FluxLight(CoordinatorEntity, LightEntity): @property def brightness(self) -> int: """Return the brightness of this light between 0..255.""" - if self._mode == MODE_WHITE: - return self.white_value return cast(int, self._bulb.brightness) @property - def hs_color(self) -> tuple[float, float] | None: - """Return the color property.""" - return color_util.color_RGB_to_hs(*self._bulb.getRgb()) + def color_temp(self) -> int: + """Return the kelvin value of this light in mired.""" + return color_temperature_kelvin_to_mired(self._bulb.getWhiteTemperature()[0]) @property - def supported_features(self) -> int: - """Flag supported features.""" - if self._mode == MODE_WHITE: - return SUPPORT_BRIGHTNESS - if self._mode in WHITE_MODES: - return SUPPORT_FLUX_LED | SUPPORT_WHITE_VALUE | SUPPORT_COLOR_TEMP - return SUPPORT_FLUX_LED + def rgb_color(self) -> tuple[int, int, int]: + """Return the rgb color value.""" + rgb: tuple[int, int, int] = self._bulb.rgb + return rgb @property - def white_value(self) -> int: - """Return the white value of this light between 0..255.""" - return cast(int, self._bulb.getRgbw()[3]) + def rgbw_color(self) -> tuple[int, int, int, int]: + """Return the rgbw color value.""" + rgbw: tuple[int, int, int, int] = self._bulb.rgbw + return rgbw @property - def effect_list(self) -> list[str]: - """Return the list of supported effects.""" - if self._custom_effect_colors: - return FLUX_EFFECT_LIST + [EFFECT_CUSTOM] - return FLUX_EFFECT_LIST + def rgbww_color(self) -> tuple[int, int, int, int, int]: + """Return the rgbww aka rgbcw color value.""" + rgbcw: tuple[int, int, int, int, int] = self._bulb.rgbcw + return rgbcw + + @property + def rgbwc_color(self) -> tuple[int, int, int, int, int]: + """Return the rgbwc color value.""" + rgbwc: tuple[int, int, int, int, int] = self._bulb.rgbww + return rgbwc + + @property + def color_mode(self) -> str: + """Return the color mode of the light.""" + return FLUX_COLOR_MODE_TO_HASS.get(self._bulb.color_mode, COLOR_MODE_ONOFF) @property def effect(self) -> str | None: """Return the current effect.""" - if (current_mode := self._bulb.raw_state[3]) == EFFECT_CUSTOM_CODE: + if (current_mode := self._bulb.raw_state.preset_pattern) == EFFECT_CUSTOM_CODE: return EFFECT_CUSTOM return EFFECT_ID_NAME.get(current_mode) @property def extra_state_attributes(self) -> dict[str, str]: """Return the attributes.""" - return { - "ip_address": self._ip_address, - } + return {"ip_address": self._bulb.ipaddr} async def async_turn_on(self, **kwargs: Any) -> None: """Turn the specified or all lights on.""" @@ -349,77 +393,101 @@ class FluxLight(CoordinatorEntity, LightEntity): """Turn the specified or all lights on.""" if not self.is_on: self._bulb.turnOn() + if not kwargs: + return - if hs_color := kwargs.get(ATTR_HS_COLOR): - rgb: tuple[int, int, int] | None = color_util.color_hs_to_RGB(*hs_color) - else: - rgb = None - - brightness = kwargs.get(ATTR_BRIGHTNESS) - # handle special modes - if (color_temp := kwargs.get(ATTR_COLOR_TEMP)) is not None: - if brightness is None: - brightness = self.brightness - if color_temp > COLOR_TEMP_WARM_VS_COLD_WHITE_CUT_OFF: - self._bulb.setRgbw(w=brightness) - else: - self._bulb.setRgbw(w2=brightness) - return - - white = kwargs.get(ATTR_WHITE_VALUE) - effect = kwargs.get(ATTR_EFFECT) - # Show warning if effect set with rgb, brightness, or white level - if effect and (brightness or white or rgb): - _LOGGER.warning( - "RGB, brightness and white level are ignored when" - " an effect is specified for a flux bulb" - ) - - # Random color effect - if effect == EFFECT_RANDOM: - self._bulb.setRgb( - random.randint(0, 255), random.randint(0, 255), random.randint(0, 255) - ) - return - - # Custom effect - if effect == EFFECT_CUSTOM: - if self._custom_effect_colors: - self._bulb.setCustomPattern( - self._custom_effect_colors, - self._custom_effect_speed_pct, - self._custom_effect_transition, - ) - return - - # Effect selection - if effect in EFFECT_MAP: - self._bulb.setPresetPattern(EFFECT_MAP[effect], 50) - return - - # Preserve current brightness on color/white level change - if brightness is None: + if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is None: brightness = self.brightness - # handle W only mode (use brightness instead of white value) - if self._mode == MODE_WHITE: - self._bulb.setRgbw(0, 0, 0, w=brightness) + # Handle switch to CCT Color Mode + if ATTR_COLOR_TEMP in kwargs: + color_temp_mired = kwargs[ATTR_COLOR_TEMP] + color_temp_kelvin = color_temperature_mired_to_kelvin(color_temp_mired) + if self.color_mode != COLOR_MODE_RGBWW: + self._bulb.setWhiteTemperature(color_temp_kelvin, brightness) + return + + # When switching to color temp from RGBWW mode, + # we do not want the overall brightness, we only + # want the brightness of the white channels + brightness = kwargs.get( + ATTR_BRIGHTNESS, self._bulb.getWhiteTemperature()[1] + ) + cold, warm = color_temp_to_white_levels(color_temp_kelvin, brightness) + self._bulb.set_levels(r=0, b=0, g=0, w=warm, w2=cold) return - - if white is None and self._mode in WHITE_MODES: - white = self.white_value - - # Preserve color on brightness/white level change - if rgb is None: - rgb = self._bulb.getRgb() - - # handle RGBW mode - if self._mode == MODE_RGBW: - self._bulb.setRgbw(*tuple(rgb), w=white, brightness=brightness) + # Handle switch to HS Color Mode + if ATTR_RGB_COLOR in kwargs: + self._bulb.set_levels(*kwargs[ATTR_RGB_COLOR], brightness=brightness) return - - # handle RGB mode - self._bulb.setRgb(*tuple(rgb), brightness=brightness) + # Handle switch to RGBW Color Mode + if ATTR_RGBW_COLOR in kwargs: + if ATTR_BRIGHTNESS in kwargs: + rgbw = rgbw_brightness(kwargs[ATTR_RGBW_COLOR], brightness) + else: + rgbw = kwargs[ATTR_RGBW_COLOR] + self._bulb.set_levels(*rgbw) + return + # Handle switch to RGBWW Color Mode + if ATTR_RGBWW_COLOR in kwargs: + if ATTR_BRIGHTNESS in kwargs: + rgbcw = rgbcw_brightness(kwargs[ATTR_RGBWW_COLOR], brightness) + else: + rgbcw = kwargs[ATTR_RGBWW_COLOR] + self._bulb.set_levels(*rgbcw_to_rgbwc(rgbcw)) + return + # Handle switch to White Color Mode + if ATTR_WHITE in kwargs: + self._bulb.set_levels(w=kwargs[ATTR_WHITE]) + return + if ATTR_EFFECT in kwargs: + effect = kwargs[ATTR_EFFECT] + # Random color effect + if effect == EFFECT_RANDOM: + self._bulb.set_levels( + random.randint(0, 255), + random.randint(0, 255), + random.randint(0, 255), + ) + return + # Custom effect + if effect == EFFECT_CUSTOM: + if self._custom_effect_colors: + self._bulb.setCustomPattern( + self._custom_effect_colors, + self._custom_effect_speed_pct, + self._custom_effect_transition, + ) + return + # Effect selection + if effect in EFFECT_MAP: + self._bulb.setPresetPattern(EFFECT_MAP[effect], DEFAULT_EFFECT_SPEED) + return + raise ValueError(f"Unknown effect {effect}") + # Handle brightness adjustment in CCT Color Mode + if self.color_mode == COLOR_MODE_COLOR_TEMP: + self._bulb.setWhiteTemperature( + self._bulb.getWhiteTemperature()[0], brightness + ) + return + # Handle brightness adjustment in RGB Color Mode + if self.color_mode == COLOR_MODE_RGB: + self._bulb.set_levels(*self.rgb_color, brightness=brightness) + return + # Handle brightness adjustment in RGBW Color Mode + if self.color_mode == COLOR_MODE_RGBW: + self._bulb.set_levels(*rgbw_brightness(self.rgbw_color, brightness)) + return + # Handle brightness adjustment in RGBWW Color Mode + if self.color_mode == COLOR_MODE_RGBWW: + rgbwc = self.rgbwc_color + self._bulb.set_levels(*rgbww_brightness(rgbwc, brightness)) + return + # Handle White Color Mode and Brightness Only Color Mode + if self.color_mode in (COLOR_MODE_WHITE, COLOR_MODE_BRIGHTNESS): + self._bulb.set_levels(w=brightness) + return + raise ValueError(f"Unsupported color mode {self.color_mode}") def set_custom_effect( self, colors: list[tuple[int, int, int]], speed_pct: int, transition: str @@ -436,24 +504,3 @@ class FluxLight(CoordinatorEntity, LightEntity): await self.hass.async_add_executor_job(self._bulb.turnOff) self.async_write_ha_state() await self.coordinator.async_request_refresh() - - async def async_added_to_hass(self) -> None: - """When entity is added to hass.""" - await super().async_added_to_hass() - if self._mode and self._mode != MODE_AUTO: - return - - if self._bulb.mode == "ww": - self._mode = MODE_WHITE - elif self._bulb.rgbwcapable: - self._mode = MODE_RGBW - else: - self._mode = MODE_RGB - _LOGGER.debug( - "Detected mode for %s (%s) with raw_state=%s rgbwcapable=%s is %s", - self.name, - self.unique_id, - self._bulb.raw_state, - self._bulb.rgbwcapable, - self._mode, - ) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index c6f06cb20ab..28d4ecd772c 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.22"], + "requirements": ["flux_led==0.24.3"], "codeowners": ["@icemanch"], "iot_class": "local_polling", "dhcp": [ @@ -30,4 +30,3 @@ ] } - diff --git a/requirements_all.txt b/requirements_all.txt index 481b8d5e220..ec5374661c6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -649,7 +649,7 @@ fjaraskupan==1.0.1 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.22 +flux_led==0.24.3 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a40976d974d..25a22280f71 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -378,7 +378,7 @@ fjaraskupan==1.0.1 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.22 +flux_led==0.24.3 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/tests/components/flux_led/__init__.py b/tests/components/flux_led/__init__.py index 5ca6244655a..1eccf9bfcd6 100644 --- a/tests/components/flux_led/__init__.py +++ b/tests/components/flux_led/__init__.py @@ -5,6 +5,11 @@ import socket from unittest.mock import MagicMock, patch from flux_led import WifiLedBulb +from flux_led.const import ( + COLOR_MODE_CCT as FLUX_COLOR_MODE_CCT, + COLOR_MODE_RGB as FLUX_COLOR_MODE_RGB, +) +from flux_led.protocol import LEDENETRawState from homeassistant.components.dhcp import ( HOSTNAME as DHCP_HOSTNAME, @@ -34,9 +39,21 @@ def _mocked_bulb() -> WifiLedBulb: bulb = MagicMock(auto_spec=WifiLedBulb) bulb.getRgb = MagicMock(return_value=[255, 0, 0]) bulb.getRgbw = MagicMock(return_value=[255, 0, 0, 50]) + bulb.getRgbww = MagicMock(return_value=[255, 0, 0, 50, 0]) + bulb.getRgbcw = MagicMock(return_value=[255, 0, 0, 0, 50]) + bulb.rgb = (255, 0, 0) + bulb.rgbw = (255, 0, 0, 50) + bulb.rgbww = (255, 0, 0, 50, 0) + bulb.rgbcw = (255, 0, 0, 0, 50) + bulb.getWhiteTemperature = MagicMock(return_value=(2700, 128)) bulb.brightness = 128 + bulb.model_num = 0x35 bulb.rgbwcapable = True - bulb.raw_state = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + bulb.color_modes = {FLUX_COLOR_MODE_RGB, FLUX_COLOR_MODE_CCT} + bulb.color_mode = FLUX_COLOR_MODE_RGB + bulb.raw_state = LEDENETRawState( + 0, 0x35, 0, 0x61, 0x5, 50, 255, 0, 0, 50, 8, 0, 0, 0 + ) return bulb diff --git a/tests/components/flux_led/test_config_flow.py b/tests/components/flux_led/test_config_flow.py index 029ba0f972b..13e427c3331 100644 --- a/tests/components/flux_led/test_config_flow.py +++ b/tests/components/flux_led/test_config_flow.py @@ -11,7 +11,6 @@ from homeassistant.components.flux_led.const import ( CONF_CUSTOM_EFFECT_SPEED_PCT, CONF_CUSTOM_EFFECT_TRANSITION, DOMAIN, - MODE_AUTO, MODE_RGB, TRANSITION_JUMP, TRANSITION_STROBE, @@ -436,7 +435,6 @@ async def test_options(hass: HomeAssistant): assert result["step_id"] == "init" user_input = { - CONF_MODE: MODE_AUTO, CONF_CUSTOM_EFFECT_COLORS: "[0,0,255], [255,0,0]", CONF_CUSTOM_EFFECT_SPEED_PCT: 50, CONF_CUSTOM_EFFECT_TRANSITION: TRANSITION_JUMP, diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index 4ddf9a7d04d..56f53f97bf4 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -1,6 +1,15 @@ """Tests for light platform.""" from datetime import timedelta +from unittest.mock import Mock +from flux_led.const import ( + COLOR_MODE_ADDRESSABLE as FLUX_COLOR_MODE_ADDRESSABLE, + COLOR_MODE_CCT as FLUX_COLOR_MODE_CCT, + COLOR_MODE_DIM as FLUX_COLOR_MODE_DIM, + COLOR_MODE_RGB as FLUX_COLOR_MODE_RGB, + COLOR_MODE_RGBW as FLUX_COLOR_MODE_RGBW, + COLOR_MODE_RGBWW as FLUX_COLOR_MODE_RGBWW, +) import pytest from homeassistant.components import flux_led @@ -25,7 +34,11 @@ from homeassistant.components.light import ( ATTR_EFFECT, ATTR_EFFECT_LIST, ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, ATTR_SUPPORTED_COLOR_MODES, + ATTR_WHITE, DOMAIN as LIGHT_DOMAIN, ) from homeassistant.const import ( @@ -108,8 +121,9 @@ async def test_light_device_registry( config_entry.add_to_hass(hass) bulb = _mocked_bulb() bulb.protocol = protocol - bulb.raw_state[1] = model - bulb.raw_state[10] = sw_version + bulb.raw_state = bulb.raw_state._replace(model_num=model, version_number=sw_version) + bulb.model_num = model + with _patch_discovery(no_device=True), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -131,8 +145,9 @@ async def test_rgb_light(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) bulb = _mocked_bulb() - bulb.rgbwcapable = False - bulb.protocol = None + bulb.raw_state = bulb.raw_state._replace(model_num=0x33) # RGB only model + bulb.color_modes = {FLUX_COLOR_MODE_RGB} + bulb.color_mode = FLUX_COLOR_MODE_RGB with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -143,9 +158,9 @@ async def test_rgb_light(hass: HomeAssistant) -> None: assert state.state == STATE_ON attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 - assert attributes[ATTR_COLOR_MODE] == "hs" + assert attributes[ATTR_COLOR_MODE] == "rgb" assert attributes[ATTR_EFFECT_LIST] == FLUX_EFFECT_LIST - assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["hs"] + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgb"] assert attributes[ATTR_HS_COLOR] == (0, 100) await hass.services.async_call( @@ -170,8 +185,8 @@ async def test_rgb_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, blocking=True, ) - bulb.setRgb.assert_called_with(255, 0, 0, brightness=100) - bulb.setRgb.reset_mock() + bulb.set_levels.assert_called_with(255, 0, 0, brightness=100) + bulb.set_levels.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -179,8 +194,8 @@ async def test_rgb_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, blocking=True, ) - bulb.setRgb.assert_called_with(255, 191, 178, brightness=128) - bulb.setRgb.reset_mock() + bulb.set_levels.assert_called_with(255, 191, 178, brightness=128) + bulb.set_levels.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -188,8 +203,8 @@ async def test_rgb_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"}, blocking=True, ) - bulb.setRgb.assert_called_once() - bulb.setRgb.reset_mock() + bulb.set_levels.assert_called_once() + bulb.set_levels.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -200,6 +215,137 @@ async def test_rgb_light(hass: HomeAssistant) -> None: bulb.setPresetPattern.assert_called_with(43, 50) bulb.setPresetPattern.reset_mock() + with pytest.raises(ValueError): + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "does not exist"}, + blocking=True, + ) + + +async def test_rgb_cct_light(hass: HomeAssistant) -> None: + """Test an rgb cct light.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.raw_state = bulb.raw_state._replace(model_num=0x35) # RGB & CCT model + bulb.color_modes = {FLUX_COLOR_MODE_RGB, FLUX_COLOR_MODE_CCT} + bulb.color_mode = FLUX_COLOR_MODE_RGB + with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.az120444_aabbccddeeff" + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 128 + assert attributes[ATTR_COLOR_MODE] == "rgb" + assert attributes[ATTR_EFFECT_LIST] == FLUX_EFFECT_LIST + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "rgb"] + assert attributes[ATTR_HS_COLOR] == (0, 100) + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.turnOff.assert_called_once() + + bulb.is_on = False + async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_OFF + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.turnOn.assert_called_once() + bulb.turnOn.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + bulb.set_levels.assert_called_with(255, 0, 0, brightness=100) + bulb.set_levels.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, + blocking=True, + ) + bulb.set_levels.assert_called_with(255, 191, 178, brightness=128) + bulb.set_levels.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"}, + blocking=True, + ) + bulb.set_levels.assert_called_once() + bulb.set_levels.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"}, + blocking=True, + ) + bulb.setPresetPattern.assert_called_with(43, 50) + bulb.setPresetPattern.reset_mock() + + bulb.is_on = True + bulb.color_mode = FLUX_COLOR_MODE_CCT + bulb.getWhiteTemperature = Mock(return_value=(5000, 128)) + bulb.raw_state = bulb.raw_state._replace( + red=0, green=0, blue=0, warm_white=1, cool_white=2 + ) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=60)) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 128 + assert attributes[ATTR_COLOR_MODE] == "color_temp" + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "rgb"] + assert attributes[ATTR_COLOR_TEMP] == 200 + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 370}, + blocking=True, + ) + bulb.setWhiteTemperature.assert_called_with(2702, 128) + bulb.setWhiteTemperature.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 255}, + blocking=True, + ) + bulb.setWhiteTemperature.assert_called_with(5000, 255) + bulb.setWhiteTemperature.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 128}, + blocking=True, + ) + bulb.setWhiteTemperature.assert_called_with(5000, 128) + bulb.setWhiteTemperature.reset_mock() + async def test_rgbw_light(hass: HomeAssistant) -> None: """Test an rgbw light.""" @@ -210,6 +356,8 @@ async def test_rgbw_light(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) bulb = _mocked_bulb() + bulb.color_modes = {FLUX_COLOR_MODE_RGBW} + bulb.color_mode = FLUX_COLOR_MODE_RGBW with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -222,8 +370,8 @@ async def test_rgbw_light(hass: HomeAssistant) -> None: assert attributes[ATTR_BRIGHTNESS] == 128 assert attributes[ATTR_COLOR_MODE] == "rgbw" assert attributes[ATTR_EFFECT_LIST] == FLUX_EFFECT_LIST - assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "hs", "rgbw"] - assert attributes[ATTR_HS_COLOR] == (0, 100) + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgbw"] + assert attributes[ATTR_RGB_COLOR] == (255, 42, 42) await hass.services.async_call( LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True @@ -240,6 +388,7 @@ async def test_rgbw_light(hass: HomeAssistant) -> None: ) bulb.turnOn.assert_called_once() bulb.turnOn.reset_mock() + bulb.is_on = True await hass.services.async_call( LIGHT_DOMAIN, @@ -247,35 +396,41 @@ async def test_rgbw_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, blocking=True, ) - bulb.setRgbw.assert_called_with(255, 0, 0, w=50, brightness=100) - bulb.setRgbw.reset_mock() + bulb.set_levels.assert_called_with(168, 0, 0, 33) + bulb.set_levels.reset_mock() + state = hass.states.get(entity_id) + assert state.state == STATE_ON await hass.services.async_call( LIGHT_DOMAIN, "turn_on", - {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 150}, + { + ATTR_ENTITY_ID: entity_id, + ATTR_RGBW_COLOR: (255, 255, 255, 255), + ATTR_BRIGHTNESS: 128, + }, blocking=True, ) - bulb.setRgbw.assert_called_with(w2=128) - bulb.setRgbw.reset_mock() + bulb.set_levels.assert_called_with(128, 128, 128, 128) + bulb.set_levels.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, "turn_on", - {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 290}, + {ATTR_ENTITY_ID: entity_id, ATTR_RGBW_COLOR: (255, 255, 255, 255)}, blocking=True, ) - bulb.setRgbw.assert_called_with(w=128) - bulb.setRgbw.reset_mock() + bulb.set_levels.assert_called_with(255, 255, 255, 255) + bulb.set_levels.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, "turn_on", - {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, + {ATTR_ENTITY_ID: entity_id, ATTR_RGBW_COLOR: (255, 191, 178, 0)}, blocking=True, ) - bulb.setRgbw.assert_called_with(255, 191, 178, w=50, brightness=128) - bulb.setRgbw.reset_mock() + bulb.set_levels.assert_called_with(255, 191, 178, 0) + bulb.set_levels.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -283,8 +438,8 @@ async def test_rgbw_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"}, blocking=True, ) - bulb.setRgb.assert_called_once() - bulb.setRgb.reset_mock() + bulb.set_levels.assert_called_once() + bulb.set_levels.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -305,9 +460,9 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None: ) config_entry.add_to_hass(hass) bulb = _mocked_bulb() - bulb.raw_state[9] = 1 - bulb.raw_state[11] = 2 - + bulb.raw_state = bulb.raw_state._replace(warm_white=1, cool_white=2) + bulb.color_modes = {FLUX_COLOR_MODE_RGBWW, FLUX_COLOR_MODE_CCT} + bulb.color_mode = FLUX_COLOR_MODE_RGBWW with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -318,10 +473,10 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None: assert state.state == STATE_ON attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 - assert attributes[ATTR_COLOR_MODE] == "rgbw" + assert attributes[ATTR_COLOR_MODE] == "rgbww" assert attributes[ATTR_EFFECT_LIST] == FLUX_EFFECT_LIST - assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "hs", "rgbw"] - assert attributes[ATTR_HS_COLOR] == (0, 100) + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "rgbww"] + assert attributes[ATTR_HS_COLOR] == (3.237, 94.51) await hass.services.async_call( LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True @@ -345,17 +500,49 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, blocking=True, ) - bulb.setRgbw.assert_called_with(255, 0, 0, w=50, brightness=100) - bulb.setRgbw.reset_mock() + bulb.set_levels.assert_called_with(250, 0, 0, 49, 0) + bulb.set_levels.reset_mock() + bulb.is_on = True await hass.services.async_call( LIGHT_DOMAIN, "turn_on", - {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 150}, + { + ATTR_ENTITY_ID: entity_id, + ATTR_RGBWW_COLOR: (255, 255, 255, 0, 255), + ATTR_BRIGHTNESS: 128, + }, blocking=True, ) - bulb.setRgbw.assert_called_with(w2=128) - bulb.setRgbw.reset_mock() + bulb.set_levels.assert_called_with(192, 192, 192, 192, 0) + bulb.set_levels.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_RGBWW_COLOR: (255, 255, 255, 255, 50)}, + blocking=True, + ) + bulb.set_levels.assert_called_with(255, 255, 255, 50, 255) + bulb.set_levels.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 154}, + blocking=True, + ) + bulb.set_levels.assert_called_with(r=0, b=0, g=0, w=0, w2=127) + bulb.set_levels.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 154, ATTR_BRIGHTNESS: 255}, + blocking=True, + ) + bulb.set_levels.assert_called_with(r=0, b=0, g=0, w=0, w2=255) + bulb.set_levels.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -363,17 +550,17 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 290}, blocking=True, ) - bulb.setRgbw.assert_called_with(w=128) - bulb.setRgbw.reset_mock() + bulb.set_levels.assert_called_with(r=0, b=0, g=0, w=102, w2=25) + bulb.set_levels.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, "turn_on", - {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, + {ATTR_ENTITY_ID: entity_id, ATTR_RGBWW_COLOR: (255, 191, 178, 0, 0)}, blocking=True, ) - bulb.setRgbw.assert_called_with(255, 191, 178, w=50, brightness=128) - bulb.setRgbw.reset_mock() + bulb.set_levels.assert_called_with(255, 191, 178, 0, 0) + bulb.set_levels.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -381,8 +568,8 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"}, blocking=True, ) - bulb.setRgb.assert_called_once() - bulb.setRgb.reset_mock() + bulb.set_levels.assert_called_once() + bulb.set_levels.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -405,6 +592,8 @@ async def test_white_light(hass: HomeAssistant) -> None: bulb = _mocked_bulb() bulb.mode = "ww" bulb.protocol = None + bulb.color_modes = {FLUX_COLOR_MODE_DIM} + bulb.color_mode = FLUX_COLOR_MODE_DIM with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -414,9 +603,9 @@ async def test_white_light(hass: HomeAssistant) -> None: state = hass.states.get(entity_id) assert state.state == STATE_ON attributes = state.attributes - assert attributes[ATTR_BRIGHTNESS] == 50 - assert attributes[ATTR_COLOR_MODE] == "brightness" - assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["brightness"] + assert attributes[ATTR_BRIGHTNESS] == 128 + assert attributes[ATTR_COLOR_MODE] == "white" + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["white"] await hass.services.async_call( LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True @@ -440,13 +629,20 @@ async def test_white_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, blocking=True, ) - bulb.setRgbw.assert_called_with(0, 0, 0, w=100) - bulb.setRgbw.reset_mock() + bulb.set_levels.assert_called_with(w=100) + bulb.set_levels.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_WHITE: 100}, + blocking=True, + ) + bulb.set_levels.assert_called_with(w=100) + bulb.set_levels.reset_mock() -async def test_rgb_light_custom_effects( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> None: +async def test_rgb_light_custom_effects(hass: HomeAssistant) -> None: """Test an rgb light with a custom effect.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -461,6 +657,8 @@ async def test_rgb_light_custom_effects( ) config_entry.add_to_hass(hass) bulb = _mocked_bulb() + bulb.color_modes = {FLUX_COLOR_MODE_RGB} + bulb.color_mode = FLUX_COLOR_MODE_RGB with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -471,9 +669,9 @@ async def test_rgb_light_custom_effects( assert state.state == STATE_ON attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 - assert attributes[ATTR_COLOR_MODE] == "rgbw" + assert attributes[ATTR_COLOR_MODE] == "rgb" assert attributes[ATTR_EFFECT_LIST] == [*FLUX_EFFECT_LIST, "custom"] - assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "hs", "rgbw"] + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgb"] assert attributes[ATTR_HS_COLOR] == (0, 100) await hass.services.async_call( @@ -494,7 +692,7 @@ async def test_rgb_light_custom_effects( ) bulb.setCustomPattern.assert_called_with([[0, 0, 255], [255, 0, 0]], 88, "jump") bulb.setCustomPattern.reset_mock() - bulb.raw_state[3] = EFFECT_CUSTOM_CODE + bulb.raw_state = bulb.raw_state._replace(preset_pattern=EFFECT_CUSTOM_CODE) bulb.is_on = True async_fire_time_changed(hass, utcnow() + timedelta(seconds=20)) await hass.async_block_till_done() @@ -503,7 +701,6 @@ async def test_rgb_light_custom_effects( attributes = state.attributes assert attributes[ATTR_EFFECT] == "custom" - caplog.clear() await hass.services.async_call( LIGHT_DOMAIN, "turn_on", @@ -512,7 +709,7 @@ async def test_rgb_light_custom_effects( ) bulb.setCustomPattern.assert_called_with([[0, 0, 255], [255, 0, 0]], 88, "jump") bulb.setCustomPattern.reset_mock() - bulb.raw_state[3] = EFFECT_CUSTOM_CODE + bulb.raw_state = bulb.raw_state._replace(preset_pattern=EFFECT_CUSTOM_CODE) bulb.is_on = True async_fire_time_changed(hass, utcnow() + timedelta(seconds=20)) await hass.async_block_till_done() @@ -520,7 +717,6 @@ async def test_rgb_light_custom_effects( assert state.state == STATE_ON attributes = state.attributes assert attributes[ATTR_EFFECT] == "custom" - assert "RGB, brightness and white level are ignored when" in caplog.text async def test_rgb_light_custom_effects_invalid_colors(hass: HomeAssistant) -> None: @@ -538,6 +734,8 @@ async def test_rgb_light_custom_effects_invalid_colors(hass: HomeAssistant) -> N ) config_entry.add_to_hass(hass) bulb = _mocked_bulb() + bulb.color_modes = {FLUX_COLOR_MODE_RGB} + bulb.color_mode = FLUX_COLOR_MODE_RGB with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -548,9 +746,9 @@ async def test_rgb_light_custom_effects_invalid_colors(hass: HomeAssistant) -> N assert state.state == STATE_ON attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 - assert attributes[ATTR_COLOR_MODE] == "rgbw" + assert attributes[ATTR_COLOR_MODE] == "rgb" assert attributes[ATTR_EFFECT_LIST] == FLUX_EFFECT_LIST - assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "hs", "rgbw"] + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgb"] assert attributes[ATTR_HS_COLOR] == (0, 100) @@ -565,6 +763,8 @@ async def test_rgb_light_custom_effect_via_service( ) config_entry.add_to_hass(hass) bulb = _mocked_bulb() + bulb.color_modes = {FLUX_COLOR_MODE_RGB} + bulb.color_mode = FLUX_COLOR_MODE_RGB with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -575,9 +775,9 @@ async def test_rgb_light_custom_effect_via_service( assert state.state == STATE_ON attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 - assert attributes[ATTR_COLOR_MODE] == "rgbw" + assert attributes[ATTR_COLOR_MODE] == "rgb" assert attributes[ATTR_EFFECT_LIST] == [*FLUX_EFFECT_LIST] - assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "hs", "rgbw"] + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgb"] assert attributes[ATTR_HS_COLOR] == (0, 100) await hass.services.async_call( @@ -605,34 +805,6 @@ async def test_rgb_light_custom_effect_via_service( bulb.setCustomPattern.reset_mock() -async def test_rgbw_detection_without_protocol(hass: HomeAssistant) -> None: - """Test an rgbw detection without protocol.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, - unique_id=MAC_ADDRESS, - ) - config_entry.add_to_hass(hass) - bulb = _mocked_bulb() - bulb.protocol = None - bulb.rgbwprotocol = None - bulb.rgbwcapable = True - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): - await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) - await hass.async_block_till_done() - - entity_id = "light.az120444_aabbccddeeff" - - state = hass.states.get(entity_id) - assert state.state == STATE_ON - attributes = state.attributes - assert attributes[ATTR_BRIGHTNESS] == 128 - assert attributes[ATTR_COLOR_MODE] == "rgbw" - assert attributes[ATTR_EFFECT_LIST] == FLUX_EFFECT_LIST - assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "hs", "rgbw"] - assert attributes[ATTR_HS_COLOR] == (0, 100) - - async def test_migrate_from_yaml(hass: HomeAssistant) -> None: """Test migrate from yaml.""" config = { @@ -680,3 +852,54 @@ async def test_migrate_from_yaml(hass: HomeAssistant) -> None: CONF_CUSTOM_EFFECT_SPEED_PCT: 30, CONF_CUSTOM_EFFECT_TRANSITION: "strobe", } + + +async def test_addressable_light(hass: HomeAssistant) -> None: + """Test an addressable light.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.raw_state = bulb.raw_state._replace(model_num=0x33) # RGB only model + bulb.color_modes = {FLUX_COLOR_MODE_ADDRESSABLE} + bulb.color_mode = FLUX_COLOR_MODE_ADDRESSABLE + with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.az120444_aabbccddeeff" + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attributes = state.attributes + assert attributes[ATTR_COLOR_MODE] == "onoff" + assert attributes[ATTR_EFFECT_LIST] == FLUX_EFFECT_LIST + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["onoff"] + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.turnOff.assert_called_once() + + bulb.is_on = False + async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_OFF + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.turnOn.assert_called_once() + bulb.turnOn.reset_mock() + bulb.is_on = True + + with pytest.raises(ValueError): + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) From ee80ccf7a64122506ef91bd630e73d8af8a2c1f8 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sun, 10 Oct 2021 22:39:34 +0300 Subject: [PATCH 0219/1038] Fix Shelly button filter empty event (#57427) --- homeassistant/components/shelly/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 87896f4380b..cb48f34cba6 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -295,7 +295,7 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): if block.type != "device": continue - if block.wakeupEvent[0] == "button": + if len(block.wakeupEvent) == 1 and block.wakeupEvent[0] == "button": self._last_input_events_count[1] = -1 break From 6820faf5a0e38b817c10b1c70ff8b9d8cac35954 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sun, 10 Oct 2021 22:39:57 +0300 Subject: [PATCH 0220/1038] Fix Shelly button type in roller mode (#57429) --- homeassistant/components/shelly/utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 6f24b4a64be..783153e2746 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -133,6 +133,10 @@ def is_block_momentary_input(settings: dict[str, Any], block: Block) -> bool: if settings["device"]["type"] in SHBTN_MODELS: return True + if settings.get("mode") == "roller": + button_type = settings["rollers"][0]["button_type"] + return button_type in ["momentary", "momentary_on_release"] + button = settings.get("relays") or settings.get("lights") or settings.get("inputs") if button is None: return False From 5c91d8d3799cf9f3690da3fa15abea0e64698e16 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 11 Oct 2021 00:12:43 +0000 Subject: [PATCH 0221/1038] [ci skip] Translation update --- homeassistant/components/weather/translations/is.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/weather/translations/is.json b/homeassistant/components/weather/translations/is.json index 2b0dc01deb9..69d3bbb4b51 100644 --- a/homeassistant/components/weather/translations/is.json +++ b/homeassistant/components/weather/translations/is.json @@ -1,19 +1,19 @@ { "state": { "_": { - "clear-night": "Hei\u00f0sk\u00fdrt, n\u00f3tt", + "clear-night": "Hei\u00f0sk\u00edrt, n\u00f3tt", "cloudy": "Sk\u00fdja\u00f0", "exceptional": "Mj\u00f6g gott", "fog": "\u00deoka", "hail": "Hagl\u00e9l", "lightning": "Eldingar", "lightning-rainy": "Eldingar, rigning", - "partlycloudy": "A\u00f0 hluta til sk\u00fdja\u00f0", + "partlycloudy": "L\u00e9ttsk\u00fdja\u00f0", "pouring": "\u00darhelli", "rainy": "Rigning", "snowy": "Snj\u00f3koma", "snowy-rainy": "Slydda", - "sunny": "S\u00f3lskin", + "sunny": "Hei\u00f0sk\u00edrt", "windy": "Vindasamt", "windy-variant": "Vindasamt" } From e148939b7892b62ec365f0c6ae75d9368f89dd81 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 10 Oct 2021 15:12:54 -1000 Subject: [PATCH 0222/1038] Convert flux_led to use asyncio (#57440) --- homeassistant/components/flux_led/__init__.py | 102 +++++-- .../components/flux_led/config_flow.py | 39 +-- homeassistant/components/flux_led/const.py | 9 +- homeassistant/components/flux_led/light.py | 120 ++++---- tests/components/flux_led/__init.py__ | 1 - tests/components/flux_led/__init__.py | 59 +++- tests/components/flux_led/test_config_flow.py | 6 +- tests/components/flux_led/test_init.py | 33 ++- tests/components/flux_led/test_light.py | 265 ++++++++++-------- 9 files changed, 399 insertions(+), 235 deletions(-) delete mode 100644 tests/components/flux_led/__init.py__ diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index 99d75e884b5..a933e127b61 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -5,13 +5,17 @@ from datetime import timedelta import logging from typing import Any, Final -from flux_led import BulbScanner, WifiLedBulb +from flux_led.aio import AIOWifiLedBulb +from flux_led.aioscanner import AIOBulbScanner from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STARTED +from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STARTED from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -19,8 +23,12 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import ( DISCOVER_SCAN_TIMEOUT, DOMAIN, + FLUX_HOST, FLUX_LED_DISCOVERY, FLUX_LED_EXCEPTIONS, + FLUX_MAC, + FLUX_MODEL, + SIGNAL_STATE_UPDATED, STARTUP_SCAN_TIMEOUT, ) @@ -31,22 +39,52 @@ DISCOVERY_INTERVAL: Final = timedelta(minutes=15) REQUEST_REFRESH_DELAY: Final = 1.5 -async def async_wifi_bulb_for_host(hass: HomeAssistant, host: str) -> WifiLedBulb: - """Create a WifiLedBulb from a host.""" - return await hass.async_add_executor_job(WifiLedBulb, host) +@callback +def async_wifi_bulb_for_host(host: str) -> AIOWifiLedBulb: + """Create a AIOWifiLedBulb from a host.""" + return AIOWifiLedBulb(host) + + +@callback +def async_update_entry_from_discovery( + hass: HomeAssistant, entry: config_entries.ConfigEntry, device: dict[str, Any] +) -> None: + """Update a config entry from a flux_led discovery.""" + name = f"{device[FLUX_MODEL]} {device[FLUX_MAC]}" + hass.config_entries.async_update_entry( + entry, + data={**entry.data, CONF_NAME: name}, + title=name, + unique_id=dr.format_mac(device[FLUX_MAC]), + ) async def async_discover_devices( - hass: HomeAssistant, timeout: int + hass: HomeAssistant, timeout: int, address: str | None = None ) -> list[dict[str, str]]: """Discover flux led devices.""" - - def _scan_with_timeout() -> list[dict[str, str]]: - scanner = BulbScanner() - discovered: list[dict[str, str]] = scanner.scan(timeout=timeout) + scanner = AIOBulbScanner() + try: + discovered: list[dict[str, str]] = await scanner.async_scan( + timeout=timeout, address=address + ) + except OSError as ex: + _LOGGER.debug("Scanning failed with error: %s", ex) + return [] + else: return discovered - return await hass.async_add_executor_job(_scan_with_timeout) + +async def async_discover_device( + hass: HomeAssistant, host: str +) -> dict[str, str] | None: + """Direct discovery at a single ip instead of broadcast.""" + # If we are missing the unique_id we should be able to fetch it + # from the device by doing a directed discovery at the host only + for device in await async_discover_devices(hass, DISCOVER_SCAN_TIMEOUT, host): + if device[FLUX_HOST] == host: + return device + return None @callback @@ -90,9 +128,26 @@ async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Flux LED/MagicLight from a config entry.""" + host = entry.data[CONF_HOST] + if not entry.unique_id: + if discovery := await async_discover_device(hass, host): + async_update_entry_from_discovery(hass, entry, discovery) - coordinator = FluxLedUpdateCoordinator(hass, entry.data[CONF_HOST]) - await coordinator.async_config_entry_first_refresh() + device: AIOWifiLedBulb = async_wifi_bulb_for_host(host) + signal = SIGNAL_STATE_UPDATED.format(device.ipaddr) + + @callback + def _async_state_changed(*_: Any) -> None: + _LOGGER.debug("%s: Device state updated: %s", device.ipaddr, device.raw_state) + async_dispatcher_send(hass, signal) + + try: + await device.async_setup(_async_state_changed) + except FLUX_LED_EXCEPTIONS as ex: + raise ConfigEntryNotReady( + str(ex) or f"Timed out trying to connect to {device.ipaddr}" + ) from ex + coordinator = FluxLedUpdateCoordinator(hass, device) hass.data[DOMAIN][entry.entry_id] = coordinator hass.config_entries.async_setup_platforms(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_update_listener)) @@ -103,7 +158,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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) + coordinator = hass.data[DOMAIN].pop(entry.entry_id) + await coordinator.device.async_stop() return unload_ok @@ -113,17 +169,15 @@ class FluxLedUpdateCoordinator(DataUpdateCoordinator): def __init__( self, hass: HomeAssistant, - host: str, + device: AIOWifiLedBulb, ) -> None: """Initialize DataUpdateCoordinator to gather data for specific device.""" - self.host = host - self.device: WifiLedBulb | None = None - update_interval = timedelta(seconds=5) + self.device = device super().__init__( hass, _LOGGER, - name=host, - update_interval=update_interval, + name=self.device.ipaddr, + update_interval=timedelta(seconds=5), # We don't want an immediate refresh since the device # takes a moment to reflect the state change request_refresh_debouncer=Debouncer( @@ -134,12 +188,6 @@ class FluxLedUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> None: """Fetch all device and sensor data from api.""" try: - if not self.device: - self.device = await async_wifi_bulb_for_host(self.hass, self.host) - else: - await self.hass.async_add_executor_job(self.device.update_state) + await self.device.async_update() except FLUX_LED_EXCEPTIONS as ex: raise UpdateFailed(ex) from ex - - if not self.device.raw_state: - raise UpdateFailed("The device failed to update") diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index 02ddd4a1530..306dbc2c25e 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -4,7 +4,6 @@ from __future__ import annotations import logging from typing import Any, Final -from flux_led import WifiLedBulb import voluptuous as vol from homeassistant import config_entries @@ -15,7 +14,12 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import device_registry as dr from homeassistant.helpers.typing import DiscoveryInfoType -from . import async_discover_devices, async_wifi_bulb_for_host +from . import ( + async_discover_device, + async_discover_devices, + async_update_entry_from_discovery, + async_wifi_bulb_for_host, +) from .const import ( CONF_CUSTOM_EFFECT_COLORS, CONF_CUSTOM_EFFECT_SPEED_PCT, @@ -34,7 +38,6 @@ from .const import ( CONF_DEVICE: Final = "device" - _LOGGER = logging.getLogger(__name__) @@ -104,13 +107,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured(updates={CONF_HOST: host}) for entry in self._async_current_entries(include_ignore=False): if entry.data[CONF_HOST] == host and not entry.unique_id: - name = f"{device[FLUX_MODEL]} {device[FLUX_MAC]}" - self.hass.config_entries.async_update_entry( - entry, - data={**entry.data, CONF_NAME: name}, - title=name, - unique_id=mac, - ) + async_update_entry_from_discovery(self.hass, entry, device) return self.async_abort(reason="already_configured") self.context[CONF_HOST] = host for progress in self._async_in_progress(): @@ -157,13 +154,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if not (host := user_input[CONF_HOST]): return await self.async_step_pick_device() try: - await self._async_try_connect(host) + device = await self._async_try_connect(host) except FLUX_LED_EXCEPTIONS: errors["base"] = "cannot_connect" else: - return self._async_create_entry_from_device( - {FLUX_MAC: None, FLUX_MODEL: None, FLUX_HOST: host} - ) + if device[FLUX_MAC]: + await self.async_set_unique_id( + dr.format_mac(device[FLUX_MAC]), raise_on_progress=False + ) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + return self._async_create_entry_from_device(device) return self.async_show_form( step_id="user", @@ -204,10 +204,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): data_schema=vol.Schema({vol.Required(CONF_DEVICE): vol.In(devices_name)}), ) - async def _async_try_connect(self, host: str) -> WifiLedBulb: + async def _async_try_connect(self, host: str) -> dict[str, Any]: """Try to connect.""" self._async_abort_entries_match({CONF_HOST: host}) - return await async_wifi_bulb_for_host(self.hass, host) + if device := await async_discover_device(self.hass, host): + return device + bulb = async_wifi_bulb_for_host(host) + try: + await bulb.async_setup(lambda: None) + finally: + await bulb.async_stop() + return {FLUX_MAC: None, FLUX_MODEL: None, FLUX_HOST: host} class OptionsFlow(config_entries.OptionsFlow): diff --git a/homeassistant/components/flux_led/const.py b/homeassistant/components/flux_led/const.py index 4c8a924df98..e7f9509c54b 100644 --- a/homeassistant/components/flux_led/const.py +++ b/homeassistant/components/flux_led/const.py @@ -1,5 +1,6 @@ """Constants of the FluxLed/MagicHome Integration.""" +import asyncio import socket from typing import Final @@ -7,6 +8,7 @@ DOMAIN: Final = "flux_led" API: Final = "flux_api" +SIGNAL_STATE_UPDATED = "flux_led_{}_state_updated" CONF_AUTOMATIC_ADD: Final = "automatic_add" DEFAULT_NETWORK_SCAN_INTERVAL: Final = 120 @@ -15,7 +17,12 @@ DEFAULT_EFFECT_SPEED: Final = 50 FLUX_LED_DISCOVERY: Final = "flux_led_discovery" -FLUX_LED_EXCEPTIONS: Final = (socket.timeout, BrokenPipeError) +FLUX_LED_EXCEPTIONS: Final = ( + asyncio.TimeoutError, + socket.error, + RuntimeError, + BrokenPipeError, +) STARTUP_SCAN_TIMEOUT: Final = 5 DISCOVER_SCAN_TIMEOUT: Final = 10 diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index a9b8bd32e32..c76e0f42b67 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -2,12 +2,11 @@ from __future__ import annotations import ast -from functools import partial import logging import random from typing import Any, Final, cast -from flux_led import WifiLedBulb +from flux_led.aiodevice import AIOWifiLedBulb from flux_led.const import ( COLOR_MODE_CCT as FLUX_COLOR_MODE_CCT, COLOR_MODE_DIM as FLUX_COLOR_MODE_DIM, @@ -15,7 +14,6 @@ from flux_led.const import ( COLOR_MODE_RGBW as FLUX_COLOR_MODE_RGBW, COLOR_MODE_RGBWW as FLUX_COLOR_MODE_RGBWW, ) -from flux_led.device import MAX_TEMP, MIN_TEMP from flux_led.utils import ( color_temp_to_white_levels, rgbcw_brightness, @@ -61,13 +59,16 @@ from homeassistant.const import ( CONF_NAME, CONF_PROTOCOL, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_platform import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.color import ( + color_hs_to_RGB, + color_RGB_to_hs, color_temperature_kelvin_to_mired, color_temperature_mired_to_kelvin, ) @@ -91,6 +92,7 @@ from .const import ( MODE_RGB, MODE_RGBW, MODE_WHITE, + SIGNAL_STATE_UPDATED, TRANSITION_GRADUAL, TRANSITION_JUMP, TRANSITION_STROBE, @@ -254,7 +256,7 @@ async def async_setup_entry( platform.async_register_entity_service( SERVICE_CUSTOM_EFFECT, CUSTOM_EFFECT_DICT, - "set_custom_effect", + "async_set_custom_effect", ) options = entry.options @@ -298,17 +300,18 @@ class FluxLight(CoordinatorEntity, LightEntity): ) -> None: """Initialize the light.""" super().__init__(coordinator) - self._bulb: WifiLedBulb = coordinator.device + self._device: AIOWifiLedBulb = coordinator.device + self._responding = True self._attr_name = name self._attr_unique_id = unique_id self._attr_supported_features = SUPPORT_FLUX_LED self._attr_min_mireds = ( - color_temperature_kelvin_to_mired(MAX_TEMP) + 1 + color_temperature_kelvin_to_mired(self._device.max_temp) + 1 ) # for rounding - self._attr_max_mireds = color_temperature_kelvin_to_mired(MIN_TEMP) + self._attr_max_mireds = color_temperature_kelvin_to_mired(self._device.min_temp) self._attr_supported_color_modes = { FLUX_COLOR_MODE_TO_HASS.get(mode, COLOR_MODE_ONOFF) - for mode in self._bulb.color_modes + for mode in self._device.color_modes } self._attr_effect_list = FLUX_EFFECT_LIST if custom_effect_colors: @@ -317,82 +320,83 @@ class FluxLight(CoordinatorEntity, LightEntity): self._custom_effect_speed_pct = custom_effect_speed_pct self._custom_effect_transition = custom_effect_transition if self.unique_id: - old_protocol = self._bulb.protocol == "LEDENET_ORIGINAL" - raw_state = self._bulb.raw_state self._attr_device_info = { "connections": {(dr.CONNECTION_NETWORK_MAC, self.unique_id)}, - ATTR_MODEL: f"0x{self._bulb.model_num:02X}", + ATTR_MODEL: f"0x{self._device.model_num:02X}", ATTR_NAME: self.name, - ATTR_SW_VERSION: "1" if old_protocol else str(raw_state.version_number), + ATTR_SW_VERSION: str(self._device.version_num), ATTR_MANUFACTURER: "FluxLED/Magic Home", } @property def is_on(self) -> bool: """Return true if device is on.""" - return cast(bool, self._bulb.is_on) + return cast(bool, self._device.is_on) @property def brightness(self) -> int: """Return the brightness of this light between 0..255.""" - return cast(int, self._bulb.brightness) + return cast(int, self._device.brightness) @property def color_temp(self) -> int: """Return the kelvin value of this light in mired.""" - return color_temperature_kelvin_to_mired(self._bulb.getWhiteTemperature()[0]) + return color_temperature_kelvin_to_mired(self._device.color_temp) @property def rgb_color(self) -> tuple[int, int, int]: """Return the rgb color value.""" - rgb: tuple[int, int, int] = self._bulb.rgb + # Note that we call color_RGB_to_hs and not color_RGB_to_hsv + # to get the unscaled value since this is what the frontend wants + # https://github.com/home-assistant/frontend/blob/e797c017614797bb11671496d6bd65863de22063/src/dialogs/more-info/controls/more-info-light.ts#L263 + rgb: tuple[int, int, int] = color_hs_to_RGB(*color_RGB_to_hs(*self._device.rgb)) return rgb @property def rgbw_color(self) -> tuple[int, int, int, int]: """Return the rgbw color value.""" - rgbw: tuple[int, int, int, int] = self._bulb.rgbw + rgbw: tuple[int, int, int, int] = self._device.rgbw return rgbw @property def rgbww_color(self) -> tuple[int, int, int, int, int]: """Return the rgbww aka rgbcw color value.""" - rgbcw: tuple[int, int, int, int, int] = self._bulb.rgbcw + rgbcw: tuple[int, int, int, int, int] = self._device.rgbcw return rgbcw @property def rgbwc_color(self) -> tuple[int, int, int, int, int]: """Return the rgbwc color value.""" - rgbwc: tuple[int, int, int, int, int] = self._bulb.rgbww + rgbwc: tuple[int, int, int, int, int] = self._device.rgbww return rgbwc @property def color_mode(self) -> str: """Return the color mode of the light.""" - return FLUX_COLOR_MODE_TO_HASS.get(self._bulb.color_mode, COLOR_MODE_ONOFF) + return FLUX_COLOR_MODE_TO_HASS.get(self._device.color_mode, COLOR_MODE_ONOFF) @property def effect(self) -> str | None: """Return the current effect.""" - if (current_mode := self._bulb.raw_state.preset_pattern) == EFFECT_CUSTOM_CODE: + if (current_mode := self._device.preset_pattern_num) == EFFECT_CUSTOM_CODE: return EFFECT_CUSTOM return EFFECT_ID_NAME.get(current_mode) @property def extra_state_attributes(self) -> dict[str, str]: """Return the attributes.""" - return {"ip_address": self._bulb.ipaddr} + return {"ip_address": self._device.ipaddr} async def async_turn_on(self, **kwargs: Any) -> None: """Turn the specified or all lights on.""" - await self.hass.async_add_executor_job(partial(self._turn_on, **kwargs)) + await self._async_turn_on(**kwargs) self.async_write_ha_state() await self.coordinator.async_request_refresh() - def _turn_on(self, **kwargs: Any) -> None: + async def _async_turn_on(self, **kwargs: Any) -> None: """Turn the specified or all lights on.""" if not self.is_on: - self._bulb.turnOn() + await self._device.async_turn_on() if not kwargs: return @@ -404,21 +408,23 @@ class FluxLight(CoordinatorEntity, LightEntity): color_temp_mired = kwargs[ATTR_COLOR_TEMP] color_temp_kelvin = color_temperature_mired_to_kelvin(color_temp_mired) if self.color_mode != COLOR_MODE_RGBWW: - self._bulb.setWhiteTemperature(color_temp_kelvin, brightness) + await self._device.async_set_white_temp(color_temp_kelvin, brightness) return # When switching to color temp from RGBWW mode, # we do not want the overall brightness, we only # want the brightness of the white channels brightness = kwargs.get( - ATTR_BRIGHTNESS, self._bulb.getWhiteTemperature()[1] + ATTR_BRIGHTNESS, self._device.getWhiteTemperature()[1] ) cold, warm = color_temp_to_white_levels(color_temp_kelvin, brightness) - self._bulb.set_levels(r=0, b=0, g=0, w=warm, w2=cold) + await self._device.async_set_levels(r=0, b=0, g=0, w=warm, w2=cold) return - # Handle switch to HS Color Mode + # Handle switch to RGB Color Mode if ATTR_RGB_COLOR in kwargs: - self._bulb.set_levels(*kwargs[ATTR_RGB_COLOR], brightness=brightness) + await self._device.async_set_levels( + *kwargs[ATTR_RGB_COLOR], brightness=brightness + ) return # Handle switch to RGBW Color Mode if ATTR_RGBW_COLOR in kwargs: @@ -426,7 +432,7 @@ class FluxLight(CoordinatorEntity, LightEntity): rgbw = rgbw_brightness(kwargs[ATTR_RGBW_COLOR], brightness) else: rgbw = kwargs[ATTR_RGBW_COLOR] - self._bulb.set_levels(*rgbw) + await self._device.async_set_levels(*rgbw) return # Handle switch to RGBWW Color Mode if ATTR_RGBWW_COLOR in kwargs: @@ -434,17 +440,17 @@ class FluxLight(CoordinatorEntity, LightEntity): rgbcw = rgbcw_brightness(kwargs[ATTR_RGBWW_COLOR], brightness) else: rgbcw = kwargs[ATTR_RGBWW_COLOR] - self._bulb.set_levels(*rgbcw_to_rgbwc(rgbcw)) + await self._device.async_set_levels(*rgbcw_to_rgbwc(rgbcw)) return # Handle switch to White Color Mode if ATTR_WHITE in kwargs: - self._bulb.set_levels(w=kwargs[ATTR_WHITE]) + await self._device.async_set_levels(w=kwargs[ATTR_WHITE]) return if ATTR_EFFECT in kwargs: effect = kwargs[ATTR_EFFECT] # Random color effect if effect == EFFECT_RANDOM: - self._bulb.set_levels( + await self._device.async_set_levels( random.randint(0, 255), random.randint(0, 255), random.randint(0, 255), @@ -453,7 +459,7 @@ class FluxLight(CoordinatorEntity, LightEntity): # Custom effect if effect == EFFECT_CUSTOM: if self._custom_effect_colors: - self._bulb.setCustomPattern( + await self._device.async_set_custom_pattern( self._custom_effect_colors, self._custom_effect_speed_pct, self._custom_effect_transition, @@ -461,39 +467,41 @@ class FluxLight(CoordinatorEntity, LightEntity): return # Effect selection if effect in EFFECT_MAP: - self._bulb.setPresetPattern(EFFECT_MAP[effect], DEFAULT_EFFECT_SPEED) + await self._device.async_set_preset_pattern( + EFFECT_MAP[effect], DEFAULT_EFFECT_SPEED + ) return raise ValueError(f"Unknown effect {effect}") # Handle brightness adjustment in CCT Color Mode if self.color_mode == COLOR_MODE_COLOR_TEMP: - self._bulb.setWhiteTemperature( - self._bulb.getWhiteTemperature()[0], brightness - ) + await self._device.async_set_white_temp(self._device.color_temp, brightness) return # Handle brightness adjustment in RGB Color Mode if self.color_mode == COLOR_MODE_RGB: - self._bulb.set_levels(*self.rgb_color, brightness=brightness) + await self._device.async_set_levels(*self.rgb_color, brightness=brightness) return # Handle brightness adjustment in RGBW Color Mode if self.color_mode == COLOR_MODE_RGBW: - self._bulb.set_levels(*rgbw_brightness(self.rgbw_color, brightness)) + await self._device.async_set_levels( + *rgbw_brightness(self.rgbw_color, brightness) + ) return # Handle brightness adjustment in RGBWW Color Mode if self.color_mode == COLOR_MODE_RGBWW: rgbwc = self.rgbwc_color - self._bulb.set_levels(*rgbww_brightness(rgbwc, brightness)) + await self._device.async_set_levels(*rgbww_brightness(rgbwc, brightness)) return # Handle White Color Mode and Brightness Only Color Mode if self.color_mode in (COLOR_MODE_WHITE, COLOR_MODE_BRIGHTNESS): - self._bulb.set_levels(w=brightness) + await self._device.async_set_levels(w=brightness) return raise ValueError(f"Unsupported color mode {self.color_mode}") - def set_custom_effect( + async def async_set_custom_effect( self, colors: list[tuple[int, int, int]], speed_pct: int, transition: str ) -> None: """Set a custom effect on the bulb.""" - self._bulb.setCustomPattern( + await self._device.async_set_custom_pattern( colors, speed_pct, transition, @@ -501,6 +509,24 @@ class FluxLight(CoordinatorEntity, LightEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the specified or all lights off.""" - await self.hass.async_add_executor_job(self._bulb.turnOff) + await self._device.async_turn_off() self.async_write_ha_state() await self.coordinator.async_request_refresh() + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + if self.coordinator.last_update_success != self._responding: + self.async_write_ha_state() + self._responding = self.coordinator.last_update_success + + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_STATE_UPDATED.format(self._device.ipaddr), + self.async_write_ha_state, + ) + ) + await super().async_added_to_hass() diff --git a/tests/components/flux_led/__init.py__ b/tests/components/flux_led/__init.py__ deleted file mode 100644 index 57af0b3751a..00000000000 --- a/tests/components/flux_led/__init.py__ +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the flux_led integration.""" \ No newline at end of file diff --git a/tests/components/flux_led/__init__.py b/tests/components/flux_led/__init__.py index 1eccf9bfcd6..d705f0d43ff 100644 --- a/tests/components/flux_led/__init__.py +++ b/tests/components/flux_led/__init__.py @@ -1,10 +1,11 @@ """Tests for the flux_led integration.""" from __future__ import annotations -import socket -from unittest.mock import MagicMock, patch +import asyncio +from typing import Callable +from unittest.mock import AsyncMock, MagicMock, patch -from flux_led import WifiLedBulb +from flux_led.aio import AIOWifiLedBulb from flux_led.const import ( COLOR_MODE_CCT as FLUX_COLOR_MODE_CCT, COLOR_MODE_RGB as FLUX_COLOR_MODE_RGB, @@ -17,6 +18,7 @@ from homeassistant.components.dhcp import ( MAC_ADDRESS as DHCP_MAC_ADDRESS, ) from homeassistant.components.flux_led.const import FLUX_HOST, FLUX_MAC, FLUX_MODEL +from homeassistant.core import HomeAssistant MODULE = "homeassistant.components.flux_led" MODULE_CONFIG_FLOW = "homeassistant.components.flux_led.config_flow" @@ -35,8 +37,23 @@ DHCP_DISCOVERY = { FLUX_DISCOVERY = {FLUX_HOST: IP_ADDRESS, FLUX_MODEL: MODEL, FLUX_MAC: FLUX_MAC_ADDRESS} -def _mocked_bulb() -> WifiLedBulb: - bulb = MagicMock(auto_spec=WifiLedBulb) +def _mocked_bulb() -> AIOWifiLedBulb: + bulb = MagicMock(auto_spec=AIOWifiLedBulb) + + async def _save_setup_callback(callback: Callable) -> None: + bulb.data_receive_callback = callback + + bulb.async_setup = AsyncMock(side_effect=_save_setup_callback) + bulb.async_set_custom_pattern = AsyncMock() + bulb.async_set_preset_pattern = AsyncMock() + bulb.async_set_white_temp = AsyncMock() + bulb.async_stop = AsyncMock() + bulb.async_update = AsyncMock() + bulb.async_turn_off = AsyncMock() + bulb.async_turn_on = AsyncMock() + bulb.async_set_levels = AsyncMock() + bulb.min_temp = 2700 + bulb.max_temp = 6500 bulb.getRgb = MagicMock(return_value=[255, 0, 0]) bulb.getRgbw = MagicMock(return_value=[255, 0, 0, 50]) bulb.getRgbww = MagicMock(return_value=[255, 0, 0, 50, 0]) @@ -45,9 +62,11 @@ def _mocked_bulb() -> WifiLedBulb: bulb.rgbw = (255, 0, 0, 50) bulb.rgbww = (255, 0, 0, 50, 0) bulb.rgbcw = (255, 0, 0, 0, 50) + bulb.color_temp = 2700 bulb.getWhiteTemperature = MagicMock(return_value=(2700, 128)) bulb.brightness = 128 bulb.model_num = 0x35 + bulb.version_num = 8 bulb.rgbwcapable = True bulb.color_modes = {FLUX_COLOR_MODE_RGB, FLUX_COLOR_MODE_CCT} bulb.color_mode = FLUX_COLOR_MODE_RGB @@ -57,19 +76,39 @@ def _mocked_bulb() -> WifiLedBulb: return bulb +async def async_mock_bulb_turn_off(hass: HomeAssistant, bulb: AIOWifiLedBulb) -> None: + """Mock the bulb being off.""" + bulb.is_on = False + bulb.raw_state._replace(power_state=0x24) + bulb.data_receive_callback() + await hass.async_block_till_done() + + +async def async_mock_bulb_turn_on(hass: HomeAssistant, bulb: AIOWifiLedBulb) -> None: + """Mock the bulb being on.""" + bulb.is_on = True + bulb.raw_state._replace(power_state=0x23) + bulb.data_receive_callback() + await hass.async_block_till_done() + + def _patch_discovery(device=None, no_device=False): - def _discovery(*args, **kwargs): + async def _discovery(*args, **kwargs): if no_device: - return [] + raise OSError return [FLUX_DISCOVERY] - return patch("homeassistant.components.flux_led.BulbScanner.scan", new=_discovery) + return patch( + "homeassistant.components.flux_led.AIOBulbScanner.async_scan", new=_discovery + ) def _patch_wifibulb(device=None, no_device=False): def _wifi_led_bulb(*args, **kwargs): + bulb = _mocked_bulb() if no_device: - raise socket.timeout + bulb.async_setup = AsyncMock(side_effect=asyncio.TimeoutError) + return bulb return device if device else _mocked_bulb() - return patch("homeassistant.components.flux_led.WifiLedBulb", new=_wifi_led_bulb) + return patch("homeassistant.components.flux_led.AIOWifiLedBulb", new=_wifi_led_bulb) diff --git a/tests/components/flux_led/test_config_flow.py b/tests/components/flux_led/test_config_flow.py index 13e427c3331..582823439f6 100644 --- a/tests/components/flux_led/test_config_flow.py +++ b/tests/components/flux_led/test_config_flow.py @@ -247,7 +247,7 @@ async def test_import(hass: HomeAssistant): assert result["reason"] == "already_configured" -async def test_manual(hass: HomeAssistant): +async def test_manual_working_discovery(hass: HomeAssistant): """Test manually setup.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -276,8 +276,8 @@ async def test_manual(hass: HomeAssistant): ) await hass.async_block_till_done() assert result4["type"] == "create_entry" - assert result4["title"] == IP_ADDRESS - assert result4["data"] == {CONF_HOST: IP_ADDRESS, CONF_NAME: IP_ADDRESS} + assert result4["title"] == DEFAULT_ENTRY_TITLE + assert result4["data"] == {CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE} # Duplicate result = await hass.config_entries.flow.async_init( diff --git a/tests/components/flux_led/test_init.py b/tests/components/flux_led/test_init.py index 734fb64520f..db4ddffbc3f 100644 --- a/tests/components/flux_led/test_init.py +++ b/tests/components/flux_led/test_init.py @@ -6,16 +6,16 @@ from unittest.mock import patch from homeassistant.components import flux_led from homeassistant.components.flux_led.const import DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STARTED +from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STARTED from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow from . import ( + DEFAULT_ENTRY_TITLE, FLUX_DISCOVERY, IP_ADDRESS, MAC_ADDRESS, - _mocked_bulb, _patch_discovery, _patch_wifibulb, ) @@ -25,7 +25,9 @@ from tests.common import MockConfigEntry, async_fire_time_changed async def test_configuring_flux_led_causes_discovery(hass: HomeAssistant) -> None: """Test that specifying empty config does discovery.""" - with patch("homeassistant.components.flux_led.BulbScanner.scan") as discover: + with patch( + "homeassistant.components.flux_led.AIOBulbScanner.async_scan" + ) as discover: discover.return_value = [FLUX_DISCOVERY] await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() @@ -65,15 +67,26 @@ async def test_config_entry_retry(hass: HomeAssistant) -> None: assert config_entry.state == ConfigEntryState.SETUP_RETRY -async def test_config_entry_retry_when_state_missing(hass: HomeAssistant) -> None: - """Test that a config entry is retried when state is missing.""" +async def test_config_entry_fills_unique_id_with_directed_discovery( + hass: HomeAssistant, +) -> None: + """Test that the unique id is added if its missing via directed (not broadcast) discovery.""" config_entry = MockConfigEntry( - domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=MAC_ADDRESS + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=None ) config_entry.add_to_hass(hass) - bulb = _mocked_bulb() - bulb.raw_state = None - with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + + async def _discovery(self, *args, address=None, **kwargs): + # Only return discovery results when doing directed discovery + return [FLUX_DISCOVERY] if address == IP_ADDRESS else [] + + with patch( + "homeassistant.components.flux_led.AIOBulbScanner.async_scan", new=_discovery + ), _patch_wifibulb(): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_RETRY + assert config_entry.state == ConfigEntryState.LOADED + + assert config_entry.unique_id == MAC_ADDRESS + assert config_entry.data[CONF_NAME] == DEFAULT_ENTRY_TITLE + assert config_entry.title == DEFAULT_ENTRY_TITLE diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index 56f53f97bf4..aa2ee650020 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -1,6 +1,6 @@ """Tests for light platform.""" from datetime import timedelta -from unittest.mock import Mock +from unittest.mock import AsyncMock, Mock from flux_led.const import ( COLOR_MODE_ADDRESSABLE as FLUX_COLOR_MODE_ADDRESSABLE, @@ -50,6 +50,7 @@ from homeassistant.const import ( CONF_PROTOCOL, STATE_OFF, STATE_ON, + STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -63,6 +64,8 @@ from . import ( _mocked_bulb, _patch_discovery, _patch_wifibulb, + async_mock_bulb_turn_off, + async_mock_bulb_turn_on, ) from tests.common import MockConfigEntry, async_fire_time_changed @@ -88,6 +91,40 @@ async def test_light_unique_id(hass: HomeAssistant) -> None: assert state.state == STATE_ON +async def test_light_goes_unavailable_and_recovers(hass: HomeAssistant) -> None: + """Test a light goes unavailable and then recovers.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.az120444_aabbccddeeff" + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == MAC_ADDRESS + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + now = utcnow() + bulb.async_update = AsyncMock(side_effect=RuntimeError) + for i in range(10, 50, 10): + async_fire_time_changed(hass, now + timedelta(seconds=i)) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE + bulb.async_update = AsyncMock() + for i in range(60, 100, 10): + async_fire_time_changed(hass, now + timedelta(seconds=i)) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + async def test_light_no_unique_id(hass: HomeAssistant) -> None: """Test a light without a unique id.""" config_entry = MockConfigEntry( @@ -120,6 +157,7 @@ async def test_light_device_registry( ) config_entry.add_to_hass(hass) bulb = _mocked_bulb() + bulb.version_num = sw_version bulb.protocol = protocol bulb.raw_state = bulb.raw_state._replace(model_num=model, version_number=sw_version) bulb.model_num = model @@ -166,18 +204,16 @@ async def test_rgb_light(hass: HomeAssistant) -> None: await hass.services.async_call( LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turnOff.assert_called_once() + bulb.async_turn_off.assert_called_once() - bulb.is_on = False - async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) - await hass.async_block_till_done() + await async_mock_bulb_turn_off(hass, bulb) assert hass.states.get(entity_id).state == STATE_OFF await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turnOn.assert_called_once() - bulb.turnOn.reset_mock() + bulb.async_turn_on.assert_called_once() + bulb.async_turn_on.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -185,8 +221,8 @@ async def test_rgb_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, blocking=True, ) - bulb.set_levels.assert_called_with(255, 0, 0, brightness=100) - bulb.set_levels.reset_mock() + bulb.async_set_levels.assert_called_with(255, 0, 0, brightness=100) + bulb.async_set_levels.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -194,8 +230,8 @@ async def test_rgb_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, blocking=True, ) - bulb.set_levels.assert_called_with(255, 191, 178, brightness=128) - bulb.set_levels.reset_mock() + bulb.async_set_levels.assert_called_with(255, 191, 178, brightness=128) + bulb.async_set_levels.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -203,8 +239,8 @@ async def test_rgb_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"}, blocking=True, ) - bulb.set_levels.assert_called_once() - bulb.set_levels.reset_mock() + bulb.async_set_levels.assert_called_once() + bulb.async_set_levels.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -212,8 +248,8 @@ async def test_rgb_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"}, blocking=True, ) - bulb.setPresetPattern.assert_called_with(43, 50) - bulb.setPresetPattern.reset_mock() + bulb.async_set_preset_pattern.assert_called_with(43, 50) + bulb.async_set_preset_pattern.reset_mock() with pytest.raises(ValueError): await hass.services.async_call( @@ -254,18 +290,16 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None: await hass.services.async_call( LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turnOff.assert_called_once() + bulb.async_turn_off.assert_called_once() + await async_mock_bulb_turn_off(hass, bulb) - bulb.is_on = False - async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) - await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turnOn.assert_called_once() - bulb.turnOn.reset_mock() + bulb.async_turn_on.assert_called_once() + bulb.async_turn_on.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -273,8 +307,8 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, blocking=True, ) - bulb.set_levels.assert_called_with(255, 0, 0, brightness=100) - bulb.set_levels.reset_mock() + bulb.async_set_levels.assert_called_with(255, 0, 0, brightness=100) + bulb.async_set_levels.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -282,8 +316,8 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, blocking=True, ) - bulb.set_levels.assert_called_with(255, 191, 178, brightness=128) - bulb.set_levels.reset_mock() + bulb.async_set_levels.assert_called_with(255, 191, 178, brightness=128) + bulb.async_set_levels.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -291,8 +325,8 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"}, blocking=True, ) - bulb.set_levels.assert_called_once() - bulb.set_levels.reset_mock() + bulb.async_set_levels.assert_called_once() + bulb.async_set_levels.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -300,17 +334,16 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"}, blocking=True, ) - bulb.setPresetPattern.assert_called_with(43, 50) - bulb.setPresetPattern.reset_mock() - - bulb.is_on = True + bulb.async_set_preset_pattern.assert_called_with(43, 50) + bulb.async_set_preset_pattern.reset_mock() bulb.color_mode = FLUX_COLOR_MODE_CCT bulb.getWhiteTemperature = Mock(return_value=(5000, 128)) + bulb.color_temp = 5000 + bulb.raw_state = bulb.raw_state._replace( red=0, green=0, blue=0, warm_white=1, cool_white=2 ) - async_fire_time_changed(hass, utcnow() + timedelta(seconds=60)) - await hass.async_block_till_done() + await async_mock_bulb_turn_on(hass, bulb) state = hass.states.get(entity_id) assert state.state == STATE_ON attributes = state.attributes @@ -325,8 +358,8 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 370}, blocking=True, ) - bulb.setWhiteTemperature.assert_called_with(2702, 128) - bulb.setWhiteTemperature.reset_mock() + bulb.async_set_white_temp.assert_called_with(2702, 128) + bulb.async_set_white_temp.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -334,8 +367,8 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 255}, blocking=True, ) - bulb.setWhiteTemperature.assert_called_with(5000, 255) - bulb.setWhiteTemperature.reset_mock() + bulb.async_set_white_temp.assert_called_with(5000, 255) + bulb.async_set_white_temp.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -343,8 +376,8 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 128}, blocking=True, ) - bulb.setWhiteTemperature.assert_called_with(5000, 128) - bulb.setWhiteTemperature.reset_mock() + bulb.async_set_white_temp.assert_called_with(5000, 128) + bulb.async_set_white_temp.reset_mock() async def test_rgbw_light(hass: HomeAssistant) -> None: @@ -376,18 +409,16 @@ async def test_rgbw_light(hass: HomeAssistant) -> None: await hass.services.async_call( LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turnOff.assert_called_once() + bulb.async_turn_off.assert_called_once() + await async_mock_bulb_turn_off(hass, bulb) - bulb.is_on = False - async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) - await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turnOn.assert_called_once() - bulb.turnOn.reset_mock() + bulb.async_turn_on.assert_called_once() + bulb.async_turn_on.reset_mock() bulb.is_on = True await hass.services.async_call( @@ -396,8 +427,8 @@ async def test_rgbw_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, blocking=True, ) - bulb.set_levels.assert_called_with(168, 0, 0, 33) - bulb.set_levels.reset_mock() + bulb.async_set_levels.assert_called_with(168, 0, 0, 33) + bulb.async_set_levels.reset_mock() state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -411,8 +442,8 @@ async def test_rgbw_light(hass: HomeAssistant) -> None: }, blocking=True, ) - bulb.set_levels.assert_called_with(128, 128, 128, 128) - bulb.set_levels.reset_mock() + bulb.async_set_levels.assert_called_with(128, 128, 128, 128) + bulb.async_set_levels.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -420,8 +451,8 @@ async def test_rgbw_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_RGBW_COLOR: (255, 255, 255, 255)}, blocking=True, ) - bulb.set_levels.assert_called_with(255, 255, 255, 255) - bulb.set_levels.reset_mock() + bulb.async_set_levels.assert_called_with(255, 255, 255, 255) + bulb.async_set_levels.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -429,8 +460,8 @@ async def test_rgbw_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_RGBW_COLOR: (255, 191, 178, 0)}, blocking=True, ) - bulb.set_levels.assert_called_with(255, 191, 178, 0) - bulb.set_levels.reset_mock() + bulb.async_set_levels.assert_called_with(255, 191, 178, 0) + bulb.async_set_levels.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -438,8 +469,8 @@ async def test_rgbw_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"}, blocking=True, ) - bulb.set_levels.assert_called_once() - bulb.set_levels.reset_mock() + bulb.async_set_levels.assert_called_once() + bulb.async_set_levels.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -447,8 +478,8 @@ async def test_rgbw_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"}, blocking=True, ) - bulb.setPresetPattern.assert_called_with(43, 50) - bulb.setPresetPattern.reset_mock() + bulb.async_set_preset_pattern.assert_called_with(43, 50) + bulb.async_set_preset_pattern.reset_mock() async def test_rgbcw_light(hass: HomeAssistant) -> None: @@ -481,18 +512,16 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None: await hass.services.async_call( LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turnOff.assert_called_once() + bulb.async_turn_off.assert_called_once() + await async_mock_bulb_turn_off(hass, bulb) - bulb.is_on = False - async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) - await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turnOn.assert_called_once() - bulb.turnOn.reset_mock() + bulb.async_turn_on.assert_called_once() + bulb.async_turn_on.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -500,8 +529,8 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, blocking=True, ) - bulb.set_levels.assert_called_with(250, 0, 0, 49, 0) - bulb.set_levels.reset_mock() + bulb.async_set_levels.assert_called_with(250, 0, 0, 49, 0) + bulb.async_set_levels.reset_mock() bulb.is_on = True await hass.services.async_call( @@ -514,8 +543,8 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None: }, blocking=True, ) - bulb.set_levels.assert_called_with(192, 192, 192, 192, 0) - bulb.set_levels.reset_mock() + bulb.async_set_levels.assert_called_with(192, 192, 192, 192, 0) + bulb.async_set_levels.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -523,8 +552,8 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_RGBWW_COLOR: (255, 255, 255, 255, 50)}, blocking=True, ) - bulb.set_levels.assert_called_with(255, 255, 255, 50, 255) - bulb.set_levels.reset_mock() + bulb.async_set_levels.assert_called_with(255, 255, 255, 50, 255) + bulb.async_set_levels.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -532,8 +561,8 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 154}, blocking=True, ) - bulb.set_levels.assert_called_with(r=0, b=0, g=0, w=0, w2=127) - bulb.set_levels.reset_mock() + bulb.async_set_levels.assert_called_with(r=0, b=0, g=0, w=0, w2=127) + bulb.async_set_levels.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -541,8 +570,8 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 154, ATTR_BRIGHTNESS: 255}, blocking=True, ) - bulb.set_levels.assert_called_with(r=0, b=0, g=0, w=0, w2=255) - bulb.set_levels.reset_mock() + bulb.async_set_levels.assert_called_with(r=0, b=0, g=0, w=0, w2=255) + bulb.async_set_levels.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -550,8 +579,8 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 290}, blocking=True, ) - bulb.set_levels.assert_called_with(r=0, b=0, g=0, w=102, w2=25) - bulb.set_levels.reset_mock() + bulb.async_set_levels.assert_called_with(r=0, b=0, g=0, w=102, w2=25) + bulb.async_set_levels.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -559,8 +588,8 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_RGBWW_COLOR: (255, 191, 178, 0, 0)}, blocking=True, ) - bulb.set_levels.assert_called_with(255, 191, 178, 0, 0) - bulb.set_levels.reset_mock() + bulb.async_set_levels.assert_called_with(255, 191, 178, 0, 0) + bulb.async_set_levels.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -568,8 +597,8 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"}, blocking=True, ) - bulb.set_levels.assert_called_once() - bulb.set_levels.reset_mock() + bulb.async_set_levels.assert_called_once() + bulb.async_set_levels.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -577,8 +606,8 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"}, blocking=True, ) - bulb.setPresetPattern.assert_called_with(43, 50) - bulb.setPresetPattern.reset_mock() + bulb.async_set_preset_pattern.assert_called_with(43, 50) + bulb.async_set_preset_pattern.reset_mock() async def test_white_light(hass: HomeAssistant) -> None: @@ -610,18 +639,16 @@ async def test_white_light(hass: HomeAssistant) -> None: await hass.services.async_call( LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turnOff.assert_called_once() + bulb.async_turn_off.assert_called_once() + await async_mock_bulb_turn_off(hass, bulb) - bulb.is_on = False - async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) - await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turnOn.assert_called_once() - bulb.turnOn.reset_mock() + bulb.async_turn_on.assert_called_once() + bulb.async_turn_on.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -629,8 +656,8 @@ async def test_white_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, blocking=True, ) - bulb.set_levels.assert_called_with(w=100) - bulb.set_levels.reset_mock() + bulb.async_set_levels.assert_called_with(w=100) + bulb.async_set_levels.reset_mock() await hass.services.async_call( LIGHT_DOMAIN, @@ -638,8 +665,8 @@ async def test_white_light(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_WHITE: 100}, blocking=True, ) - bulb.set_levels.assert_called_with(w=100) - bulb.set_levels.reset_mock() + bulb.async_set_levels.assert_called_with(w=100) + bulb.async_set_levels.reset_mock() async def test_rgb_light_custom_effects(hass: HomeAssistant) -> None: @@ -677,10 +704,8 @@ async def test_rgb_light_custom_effects(hass: HomeAssistant) -> None: await hass.services.async_call( LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turnOff.assert_called_once() - - bulb.is_on = False - async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) + bulb.async_turn_off.assert_called_once() + await async_mock_bulb_turn_off(hass, bulb) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF @@ -690,12 +715,13 @@ async def test_rgb_light_custom_effects(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "custom"}, blocking=True, ) - bulb.setCustomPattern.assert_called_with([[0, 0, 255], [255, 0, 0]], 88, "jump") - bulb.setCustomPattern.reset_mock() - bulb.raw_state = bulb.raw_state._replace(preset_pattern=EFFECT_CUSTOM_CODE) - bulb.is_on = True - async_fire_time_changed(hass, utcnow() + timedelta(seconds=20)) - await hass.async_block_till_done() + bulb.async_set_custom_pattern.assert_called_with( + [[0, 0, 255], [255, 0, 0]], 88, "jump" + ) + bulb.async_set_custom_pattern.reset_mock() + bulb.preset_pattern_num = EFFECT_CUSTOM_CODE + await async_mock_bulb_turn_on(hass, bulb) + state = hass.states.get(entity_id) assert state.state == STATE_ON attributes = state.attributes @@ -707,12 +733,13 @@ async def test_rgb_light_custom_effects(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 55, ATTR_EFFECT: "custom"}, blocking=True, ) - bulb.setCustomPattern.assert_called_with([[0, 0, 255], [255, 0, 0]], 88, "jump") - bulb.setCustomPattern.reset_mock() - bulb.raw_state = bulb.raw_state._replace(preset_pattern=EFFECT_CUSTOM_CODE) - bulb.is_on = True - async_fire_time_changed(hass, utcnow() + timedelta(seconds=20)) - await hass.async_block_till_done() + bulb.async_set_custom_pattern.assert_called_with( + [[0, 0, 255], [255, 0, 0]], 88, "jump" + ) + bulb.async_set_custom_pattern.reset_mock() + bulb.preset_pattern_num = EFFECT_CUSTOM_CODE + await async_mock_bulb_turn_on(hass, bulb) + state = hass.states.get(entity_id) assert state.state == STATE_ON attributes = state.attributes @@ -783,11 +810,9 @@ async def test_rgb_light_custom_effect_via_service( await hass.services.async_call( LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turnOff.assert_called_once() + bulb.async_turn_off.assert_called_once() - bulb.is_on = False - async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) - await hass.async_block_till_done() + await async_mock_bulb_turn_off(hass, bulb) assert hass.states.get(entity_id).state == STATE_OFF await hass.services.async_call( @@ -801,8 +826,10 @@ async def test_rgb_light_custom_effect_via_service( }, blocking=True, ) - bulb.setCustomPattern.assert_called_with([(0, 0, 255), (255, 0, 0)], 30, "jump") - bulb.setCustomPattern.reset_mock() + bulb.async_set_custom_pattern.assert_called_with( + [(0, 0, 255), (255, 0, 0)], 30, "jump" + ) + bulb.async_set_custom_pattern.reset_mock() async def test_migrate_from_yaml(hass: HomeAssistant) -> None: @@ -882,19 +909,17 @@ async def test_addressable_light(hass: HomeAssistant) -> None: await hass.services.async_call( LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turnOff.assert_called_once() + bulb.async_turn_off.assert_called_once() - bulb.is_on = False - async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) - await hass.async_block_till_done() + await async_mock_bulb_turn_off(hass, bulb) assert hass.states.get(entity_id).state == STATE_OFF await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - bulb.turnOn.assert_called_once() - bulb.turnOn.reset_mock() - bulb.is_on = True + bulb.async_turn_on.assert_called_once() + bulb.async_turn_on.reset_mock() + await async_mock_bulb_turn_on(hass, bulb) with pytest.raises(ValueError): await hass.services.async_call( From c0a3c7a4b74bdf64bd242e1344594989bbf0d43a Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 11 Oct 2021 04:37:44 +0200 Subject: [PATCH 0223/1038] Update pyfronius to 0.7.0 (#57279) * update to pyfronius 0.7.0 * exception handling * exception handling --- homeassistant/components/fronius/manifest.json | 2 +- homeassistant/components/fronius/sensor.py | 12 +++--------- requirements_all.txt | 2 +- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/fronius/manifest.json b/homeassistant/components/fronius/manifest.json index 4c21e83e191..e40b5303eca 100644 --- a/homeassistant/components/fronius/manifest.json +++ b/homeassistant/components/fronius/manifest.json @@ -2,7 +2,7 @@ "domain": "fronius", "name": "Fronius", "documentation": "https://www.home-assistant.io/integrations/fronius", - "requirements": ["pyfronius==0.6.0"], + "requirements": ["pyfronius==0.7.0"], "codeowners": ["@nielstron"], "iot_class": "local_polling" } diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index 076eee9acc8..0ad172c2ab0 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -6,7 +6,7 @@ from datetime import timedelta import logging from typing import Any -from pyfronius import Fronius +from pyfronius import Fronius, FroniusError import voluptuous as vol from homeassistant.components.sensor import ( @@ -205,19 +205,13 @@ class FroniusAdapter: """Retrieve and update latest state.""" try: values = await self._update() - except ConnectionError: + except FroniusError as err: # fronius devices are often powered by self-produced solar energy # and henced turned off at night. # Therefore we will not print multiple errors when connection fails if self._available: self._available = False - _LOGGER.error("Failed to update: connection error") - return - except ValueError: - _LOGGER.error( - "Failed to update: invalid response returned." - "Maybe the configured device is not supported" - ) + _LOGGER.error("Failed to update: %s", err) return self._available = True # reset connection failure diff --git a/requirements_all.txt b/requirements_all.txt index ec5374661c6..744403d7968 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1495,7 +1495,7 @@ pyfreedompro==1.1.0 pyfritzhome==0.6.2 # homeassistant.components.fronius -pyfronius==0.6.0 +pyfronius==0.7.0 # homeassistant.components.ifttt pyfttt==0.3 From 77c77093235b89a68a706f5f29f9318ab5536fad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 10 Oct 2021 16:40:17 -1000 Subject: [PATCH 0224/1038] Bump zeroconf to 0.36.8 (#57451) - Changelog: https://github.com/jstasiak/python-zeroconf/releases/tag/0.36.8 --- 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 e38a8d92a94..e89697f2131 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.36.7"], + "requirements": ["zeroconf==0.36.8"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 455f5d2ddaf..a650fe4ab83 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ sqlalchemy==1.4.23 voluptuous-serialize==2.4.0 voluptuous==0.12.2 yarl==1.6.3 -zeroconf==0.36.7 +zeroconf==0.36.8 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 744403d7968..db6449fa793 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2471,7 +2471,7 @@ youtube_dl==2021.04.26 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.36.7 +zeroconf==0.36.8 # homeassistant.components.zha zha-quirks==0.0.62 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 25a22280f71..05ab17dce48 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1415,7 +1415,7 @@ yeelight==0.7.7 youless-api==0.13 # homeassistant.components.zeroconf -zeroconf==0.36.7 +zeroconf==0.36.8 # homeassistant.components.zha zha-quirks==0.0.62 From 6e95ce70bcb58c2157d26d1d2481460f871a622c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 10 Oct 2021 17:14:42 -1000 Subject: [PATCH 0225/1038] Bump aiodiscover to 2.4.5 (#57439) - Disable scanning if the network size exceeds the maximum number of allowed hosts (8192) - Changelog: https://github.com/bdraco/aiodiscover/compare/v1.4.4...v1.4.5 - Closes #57378 --- homeassistant/components/dhcp/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/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index 8ec6bf855c8..312b83c3311 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -2,7 +2,7 @@ "domain": "dhcp", "name": "DHCP Discovery", "documentation": "https://www.home-assistant.io/integrations/dhcp", - "requirements": ["scapy==2.4.5", "aiodiscover==1.4.4"], + "requirements": ["scapy==2.4.5", "aiodiscover==1.4.5"], "codeowners": ["@bdraco"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a650fe4ab83..7a9875f05aa 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ PyJWT==2.1.0 PyNaCl==1.4.0 -aiodiscover==1.4.4 +aiodiscover==1.4.5 aiohttp==3.7.4.post0 aiohttp_cors==0.7.0 astral==2.2 diff --git a/requirements_all.txt b/requirements_all.txt index db6449fa793..97850d1fd98 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -145,7 +145,7 @@ aioazuredevops==1.3.5 aiobotocore==1.2.2 # homeassistant.components.dhcp -aiodiscover==1.4.4 +aiodiscover==1.4.5 # homeassistant.components.dnsip # homeassistant.components.minecraft_server diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 05ab17dce48..109912b83ba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -93,7 +93,7 @@ aioazuredevops==1.3.5 aiobotocore==1.2.2 # homeassistant.components.dhcp -aiodiscover==1.4.4 +aiodiscover==1.4.5 # homeassistant.components.dnsip # homeassistant.components.minecraft_server From 4129119b691bfb2c6b82a498fd8a320da16f1874 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 11 Oct 2021 06:30:16 +0300 Subject: [PATCH 0226/1038] Fix netgear renamed mdi icons (#57431) --- homeassistant/components/netgear/const.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/netgear/const.py b/homeassistant/components/netgear/const.py index 325d9e68cd8..bfcf76a6119 100644 --- a/homeassistant/components/netgear/const.py +++ b/homeassistant/components/netgear/const.py @@ -35,7 +35,7 @@ DEVICE_ICONS = { 0: "mdi:access-point-network", # Router (Orbi ...) 1: "mdi:book-open-variant", # Amazon Kindle 2: "mdi:android", # Android Device - 3: "mdi:cellphone-android", # Android Phone + 3: "mdi:cellphone", # Android Phone 4: "mdi:tablet-android", # Android Tablet 5: "mdi:router-wireless", # Apple Airport Express 6: "mdi:disc-player", # Blu-ray Player @@ -46,15 +46,15 @@ DEVICE_ICONS = { 11: "mdi:play-network", # DVR 12: "mdi:gamepad-variant", # Gaming Console 13: "mdi:desktop-mac", # iMac - 14: "mdi:tablet-ipad", # iPad - 15: "mdi:tablet-ipad", # iPad Mini - 16: "mdi:cellphone-iphone", # iPhone 5/5S/5C - 17: "mdi:cellphone-iphone", # iPhone + 14: "mdi:tablet", # iPad + 15: "mdi:tablet", # iPad Mini + 16: "mdi:cellphone", # iPhone 5/5S/5C + 17: "mdi:cellphone", # iPhone 18: "mdi:ipod", # iPod Touch 19: "mdi:linux", # Linux PC 20: "mdi:apple-finder", # Mac Mini 21: "mdi:desktop-tower", # Mac Pro - 22: "mdi:laptop-mac", # MacBook + 22: "mdi:laptop", # MacBook 23: "mdi:play-network", # Media Device 24: "mdi:network", # Network Device 25: "mdi:play-network", # Other STB @@ -71,7 +71,7 @@ DEVICE_ICONS = { 36: "mdi:tablet", # Tablet 37: "mdi:desktop-classic", # UNIX PC 38: "mdi:desktop-tower-monitor", # Windows PC - 39: "mdi:laptop-windows", # Surface + 39: "mdi:laptop", # Surface 40: "mdi:access-point-network", # Wifi Extender - 41: "mdi:apple-airplay", # Apple TV + 41: "mdi:cast-variant", # Apple TV } From 14050966ccbc2f8b12c1168c8ea3ad1786e2f6ee Mon Sep 17 00:00:00 2001 From: spahlimi <44210026+spahlimi@users.noreply.github.com> Date: Mon, 11 Oct 2021 06:47:38 +0200 Subject: [PATCH 0227/1038] Upgrade rvx to 0.7.0 (#57430) --- homeassistant/components/yamaha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yamaha/manifest.json b/homeassistant/components/yamaha/manifest.json index 46752fee699..437e9479ae1 100644 --- a/homeassistant/components/yamaha/manifest.json +++ b/homeassistant/components/yamaha/manifest.json @@ -2,7 +2,7 @@ "domain": "yamaha", "name": "Yamaha Network Receivers", "documentation": "https://www.home-assistant.io/integrations/yamaha", - "requirements": ["rxv==0.6.0"], + "requirements": ["rxv==0.7.0"], "codeowners": [], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 97850d1fd98..80d65cd2f98 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2094,7 +2094,7 @@ russound==0.1.9 russound_rio==0.1.7 # homeassistant.components.yamaha -rxv==0.6.0 +rxv==0.7.0 # homeassistant.components.samsungtv samsungctl[websocket]==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 109912b83ba..6c608b3e135 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1199,7 +1199,7 @@ roonapi==0.0.38 rpi-bad-power==0.1.0 # homeassistant.components.yamaha -rxv==0.6.0 +rxv==0.7.0 # homeassistant.components.samsungtv samsungctl[websocket]==0.7.1 From ba0196137e9bf07c5c32a72b772a11e6288cc290 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 11 Oct 2021 02:32:00 -0400 Subject: [PATCH 0228/1038] Bump pytautulli to 21.10.0 (#57449) --- homeassistant/components/tautulli/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tautulli/manifest.json b/homeassistant/components/tautulli/manifest.json index d413e477397..cad31683c73 100644 --- a/homeassistant/components/tautulli/manifest.json +++ b/homeassistant/components/tautulli/manifest.json @@ -2,7 +2,7 @@ "domain": "tautulli", "name": "Tautulli", "documentation": "https://www.home-assistant.io/integrations/tautulli", - "requirements": ["pytautulli==21.8.1"], + "requirements": ["pytautulli==21.10.0"], "codeowners": ["@ludeeus"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 80d65cd2f98..e790f7e4e09 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1838,7 +1838,7 @@ pysyncthru==0.7.10 pytankerkoenig==0.0.6 # homeassistant.components.tautulli -pytautulli==21.8.1 +pytautulli==21.10.0 # homeassistant.components.tfiac pytfiac==0.4 From 3825f80a2dd087ae70654079cd9f3071289b8423 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Mon, 11 Oct 2021 09:35:26 +0200 Subject: [PATCH 0229/1038] Fix upnp creating derived sensors (#57436) --- homeassistant/components/upnp/__init__.py | 4 +++- homeassistant/components/upnp/binary_sensor.py | 6 ++++-- homeassistant/components/upnp/sensor.py | 14 ++++++++++---- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 6db8b087378..d2d59d78c0e 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -198,6 +198,7 @@ class UpnpBinarySensorEntityDescription(BinarySensorEntityDescription): """A class that describes UPnP entities.""" format: str = "s" + unique_id: str | None = None @dataclass @@ -205,6 +206,7 @@ class UpnpSensorEntityDescription(SensorEntityDescription): """A class that describes a sensor UPnP entities.""" format: str = "s" + unique_id: str | None = None class UpnpDataUpdateCoordinator(DataUpdateCoordinator): @@ -250,7 +252,7 @@ class UpnpEntity(CoordinatorEntity): self._device = coordinator.device self.entity_description = entity_description self._attr_name = f"{coordinator.device.name} {entity_description.name}" - self._attr_unique_id = f"{coordinator.device.udn}_{entity_description.key}" + self._attr_unique_id = f"{coordinator.device.udn}_{entity_description.unique_id or entity_description.key}" self._attr_device_info = { "connections": {(dr.CONNECTION_UPNP, coordinator.device.udn)}, "name": coordinator.device.name, diff --git a/homeassistant/components/upnp/binary_sensor.py b/homeassistant/components/upnp/binary_sensor.py index 3bf9635c78b..c4e7264c34b 100644 --- a/homeassistant/components/upnp/binary_sensor.py +++ b/homeassistant/components/upnp/binary_sensor.py @@ -30,14 +30,16 @@ async def async_setup_entry( LOGGER.debug("Adding binary sensor") - async_add_entities( + entities = [ UpnpStatusBinarySensor( coordinator=coordinator, entity_description=entity_description, ) for entity_description in BINARYSENSOR_ENTITY_DESCRIPTIONS if coordinator.data.get(entity_description.key) is not None - ) + ] + LOGGER.debug("Adding entities: %s", entities) + async_add_entities(entities) class UpnpStatusBinarySensor(UpnpEntity, BinarySensorEntity): diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 8ad8677b647..334dc9e8c22 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -15,6 +15,7 @@ from .const import ( DATA_RATE_PACKETS_PER_SECOND, DOMAIN, KIBIBYTE, + LOGGER, PACKETS_RECEIVED, PACKETS_SENT, ROUTER_IP, @@ -74,28 +75,32 @@ RAW_SENSORS: tuple[UpnpSensorEntityDescription, ...] = ( DERIVED_SENSORS: tuple[UpnpSensorEntityDescription, ...] = ( UpnpSensorEntityDescription( - key="KiB/sec_received", + key=BYTES_RECEIVED, + unique_id="KiB/sec_received", name=f"{DATA_RATE_KIBIBYTES_PER_SECOND} received", icon="mdi:server-network", native_unit_of_measurement=DATA_RATE_KIBIBYTES_PER_SECOND, format=".1f", ), UpnpSensorEntityDescription( - key="KiB/sent", + key=BYTES_SENT, + unique_id="KiB/sent", name=f"{DATA_RATE_KIBIBYTES_PER_SECOND} sent", icon="mdi:server-network", native_unit_of_measurement=DATA_RATE_KIBIBYTES_PER_SECOND, format=".1f", ), UpnpSensorEntityDescription( - key="packets/sec_received", + key=PACKETS_RECEIVED, + unique_id="packets/sec_received", name=f"{DATA_RATE_PACKETS_PER_SECOND} received", icon="mdi:server-network", native_unit_of_measurement=DATA_RATE_PACKETS_PER_SECOND, format=".1f", ), UpnpSensorEntityDescription( - key="packets/sent", + key=PACKETS_SENT, + unique_id="packets/sent", name=f"{DATA_RATE_PACKETS_PER_SECOND} sent", icon="mdi:server-network", native_unit_of_measurement=DATA_RATE_PACKETS_PER_SECOND, @@ -131,6 +136,7 @@ async def async_setup_entry( ] ) + LOGGER.debug("Adding entities: %s", entities) async_add_entities(entities) From c4eeebd7a7e6f1c20610c6907238143b13389a46 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 11 Oct 2021 04:07:31 -0400 Subject: [PATCH 0230/1038] Add config flow for efergy (#56890) --- homeassistant/components/efergy/__init__.py | 84 +++++++- .../components/efergy/config_flow.py | 85 ++++++++ homeassistant/components/efergy/const.py | 13 ++ homeassistant/components/efergy/manifest.json | 3 +- homeassistant/components/efergy/sensor.py | 189 ++++++++++------- homeassistant/components/efergy/strings.json | 21 ++ .../components/efergy/translations/en.json | 21 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/efergy/__init__.py | 158 +++++++++++++- tests/components/efergy/test_config_flow.py | 134 ++++++++++++ tests/components/efergy/test_init.py | 61 ++++++ tests/components/efergy/test_sensor.py | 200 +++++++++--------- .../{efergy_budget.json => budget.json} | 0 ...s_multi.json => current_values_multi.json} | 0 ...single.json => current_values_single.json} | 0 .../{efergy_cost.json => daily_cost.json} | 0 .../{efergy_energy.json => daily_energy.json} | 0 .../{efergy_instant.json => instant.json} | 0 tests/fixtures/efergy/monthly_cost.json | 5 + tests/fixtures/efergy/monthly_energy.json | 5 + tests/fixtures/efergy/status.json | 31 +++ tests/fixtures/efergy/weekly_cost.json | 5 + tests/fixtures/efergy/weekly_energy.json | 5 + tests/fixtures/efergy/yearly_cost.json | 5 + tests/fixtures/efergy/yearly_energy.json | 5 + 27 files changed, 851 insertions(+), 184 deletions(-) create mode 100644 homeassistant/components/efergy/config_flow.py create mode 100644 homeassistant/components/efergy/const.py create mode 100644 homeassistant/components/efergy/strings.json create mode 100644 homeassistant/components/efergy/translations/en.json create mode 100644 tests/components/efergy/test_config_flow.py create mode 100644 tests/components/efergy/test_init.py rename tests/fixtures/efergy/{efergy_budget.json => budget.json} (100%) rename tests/fixtures/efergy/{efergy_current_values_multi.json => current_values_multi.json} (100%) rename tests/fixtures/efergy/{efergy_current_values_single.json => current_values_single.json} (100%) rename tests/fixtures/efergy/{efergy_cost.json => daily_cost.json} (100%) rename tests/fixtures/efergy/{efergy_energy.json => daily_energy.json} (100%) rename tests/fixtures/efergy/{efergy_instant.json => instant.json} (100%) create mode 100644 tests/fixtures/efergy/monthly_cost.json create mode 100644 tests/fixtures/efergy/monthly_energy.json create mode 100644 tests/fixtures/efergy/status.json create mode 100644 tests/fixtures/efergy/weekly_cost.json create mode 100644 tests/fixtures/efergy/weekly_energy.json create mode 100644 tests/fixtures/efergy/yearly_cost.json create mode 100644 tests/fixtures/efergy/yearly_energy.json diff --git a/homeassistant/components/efergy/__init__.py b/homeassistant/components/efergy/__init__.py index 8ceeb1585a4..74bcf6ff7b0 100644 --- a/homeassistant/components/efergy/__init__.py +++ b/homeassistant/components/efergy/__init__.py @@ -1 +1,83 @@ -"""The efergy component.""" +"""The Efergy integration.""" +from __future__ import annotations + +from pyefergy import Efergy, exceptions + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + ATTR_SW_VERSION, + CONF_API_KEY, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo, Entity + +from .const import ATTRIBUTION, DATA_KEY_API, DEFAULT_NAME, DOMAIN + +PLATFORMS = [SENSOR_DOMAIN] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Efergy from a config entry.""" + api = Efergy( + entry.data[CONF_API_KEY], + session=async_get_clientsession(hass), + utc_offset=hass.config.time_zone, + currency=hass.config.currency, + ) + + try: + await api.async_status(get_sids=True) + except (exceptions.ConnectError, exceptions.DataError) as ex: + raise ConfigEntryNotReady(f"Failed to connect to device: {ex}") from ex + except exceptions.InvalidAuth as ex: + raise ConfigEntryAuthFailed( + "API Key is no longer valid. Please reauthenticate" + ) from ex + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_KEY_API: api} + hass.config_entries.async_setup_platforms(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 + + +class EfergyEntity(Entity): + """Representation of a Efergy entity.""" + + def __init__( + self, + api: Efergy, + server_unique_id: str, + ) -> None: + """Initialize an Efergy entity.""" + self.api = api + self._server_unique_id = server_unique_id + self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + + @property + def device_info(self) -> DeviceInfo: + """Return the device information of the entity.""" + return { + "connections": {(dr.CONNECTION_NETWORK_MAC, self.api.info["mac"])}, + ATTR_IDENTIFIERS: {(DOMAIN, self._server_unique_id)}, + ATTR_MANUFACTURER: DEFAULT_NAME, + ATTR_NAME: DEFAULT_NAME, + ATTR_MODEL: self.api.info["type"], + ATTR_SW_VERSION: self.api.info["version"], + } diff --git a/homeassistant/components/efergy/config_flow.py b/homeassistant/components/efergy/config_flow.py new file mode 100644 index 00000000000..3fb5fbec4a6 --- /dev/null +++ b/homeassistant/components/efergy/config_flow.py @@ -0,0 +1,85 @@ +"""Config flow for Efergy integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from pyefergy import Efergy, exceptions +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY +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 CONF_APPTOKEN, DEFAULT_NAME, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class EfergyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Efergy.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initiated by the user.""" + errors = {} + if user_input is not None: + api_key = user_input[CONF_API_KEY] + + self._async_abort_entries_match({CONF_API_KEY: api_key}) + hid, error = await self._async_try_connect(api_key) + if error is None: + entry = await self.async_set_unique_id(hid) + if entry: + self.hass.config_entries.async_update_entry(entry, data=user_input) + await self.hass.config_entries.async_reload(entry.entry_id) + return self.async_abort(reason="reauth_successful") + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=DEFAULT_NAME, + data={CONF_API_KEY: api_key}, + ) + errors["base"] = error + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_API_KEY): str, + } + ), + errors=errors, + ) + + async def async_step_import(self, import_config: ConfigType): + """Import a config entry from configuration.yaml.""" + for entry in self._async_current_entries(): + if entry.data[CONF_API_KEY] == import_config[CONF_APPTOKEN]: + _part = import_config[CONF_APPTOKEN][0:4] + _msg = f"Efergy yaml config with partial key {_part} has been imported. Please remove it" + _LOGGER.warning(_msg) + return self.async_abort(reason="already_configured") + return await self.async_step_user({CONF_API_KEY: import_config[CONF_APPTOKEN]}) + + async def async_step_reauth(self, config: dict[str, Any]) -> FlowResult: + """Handle a reauthorization flow request.""" + return await self.async_step_user() + + async def _async_try_connect(self, api_key: str) -> tuple[str | None, str | None]: + """Try connecting to Efergy servers.""" + api = Efergy(api_key, session=async_get_clientsession(self.hass)) + try: + await api.async_status() + except exceptions.ConnectError: + return None, "cannot_connect" + except exceptions.InvalidAuth: + return None, "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + return None, "unknown" + return api.info["hid"], None diff --git a/homeassistant/components/efergy/const.py b/homeassistant/components/efergy/const.py new file mode 100644 index 00000000000..b141c3ebdb8 --- /dev/null +++ b/homeassistant/components/efergy/const.py @@ -0,0 +1,13 @@ +"""Constants for the Efergy integration.""" +from datetime import timedelta + +ATTRIBUTION = "Data provided by Efergy" + +CONF_APPTOKEN = "app_token" +CONF_CURRENT_VALUES = "current_values" + +DATA_KEY_API = "api" +DEFAULT_NAME = "Efergy" +DOMAIN = "efergy" + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) diff --git a/homeassistant/components/efergy/manifest.json b/homeassistant/components/efergy/manifest.json index 3b84d243d46..d95c0b69415 100644 --- a/homeassistant/components/efergy/manifest.json +++ b/homeassistant/components/efergy/manifest.json @@ -1,8 +1,9 @@ { "domain": "efergy", "name": "Efergy", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/efergy", - "requirements": ["pyefergy==0.0.3"], + "requirements": ["pyefergy==0.1.2"], "codeowners": ["@tkdrob"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/efergy/sensor.py b/homeassistant/components/efergy/sensor.py index a11fe5f3ac6..6b5c5ec44ff 100644 --- a/homeassistant/components/efergy/sensor.py +++ b/homeassistant/components/efergy/sensor.py @@ -2,15 +2,18 @@ from __future__ import annotations import logging +from re import sub from pyefergy import Efergy, exceptions import voluptuous as vol +from homeassistant.components.efergy import EfergyEntity from homeassistant.components.sensor import ( PLATFORM_SCHEMA, SensorEntity, SensorEntityDescription, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_CURRENCY, CONF_MONITORED_VARIABLES, @@ -22,72 +25,103 @@ from homeassistant.const import ( POWER_WATT, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -CONF_APPTOKEN = "app_token" -CONF_UTC_OFFSET = "utc_offset" - -CONF_PERIOD = "period" - -CONF_INSTANT = "instant_readings" -CONF_AMOUNT = "amount" -CONF_BUDGET = "budget" -CONF_COST = "cost" -CONF_CURRENT_VALUES = "current_values" - -DEFAULT_PERIOD = "year" -DEFAULT_UTC_OFFSET = "0" +from .const import CONF_APPTOKEN, CONF_CURRENT_VALUES, DATA_KEY_API, DOMAIN _LOGGER = logging.getLogger(__name__) -SENSOR_TYPES: dict[str, SensorEntityDescription] = { - CONF_INSTANT: SensorEntityDescription( - key=CONF_INSTANT, - name="Energy Usage", +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="instant_readings", + name="Power Usage", device_class=DEVICE_CLASS_POWER, native_unit_of_measurement=POWER_WATT, ), - CONF_AMOUNT: SensorEntityDescription( - key=CONF_AMOUNT, - name="Energy Consumed", + SensorEntityDescription( + key="energy_day", + name="Daily Consumption", + device_class=DEVICE_CLASS_ENERGY, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="energy_week", + name="Weekly Consumption", + device_class=DEVICE_CLASS_ENERGY, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="energy_month", + name="Monthly Consumption", device_class=DEVICE_CLASS_ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), - CONF_BUDGET: SensorEntityDescription( - key=CONF_BUDGET, - name="Energy Budget", + SensorEntityDescription( + key="energy_year", + name="Yearly Consumption", + device_class=DEVICE_CLASS_ENERGY, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + entity_registry_enabled_default=False, ), - CONF_COST: SensorEntityDescription( - key=CONF_COST, - name="Energy Cost", + SensorEntityDescription( + key="budget", + name="Energy Budget", + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="cost_day", + name="Daily Energy Cost", + device_class=DEVICE_CLASS_MONETARY, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="cost_week", + name="Weekly Energy Cost", + device_class=DEVICE_CLASS_MONETARY, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="cost_month", + name="Monthly Energy Cost", device_class=DEVICE_CLASS_MONETARY, ), - CONF_CURRENT_VALUES: SensorEntityDescription( + SensorEntityDescription( + key="cost_year", + name="Yearly Energy Cost", + device_class=DEVICE_CLASS_MONETARY, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( key=CONF_CURRENT_VALUES, - name="Per-Device Usage", + name="Power Usage", device_class=DEVICE_CLASS_POWER, native_unit_of_measurement=POWER_WATT, ), -} +) + +TYPES_SCHEMA = vol.In( + ["current_values", "instant_readings", "amount", "budget", "cost"] +) -TYPES_SCHEMA = vol.In(SENSOR_TYPES) SENSORS_SCHEMA = vol.Schema( { vol.Required(CONF_TYPE): TYPES_SCHEMA, vol.Optional(CONF_CURRENCY, default=""): cv.string, - vol.Optional(CONF_PERIOD, default=DEFAULT_PERIOD): cv.string, + vol.Optional("period", default="year"): cv.string, } ) +# Deprecated in Home Assistant 2021.11 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_APPTOKEN): cv.string, - vol.Optional(CONF_UTC_OFFSET, default=DEFAULT_UTC_OFFSET): cv.string, + vol.Optional("utc_offset", default="0"): cv.string, vol.Required(CONF_MONITORED_VARIABLES): [SENSORS_SCHEMA], } ) @@ -99,62 +133,69 @@ async def async_setup_platform( add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType = None, ) -> None: - """Set up the Efergy sensor.""" - api = Efergy( - config[CONF_APPTOKEN], - async_get_clientsession(hass), - utc_offset=config[CONF_UTC_OFFSET], + """Set up the Efergy sensor from yaml.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) ) - dev = [] - try: - sensors = await api.get_sids() - except (exceptions.DataError, exceptions.ConnectTimeout) as ex: - raise PlatformNotReady("Error getting data from Efergy:") from ex - for variable in config[CONF_MONITORED_VARIABLES]: - if variable[CONF_TYPE] == CONF_CURRENT_VALUES: - for sensor in sensors: - dev.append( + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: entity_platform.AddEntitiesCallback, +) -> None: + """Set up Efergy sensors.""" + api: Efergy = hass.data[DOMAIN][entry.entry_id][DATA_KEY_API] + sensors = [] + for description in SENSOR_TYPES: + if description.key != CONF_CURRENT_VALUES: + sensors.append( + EfergySensor( + api, + description, + entry.entry_id, + period=sub("^energy_|^cost_", "", description.key), + currency=hass.config.currency, + ) + ) + else: + description.entity_registry_enabled_default = len(api.info["sids"]) > 1 + for sid in api.info["sids"]: + sensors.append( EfergySensor( api, - variable[CONF_PERIOD], - variable[CONF_CURRENCY], - SENSOR_TYPES[variable[CONF_TYPE]], - sid=sensor["sid"], + description, + entry.entry_id, + sid=sid, ) ) - dev.append( - EfergySensor( - api, - variable[CONF_PERIOD], - variable[CONF_CURRENCY], - SENSOR_TYPES[variable[CONF_TYPE]], - ) - ) - - add_entities(dev, True) + async_add_entities(sensors, True) -class EfergySensor(SensorEntity): +class EfergySensor(EfergyEntity, SensorEntity): """Implementation of an Efergy sensor.""" def __init__( self, api: Efergy, - period: str, - currency: str, description: SensorEntityDescription, - sid: str = None, + server_unique_id: str, + period: str = None, + currency: str = None, + sid: str = "", ) -> None: """Initialize the sensor.""" + super().__init__(api, server_unique_id) self.entity_description = description + if description.key == CONF_CURRENT_VALUES: + self._attr_name = f"{description.name}_{sid}" + self._attr_unique_id = f"{server_unique_id}/{description.key}_{sid}" + if "cost" in description.key: + self._attr_native_unit_of_measurement = currency self.sid = sid - self.api = api self.period = period - if sid: - self._attr_name = f"efergy_{sid}" - if description.key == CONF_COST: - self._attr_native_unit_of_measurement = f"{currency}/{period}" async def async_update(self) -> None: """Get the Efergy monitor data from the web service.""" @@ -162,11 +203,11 @@ class EfergySensor(SensorEntity): self._attr_native_value = await self.api.async_get_reading( self.entity_description.key, period=self.period, sid=self.sid ) - except (exceptions.DataError, exceptions.ConnectTimeout) as ex: + except (exceptions.DataError, exceptions.ConnectError) as ex: if self._attr_available: self._attr_available = False - _LOGGER.error("Error getting data from Efergy: %s", ex) + _LOGGER.error("Error getting data: %s", ex) return if not self._attr_available: self._attr_available = True - _LOGGER.info("Connection to Efergy has resumed") + _LOGGER.info("Connection has resumed") diff --git a/homeassistant/components/efergy/strings.json b/homeassistant/components/efergy/strings.json new file mode 100644 index 00000000000..dc625c92840 --- /dev/null +++ b/homeassistant/components/efergy/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "user": { + "title": "Efergy", + "data": { + "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%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + } +} diff --git a/homeassistant/components/efergy/translations/en.json b/homeassistant/components/efergy/translations/en.json new file mode 100644 index 00000000000..aa76f9c0636 --- /dev/null +++ b/homeassistant/components/efergy/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "api_key": "API Key" + }, + "title": "Efergy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 30617373dbf..ed698fde8dc 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -70,6 +70,7 @@ FLOWS = [ "eafm", "ecobee", "econet", + "efergy", "elgato", "elkm1", "emonitor", diff --git a/requirements_all.txt b/requirements_all.txt index e790f7e4e09..4ecbd61c0dc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1447,7 +1447,7 @@ pyeconet==0.1.14 pyedimax==0.2.1 # homeassistant.components.efergy -pyefergy==0.0.3 +pyefergy==0.1.2 # homeassistant.components.eight_sleep pyeight==0.1.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6c608b3e135..8f9f6992d9f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -844,7 +844,7 @@ pydispatcher==2.0.5 pyeconet==0.1.14 # homeassistant.components.efergy -pyefergy==0.0.3 +pyefergy==0.1.2 # homeassistant.components.everlights pyeverlights==0.1.0 diff --git a/tests/components/efergy/__init__.py b/tests/components/efergy/__init__.py index 242d36fb932..c4f099df822 100644 --- a/tests/components/efergy/__init__.py +++ b/tests/components/efergy/__init__.py @@ -1 +1,157 @@ -"""Tests for the efergy component.""" +"""Tests for Efergy integration.""" +from unittest.mock import AsyncMock, patch + +from pyefergy import Efergy, exceptions + +from homeassistant.components.efergy import DOMAIN +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + +TOKEN = "9p6QGJ7dpZfO3fqPTBk1fyEmjV1cGoLT" +MULTI_SENSOR_TOKEN = "9r6QGF7dpZfO3fqPTBl1fyRmjV1cGoLT" + +CONF_DATA = {CONF_API_KEY: TOKEN} +HID = "12345678901234567890123456789012" +IMPORT_DATA = {"platform": "efergy", "app_token": TOKEN} + +BASE_URL = "https://engage.efergy.com/mobile_proxy/" + + +def create_entry(hass: HomeAssistant, token: str = TOKEN) -> MockConfigEntry: + """Create Efergy entry in Home Assistant.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=HID, + data={CONF_API_KEY: token}, + ) + entry.add_to_hass(hass) + return entry + + +async def init_integration( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + token: str = TOKEN, + error: bool = False, +) -> MockConfigEntry: + """Set up the Efergy integration in Home Assistant.""" + entry = create_entry(hass, token=token) + await mock_responses(hass, aioclient_mock, token=token, error=error) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry + + +async def mock_responses( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + token: str = TOKEN, + error: bool = False, +): + """Mock responses from Efergy.""" + base_url = "https://engage.efergy.com/mobile_proxy/" + api = Efergy( + token, session=async_get_clientsession(hass), utc_offset=hass.config.time_zone + ) + offset = api._utc_offset # pylint: disable=protected-access + if error: + aioclient_mock.get( + f"{base_url}getInstant?token={token}", + exc=exceptions.ConnectError, + ) + return + aioclient_mock.get( + f"{base_url}getStatus?token={token}", + text=load_fixture("efergy/status.json"), + ) + aioclient_mock.get( + f"{base_url}getInstant?token={token}", + text=load_fixture("efergy/instant.json"), + ) + aioclient_mock.get( + f"{base_url}getEnergy?token={token}&offset={offset}&period=day", + text=load_fixture("efergy/daily_energy.json"), + ) + aioclient_mock.get( + f"{base_url}getEnergy?token={token}&offset={offset}&period=week", + text=load_fixture("efergy/weekly_energy.json"), + ) + aioclient_mock.get( + f"{base_url}getEnergy?token={token}&offset={offset}&period=month", + text=load_fixture("efergy/monthly_energy.json"), + ) + aioclient_mock.get( + f"{base_url}getEnergy?token={token}&offset={offset}&period=year", + text=load_fixture("efergy/yearly_energy.json"), + ) + aioclient_mock.get( + f"{base_url}getBudget?token={token}", + text=load_fixture("efergy/budget.json"), + ) + aioclient_mock.get( + f"{base_url}getCost?token={token}&offset={offset}&period=day", + text=load_fixture("efergy/daily_cost.json"), + ) + aioclient_mock.get( + f"{base_url}getCost?token={token}&offset={offset}&period=week", + text=load_fixture("efergy/weekly_cost.json"), + ) + aioclient_mock.get( + f"{base_url}getCost?token={token}&offset={offset}&period=month", + text=load_fixture("efergy/monthly_cost.json"), + ) + aioclient_mock.get( + f"{base_url}getCost?token={token}&offset={offset}&period=year", + text=load_fixture("efergy/yearly_cost.json"), + ) + if token == TOKEN: + aioclient_mock.get( + f"{base_url}getCurrentValuesSummary?token={token}", + text=load_fixture("efergy/current_values_single.json"), + ) + else: + aioclient_mock.get( + f"{base_url}getCurrentValuesSummary?token={token}", + text=load_fixture("efergy/current_values_multi.json"), + ) + + +def _patch_efergy(): + mocked_efergy = AsyncMock() + mocked_efergy.info = {} + mocked_efergy.info["hid"] = HID + mocked_efergy.info["mac"] = "AA:BB:CC:DD:EE:FF" + mocked_efergy.info["status"] = "on" + mocked_efergy.info["type"] = "EEEHub" + mocked_efergy.info["version"] = "2.3.7" + return patch( + "homeassistant.components.efergy.config_flow.Efergy", + return_value=mocked_efergy, + ) + + +def _patch_efergy_status(): + return patch("homeassistant.components.efergy.config_flow.Efergy.async_status") + + +async def setup_platform( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + platform: str, + token: str = TOKEN, + error: bool = False, +): + """Set up the platform.""" + entry = await init_integration(hass, aioclient_mock, token=token, error=error) + + with patch("homeassistant.components.efergy.PLATFORMS", [platform]): + assert await async_setup_component(hass, DOMAIN, {}) + + return entry diff --git a/tests/components/efergy/test_config_flow.py b/tests/components/efergy/test_config_flow.py new file mode 100644 index 00000000000..d49b89984e1 --- /dev/null +++ b/tests/components/efergy/test_config_flow.py @@ -0,0 +1,134 @@ +"""Test Efergy config flow.""" +from unittest.mock import patch + +from pyefergy import exceptions + +from homeassistant.components.efergy.const import DEFAULT_NAME, DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER +from homeassistant.const import CONF_API_KEY, CONF_SOURCE +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from . import ( + CONF_DATA, + HID, + IMPORT_DATA, + _patch_efergy, + _patch_efergy_status, + create_entry, +) + + +def _patch_setup(): + return patch("homeassistant.components.efergy.async_setup_entry") + + +async def test_flow_user(hass: HomeAssistant): + """Test user initialized flow.""" + with _patch_efergy(), _patch_setup(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_DATA, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"] == CONF_DATA + assert result["result"].unique_id == HID + + +async def test_flow_user_cannot_connect(hass: HomeAssistant): + """Test user initialized flow with unreachable service.""" + with _patch_efergy_status() as efergymock: + efergymock.side_effect = exceptions.ConnectError + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"]["base"] == "cannot_connect" + + +async def test_flow_user_invalid_auth(hass: HomeAssistant): + """Test user initialized flow with invalid authentication.""" + with _patch_efergy_status() as efergymock: + efergymock.side_effect = exceptions.InvalidAuth + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"]["base"] == "invalid_auth" + + +async def test_flow_user_unknown(hass: HomeAssistant): + """Test user initialized flow with unknown error.""" + with _patch_efergy_status() as efergymock: + efergymock.side_effect = Exception + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"]["base"] == "unknown" + + +async def test_flow_import(hass: HomeAssistant): + """Test import step.""" + with _patch_efergy(), _patch_setup(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_IMPORT}, data=IMPORT_DATA + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"] == CONF_DATA + assert result["result"].unique_id == HID + + +async def test_flow_import_already_configured(hass: HomeAssistant): + """Test import step already configured.""" + create_entry(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_IMPORT}, data=IMPORT_DATA + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_flow_reauth(hass: HomeAssistant): + """Test reauth step.""" + entry = create_entry(hass) + with _patch_efergy(), _patch_setup(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + CONF_SOURCE: SOURCE_REAUTH, + "entry_id": entry.entry_id, + "unique_id": entry.unique_id, + }, + data=CONF_DATA, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + + new_conf = {CONF_API_KEY: "1234567890"} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=new_conf, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + assert entry.data == new_conf diff --git a/tests/components/efergy/test_init.py b/tests/components/efergy/test_init.py new file mode 100644 index 00000000000..f32551a4e9b --- /dev/null +++ b/tests/components/efergy/test_init.py @@ -0,0 +1,61 @@ +"""Test Efergy integration.""" +from pyefergy import exceptions + +from homeassistant.components.efergy.const import DEFAULT_NAME, DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from . import _patch_efergy_status, create_entry, init_integration, setup_platform + +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_setup(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): + """Test unload.""" + entry = await init_integration(hass, aioclient_mock) + assert entry.state == ConfigEntryState.LOADED + + 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.get(DOMAIN) + + +async def test_async_setup_entry_not_ready(hass: HomeAssistant): + """Test that it throws ConfigEntryNotReady when exception occurs during setup.""" + entry = create_entry(hass) + with _patch_efergy_status() as efergymock: + efergymock.side_effect = (exceptions.ConnectError, exceptions.DataError) + await hass.config_entries.async_setup(entry.entry_id) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state == ConfigEntryState.SETUP_RETRY + assert not hass.data.get(DOMAIN) + + +async def test_async_setup_entry_auth_failed(hass: HomeAssistant): + """Test that it throws ConfigEntryAuthFailed when authentication fails.""" + entry = create_entry(hass) + with _patch_efergy_status() as efergymock: + efergymock.side_effect = exceptions.InvalidAuth + await hass.config_entries.async_setup(entry.entry_id) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state == ConfigEntryState.SETUP_ERROR + assert not hass.data.get(DOMAIN) + + +async def test_device_info(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): + """Test device info.""" + entry = await setup_platform(hass, aioclient_mock, SENSOR_DOMAIN) + device_registry = await dr.async_get_registry(hass) + + device = device_registry.async_get_device({(DOMAIN, entry.entry_id)}) + + assert device.connections == {("mac", "ff:ff:ff:ff:ff:ff")} + assert device.identifiers == {(DOMAIN, entry.entry_id)} + assert device.manufacturer == DEFAULT_NAME + assert device.model == "EEEHub" + assert device.name == DEFAULT_NAME + assert device.sw_version == "2.3.7" diff --git a/tests/components/efergy/test_sensor.py b/tests/components/efergy/test_sensor.py index 94a381b9048..940d178e972 100644 --- a/tests/components/efergy/test_sensor.py +++ b/tests/components/efergy/test_sensor.py @@ -1,135 +1,125 @@ """The tests for Efergy sensor platform.""" - -import asyncio from datetime import timedelta +from homeassistant.components.efergy.sensor import SENSOR_TYPES from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_MONETARY, + DEVICE_CLASS_POWER, + ENERGY_KILO_WATT_HOUR, + POWER_WATT, + STATE_UNAVAILABLE, +) from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_registry import EntityRegistry import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed, load_fixture +from . import MULTI_SENSOR_TOKEN, mock_responses, setup_platform + +from tests.common import async_fire_time_changed from tests.test_util.aiohttp import AiohttpClientMocker -token = "9p6QGJ7dpZfO3fqPTBk1fyEmjV1cGoLT" -multi_sensor_token = "9r6QGF7dpZfO3fqPTBl1fyRmjV1cGoLT" -ONE_SENSOR_CONFIG = { - "platform": "efergy", - "app_token": token, - "utc_offset": "300", - "monitored_variables": [ - {"type": "amount", "period": "day"}, - {"type": "instant_readings"}, - {"type": "budget"}, - {"type": "cost", "period": "day", "currency": "$"}, - {"type": "current_values"}, - ], -} - -MULTI_SENSOR_CONFIG = { - "platform": "efergy", - "app_token": multi_sensor_token, - "utc_offset": "300", - "monitored_variables": [{"type": "current_values"}], -} - - -def mock_responses(aioclient_mock: AiohttpClientMocker, error: bool = False): - """Mock responses for Efergy.""" - base_url = "https://engage.efergy.com/mobile_proxy/" - if error: - aioclient_mock.get( - f"{base_url}getCurrentValuesSummary?token={token}", exc=asyncio.TimeoutError - ) - return - aioclient_mock.get( - f"{base_url}getInstant?token={token}", - text=load_fixture("efergy/efergy_instant.json"), - ) - aioclient_mock.get( - f"{base_url}getEnergy?token={token}&offset=300&period=day", - text=load_fixture("efergy/efergy_energy.json"), - ) - aioclient_mock.get( - f"{base_url}getBudget?token={token}", - text=load_fixture("efergy/efergy_budget.json"), - ) - aioclient_mock.get( - f"{base_url}getCost?token={token}&offset=300&period=day", - text=load_fixture("efergy/efergy_cost.json"), - ) - aioclient_mock.get( - f"{base_url}getCurrentValuesSummary?token={token}", - text=load_fixture("efergy/efergy_current_values_single.json"), - ) - aioclient_mock.get( - f"{base_url}getCurrentValuesSummary?token={multi_sensor_token}", - text=load_fixture("efergy/efergy_current_values_multi.json"), - ) - - -async def test_single_sensor_readings( +async def test_sensor_readings( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ): """Test for successfully setting up the Efergy platform.""" - mock_responses(aioclient_mock) - assert await async_setup_component( - hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: ONE_SENSOR_CONFIG} - ) - await hass.async_block_till_done() + for description in SENSOR_TYPES: + description.entity_registry_enabled_default = True + entry = await setup_platform(hass, aioclient_mock, SENSOR_DOMAIN) + ent_reg: EntityRegistry = er.async_get(hass) - assert hass.states.get("sensor.energy_consumed").state == "38.21" - assert hass.states.get("sensor.energy_usage").state == "1580" - assert hass.states.get("sensor.energy_budget").state == "ok" - assert hass.states.get("sensor.energy_cost").state == "5.27" - assert hass.states.get("sensor.efergy_728386").state == "1628" + state = hass.states.get("sensor.power_usage") + assert state.state == "1580" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + state = hass.states.get("sensor.energy_budget") + assert state.state == "ok" + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + state = hass.states.get("sensor.daily_consumption") + assert state.state == "38.21" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + state = hass.states.get("sensor.weekly_consumption") + assert state.state == "267.47" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + state = hass.states.get("sensor.monthly_consumption") + assert state.state == "1069.88" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + state = hass.states.get("sensor.yearly_consumption") + assert state.state == "13373.50" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + state = hass.states.get("sensor.daily_energy_cost") + assert state.state == "5.27" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_MONETARY + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "EUR" + state = hass.states.get("sensor.weekly_energy_cost") + assert state.state == "36.89" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_MONETARY + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "EUR" + state = hass.states.get("sensor.monthly_energy_cost") + assert state.state == "147.56" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_MONETARY + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "EUR" + state = hass.states.get("sensor.yearly_energy_cost") + assert state.state == "1844.50" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_MONETARY + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "EUR" + entity = ent_reg.async_get("sensor.power_usage_728386") + assert entity.disabled_by == er.DISABLED_INTEGRATION + ent_reg.async_update_entity(entity.entity_id, **{"disabled_by": None}) + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + state = hass.states.get("sensor.power_usage_728386") + assert state.state == "1628" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT async def test_multi_sensor_readings( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ): """Test for multiple sensors in one household.""" - mock_responses(aioclient_mock) - assert await async_setup_component( - hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: MULTI_SENSOR_CONFIG} - ) - await hass.async_block_till_done() - - assert hass.states.get("sensor.efergy_728386").state == "218" - assert hass.states.get("sensor.efergy_0").state == "1808" - assert hass.states.get("sensor.efergy_728387").state == "312" - - -async def test_failed_getting_sids( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -): - """Test failed gettings sids.""" - mock_responses(aioclient_mock, error=True) - assert await async_setup_component( - hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: ONE_SENSOR_CONFIG} - ) - assert not hass.states.async_all("sensor") + for description in SENSOR_TYPES: + description.entity_registry_enabled_default = True + await setup_platform(hass, aioclient_mock, SENSOR_DOMAIN, MULTI_SENSOR_TOKEN) + state = hass.states.get("sensor.power_usage_728386") + assert state.state == "218" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + state = hass.states.get("sensor.power_usage_0") + assert state.state == "1808" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + state = hass.states.get("sensor.power_usage_728387") + assert state.state == "312" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT async def test_failed_update_and_reconnection( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ): """Test failed update and reconnection.""" - mock_responses(aioclient_mock) - assert await async_setup_component( - hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: ONE_SENSOR_CONFIG} - ) + await setup_platform(hass, aioclient_mock, SENSOR_DOMAIN) + assert hass.states.get("sensor.power_usage").state == "1580" aioclient_mock.clear_requests() - mock_responses(aioclient_mock, error=True) - next_update = dt_util.utcnow() + timedelta(seconds=3) - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - assert hass.states.get("sensor.efergy_728386").state == STATE_UNAVAILABLE - aioclient_mock.clear_requests() - mock_responses(aioclient_mock) + await mock_responses(hass, aioclient_mock, error=True) next_update = dt_util.utcnow() + timedelta(seconds=30) async_fire_time_changed(hass, next_update) await hass.async_block_till_done() - assert hass.states.get("sensor.efergy_728386").state == "1628" + assert hass.states.get("sensor.power_usage").state == STATE_UNAVAILABLE + aioclient_mock.clear_requests() + await mock_responses(hass, aioclient_mock) + next_update = dt_util.utcnow() + timedelta(seconds=30) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + assert hass.states.get("sensor.power_usage").state == "1580" diff --git a/tests/fixtures/efergy/efergy_budget.json b/tests/fixtures/efergy/budget.json similarity index 100% rename from tests/fixtures/efergy/efergy_budget.json rename to tests/fixtures/efergy/budget.json diff --git a/tests/fixtures/efergy/efergy_current_values_multi.json b/tests/fixtures/efergy/current_values_multi.json similarity index 100% rename from tests/fixtures/efergy/efergy_current_values_multi.json rename to tests/fixtures/efergy/current_values_multi.json diff --git a/tests/fixtures/efergy/efergy_current_values_single.json b/tests/fixtures/efergy/current_values_single.json similarity index 100% rename from tests/fixtures/efergy/efergy_current_values_single.json rename to tests/fixtures/efergy/current_values_single.json diff --git a/tests/fixtures/efergy/efergy_cost.json b/tests/fixtures/efergy/daily_cost.json similarity index 100% rename from tests/fixtures/efergy/efergy_cost.json rename to tests/fixtures/efergy/daily_cost.json diff --git a/tests/fixtures/efergy/efergy_energy.json b/tests/fixtures/efergy/daily_energy.json similarity index 100% rename from tests/fixtures/efergy/efergy_energy.json rename to tests/fixtures/efergy/daily_energy.json diff --git a/tests/fixtures/efergy/efergy_instant.json b/tests/fixtures/efergy/instant.json similarity index 100% rename from tests/fixtures/efergy/efergy_instant.json rename to tests/fixtures/efergy/instant.json diff --git a/tests/fixtures/efergy/monthly_cost.json b/tests/fixtures/efergy/monthly_cost.json new file mode 100644 index 00000000000..a3b499cd181 --- /dev/null +++ b/tests/fixtures/efergy/monthly_cost.json @@ -0,0 +1,5 @@ +{ + "sum": "147.56", + "duration": 2537340, + "units": "GBP" +} \ No newline at end of file diff --git a/tests/fixtures/efergy/monthly_energy.json b/tests/fixtures/efergy/monthly_energy.json new file mode 100644 index 00000000000..ab4603e8959 --- /dev/null +++ b/tests/fixtures/efergy/monthly_energy.json @@ -0,0 +1,5 @@ +{ + "sum": "1069.88", + "duration": 2537340, + "units": "kWh" +} \ No newline at end of file diff --git a/tests/fixtures/efergy/status.json b/tests/fixtures/efergy/status.json new file mode 100644 index 00000000000..2e38374831a --- /dev/null +++ b/tests/fixtures/efergy/status.json @@ -0,0 +1,31 @@ +{ + "hid":"1234567890abcdef1234567890abcdef", + "listOfMacs":[ + { + "listofchannels":[ + { + "assoc":1, + "cid":"cid.ffffffffffff", + "reading":null, + "ts":1632961265, + "tsDelta":1, + "tsHuman":"Thu Sep 30 00:00:00 2021", + "type":{ + "battery":5, + "falseBattery":0, + "id":null, + "name":"EFCT" + } + } + ], + "mac":"ffffffffffff", + "personality":"E1", + "status":"on", + "ts":1632961265, + "tsDelta":1, + "tsHuman":"Thu Sep 30 00:00:00 2021", + "type":"EEEHub", + "version":"2.3.7" + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/efergy/weekly_cost.json b/tests/fixtures/efergy/weekly_cost.json new file mode 100644 index 00000000000..f5267f70d2d --- /dev/null +++ b/tests/fixtures/efergy/weekly_cost.json @@ -0,0 +1,5 @@ +{ + "sum": "36.89", + "duration": 377280, + "units": "GBP" +} \ No newline at end of file diff --git a/tests/fixtures/efergy/weekly_energy.json b/tests/fixtures/efergy/weekly_energy.json new file mode 100644 index 00000000000..f4ae92c0af2 --- /dev/null +++ b/tests/fixtures/efergy/weekly_energy.json @@ -0,0 +1,5 @@ +{ + "sum": "267.47", + "duration": 377280, + "units": "kWh" +} \ No newline at end of file diff --git a/tests/fixtures/efergy/yearly_cost.json b/tests/fixtures/efergy/yearly_cost.json new file mode 100644 index 00000000000..375dbde542e --- /dev/null +++ b/tests/fixtures/efergy/yearly_cost.json @@ -0,0 +1,5 @@ +{ + "sum": "1844.50", + "duration": 23532540, + "units": "GBP" +} \ No newline at end of file diff --git a/tests/fixtures/efergy/yearly_energy.json b/tests/fixtures/efergy/yearly_energy.json new file mode 100644 index 00000000000..b6026d8ea2d --- /dev/null +++ b/tests/fixtures/efergy/yearly_energy.json @@ -0,0 +1,5 @@ +{ + "sum": "13373.50", + "duration": 23532540, + "units": "kWh" +} \ No newline at end of file From 3dc1a268ae8ae37a7d8b6c1a261afc5608d2375c Mon Sep 17 00:00:00 2001 From: gjong Date: Mon, 11 Oct 2021 11:30:23 +0200 Subject: [PATCH 0231/1038] Upgrade youless library to fix missing sensor LS110 (#57366) --- homeassistant/components/youless/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/youless/manifest.json b/homeassistant/components/youless/manifest.json index 04a66d507ef..514c73fbd2c 100644 --- a/homeassistant/components/youless/manifest.json +++ b/homeassistant/components/youless/manifest.json @@ -3,7 +3,7 @@ "name": "YouLess", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/youless", - "requirements": ["youless-api==0.13"], + "requirements": ["youless-api==0.14"], "codeowners": ["@gjong"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 4ecbd61c0dc..5f1d17c4941 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2462,7 +2462,7 @@ yeelight==0.7.7 yeelightsunflower==0.0.10 # homeassistant.components.youless -youless-api==0.13 +youless-api==0.14 # homeassistant.components.media_extractor youtube_dl==2021.04.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8f9f6992d9f..da28c1f802c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1412,7 +1412,7 @@ yalexs==1.1.13 yeelight==0.7.7 # homeassistant.components.youless -youless-api==0.13 +youless-api==0.14 # homeassistant.components.zeroconf zeroconf==0.36.8 From 20d08fa470b78a5e7e5d45f68ff238535fc7cfda Mon Sep 17 00:00:00 2001 From: micha91 Date: Mon, 11 Oct 2021 11:34:37 +0200 Subject: [PATCH 0232/1038] Upgrade aiomusiccast to tolererate not decodable characters (#57461) --- homeassistant/components/yamaha_musiccast/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yamaha_musiccast/manifest.json b/homeassistant/components/yamaha_musiccast/manifest.json index be52b8a4558..f7751dfe859 100644 --- a/homeassistant/components/yamaha_musiccast/manifest.json +++ b/homeassistant/components/yamaha_musiccast/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yamaha_musiccast", "requirements": [ - "aiomusiccast==0.9.2" + "aiomusiccast==0.10.0" ], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 5f1d17c4941..40a37721889 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -213,7 +213,7 @@ aiolyric==1.0.7 aiomodernforms==0.1.8 # homeassistant.components.yamaha_musiccast -aiomusiccast==0.9.2 +aiomusiccast==0.10.0 # homeassistant.components.nanoleaf aionanoleaf==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index da28c1f802c..684a8d2769a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -143,7 +143,7 @@ aiolyric==1.0.7 aiomodernforms==0.1.8 # homeassistant.components.yamaha_musiccast -aiomusiccast==0.9.2 +aiomusiccast==0.10.0 # homeassistant.components.nanoleaf aionanoleaf==0.0.3 From d84722c3c2af035cc136baaf788185012d0a2457 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 11 Oct 2021 11:35:29 +0200 Subject: [PATCH 0233/1038] Fix Netgear orbi port in ssdp discovery (#57432) --- homeassistant/components/netgear/config_flow.py | 15 ++++++++++++--- homeassistant/components/netgear/const.py | 1 + tests/components/netgear/test_config_flow.py | 4 ++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/netgear/config_flow.py b/homeassistant/components/netgear/config_flow.py index 62985c7104c..871cba5a95d 100644 --- a/homeassistant/components/netgear/config_flow.py +++ b/homeassistant/components/netgear/config_flow.py @@ -17,7 +17,14 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from .const import CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME, DEFAULT_NAME, DOMAIN +from .const import ( + CONF_CONSIDER_HOME, + DEFAULT_CONSIDER_HOME, + DEFAULT_NAME, + DOMAIN, + MODELS_V2, + ORBI_PORT, +) from .errors import CannotLoginException from .router import get_api @@ -133,8 +140,10 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(discovery_info[ssdp.ATTR_UPNP_SERIAL]) self._abort_if_unique_id_configured(updates=updated_data) - if device_url.port: - updated_data[CONF_PORT] = device_url.port + updated_data[CONF_PORT] = DEFAULT_PORT + for model in MODELS_V2: + if discovery_info.get(ssdp.ATTR_UPNP_MODEL_NUMBER, "").startswith(model): + updated_data[CONF_PORT] = ORBI_PORT self.placeholders.update(updated_data) self.discovered = True diff --git a/homeassistant/components/netgear/const.py b/homeassistant/components/netgear/const.py index bfcf76a6119..cba2d7ff875 100644 --- a/homeassistant/components/netgear/const.py +++ b/homeassistant/components/netgear/const.py @@ -29,6 +29,7 @@ MODELS_V2 = [ "SXR", "SXS", ] +ORBI_PORT = 80 # Icons DEVICE_ICONS = { diff --git a/tests/components/netgear/test_config_flow.py b/tests/components/netgear/test_config_flow.py index de4f4fba510..ad060b60d36 100644 --- a/tests/components/netgear/test_config_flow.py +++ b/tests/components/netgear/test_config_flow.py @@ -7,7 +7,7 @@ import pytest from homeassistant import data_entry_flow from homeassistant.components import ssdp -from homeassistant.components.netgear.const import CONF_CONSIDER_HOME, DOMAIN +from homeassistant.components.netgear.const import CONF_CONSIDER_HOME, DOMAIN, ORBI_PORT from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP, SOURCE_USER from homeassistant.const import ( CONF_HOST, @@ -247,7 +247,7 @@ async def test_ssdp(hass, service): assert result["result"].unique_id == SERIAL assert result["title"] == TITLE assert result["data"].get(CONF_HOST) == HOST - assert result["data"].get(CONF_PORT) == PORT + assert result["data"].get(CONF_PORT) == ORBI_PORT assert result["data"].get(CONF_SSL) == SSL assert result["data"].get(CONF_USERNAME) == DEFAULT_USER assert result["data"][CONF_PASSWORD] == PASSWORD From 8275110c44f81bbf8dbcb4cde7a78a208985ac91 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 11 Oct 2021 05:36:53 -0400 Subject: [PATCH 0234/1038] Fix referenced before assignment in modem_callerid (#57460) --- homeassistant/components/modem_callerid/config_flow.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/modem_callerid/config_flow.py b/homeassistant/components/modem_callerid/config_flow.py index fbb68381c41..fd90f46d94a 100644 --- a/homeassistant/components/modem_callerid/config_flow.py +++ b/homeassistant/components/modem_callerid/config_flow.py @@ -62,6 +62,7 @@ class PhoneModemFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a flow initiated by the user.""" + errors: dict[str, str] | None = {} if self._async_in_progress(): return self.async_abort(reason="already_in_progress") ports = await self.hass.async_add_executor_job(serial.tools.list_ports.comports) @@ -88,7 +89,7 @@ class PhoneModemFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): dev_path = await self.hass.async_add_executor_job( usb.get_serial_by_id, port.device ) - errors: dict | None = await self.validate_device_errors( + errors = await self.validate_device_errors( dev_path=dev_path, unique_id=_generate_unique_id(port) ) if errors is None: @@ -98,9 +99,7 @@ class PhoneModemFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) user_input = user_input or {} schema = vol.Schema({vol.Required(CONF_DEVICE): vol.In(unused_ports)}) - return self.async_show_form( - step_id="user", data_schema=schema, errors=errors or {} - ) + return self.async_show_form(step_id="user", data_schema=schema, errors=errors) async def async_step_import(self, config: dict[str, Any]) -> FlowResult: """Import a config entry from configuration.yaml.""" From 3a2d6a63430a8974cd692b15fe4a83ef3c9a5120 Mon Sep 17 00:00:00 2001 From: Yuval Aboulafia Date: Mon, 11 Oct 2021 13:20:19 +0300 Subject: [PATCH 0235/1038] Use _attr for Suez water (#57278) --- homeassistant/components/suez_water/sensor.py | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/suez_water/sensor.py b/homeassistant/components/suez_water/sensor.py index c9c125e8e7e..04450fd2707 100644 --- a/homeassistant/components/suez_water/sensor.py +++ b/homeassistant/components/suez_water/sensor.py @@ -11,12 +11,13 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, VOLUME_LITERS import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -CONF_COUNTER_ID = "counter_id" SCAN_INTERVAL = timedelta(hours=12) -COMPONENT_ICON = "mdi:water-pump" -COMPONENT_NAME = "Suez Water Client" +CONF_COUNTER_ID = "counter_id" + +NAME = "Suez Water Client" +ICON = "mdi:water-pump" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -49,6 +50,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class SuezSensor(SensorEntity): """Representation of a Sensor.""" + _attr_name = NAME + _attr_icon = ICON + _attr_native_unit_of_measurement = VOLUME_LITERS + def __init__(self, client): """Initialize the data object.""" self._attributes = {} @@ -56,31 +61,16 @@ class SuezSensor(SensorEntity): self._available = None self.client = client - @property - def name(self): - """Return the name of the sensor.""" - return COMPONENT_NAME - @property def native_value(self): """Return the state of the sensor.""" return self._state - @property - def native_unit_of_measurement(self): - """Return the unit of measurement.""" - return VOLUME_LITERS - @property def extra_state_attributes(self): """Return the state attributes.""" return self._attributes - @property - def icon(self): - """Return the icon of the sensor.""" - return COMPONENT_ICON - def _fetch_data(self): """Fetch latest data from Suez.""" try: From 199cf649bea3c71a699db6f74f5ebd77a7dcec25 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 11 Oct 2021 12:43:05 +0200 Subject: [PATCH 0236/1038] Add test of lazy_error in modbus (#57170) --- .coveragerc | 3 -- tests/components/modbus/conftest.py | 18 +++++++++ tests/components/modbus/test_binary_sensor.py | 39 +++++++++++++++++- tests/components/modbus/test_cover.py | 40 ++++++++++++++++++- tests/components/modbus/test_sensor.py | 40 ++++++++++++++++++- 5 files changed, 134 insertions(+), 6 deletions(-) diff --git a/.coveragerc b/.coveragerc index 70f2b8cee34..26e0a214e40 100644 --- a/.coveragerc +++ b/.coveragerc @@ -648,11 +648,8 @@ omit = homeassistant/components/mjpeg/camera.py homeassistant/components/mochad/* homeassistant/components/modbus/base_platform.py - homeassistant/components/modbus/binary_sensor.py - homeassistant/components/modbus/cover.py homeassistant/components/modbus/climate.py homeassistant/components/modbus/modbus.py - homeassistant/components/modbus/sensor.py homeassistant/components/modbus/validators.py homeassistant/components/modem_callerid/sensor.py homeassistant/components/motion_blinds/__init__.py diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index bd2ed9f7778..9b85d23df1c 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -147,6 +147,18 @@ async def mock_do_cycle(hass, mock_pymodbus_exception, mock_pymodbus_return): ): async_fire_time_changed(hass, now) await hass.async_block_till_done() + return now + + +async def do_next_cycle(hass, now, cycle): + """Trigger update call with time_changed event.""" + now += timedelta(seconds=cycle) + with mock.patch( + "homeassistant.helpers.event.dt_util.utcnow", return_value=now, autospec=True + ): + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + return now @pytest.fixture @@ -161,3 +173,9 @@ async def mock_ha(hass, mock_pymodbus_return): """Load homeassistant to allow service calls.""" assert await async_setup_component(hass, "homeassistant", {}) await hass.async_block_till_done() + + +@pytest.fixture +async def caplog_setup_text(caplog): + """Return setup log of integration.""" + yield caplog.text diff --git a/tests/components/modbus/test_binary_sensor.py b/tests/components/modbus/test_binary_sensor.py index 5de03287592..d36a11e3eab 100644 --- a/tests/components/modbus/test_binary_sensor.py +++ b/tests/components/modbus/test_binary_sensor.py @@ -21,7 +21,7 @@ from homeassistant.const import ( ) from homeassistant.core import State -from .conftest import TEST_ENTITY_NAME, ReadResult +from .conftest import TEST_ENTITY_NAME, ReadResult, do_next_cycle ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}" @@ -119,6 +119,43 @@ async def test_all_binary_sensor(hass, expected, mock_do_cycle): assert hass.states.get(ENTITY_ID).state == expected +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_BINARY_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 51, + CONF_INPUT_TYPE: CALL_TYPE_COIL, + CONF_SCAN_INTERVAL: 10, + CONF_LAZY_ERROR: 2, + }, + ], + }, + ], +) +@pytest.mark.parametrize( + "register_words,do_exception,start_expect,end_expect", + [ + ( + [0x00], + True, + STATE_OFF, + STATE_UNAVAILABLE, + ), + ], +) +async def test_lazy_error_binary_sensor(hass, start_expect, end_expect, mock_do_cycle): + """Run test for given config.""" + now = mock_do_cycle + assert hass.states.get(ENTITY_ID).state == start_expect + now = await do_next_cycle(hass, now, 11) + assert hass.states.get(ENTITY_ID).state == start_expect + now = await do_next_cycle(hass, now, 11) + assert hass.states.get(ENTITY_ID).state == end_expect + + @pytest.mark.parametrize( "do_config", [ diff --git a/tests/components/modbus/test_cover.py b/tests/components/modbus/test_cover.py index 9c5b08d59b6..f772955e55c 100644 --- a/tests/components/modbus/test_cover.py +++ b/tests/components/modbus/test_cover.py @@ -30,7 +30,7 @@ from homeassistant.const import ( ) from homeassistant.core import State -from .conftest import TEST_ENTITY_NAME, ReadResult +from .conftest import TEST_ENTITY_NAME, ReadResult, do_next_cycle ENTITY_ID = f"{COVER_DOMAIN}.{TEST_ENTITY_NAME}" @@ -111,6 +111,44 @@ async def test_coil_cover(hass, expected, mock_do_cycle): assert hass.states.get(ENTITY_ID).state == expected +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_COVERS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_INPUT_TYPE: CALL_TYPE_COIL, + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_SCAN_INTERVAL: 10, + CONF_LAZY_ERROR: 2, + }, + ], + }, + ], +) +@pytest.mark.parametrize( + "register_words,do_exception, start_expect,end_expect", + [ + ( + [0x00], + True, + STATE_OPEN, + STATE_UNAVAILABLE, + ), + ], +) +async def test_lazy_error_cover(hass, start_expect, end_expect, mock_do_cycle): + """Run test for given config.""" + now = mock_do_cycle + assert hass.states.get(ENTITY_ID).state == start_expect + now = await do_next_cycle(hass, now, 11) + assert hass.states.get(ENTITY_ID).state == start_expect + now = await do_next_cycle(hass, now, 11) + assert hass.states.get(ENTITY_ID).state == end_expect + + @pytest.mark.parametrize( "do_config", [ diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index e2435d6acc8..b979ac0435e 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -39,7 +39,7 @@ from homeassistant.const import ( ) from homeassistant.core import State -from .conftest import TEST_ENTITY_NAME, ReadResult +from .conftest import TEST_ENTITY_NAME, ReadResult, do_next_cycle ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}" @@ -552,6 +552,44 @@ async def test_all_sensor(hass, mock_do_cycle, expected): assert hass.states.get(ENTITY_ID).state == expected +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 51, + CONF_SCAN_INTERVAL: 10, + CONF_LAZY_ERROR: 1, + }, + ], + }, + ], +) +@pytest.mark.parametrize( + "register_words,do_exception,start_expect,end_expect", + [ + ( + [0x8000], + True, + "17", + STATE_UNAVAILABLE, + ), + ], +) +async def test_lazy_error_sensor(hass, mock_do_cycle, start_expect, end_expect): + """Run test for sensor.""" + hass.states.async_set(ENTITY_ID, 17) + await hass.async_block_till_done() + now = mock_do_cycle + assert hass.states.get(ENTITY_ID).state == start_expect + now = await do_next_cycle(hass, now, 11) + assert hass.states.get(ENTITY_ID).state == start_expect + now = await do_next_cycle(hass, now, 11) + assert hass.states.get(ENTITY_ID).state == end_expect + + @pytest.mark.parametrize( "do_config", [ From 9040b6a59ef1e1bf5baa18dc147c77a3d8ccbe45 Mon Sep 17 00:00:00 2001 From: RDFurman Date: Mon, 11 Oct 2021 04:49:02 -0600 Subject: [PATCH 0237/1038] Update somecomfort library to 0.7.0 (#57214) --- homeassistant/components/honeywell/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/honeywell/manifest.json b/homeassistant/components/honeywell/manifest.json index 5ba4947e046..a308a704c74 100644 --- a/homeassistant/components/honeywell/manifest.json +++ b/homeassistant/components/honeywell/manifest.json @@ -3,7 +3,7 @@ "name": "Honeywell Total Connect Comfort (US)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/honeywell", - "requirements": ["somecomfort==0.5.2"], + "requirements": ["somecomfort==0.7.0"], "codeowners": ["@rdfurman"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 40a37721889..f93ee194f9c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2196,7 +2196,7 @@ solaredge==0.0.2 solax==0.2.8 # homeassistant.components.honeywell -somecomfort==0.5.2 +somecomfort==0.7.0 # homeassistant.components.somfy_mylink somfy-mylink-synergy==1.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 684a8d2769a..67765b7a649 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1251,7 +1251,7 @@ soco==0.24.0 solaredge==0.0.2 # homeassistant.components.honeywell -somecomfort==0.5.2 +somecomfort==0.7.0 # homeassistant.components.somfy_mylink somfy-mylink-synergy==1.0.6 From 1fbc94f56d42b5aa84062dddd4e9f38303b61b20 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 11 Oct 2021 13:08:56 +0200 Subject: [PATCH 0238/1038] Add Netgear ssid and conn_ap_mac sensors (#57226) Co-authored-by: Martin Hjelmare --- homeassistant/components/netgear/router.py | 6 +++--- homeassistant/components/netgear/sensor.py | 20 +++++++++++++------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index cc508f043ff..fc5e2c72e14 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -120,7 +120,7 @@ class NetgearRouter: self.device_name = None self.firmware_version = None - self._method_version = 1 + self.method_version = 1 consider_home_int = entry.options.get( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds() ) @@ -148,7 +148,7 @@ class NetgearRouter: for model in MODELS_V2: if self.model.startswith(model): - self._method_version = 2 + self.method_version = 2 async def async_setup(self) -> None: """Set up a Netgear router.""" @@ -186,7 +186,7 @@ class NetgearRouter: async def async_get_attached_devices(self) -> list: """Get the devices connected to the router.""" - if self._method_version == 1: + if self.method_version == 1: return await self.hass.async_add_executor_job( self._api.get_attached_devices ) diff --git a/homeassistant/components/netgear/sensor.py b/homeassistant/components/netgear/sensor.py index 62867383d6e..d5b8bfd368e 100644 --- a/homeassistant/components/netgear/sensor.py +++ b/homeassistant/components/netgear/sensor.py @@ -21,14 +21,11 @@ SENSOR_TYPES = { "type": SensorEntityDescription( key="type", name="link type", - native_unit_of_measurement=None, - device_class=None, ), "link_rate": SensorEntityDescription( key="link_rate", name="link rate", native_unit_of_measurement="Mbps", - device_class=None, ), "signal": SensorEntityDescription( key="signal", @@ -36,6 +33,14 @@ SENSOR_TYPES = { native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_SIGNAL_STRENGTH, ), + "ssid": SensorEntityDescription( + key="ssid", + name="ssid", + ), + "conn_ap_mac": SensorEntityDescription( + key="conn_ap_mac", + name="access point mac", + ), } @@ -45,10 +50,11 @@ async def async_setup_entry( """Set up device tracker for Netgear component.""" def generate_sensor_classes(router: NetgearRouter, device: dict): - return [ - NetgearSensorEntity(router, device, attribute) - for attribute in ("type", "link_rate", "signal") - ] + sensors = ["type", "link_rate", "signal"] + if router.method_version == 2: + sensors.extend(["ssid", "conn_ap_mac"]) + + return [NetgearSensorEntity(router, device, attribute) for attribute in sensors] async_setup_netgear_entry(hass, entry, async_add_entities, generate_sensor_classes) From cadbf7f6a99736087da34d57b67e9ee44a5c5b8c Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Mon, 11 Oct 2021 13:11:02 +0200 Subject: [PATCH 0239/1038] Bump Daikin version, catch new exception during config_flow (#57080) --- homeassistant/components/daikin/config_flow.py | 9 ++++++++- homeassistant/components/daikin/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/daikin/test_config_flow.py | 2 ++ 5 files changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/daikin/config_flow.py b/homeassistant/components/daikin/config_flow.py index 43d169e3440..18447c56d18 100644 --- a/homeassistant/components/daikin/config_flow.py +++ b/homeassistant/components/daikin/config_flow.py @@ -5,7 +5,7 @@ from uuid import uuid4 from aiohttp import ClientError, web_exceptions from async_timeout import timeout -from pydaikin.daikin_base import Appliance +from pydaikin.daikin_base import Appliance, DaikinException from pydaikin.discovery import Discovery import voluptuous as vol @@ -88,6 +88,13 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data_schema=self.schema, errors={"base": "invalid_auth"}, ) + except DaikinException as daikin_exp: + _LOGGER.error(daikin_exp) + return self.async_show_form( + step_id="user", + data_schema=self.schema, + errors={"base": "unknown"}, + ) except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected error creating device") return self.async_show_form( diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index f34dc8edc57..2a1619594ba 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -3,7 +3,7 @@ "name": "Daikin AC", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/daikin", - "requirements": ["pydaikin==2.4.4"], + "requirements": ["pydaikin==2.6.0"], "codeowners": ["@fredrike"], "zeroconf": ["_dkapi._tcp.local."], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index f93ee194f9c..71c6135eeae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1414,7 +1414,7 @@ pycsspeechtts==1.0.4 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==2.4.4 +pydaikin==2.6.0 # homeassistant.components.danfoss_air pydanfossair==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 67765b7a649..c47a70b1a39 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -829,7 +829,7 @@ pycomfoconnect==0.4 pycoolmasternet-async==0.1.2 # homeassistant.components.daikin -pydaikin==2.4.4 +pydaikin==2.6.0 # homeassistant.components.deconz pydeconz==84 diff --git a/tests/components/daikin/test_config_flow.py b/tests/components/daikin/test_config_flow.py index 91ea79f4aa7..f39d117ed39 100644 --- a/tests/components/daikin/test_config_flow.py +++ b/tests/components/daikin/test_config_flow.py @@ -4,6 +4,7 @@ import asyncio from unittest.mock import PropertyMock, patch from aiohttp import ClientError, web_exceptions +from pydaikin.exceptions import DaikinException import pytest from homeassistant.components.daikin.const import KEY_MAC @@ -85,6 +86,7 @@ async def test_abort_if_already_setup(hass, mock_daikin): (asyncio.TimeoutError, "cannot_connect"), (ClientError, "cannot_connect"), (web_exceptions.HTTPForbidden, "invalid_auth"), + (DaikinException, "unknown"), (Exception, "unknown"), ], ) From 30154763f86e953e305eebde8ba0b9838fe8b7f6 Mon Sep 17 00:00:00 2001 From: Kevin Hellemun <17928966+OGKevin@users.noreply.github.com> Date: Mon, 11 Oct 2021 13:11:45 +0200 Subject: [PATCH 0240/1038] Add xiaomi vacuum -9999 fix back (#57473) --- homeassistant/components/xiaomi_miio/__init__.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 840212d3dd6..5382adb8d96 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -235,12 +235,23 @@ def _async_update_data_vacuum(hass, device: Vacuum): async def update_async(): """Fetch data from the device using async_add_executor_job.""" - try: + + async def execute_update(): async with async_timeout.timeout(10): state = await hass.async_add_executor_job(update) _LOGGER.debug("Got new vacuum state: %s", state) return state + try: + return await execute_update() + except DeviceException as ex: + if getattr(ex, "code", None) != -9999: + raise UpdateFailed(ex) from ex + _LOGGER.info("Got exception while fetching the state, trying again: %s", ex) + + # Try to fetch the data a second time after error code -9999 + try: + return await execute_update() except DeviceException as ex: raise UpdateFailed(ex) from ex From a827521138e3d961a98bdf0273ba34194b093919 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 11 Oct 2021 07:16:55 -0400 Subject: [PATCH 0241/1038] Add energy management for efergy (#57472) --- homeassistant/components/efergy/sensor.py | 12 ++++++++++++ tests/components/efergy/test_sensor.py | 21 ++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/efergy/sensor.py b/homeassistant/components/efergy/sensor.py index 6b5c5ec44ff..cc12fd8486e 100644 --- a/homeassistant/components/efergy/sensor.py +++ b/homeassistant/components/efergy/sensor.py @@ -10,6 +10,8 @@ import voluptuous as vol from homeassistant.components.efergy import EfergyEntity from homeassistant.components.sensor import ( PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, SensorEntityDescription, ) @@ -40,12 +42,14 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Power Usage", device_class=DEVICE_CLASS_POWER, native_unit_of_measurement=POWER_WATT, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="energy_day", name="Daily Consumption", device_class=DEVICE_CLASS_ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -53,6 +57,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Weekly Consumption", device_class=DEVICE_CLASS_ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -60,12 +65,14 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Monthly Consumption", device_class=DEVICE_CLASS_ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, ), SensorEntityDescription( key="energy_year", name="Yearly Consumption", device_class=DEVICE_CLASS_ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=STATE_CLASS_TOTAL_INCREASING, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -77,23 +84,27 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="cost_day", name="Daily Energy Cost", device_class=DEVICE_CLASS_MONETARY, + state_class=STATE_CLASS_TOTAL_INCREASING, entity_registry_enabled_default=False, ), SensorEntityDescription( key="cost_week", name="Weekly Energy Cost", device_class=DEVICE_CLASS_MONETARY, + state_class=STATE_CLASS_TOTAL_INCREASING, entity_registry_enabled_default=False, ), SensorEntityDescription( key="cost_month", name="Monthly Energy Cost", device_class=DEVICE_CLASS_MONETARY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), SensorEntityDescription( key="cost_year", name="Yearly Energy Cost", device_class=DEVICE_CLASS_MONETARY, + state_class=STATE_CLASS_TOTAL_INCREASING, entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -101,6 +112,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Power Usage", device_class=DEVICE_CLASS_POWER, native_unit_of_measurement=POWER_WATT, + state_class=STATE_CLASS_MEASUREMENT, ), ) diff --git a/tests/components/efergy/test_sensor.py b/tests/components/efergy/test_sensor.py index 940d178e972..4f6c7f53209 100644 --- a/tests/components/efergy/test_sensor.py +++ b/tests/components/efergy/test_sensor.py @@ -2,7 +2,12 @@ from datetime import timedelta from homeassistant.components.efergy.sensor import SENSOR_TYPES -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DOMAIN as SENSOR_DOMAIN, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, +) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, @@ -37,42 +42,52 @@ async def test_sensor_readings( assert state.state == "1580" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT state = hass.states.get("sensor.energy_budget") assert state.state == "ok" assert state.attributes.get(ATTR_DEVICE_CLASS) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + assert state.attributes.get(ATTR_STATE_CLASS) is None state = hass.states.get("sensor.daily_consumption") assert state.state == "38.21" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING state = hass.states.get("sensor.weekly_consumption") assert state.state == "267.47" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING state = hass.states.get("sensor.monthly_consumption") assert state.state == "1069.88" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING state = hass.states.get("sensor.yearly_consumption") assert state.state == "13373.50" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING state = hass.states.get("sensor.daily_energy_cost") assert state.state == "5.27" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_MONETARY assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "EUR" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING state = hass.states.get("sensor.weekly_energy_cost") assert state.state == "36.89" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_MONETARY assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "EUR" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING state = hass.states.get("sensor.monthly_energy_cost") assert state.state == "147.56" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_MONETARY assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "EUR" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING state = hass.states.get("sensor.yearly_energy_cost") assert state.state == "1844.50" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_MONETARY assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "EUR" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING entity = ent_reg.async_get("sensor.power_usage_728386") assert entity.disabled_by == er.DISABLED_INTEGRATION ent_reg.async_update_entity(entity.entity_id, **{"disabled_by": None}) @@ -82,6 +97,7 @@ async def test_sensor_readings( assert state.state == "1628" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT async def test_multi_sensor_readings( @@ -95,14 +111,17 @@ async def test_multi_sensor_readings( assert state.state == "218" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT state = hass.states.get("sensor.power_usage_0") assert state.state == "1808" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT state = hass.states.get("sensor.power_usage_728387") assert state.state == "312" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT async def test_failed_update_and_reconnection( From 748d915909d6e73f9d6bc343551d6a195d239ef7 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 11 Oct 2021 15:24:06 +0200 Subject: [PATCH 0242/1038] Don't override methods marked as final (#57477) --- homeassistant/components/blebox/cover.py | 7 +------ homeassistant/components/deluge/switch.py | 5 ----- homeassistant/components/evohome/water_heater.py | 5 ----- homeassistant/components/generic_hygrostat/humidifier.py | 9 +++------ homeassistant/components/hikvisioncam/switch.py | 5 ----- homeassistant/components/homekit_controller/cover.py | 8 ++++---- homeassistant/components/litterrobot/vacuum.py | 4 ++-- homeassistant/components/transmission/switch.py | 5 ----- 8 files changed, 10 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/blebox/cover.py b/homeassistant/components/blebox/cover.py index 5dc6a486ed3..b107dba1e7f 100644 --- a/homeassistant/components/blebox/cover.py +++ b/homeassistant/components/blebox/cover.py @@ -35,11 +35,6 @@ class BleBoxCoverEntity(BleBoxEntity, CoverEntity): stop = SUPPORT_STOP if feature.has_stop else 0 self._attr_supported_features = position | stop | SUPPORT_OPEN | SUPPORT_CLOSE - @property - def state(self): - """Return the equivalent HA cover state.""" - return BLEBOX_TO_HASS_COVER_STATES[self._feature.state] - @property def current_cover_position(self): """Return the current cover position.""" @@ -83,5 +78,5 @@ class BleBoxCoverEntity(BleBoxEntity, CoverEntity): await self._feature.async_stop() def _is_state(self, state_name): - value = self.state + value = BLEBOX_TO_HASS_COVER_STATES[self._feature.state] return None if value is None else value == state_name diff --git a/homeassistant/components/deluge/switch.py b/homeassistant/components/deluge/switch.py index 2aff1b5266c..bc94b7ad014 100644 --- a/homeassistant/components/deluge/switch.py +++ b/homeassistant/components/deluge/switch.py @@ -68,11 +68,6 @@ class DelugeSwitch(ToggleEntity): """Return the name of the switch.""" return self._name - @property - def state(self): - """Return the state of the device.""" - return self._state - @property def is_on(self): """Return true if device is on.""" diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index 495df9e697e..3d799a64e4d 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -59,11 +59,6 @@ class EvoDHW(EvoChild, WaterHeaterEntity): self._precision = PRECISION_TENTHS if evo_broker.client_v1 else PRECISION_WHOLE self._supported_features = SUPPORT_AWAY_MODE | SUPPORT_OPERATION_MODE - @property - def state(self): - """Return the current state.""" - return EVO_STATE_TO_HA[self._evo_device.stateStatus["state"]] - @property def current_operation(self) -> str: """Return the current operating mode (Auto, On, or Off).""" diff --git a/homeassistant/components/generic_hygrostat/humidifier.py b/homeassistant/components/generic_hygrostat/humidifier.py index ee1c8f65d1a..726b6e654e7 100644 --- a/homeassistant/components/generic_hygrostat/humidifier.py +++ b/homeassistant/components/generic_hygrostat/humidifier.py @@ -204,14 +204,11 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): return self._active @property - def state_attributes(self): + def extra_state_attributes(self): """Return the optional state attributes.""" - data = super().state_attributes - if self._saved_target_humidity: - data[ATTR_SAVED_HUMIDITY] = self._saved_target_humidity - - return data + return {ATTR_SAVED_HUMIDITY: self._saved_target_humidity} + return None @property def should_poll(self): diff --git a/homeassistant/components/hikvisioncam/switch.py b/homeassistant/components/hikvisioncam/switch.py index 2f1f89cd261..aa4a430e72e 100644 --- a/homeassistant/components/hikvisioncam/switch.py +++ b/homeassistant/components/hikvisioncam/switch.py @@ -73,11 +73,6 @@ class HikvisionMotionSwitch(SwitchEntity): """Return the name of the device if any.""" return self._name - @property - def state(self): - """Return the state of the device if any.""" - return self._state - @property def is_on(self): """Return true if device is on.""" diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index dd25e32b3c4..7e71edd6a75 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -73,7 +73,7 @@ class HomeKitGarageDoorCover(HomeKitEntity, CoverEntity): return SUPPORT_OPEN | SUPPORT_CLOSE @property - def state(self): + def _state(self): """Return the current state of the garage door.""" value = self.service.value(CharacteristicsTypes.DOOR_STATE_CURRENT) return CURRENT_GARAGE_STATE_MAP[value] @@ -81,17 +81,17 @@ class HomeKitGarageDoorCover(HomeKitEntity, CoverEntity): @property def is_closed(self): """Return true if cover is closed, else False.""" - return self.state == STATE_CLOSED + return self._state == STATE_CLOSED @property def is_closing(self): """Return if the cover is closing or not.""" - return self.state == STATE_CLOSING + return self._state == STATE_CLOSING @property def is_opening(self): """Return if the cover is opening or not.""" - return self.state == STATE_OPENING + return self._state == STATE_OPENING async def async_open_cover(self, **kwargs): """Send open command.""" diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index 2cfe104c753..e40a971f43f 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -17,7 +17,7 @@ from homeassistant.components.vacuum import ( SUPPORT_STATUS, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - VacuumEntity, + StateVacuumEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF @@ -76,7 +76,7 @@ async def async_setup_entry( ) -class LitterRobotCleaner(LitterRobotControlEntity, VacuumEntity): +class LitterRobotCleaner(LitterRobotControlEntity, StateVacuumEntity): """Litter-Robot "Vacuum" Cleaner.""" @property diff --git a/homeassistant/components/transmission/switch.py b/homeassistant/components/transmission/switch.py index 3d85a76f2bd..f706d703565 100644 --- a/homeassistant/components/transmission/switch.py +++ b/homeassistant/components/transmission/switch.py @@ -47,11 +47,6 @@ class TransmissionSwitch(ToggleEntity): """Return the unique id of the entity.""" return f"{self._tm_client.api.host}-{self.name}" - @property - def state(self): - """Return the state of the device.""" - return self._state - @property def should_poll(self): """Poll for status regularly.""" From 858739949b36631d7c45cd8091fb02ea707fbde5 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 11 Oct 2021 16:18:18 +0200 Subject: [PATCH 0243/1038] Use EntityDescription - openweathermap (#56888) --- .coveragerc | 1 - .../openweathermap/abstract_owm_sensor.py | 96 ------- .../components/openweathermap/const.py | 248 ++++++++++-------- .../components/openweathermap/sensor.py | 114 +++++--- 4 files changed, 213 insertions(+), 246 deletions(-) delete mode 100644 homeassistant/components/openweathermap/abstract_owm_sensor.py diff --git a/.coveragerc b/.coveragerc index 26e0a214e40..5f9ccd9e014 100644 --- a/.coveragerc +++ b/.coveragerc @@ -776,7 +776,6 @@ omit = homeassistant/components/openweathermap/sensor.py homeassistant/components/openweathermap/weather.py homeassistant/components/openweathermap/weather_update_coordinator.py - homeassistant/components/openweathermap/abstract_owm_sensor.py homeassistant/components/opnsense/* homeassistant/components/opple/light.py homeassistant/components/orangepi_gpio/* diff --git a/homeassistant/components/openweathermap/abstract_owm_sensor.py b/homeassistant/components/openweathermap/abstract_owm_sensor.py deleted file mode 100644 index 3c66ca50f3c..00000000000 --- a/homeassistant/components/openweathermap/abstract_owm_sensor.py +++ /dev/null @@ -1,96 +0,0 @@ -"""Abstraction form OWM sensors.""" -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ATTR_ATTRIBUTION -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator - -from .const import ( - ATTRIBUTION, - DEFAULT_NAME, - DOMAIN, - MANUFACTURER, - SENSOR_DEVICE_CLASS, - SENSOR_NAME, - SENSOR_UNIT, -) - - -class AbstractOpenWeatherMapSensor(SensorEntity): - """Abstract class for an OpenWeatherMap sensor.""" - - def __init__( - self, - name, - unique_id, - sensor_type, - sensor_configuration, - coordinator: DataUpdateCoordinator, - ): - """Initialize the sensor.""" - self._name = name - self._unique_id = unique_id - self._sensor_type = sensor_type - self._sensor_name = sensor_configuration[SENSOR_NAME] - self._unit_of_measurement = sensor_configuration.get(SENSOR_UNIT) - self._device_class = sensor_configuration.get(SENSOR_DEVICE_CLASS) - self._coordinator = coordinator - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._name} {self._sensor_name}" - - @property - def unique_id(self): - """Return a unique_id for this entity.""" - return self._unique_id - - @property - def device_info(self): - """Return the device info.""" - split_unique_id = self._unique_id.split("-") - return { - "identifiers": {(DOMAIN, f"{split_unique_id[0]}-{split_unique_id[1]}")}, - "name": DEFAULT_NAME, - "manufacturer": MANUFACTURER, - "entry_type": "service", - } - - @property - def should_poll(self): - """Return the polling requirement of the entity.""" - return False - - @property - def attribution(self): - """Return the attribution.""" - return ATTRIBUTION - - @property - def device_class(self): - """Return the device_class.""" - return self._device_class - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION} - - @property - def available(self): - """Return True if entity is available.""" - return self._coordinator.last_update_success - - async def async_added_to_hass(self): - """Connect to dispatcher listening for entity data notifications.""" - self.async_on_remove( - self._coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self): - """Get the latest data from OWM and updates the states.""" - await self._coordinator.async_request_refresh() diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index c1ca96188d8..7647d64d7a2 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -1,4 +1,7 @@ """Consts for the OpenWeatherMap.""" +from __future__ import annotations + +from homeassistant.components.sensor import SensorEntityDescription from homeassistant.components.weather import ( ATTR_CONDITION_CLOUDY, ATTR_CONDITION_EXCEPTIONAL, @@ -21,8 +24,6 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, ) from homeassistant.const import ( DEGREE, @@ -65,9 +66,6 @@ ATTR_API_SNOW = "snow" ATTR_API_UV_INDEX = "uv_index" ATTR_API_WEATHER_CODE = "weather_code" ATTR_API_FORECAST = "forecast" -SENSOR_NAME = "sensor_name" -SENSOR_UNIT = "sensor_unit" -SENSOR_DEVICE_CLASS = "sensor_device_class" UPDATE_LISTENER = "update_listener" PLATFORMS = ["sensor", "weather"] @@ -84,35 +82,6 @@ FORECAST_MODES = [ ] DEFAULT_FORECAST_MODE = FORECAST_MODE_ONECALL_DAILY -MONITORED_CONDITIONS = [ - ATTR_API_WEATHER, - ATTR_API_DEW_POINT, - ATTR_API_TEMPERATURE, - ATTR_API_FEELS_LIKE_TEMPERATURE, - ATTR_API_WIND_SPEED, - ATTR_API_WIND_BEARING, - ATTR_API_HUMIDITY, - ATTR_API_PRESSURE, - ATTR_API_CLOUDS, - ATTR_API_RAIN, - ATTR_API_SNOW, - ATTR_API_PRECIPITATION_KIND, - ATTR_API_UV_INDEX, - ATTR_API_CONDITION, - ATTR_API_WEATHER_CODE, -] -FORECAST_MONITORED_CONDITIONS = [ - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_PRESSURE, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, - ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, - ATTR_API_CLOUDS, -] LANGUAGES = [ "af", "al", @@ -194,82 +163,135 @@ CONDITION_CLASSES = { 904, ], } -WEATHER_SENSOR_TYPES = { - ATTR_API_WEATHER: {SENSOR_NAME: "Weather"}, - ATTR_API_DEW_POINT: { - SENSOR_NAME: "Dew Point", - SENSOR_UNIT: TEMP_CELSIUS, - SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - }, - ATTR_API_TEMPERATURE: { - SENSOR_NAME: "Temperature", - SENSOR_UNIT: TEMP_CELSIUS, - SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - }, - ATTR_API_FEELS_LIKE_TEMPERATURE: { - SENSOR_NAME: "Feels like temperature", - SENSOR_UNIT: TEMP_CELSIUS, - SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - }, - ATTR_API_WIND_SPEED: { - SENSOR_NAME: "Wind speed", - SENSOR_UNIT: SPEED_METERS_PER_SECOND, - }, - ATTR_API_WIND_BEARING: {SENSOR_NAME: "Wind bearing", SENSOR_UNIT: DEGREE}, - ATTR_API_HUMIDITY: { - SENSOR_NAME: "Humidity", - SENSOR_UNIT: PERCENTAGE, - SENSOR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, - }, - ATTR_API_PRESSURE: { - SENSOR_NAME: "Pressure", - SENSOR_UNIT: PRESSURE_HPA, - SENSOR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, - }, - ATTR_API_CLOUDS: {SENSOR_NAME: "Cloud coverage", SENSOR_UNIT: PERCENTAGE}, - ATTR_API_RAIN: {SENSOR_NAME: "Rain", SENSOR_UNIT: LENGTH_MILLIMETERS}, - ATTR_API_SNOW: {SENSOR_NAME: "Snow", SENSOR_UNIT: LENGTH_MILLIMETERS}, - ATTR_API_PRECIPITATION_KIND: {SENSOR_NAME: "Precipitation kind"}, - ATTR_API_UV_INDEX: { - SENSOR_NAME: "UV Index", - SENSOR_UNIT: UV_INDEX, - }, - ATTR_API_CONDITION: {SENSOR_NAME: "Condition"}, - ATTR_API_WEATHER_CODE: {SENSOR_NAME: "Weather Code"}, -} -FORECAST_SENSOR_TYPES = { - ATTR_FORECAST_CONDITION: {SENSOR_NAME: "Condition"}, - ATTR_FORECAST_PRECIPITATION: { - SENSOR_NAME: "Precipitation", - SENSOR_UNIT: LENGTH_MILLIMETERS, - }, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: { - SENSOR_NAME: "Precipitation probability", - SENSOR_UNIT: PERCENTAGE, - }, - ATTR_FORECAST_PRESSURE: { - SENSOR_NAME: "Pressure", - SENSOR_UNIT: PRESSURE_HPA, - SENSOR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, - }, - ATTR_FORECAST_TEMP: { - SENSOR_NAME: "Temperature", - SENSOR_UNIT: TEMP_CELSIUS, - SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - }, - ATTR_FORECAST_TEMP_LOW: { - SENSOR_NAME: "Temperature Low", - SENSOR_UNIT: TEMP_CELSIUS, - SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - }, - ATTR_FORECAST_TIME: { - SENSOR_NAME: "Time", - SENSOR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, - }, - ATTR_API_WIND_BEARING: {SENSOR_NAME: "Wind bearing", SENSOR_UNIT: DEGREE}, - ATTR_API_WIND_SPEED: { - SENSOR_NAME: "Wind speed", - SENSOR_UNIT: SPEED_METERS_PER_SECOND, - }, - ATTR_API_CLOUDS: {SENSOR_NAME: "Cloud coverage", SENSOR_UNIT: PERCENTAGE}, -} +WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key=ATTR_API_WEATHER, + name="Weather", + ), + SensorEntityDescription( + key=ATTR_API_DEW_POINT, + name="Dew Point", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + SensorEntityDescription( + key=ATTR_API_TEMPERATURE, + name="Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + SensorEntityDescription( + key=ATTR_API_FEELS_LIKE_TEMPERATURE, + name="Feels like temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + SensorEntityDescription( + key=ATTR_API_WIND_SPEED, + name="Wind speed", + native_unit_of_measurement=SPEED_METERS_PER_SECOND, + ), + SensorEntityDescription( + key=ATTR_API_WIND_BEARING, + name="Wind bearing", + native_unit_of_measurement=DEGREE, + ), + SensorEntityDescription( + key=ATTR_API_HUMIDITY, + name="Humidity", + native_unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_HUMIDITY, + ), + SensorEntityDescription( + key=ATTR_API_PRESSURE, + name="Pressure", + native_unit_of_measurement=PRESSURE_HPA, + device_class=DEVICE_CLASS_PRESSURE, + ), + SensorEntityDescription( + key=ATTR_API_CLOUDS, + name="Cloud coverage", + native_unit_of_measurement=PERCENTAGE, + ), + SensorEntityDescription( + key=ATTR_API_RAIN, + name="Rain", + native_unit_of_measurement=LENGTH_MILLIMETERS, + ), + SensorEntityDescription( + key=ATTR_API_SNOW, + name="Snow", + native_unit_of_measurement=LENGTH_MILLIMETERS, + ), + SensorEntityDescription( + key=ATTR_API_PRECIPITATION_KIND, + name="Precipitation kind", + ), + SensorEntityDescription( + key=ATTR_API_UV_INDEX, + name="UV Index", + native_unit_of_measurement=UV_INDEX, + ), + SensorEntityDescription( + key=ATTR_API_CONDITION, + name="Condition", + ), + SensorEntityDescription( + key=ATTR_API_WEATHER_CODE, + name="Weather Code", + ), +) +FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key=ATTR_FORECAST_CONDITION, + name="Condition", + ), + SensorEntityDescription( + key=ATTR_FORECAST_PRECIPITATION, + name="Precipitation", + native_unit_of_measurement=LENGTH_MILLIMETERS, + ), + SensorEntityDescription( + key=ATTR_FORECAST_PRECIPITATION_PROBABILITY, + name="Precipitation probability", + native_unit_of_measurement=PERCENTAGE, + ), + SensorEntityDescription( + key=ATTR_FORECAST_PRESSURE, + name="Pressure", + native_unit_of_measurement=PRESSURE_HPA, + device_class=DEVICE_CLASS_PRESSURE, + ), + SensorEntityDescription( + key=ATTR_FORECAST_TEMP, + name="Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + SensorEntityDescription( + key=ATTR_FORECAST_TEMP_LOW, + name="Temperature Low", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + ), + SensorEntityDescription( + key=ATTR_FORECAST_TIME, + name="Time", + device_class=DEVICE_CLASS_TIMESTAMP, + ), + SensorEntityDescription( + key=ATTR_API_WIND_BEARING, + name="Wind bearing", + native_unit_of_measurement=DEGREE, + ), + SensorEntityDescription( + key=ATTR_API_WIND_SPEED, + name="Wind speed", + native_unit_of_measurement=SPEED_METERS_PER_SECOND, + ), + SensorEntityDescription( + key=ATTR_API_CLOUDS, + name="Cloud coverage", + native_unit_of_measurement=PERCENTAGE, + ), +) diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index 3586f958a6a..6da352abf0a 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -1,13 +1,19 @@ """Support for the OpenWeatherMap (OWM) service.""" -from .abstract_owm_sensor import AbstractOpenWeatherMapSensor +from __future__ import annotations + +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + from .const import ( ATTR_API_FORECAST, + ATTRIBUTION, + DEFAULT_NAME, DOMAIN, ENTRY_NAME, ENTRY_WEATHER_COORDINATOR, - FORECAST_MONITORED_CONDITIONS, FORECAST_SENSOR_TYPES, - MONITORED_CONDITIONS, + MANUFACTURER, WEATHER_SENSOR_TYPES, ) from .weather_update_coordinator import WeatherUpdateCoordinator @@ -19,37 +25,79 @@ async def async_setup_entry(hass, config_entry, async_add_entities): name = domain_data[ENTRY_NAME] weather_coordinator = domain_data[ENTRY_WEATHER_COORDINATOR] - weather_sensor_types = WEATHER_SENSOR_TYPES - forecast_sensor_types = FORECAST_SENSOR_TYPES - - entities = [] - for sensor_type in MONITORED_CONDITIONS: - unique_id = f"{config_entry.unique_id}-{sensor_type}" - entities.append( - OpenWeatherMapSensor( - name, - unique_id, - sensor_type, - weather_sensor_types[sensor_type], - weather_coordinator, - ) + entities: list[AbstractOpenWeatherMapSensor] = [ + OpenWeatherMapSensor( + name, + f"{config_entry.unique_id}-{description.key}", + description, + weather_coordinator, ) + for description in WEATHER_SENSOR_TYPES + ] - for sensor_type in FORECAST_MONITORED_CONDITIONS: - unique_id = f"{config_entry.unique_id}-forecast-{sensor_type}" - entities.append( + entities.extend( + [ OpenWeatherMapForecastSensor( f"{name} Forecast", - unique_id, - sensor_type, - forecast_sensor_types[sensor_type], + f"{config_entry.unique_id}-forecast-{description.key}", + description, weather_coordinator, ) - ) + for description in FORECAST_SENSOR_TYPES + ] + ) async_add_entities(entities) +class AbstractOpenWeatherMapSensor(SensorEntity): + """Abstract class for an OpenWeatherMap sensor.""" + + _attr_should_poll = False + _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + + def __init__( + self, + name, + unique_id, + description: SensorEntityDescription, + coordinator: DataUpdateCoordinator, + ): + """Initialize the sensor.""" + self.entity_description = description + self._coordinator = coordinator + + self._attr_name = f"{name} {description.name}" + self._attr_unique_id = unique_id + split_unique_id = unique_id.split("-") + self._attr_device_info = { + "identifiers": {(DOMAIN, f"{split_unique_id[0]}-{split_unique_id[1]}")}, + "name": DEFAULT_NAME, + "manufacturer": MANUFACTURER, + "entry_type": "service", + } + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def available(self): + """Return True if entity is available.""" + return self._coordinator.last_update_success + + async def async_added_to_hass(self): + """Connect to dispatcher listening for entity data notifications.""" + self.async_on_remove( + self._coordinator.async_add_listener(self.async_write_ha_state) + ) + + async def async_update(self): + """Get the latest data from OWM and updates the states.""" + await self._coordinator.async_request_refresh() + + class OpenWeatherMapSensor(AbstractOpenWeatherMapSensor): """Implementation of an OpenWeatherMap sensor.""" @@ -57,20 +105,17 @@ class OpenWeatherMapSensor(AbstractOpenWeatherMapSensor): self, name, unique_id, - sensor_type, - sensor_configuration, + description: SensorEntityDescription, weather_coordinator: WeatherUpdateCoordinator, ): """Initialize the sensor.""" - super().__init__( - name, unique_id, sensor_type, sensor_configuration, weather_coordinator - ) + super().__init__(name, unique_id, description, weather_coordinator) self._weather_coordinator = weather_coordinator @property def native_value(self): """Return the state of the device.""" - return self._weather_coordinator.data.get(self._sensor_type, None) + return self._weather_coordinator.data.get(self.entity_description.key, None) class OpenWeatherMapForecastSensor(AbstractOpenWeatherMapSensor): @@ -80,14 +125,11 @@ class OpenWeatherMapForecastSensor(AbstractOpenWeatherMapSensor): self, name, unique_id, - sensor_type, - sensor_configuration, + description: SensorEntityDescription, weather_coordinator: WeatherUpdateCoordinator, ): """Initialize the sensor.""" - super().__init__( - name, unique_id, sensor_type, sensor_configuration, weather_coordinator - ) + super().__init__(name, unique_id, description, weather_coordinator) self._weather_coordinator = weather_coordinator @property @@ -95,5 +137,5 @@ class OpenWeatherMapForecastSensor(AbstractOpenWeatherMapSensor): """Return the state of the device.""" forecasts = self._weather_coordinator.data.get(ATTR_API_FORECAST) if forecasts is not None and len(forecasts) > 0: - return forecasts[0].get(self._sensor_type, None) + return forecasts[0].get(self.entity_description.key, None) return None From 6c470ac28b10415afb27fd6e26a65c91e62a21fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 11 Oct 2021 05:15:31 -1000 Subject: [PATCH 0244/1038] Add dhcp support for tplink KP401 (#57456) --- homeassistant/components/tplink/manifest.json | 4 ++++ homeassistant/generated/dhcp.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 0c45ca84ac6..c82eafb96d8 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -9,6 +9,10 @@ "quality_scale": "platinum", "iot_class": "local_polling", "dhcp": [ + { + "hostname": "k[lp]*", + "macaddress": "1027F5*" + }, { "hostname": "k[lp]*", "macaddress": "403F8C*" diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 1ab07fc414a..7fb65c45421 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -289,6 +289,11 @@ DHCP = [ "hostname": "eneco-*", "macaddress": "74C63B*" }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "1027F5*" + }, { "domain": "tplink", "hostname": "k[lp]*", From b72f1553eaa2ce0fefb18275763ca4f786b5c0c1 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 11 Oct 2021 09:17:43 -0600 Subject: [PATCH 0245/1038] Use current config entry standards for AirVisual (#57132) --- .../components/airvisual/__init__.py | 110 +++++++++--------- .../components/airvisual/config_flow.py | 6 +- homeassistant/components/airvisual/sensor.py | 38 +++--- .../components/airvisual/test_config_flow.py | 16 ++- 4 files changed, 81 insertions(+), 89 deletions(-) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index cda0eb52868..39df25c0f4a 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -106,8 +106,8 @@ def async_get_cloud_coordinators_by_api_key( """Get all DataUpdateCoordinator objects related to a particular API key.""" coordinators = [] for entry_id, coordinator in hass.data[DOMAIN][DATA_COORDINATOR].items(): - config_entry = hass.config_entries.async_get_entry(entry_id) - if config_entry and config_entry.data.get(CONF_API_KEY) == api_key: + entry = hass.config_entries.async_get_entry(entry_id) + if entry and entry.data.get(CONF_API_KEY) == api_key: coordinators.append(coordinator) return coordinators @@ -137,25 +137,25 @@ def async_sync_geo_coordinator_update_intervals( @callback def _standardize_geography_config_entry( - hass: HomeAssistant, config_entry: ConfigEntry + hass: HomeAssistant, entry: ConfigEntry ) -> None: """Ensure that geography config entries have appropriate properties.""" entry_updates = {} - if not config_entry.unique_id: + if not entry.unique_id: # If the config entry doesn't already have a unique ID, set one: - entry_updates["unique_id"] = config_entry.data[CONF_API_KEY] - if not config_entry.options: + entry_updates["unique_id"] = entry.data[CONF_API_KEY] + if not entry.options: # If the config entry doesn't already have any options set, set defaults: entry_updates["options"] = {CONF_SHOW_ON_MAP: True} - if config_entry.data.get(CONF_INTEGRATION_TYPE) not in [ + if entry.data.get(CONF_INTEGRATION_TYPE) not in [ INTEGRATION_TYPE_GEOGRAPHY_COORDS, INTEGRATION_TYPE_GEOGRAPHY_NAME, ]: # If the config entry data doesn't contain an integration type that we know # about, infer it from the data we have: - entry_updates["data"] = {**config_entry.data} - if CONF_CITY in config_entry.data: + entry_updates["data"] = {**entry.data} + if CONF_CITY in entry.data: entry_updates["data"][ CONF_INTEGRATION_TYPE ] = INTEGRATION_TYPE_GEOGRAPHY_NAME @@ -167,51 +167,49 @@ def _standardize_geography_config_entry( if not entry_updates: return - hass.config_entries.async_update_entry(config_entry, **entry_updates) + hass.config_entries.async_update_entry(entry, **entry_updates) @callback -def _standardize_node_pro_config_entry( - hass: HomeAssistant, config_entry: ConfigEntry -) -> None: +def _standardize_node_pro_config_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Ensure that Node/Pro config entries have appropriate properties.""" entry_updates: dict[str, Any] = {} - if CONF_INTEGRATION_TYPE not in config_entry.data: + if CONF_INTEGRATION_TYPE not in entry.data: # If the config entry data doesn't contain the integration type, add it: entry_updates["data"] = { - **config_entry.data, + **entry.data, CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO, } if not entry_updates: return - hass.config_entries.async_update_entry(config_entry, **entry_updates) + hass.config_entries.async_update_entry(entry, **entry_updates) -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up AirVisual as config entry.""" hass.data.setdefault(DOMAIN, {DATA_COORDINATOR: {}}) - if CONF_API_KEY in config_entry.data: - _standardize_geography_config_entry(hass, config_entry) + if CONF_API_KEY in entry.data: + _standardize_geography_config_entry(hass, entry) websession = aiohttp_client.async_get_clientsession(hass) - cloud_api = CloudAPI(config_entry.data[CONF_API_KEY], session=websession) + cloud_api = CloudAPI(entry.data[CONF_API_KEY], session=websession) async def async_update_data() -> dict[str, Any]: """Get new data from the API.""" - if CONF_CITY in config_entry.data: + if CONF_CITY in entry.data: api_coro = cloud_api.air_quality.city( - config_entry.data[CONF_CITY], - config_entry.data[CONF_STATE], - config_entry.data[CONF_COUNTRY], + entry.data[CONF_CITY], + entry.data[CONF_STATE], + entry.data[CONF_COUNTRY], ) else: api_coro = cloud_api.air_quality.nearest_city( - config_entry.data[CONF_LATITUDE], - config_entry.data[CONF_LONGITUDE], + entry.data[CONF_LATITUDE], + entry.data[CONF_LONGITUDE], ) try: @@ -225,7 +223,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b coordinator = DataUpdateCoordinator( hass, LOGGER, - name=async_get_geography_id(config_entry.data), + name=async_get_geography_id(entry.data), # We give a placeholder update interval in order to create the coordinator; # then, below, we use the coordinator's presence (along with any other # coordinators using the same API key) to calculate an actual, leveled @@ -235,16 +233,14 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ) # Only geography-based entries have options: - config_entry.async_on_unload( - config_entry.add_update_listener(async_reload_entry) - ) + entry.async_on_unload(entry.add_update_listener(async_reload_entry)) else: # Remove outdated air_quality entities from the entity registry if they exist: ent_reg = entity_registry.async_get(hass) for entity_entry in [ e for e in ent_reg.entities.values() - if e.config_entry_id == config_entry.entry_id + if e.config_entry_id == entry.entry_id and e.entity_id.startswith("air_quality") ]: LOGGER.debug( @@ -252,13 +248,13 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ) ent_reg.async_remove(entity_entry.entity_id) - _standardize_node_pro_config_entry(hass, config_entry) + _standardize_node_pro_config_entry(hass, entry) async def async_update_data() -> dict[str, Any]: """Get new data from the API.""" try: async with NodeSamba( - config_entry.data[CONF_IP_ADDRESS], config_entry.data[CONF_PASSWORD] + entry.data[CONF_IP_ADDRESS], entry.data[CONF_PASSWORD] ) as node: data = await node.async_get_latest_measurements() return cast(Dict[str, Any], data) @@ -275,40 +271,38 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator + hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = coordinator # Reassess the interval between 2 server requests - if CONF_API_KEY in config_entry.data: - async_sync_geo_coordinator_update_intervals( - hass, config_entry.data[CONF_API_KEY] - ) + if CONF_API_KEY in entry.data: + async_sync_geo_coordinator_update_intervals(hass, entry.data[CONF_API_KEY]) - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Migrate an old config entry.""" - version = config_entry.version + version = entry.version LOGGER.debug("Migrating from version %s", version) # 1 -> 2: One geography per config entry if version == 1: - version = config_entry.version = 2 + version = entry.version = 2 # Update the config entry to only include the first geography (there is always # guaranteed to be at least one): - geographies = list(config_entry.data[CONF_GEOGRAPHIES]) + geographies = list(entry.data[CONF_GEOGRAPHIES]) first_geography = geographies.pop(0) first_id = async_get_geography_id(first_geography) hass.config_entries.async_update_entry( - config_entry, + entry, unique_id=first_id, title=f"Cloud API ({first_id})", - data={CONF_API_KEY: config_entry.data[CONF_API_KEY], **first_geography}, + data={CONF_API_KEY: entry.data[CONF_API_KEY], **first_geography}, ) # For any geographies that remain, create a new config entry for each one: @@ -321,7 +315,7 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, - data={CONF_API_KEY: config_entry.data[CONF_API_KEY], **geography}, + data={CONF_API_KEY: entry.data[CONF_API_KEY], **geography}, ) ) @@ -330,40 +324,40 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return True -async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload an AirVisual config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms( - config_entry, PLATFORMS - ) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - hass.data[DOMAIN][DATA_COORDINATOR].pop(config_entry.entry_id) + hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) - if CONF_API_KEY in config_entry.data: + if CONF_API_KEY in entry.data: # Re-calculate the update interval period for any remaining consumers of # this API key: - async_sync_geo_coordinator_update_intervals( - hass, config_entry.data[CONF_API_KEY] - ) + async_sync_geo_coordinator_update_intervals(hass, entry.data[CONF_API_KEY]) return unload_ok -async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None: +async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle an options update.""" - await hass.config_entries.async_reload(config_entry.entry_id) + await hass.config_entries.async_reload(entry.entry_id) class AirVisualEntity(CoordinatorEntity): """Define a generic AirVisual entity.""" def __init__( - self, coordinator: DataUpdateCoordinator, description: EntityDescription + self, + coordinator: DataUpdateCoordinator, + entry: ConfigEntry, + description: EntityDescription, ) -> None: """Initialize.""" super().__init__(coordinator) self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._entry = entry self.entity_description = description async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py index 971dee161cb..636da54899f 100644 --- a/homeassistant/components/airvisual/config_flow.py +++ b/homeassistant/components/airvisual/config_flow.py @@ -262,9 +262,9 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class AirVisualOptionsFlowHandler(config_entries.OptionsFlow): """Handle an AirVisual options flow.""" - def __init__(self, config_entry: ConfigEntry) -> None: + def __init__(self, entry: ConfigEntry) -> None: """Initialize.""" - self.config_entry = config_entry + self.entry = entry async def async_step_init( self, user_input: dict[str, str] | None = None @@ -279,7 +279,7 @@ class AirVisualOptionsFlowHandler(config_entries.OptionsFlow): { vol.Required( CONF_SHOW_ON_MAP, - default=self.config_entry.options.get(CONF_SHOW_ON_MAP), + default=self.entry.options.get(CONF_SHOW_ON_MAP), ): bool } ), diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index 486ef072f24..26f89d06dde 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -189,26 +189,24 @@ POLLUTANT_UNITS = { async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up AirVisual sensors based on a config entry.""" - coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] + coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] sensors: list[AirVisualGeographySensor | AirVisualNodeProSensor] - if config_entry.data[CONF_INTEGRATION_TYPE] in ( + if entry.data[CONF_INTEGRATION_TYPE] in ( INTEGRATION_TYPE_GEOGRAPHY_COORDS, INTEGRATION_TYPE_GEOGRAPHY_NAME, ): sensors = [ - AirVisualGeographySensor(coordinator, config_entry, description, locale) + AirVisualGeographySensor(coordinator, entry, description, locale) for locale in GEOGRAPHY_SENSOR_LOCALES for description in GEOGRAPHY_SENSOR_DESCRIPTIONS ] else: sensors = [ - AirVisualNodeProSensor(coordinator, description) + AirVisualNodeProSensor(coordinator, entry, description) for description in NODE_PRO_SENSOR_DESCRIPTIONS ] @@ -221,23 +219,22 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity): def __init__( self, coordinator: DataUpdateCoordinator, - config_entry: ConfigEntry, + entry: ConfigEntry, description: SensorEntityDescription, locale: str, ) -> None: """Initialize.""" - super().__init__(coordinator, description) + super().__init__(coordinator, entry, description) self._attr_extra_state_attributes.update( { - ATTR_CITY: config_entry.data.get(CONF_CITY), - ATTR_STATE: config_entry.data.get(CONF_STATE), - ATTR_COUNTRY: config_entry.data.get(CONF_COUNTRY), + ATTR_CITY: entry.data.get(CONF_CITY), + ATTR_STATE: entry.data.get(CONF_STATE), + ATTR_COUNTRY: entry.data.get(CONF_COUNTRY), } ) self._attr_name = f"{GEOGRAPHY_SENSOR_LOCALES[locale]} {description.name}" - self._attr_unique_id = f"{config_entry.unique_id}_{locale}_{description.key}" - self._config_entry = config_entry + self._attr_unique_id = f"{entry.unique_id}_{locale}_{description.key}" self._locale = locale @property @@ -279,16 +276,16 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity): # # We use any coordinates in the config entry and, in the case of a geography by # name, we fall back to the latitude longitude provided in the coordinator data: - latitude = self._config_entry.data.get( + latitude = self._entry.data.get( CONF_LATITUDE, self.coordinator.data["location"]["coordinates"][1], ) - longitude = self._config_entry.data.get( + longitude = self._entry.data.get( CONF_LONGITUDE, self.coordinator.data["location"]["coordinates"][0], ) - if self._config_entry.options[CONF_SHOW_ON_MAP]: + if self._entry.options[CONF_SHOW_ON_MAP]: self._attr_extra_state_attributes[ATTR_LATITUDE] = latitude self._attr_extra_state_attributes[ATTR_LONGITUDE] = longitude self._attr_extra_state_attributes.pop("lati", None) @@ -304,10 +301,13 @@ class AirVisualNodeProSensor(AirVisualEntity, SensorEntity): """Define an AirVisual sensor related to a Node/Pro unit.""" def __init__( - self, coordinator: DataUpdateCoordinator, description: SensorEntityDescription + self, + coordinator: DataUpdateCoordinator, + entry: ConfigEntry, + description: SensorEntityDescription, ) -> None: """Initialize.""" - super().__init__(coordinator, description) + super().__init__(coordinator, entry, description) self._attr_name = ( f"{coordinator.data['settings']['node_name']} Node/Pro: {description.name}" diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index d7c5a08b62a..6125b71e303 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -177,10 +177,8 @@ async def test_migration(hass): ], } - config_entry = MockConfigEntry( - domain=DOMAIN, version=1, unique_id="abcde12345", data=conf - ) - config_entry.add_to_hass(hass) + entry = MockConfigEntry(domain=DOMAIN, version=1, unique_id="abcde12345", data=conf) + entry.add_to_hass(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -222,19 +220,19 @@ async def test_options_flow(hass): CONF_LONGITUDE: -0.3817765, } - config_entry = MockConfigEntry( + entry = MockConfigEntry( domain=DOMAIN, unique_id="51.528308, -0.3817765", data=geography_conf, options={CONF_SHOW_ON_MAP: True}, ) - config_entry.add_to_hass(hass) + entry.add_to_hass(hass) with patch( "homeassistant.components.airvisual.async_setup_entry", return_value=True ): - await hass.config_entries.async_setup(config_entry.entry_id) - result = await hass.config_entries.options.async_init(config_entry.entry_id) + await hass.config_entries.async_setup(entry.entry_id) + result = await hass.config_entries.options.async_init(entry.entry_id) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "init" @@ -244,7 +242,7 @@ async def test_options_flow(hass): ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert config_entry.options == {CONF_SHOW_ON_MAP: False} + assert entry.options == {CONF_SHOW_ON_MAP: False} async def test_step_geography_by_coords(hass): From 8ee6662cff0cd94d262abe4a5da9813e92f1a35e Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 11 Oct 2021 17:27:04 +0200 Subject: [PATCH 0246/1038] Bump `nettigo_air_monitor` library to version 1.1.1 (#57483) --- homeassistant/components/nam/const.py | 16 ++++++++++++++++ homeassistant/components/nam/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nam/__init__.py | 2 ++ tests/components/nam/test_sensor.py | 22 ++++++++++++++++++++++ 6 files changed, 43 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nam/const.py b/homeassistant/components/nam/const.py index da4831de9e5..d4781fa0c49 100644 --- a/homeassistant/components/nam/const.py +++ b/homeassistant/components/nam/const.py @@ -34,6 +34,8 @@ SUFFIX_P4: Final = "_p4" ATTR_BME280_HUMIDITY: Final = "bme280_humidity" ATTR_BME280_PRESSURE: Final = "bme280_pressure" ATTR_BME280_TEMPERATURE: Final = "bme280_temperature" +ATTR_BMP180_PRESSURE: Final = "bmp180_pressure" +ATTR_BMP180_TEMPERATURE: Final = "bmp180_temperature" ATTR_BMP280_PRESSURE: Final = "bmp280_pressure" ATTR_BMP280_TEMPERATURE: Final = "bmp280_temperature" ATTR_DHT22_HUMIDITY: Final = "dht22_humidity" @@ -86,6 +88,20 @@ SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), + SensorEntityDescription( + key=ATTR_BMP180_PRESSURE, + name=f"{DEFAULT_NAME} BMP180 Pressure", + native_unit_of_measurement=PRESSURE_HPA, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_BMP180_TEMPERATURE, + name=f"{DEFAULT_NAME} BMP180 Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), SensorEntityDescription( key=ATTR_BMP280_PRESSURE, name=f"{DEFAULT_NAME} BMP280 Pressure", diff --git a/homeassistant/components/nam/manifest.json b/homeassistant/components/nam/manifest.json index 74555941351..114fc4dd48d 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.1.0"], + "requirements": ["nettigo-air-monitor==1.1.1"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 71c6135eeae..e91134d5d2f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1050,7 +1050,7 @@ netdisco==3.0.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==1.1.0 +nettigo-air-monitor==1.1.1 # homeassistant.components.neurio_energy neurio==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c47a70b1a39..e6e464acb75 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -621,7 +621,7 @@ netdisco==3.0.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==1.1.0 +nettigo-air-monitor==1.1.1 # homeassistant.components.nexia nexia==0.9.11 diff --git a/tests/components/nam/__init__.py b/tests/components/nam/__init__.py index b4a6ebbf792..8106f97ef31 100644 --- a/tests/components/nam/__init__.py +++ b/tests/components/nam/__init__.py @@ -24,6 +24,8 @@ nam_data = { {"value_type": "BME280_temperature", "value": "7.56"}, {"value_type": "BME280_humidity", "value": "45.69"}, {"value_type": "BME280_pressure", "value": "101101.17"}, + {"value_type": "BMP_temperature", "value": "7.56"}, + {"value_type": "BMP_pressure", "value": "103201.18"}, {"value_type": "BMP280_temperature", "value": "5.56"}, {"value_type": "BMP280_pressure", "value": "102201.18"}, {"value_type": "SHT3X_temperature", "value": "6.28"}, diff --git a/tests/components/nam/test_sensor.py b/tests/components/nam/test_sensor.py index ce9a221007a..68c0044e590 100644 --- a/tests/components/nam/test_sensor.py +++ b/tests/components/nam/test_sensor.py @@ -97,6 +97,28 @@ async def test_sensor(hass): assert entry assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bme280_pressure" + state = hass.states.get("sensor.nettigo_air_monitor_bmp180_temperature") + assert state + assert state.state == "7.6" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + + entry = registry.async_get("sensor.nettigo_air_monitor_bmp180_temperature") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bmp180_temperature" + + state = hass.states.get("sensor.nettigo_air_monitor_bmp180_pressure") + assert state + assert state.state == "1032" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PRESSURE + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_HPA + + entry = registry.async_get("sensor.nettigo_air_monitor_bmp180_pressure") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bmp180_pressure" + state = hass.states.get("sensor.nettigo_air_monitor_bmp280_temperature") assert state assert state.state == "5.6" From d0b37229dd165ebfa81ae3c18f17b331f66410d8 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Mon, 11 Oct 2021 11:33:29 -0400 Subject: [PATCH 0247/1038] Switch to config_flow for Environment Canada (#57127) * Add config_flow to Environment Canada * Add unique_id * Remove erroneous directory. * Tests working!! * Add back setup. * First cut of import. * Temp * Tweak names. * Import config.yaml. * Clean up imports. * Import working! Some refactor to clean it up. * Add import test. * Small optimization. * Fix comments from code review. * Remove CONF_NAME and config_flow for it. * Fixup strings to match new config_flow. * Fixes for comments from last review. * Update tests to match new import code. * Clean up use of CONF_TITLE; fix lint error on push. * Phew. More cleanup on import. Really streamlined now! * Update tests. * Fix lint error. * Fix lint error, try 2. * Revert unique_id to use location as part of ID. * Fix code review comments. * Fix review comments. --- .coveragerc | 5 +- CODEOWNERS | 2 +- .../components/environment_canada/__init__.py | 80 +++++++- .../components/environment_canada/camera.py | 63 ++++--- .../environment_canada/config_flow.py | 108 +++++++++++ .../components/environment_canada/const.py | 9 + .../environment_canada/manifest.json | 5 +- .../components/environment_canada/sensor.py | 64 +++---- .../environment_canada/strings.json | 23 +++ .../environment_canada/translations/en.json | 23 +++ .../components/environment_canada/weather.py | 71 ++++--- homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + .../components/environment_canada/__init__.py | 1 + .../environment_canada/test_config_flow.py | 178 ++++++++++++++++++ 16 files changed, 546 insertions(+), 92 deletions(-) create mode 100644 homeassistant/components/environment_canada/config_flow.py create mode 100644 homeassistant/components/environment_canada/const.py create mode 100644 homeassistant/components/environment_canada/strings.json create mode 100644 homeassistant/components/environment_canada/translations/en.json create mode 100644 tests/components/environment_canada/__init__.py create mode 100644 tests/components/environment_canada/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 5f9ccd9e014..829915400bd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -269,7 +269,10 @@ omit = homeassistant/components/enphase_envoy/__init__.py homeassistant/components/enphase_envoy/sensor.py homeassistant/components/entur_public_transport/* - homeassistant/components/environment_canada/* + homeassistant/components/environment_canada/__init__.py + homeassistant/components/environment_canada/camera.py + homeassistant/components/environment_canada/sensor.py + homeassistant/components/environment_canada/weather.py homeassistant/components/envirophat/sensor.py homeassistant/components/envisalink/* homeassistant/components/ephember/climate.py diff --git a/CODEOWNERS b/CODEOWNERS index a4756f961be..e3a4cb00c83 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -151,7 +151,7 @@ homeassistant/components/enigma2/* @fbradyirl homeassistant/components/enocean/* @bdurrer homeassistant/components/enphase_envoy/* @gtdiehl homeassistant/components/entur_public_transport/* @hfurubotten -homeassistant/components/environment_canada/* @michaeldavie +homeassistant/components/environment_canada/* @gwww @michaeldavie homeassistant/components/ephember/* @ttroy50 homeassistant/components/epson/* @pszafer homeassistant/components/epsonworkforce/* @ThaStealth diff --git a/homeassistant/components/environment_canada/__init__.py b/homeassistant/components/environment_canada/__init__.py index 356e18fe23f..0821059fcdf 100644 --- a/homeassistant/components/environment_canada/__init__.py +++ b/homeassistant/components/environment_canada/__init__.py @@ -1 +1,79 @@ -"""A component for Environment Canada weather.""" +"""The Environment Canada (EC) component.""" +from functools import partial +import logging + +from env_canada import ECData, ECRadar + +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE + +from .const import CONF_LANGUAGE, CONF_STATION, DOMAIN + +PLATFORMS = ["camera", "sensor", "weather"] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry): + """Set up EC as config entry.""" + lat = config_entry.data.get(CONF_LATITUDE) + lon = config_entry.data.get(CONF_LONGITUDE) + station = config_entry.data.get(CONF_STATION) + lang = config_entry.data.get(CONF_LANGUAGE, "English") + + weather_api = {} + + weather_init = partial( + ECData, station_id=station, coordinates=(lat, lon), language=lang.lower() + ) + weather_data = await hass.async_add_executor_job(weather_init) + weather_api["weather_data"] = weather_data + + radar_init = partial(ECRadar, coordinates=(lat, lon)) + radar_data = await hass.async_add_executor_job(radar_init) + weather_api["radar_data"] = radar_data + await hass.async_add_executor_job(radar_data.get_loop) + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][config_entry.entry_id] = weather_api + + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) + + hass.data[DOMAIN].pop(config_entry.entry_id) + + return unload_ok + + +def trigger_import(hass, config): + """Trigger a import of YAML config into a config_entry.""" + _LOGGER.warning( + "Environment Canada YAML configuration is deprecated; your YAML configuration " + "has been imported into the UI and can be safely removed" + ) + if not config.get(CONF_LANGUAGE): + config[CONF_LANGUAGE] = "English" + + data = {} + for key in ( + CONF_STATION, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_LANGUAGE, + ): # pylint: disable=consider-using-tuple + if config.get(key): + data[key] = config[key] + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=data + ) + ) diff --git a/homeassistant/components/environment_canada/camera.py b/homeassistant/components/environment_canada/camera.py index ecd0c562d16..0c8e1de6107 100644 --- a/homeassistant/components/environment_canada/camera.py +++ b/homeassistant/components/environment_canada/camera.py @@ -2,8 +2,10 @@ from __future__ import annotations import datetime +import logging -from env_canada import ECRadar +from env_canada import get_station_coords +from requests.exceptions import ConnectionError as RequestsConnectionError import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera @@ -16,15 +18,17 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -ATTR_UPDATED = "updated" +from . import trigger_import +from .const import CONF_ATTRIBUTION, CONF_STATION, DOMAIN -CONF_ATTRIBUTION = "Data provided by Environment Canada" -CONF_STATION = "station" CONF_LOOP = "loop" CONF_PRECIP_TYPE = "precip_type" +ATTR_UPDATED = "updated" MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(minutes=10) +_LOGGER = logging.getLogger(__name__) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_LOOP, default=True): cv.boolean, @@ -37,35 +41,47 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Environment Canada camera.""" - if config.get(CONF_STATION): - radar_object = ECRadar( - station_id=config[CONF_STATION], precip_type=config.get(CONF_PRECIP_TYPE) + lat, lon = await hass.async_add_executor_job( + get_station_coords, config[CONF_STATION] ) else: lat = config.get(CONF_LATITUDE, hass.config.latitude) lon = config.get(CONF_LONGITUDE, hass.config.longitude) - radar_object = ECRadar( - coordinates=(lat, lon), precip_type=config.get(CONF_PRECIP_TYPE) - ) - add_devices( - [ECCamera(radar_object, config.get(CONF_NAME), config[CONF_LOOP])], True + config[CONF_LATITUDE] = lat + config[CONF_LONGITUDE] = lon + + trigger_import(hass, config) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add a weather entity from a config_entry.""" + radar_data = hass.data[DOMAIN][config_entry.entry_id]["radar_data"] + + async_add_entities( + [ + ECCamera( + radar_data, + f"{config_entry.title} Radar", + f"{config_entry.unique_id}-radar", + ), + ] ) class ECCamera(Camera): """Implementation of an Environment Canada radar camera.""" - def __init__(self, radar_object, camera_name, is_loop): + def __init__(self, radar_object, camera_name, unique_id): """Initialize the camera.""" super().__init__() self.radar_object = radar_object - self.camera_name = camera_name - self.is_loop = is_loop + self._attr_name = camera_name + self._attr_unique_id = unique_id self.content_type = "image/gif" self.image = None self.timestamp = None @@ -77,13 +93,6 @@ class ECCamera(Camera): self.update() return self.image - @property - def name(self): - """Return the name of the camera.""" - if self.camera_name is not None: - return self.camera_name - return "Environment Canada Radar" - @property def extra_state_attributes(self): """Return the state attributes of the device.""" @@ -92,8 +101,10 @@ class ECCamera(Camera): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update radar image.""" - if self.is_loop: + try: self.image = self.radar_object.get_loop() - else: - self.image = self.radar_object.get_latest_frame() + except RequestsConnectionError: + _LOGGER.warning("Radar data update failed due to rate limiting") + return + self.timestamp = self.radar_object.timestamp diff --git a/homeassistant/components/environment_canada/config_flow.py b/homeassistant/components/environment_canada/config_flow.py new file mode 100644 index 00000000000..c4c4835a44f --- /dev/null +++ b/homeassistant/components/environment_canada/config_flow.py @@ -0,0 +1,108 @@ +"""Config flow for Environment Canada integration.""" +from functools import partial +import logging +import xml.etree.ElementTree as et + +import aiohttp +from env_canada import ECData +import voluptuous as vol + +from homeassistant import config_entries, exceptions +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.helpers import config_validation as cv + +from .const import CONF_LANGUAGE, CONF_STATION, CONF_TITLE, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def validate_input(hass, data): + """Validate the user input allows us to connect.""" + lat = data.get(CONF_LATITUDE) + lon = data.get(CONF_LONGITUDE) + station = data.get(CONF_STATION) + lang = data.get(CONF_LANGUAGE) + + weather_init = partial( + ECData, station_id=station, coordinates=(lat, lon), language=lang.lower() + ) + weather_data = await hass.async_add_executor_job(weather_init) + if weather_data.metadata.get("location") is None: + raise TooManyAttempts + + if lat is None or lon is None: + lat = weather_data.lat + lon = weather_data.lon + + return { + CONF_TITLE: weather_data.metadata.get("location"), + CONF_STATION: weather_data.station_id, + CONF_LATITUDE: lat, + CONF_LONGITUDE: lon, + } + + +class EnvironmentCanadaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Environment Canada weather.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + except TooManyAttempts: + errors["base"] = "too_many_attempts" + except et.ParseError: + errors["base"] = "bad_station_id" + except aiohttp.ClientConnectionError: + errors["base"] = "cannot_connect" + except aiohttp.ClientResponseError as err: + if err.status == 404: + errors["base"] = "bad_station_id" + else: + errors["base"] = "error_response" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + if not errors: + user_input[CONF_STATION] = info[CONF_STATION] + user_input[CONF_LATITUDE] = info[CONF_LATITUDE] + user_input[CONF_LONGITUDE] = info[CONF_LONGITUDE] + + # The combination of station and language are unique for all EC weather reporting + await self.async_set_unique_id( + f"{user_input[CONF_STATION]}-{user_input[CONF_LANGUAGE].lower()}" + ) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=info[CONF_TITLE], data=user_input) + + data_schema = vol.Schema( + { + vol.Optional(CONF_STATION): str, + vol.Optional( + CONF_LATITUDE, default=self.hass.config.latitude + ): cv.latitude, + vol.Optional( + CONF_LONGITUDE, default=self.hass.config.longitude + ): cv.longitude, + vol.Required(CONF_LANGUAGE, default="English"): vol.In( + ["English", "French"] + ), + } + ) + + return self.async_show_form( + step_id="user", data_schema=data_schema, errors=errors + ) + + async def async_step_import(self, import_data): + """Import entry from configuration.yaml.""" + return await self.async_step_user(import_data) + + +class TooManyAttempts(exceptions.HomeAssistantError): + """Error to indicate station ID is missing, invalid, or not in EC database.""" diff --git a/homeassistant/components/environment_canada/const.py b/homeassistant/components/environment_canada/const.py new file mode 100644 index 00000000000..ff32f02b21e --- /dev/null +++ b/homeassistant/components/environment_canada/const.py @@ -0,0 +1,9 @@ +"""Constants for EC component.""" + +ATTR_OBSERVATION_TIME = "observation_time" +ATTR_STATION = "station" +CONF_ATTRIBUTION = "Data provided by Environment Canada" +CONF_LANGUAGE = "language" +CONF_STATION = "station" +CONF_TITLE = "title" +DOMAIN = "environment_canada" diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 62c3e935d69..e41c0969a87 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -2,7 +2,8 @@ "domain": "environment_canada", "name": "Environment Canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada", - "requirements": ["env_canada==0.2.5"], - "codeowners": ["@michaeldavie"], + "requirements": ["env_canada==0.2.7"], + "codeowners": ["@gwww", "@michaeldavie"], + "config_flow": true, "iot_class": "cloud_polling" } diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 3690703d8d2..8e4c1483261 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -3,7 +3,6 @@ from datetime import datetime, timedelta import logging import re -from env_canada import ECData import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity @@ -17,23 +16,20 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv -_LOGGER = logging.getLogger(__name__) +from . import trigger_import +from .const import ATTR_STATION, CONF_ATTRIBUTION, CONF_LANGUAGE, CONF_STATION, DOMAIN SCAN_INTERVAL = timedelta(minutes=10) - ATTR_UPDATED = "updated" -ATTR_STATION = "station" ATTR_TIME = "alert time" -CONF_ATTRIBUTION = "Data provided by Environment Canada" -CONF_STATION = "station" -CONF_LANGUAGE = "language" +_LOGGER = logging.getLogger(__name__) def validate_station(station): """Check that the station ID is well-formed.""" if station is None: - return + return None if not re.fullmatch(r"[A-Z]{2}/s0000\d{3}", station): raise vol.error.Invalid('Station ID must be of the form "XX/s0000###"') return station @@ -49,47 +45,45 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Environment Canada sensor.""" + trigger_import(hass, config) - if config.get(CONF_STATION): - ec_data = ECData( - station_id=config[CONF_STATION], language=config.get(CONF_LANGUAGE) - ) - else: - lat = config.get(CONF_LATITUDE, hass.config.latitude) - lon = config.get(CONF_LONGITUDE, hass.config.longitude) - ec_data = ECData(coordinates=(lat, lon), language=config.get(CONF_LANGUAGE)) - sensor_list = list(ec_data.conditions) + list(ec_data.alerts) - add_entities([ECSensor(sensor_type, ec_data) for sensor_type in sensor_list], True) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add a weather entity from a config_entry.""" + weather_data = hass.data[DOMAIN][config_entry.entry_id]["weather_data"] + sensor_list = list(weather_data.conditions) + list(weather_data.alerts) + + async_add_entities( + [ + ECSensor( + sensor_type, + f"{config_entry.title} {sensor_type}", + weather_data, + f"{weather_data.metadata['location']}-{sensor_type}", + ) + for sensor_type in sensor_list + ], + True, + ) class ECSensor(SensorEntity): """Implementation of an Environment Canada sensor.""" - def __init__(self, sensor_type, ec_data): + def __init__(self, sensor_type, name, ec_data, unique_id): """Initialize the sensor.""" self.sensor_type = sensor_type self.ec_data = ec_data - self._unique_id = None - self._name = None + self._attr_unique_id = unique_id + self._attr_name = name self._state = None self._attr = None self._unit = None self._device_class = None - @property - def unique_id(self) -> str: - """Return the unique ID of the sensor.""" - return self._unique_id - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - @property def native_value(self): """Return the state of the sensor.""" @@ -119,9 +113,7 @@ class ECSensor(SensorEntity): metadata = self.ec_data.metadata sensor_data = conditions.get(self.sensor_type) - self._unique_id = f"{metadata['location']}-{self.sensor_type}" self._attr = {} - self._name = sensor_data.get("label") value = sensor_data.get("value") if isinstance(value, list): @@ -133,7 +125,9 @@ class ECSensor(SensorEntity): self._state = str(value).capitalize() elif value is not None and len(value) > 255: self._state = value[:255] - _LOGGER.info("Value for %s truncated to 255 characters", self._unique_id) + _LOGGER.info( + "Value for %s truncated to 255 characters", self._attr_unique_id + ) else: self._state = value diff --git a/homeassistant/components/environment_canada/strings.json b/homeassistant/components/environment_canada/strings.json new file mode 100644 index 00000000000..49686cba123 --- /dev/null +++ b/homeassistant/components/environment_canada/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "title": "Environment Canada: weather location and language", + "description": "Either a station ID or latitude/longitude must be specified. The default latitude/longitude used are the values configured in your Home Assistant installation. The closest weather station to the coordinates will be used if specifying coordinates. If a station code is used it must follow the format: PP/code, where PP is the two-letter province and code is the station ID. The list of station IDs can be found here: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. Weather information can be retrieved in either English or French.", + "data": { + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]", + "station": "Weather station ID", + "language": "Weather information language" + } + } + }, + "error": { + "bad_station_id": "Station ID is invalid, missing, or not found in the station ID database", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "error_response": "Response from Environment Canada in error", + "too_many_attempts": "Connections to Environment Canada are rate limited; Try again in 60 seconds", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + } +} diff --git a/homeassistant/components/environment_canada/translations/en.json b/homeassistant/components/environment_canada/translations/en.json new file mode 100644 index 00000000000..94c0b947fa4 --- /dev/null +++ b/homeassistant/components/environment_canada/translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "bad_station_id": "Station ID is invalid, missing, or not found in the station ID database", + "cannot_connect": "Failed to connect", + "error_response": "Response from Environment Canada in error", + "too_many_attempts": "Connections to Environment Canada are rate limited; Try again in 60 seconds", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "language": "Weather information language", + "latitude": "Latitude", + "longitude": "Longitude", + "station": "Weather station ID" + }, + "description": "Either a station ID or latitude/longitude must be specified. The default latitude/longitude used are the values configured in your Home Assistant installation. The closest weather station to the coordinates will be used if specifying coordinates. If a station code is used it must follow the format: PP/code, where PP is the two-letter province and code is the station ID. The list of station IDs can be found here: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. Weather information can be retrieved in either English or French.", + "title": "Environment Canada: weather location and language" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index cf24146da14..281cf117426 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -1,8 +1,8 @@ """Platform for retrieving meteorological data from Environment Canada.""" import datetime +import logging import re -from env_canada import ECData import voluptuous as vol from homeassistant.components.weather import ( @@ -30,17 +30,20 @@ from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_C import homeassistant.helpers.config_validation as cv from homeassistant.util import dt +from . import trigger_import +from .const import CONF_ATTRIBUTION, CONF_STATION, DOMAIN + CONF_FORECAST = "forecast" -CONF_ATTRIBUTION = "Data provided by Environment Canada" -CONF_STATION = "station" + +_LOGGER = logging.getLogger(__name__) def validate_station(station): """Check that the station ID is well-formed.""" if station is None: - return + return None if not re.fullmatch(r"[A-Z]{2}/s0000\d{3}", station): - raise vol.error.Invalid('Station ID must be of the form "XX/s0000###"') + raise vol.Invalid('Station ID must be of the form "XX/s0000###"') return station @@ -72,45 +75,59 @@ ICON_CONDITION_MAP = { } -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entries, discovery_info=None): """Set up the Environment Canada weather.""" - if config.get(CONF_STATION): - ec_data = ECData(station_id=config[CONF_STATION]) - else: - lat = config.get(CONF_LATITUDE, hass.config.latitude) - lon = config.get(CONF_LONGITUDE, hass.config.longitude) - ec_data = ECData(coordinates=(lat, lon)) + trigger_import(hass, config) - add_devices([ECWeather(ec_data, config)]) + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add a weather entity from a config_entry.""" + weather_data = hass.data[DOMAIN][config_entry.entry_id]["weather_data"] + + async_add_entities( + [ + ECWeather( + weather_data, + f"{config_entry.title}", + config_entry.data, + "daily", + f"{config_entry.unique_id}-daily", + ), + ECWeather( + weather_data, + f"{config_entry.title} Hourly", + config_entry.data, + "hourly", + f"{config_entry.unique_id}-hourly", + ), + ] + ) class ECWeather(WeatherEntity): """Representation of a weather condition.""" - def __init__(self, ec_data, config): + def __init__(self, ec_data, name, config, forecast_type, unique_id): """Initialize Environment Canada weather.""" self.ec_data = ec_data - self.platform_name = config.get(CONF_NAME) - self.forecast_type = config[CONF_FORECAST] + self.config = config + self._attr_name = name + self._attr_unique_id = unique_id + self.forecast_type = forecast_type @property def attribution(self): """Return the attribution.""" return CONF_ATTRIBUTION - @property - def name(self): - """Return the name of the weather entity.""" - if self.platform_name: - return self.platform_name - return self.ec_data.metadata.get("location") - @property def temperature(self): """Return the temperature.""" if self.ec_data.conditions.get("temperature", {}).get("value"): return float(self.ec_data.conditions["temperature"]["value"]) - if self.ec_data.hourly_forecasts[0].get("temperature"): + if self.ec_data.hourly_forecasts and self.ec_data.hourly_forecasts[0].get( + "temperature" + ): return float(self.ec_data.hourly_forecasts[0]["temperature"]) return None @@ -161,7 +178,9 @@ class ECWeather(WeatherEntity): if self.ec_data.conditions.get("icon_code", {}).get("value"): icon_code = self.ec_data.conditions["icon_code"]["value"] - elif self.ec_data.hourly_forecasts[0].get("icon_code"): + elif self.ec_data.hourly_forecasts and self.ec_data.hourly_forecasts[0].get( + "icon_code" + ): icon_code = self.ec_data.hourly_forecasts[0]["icon_code"] if icon_code: @@ -184,6 +203,8 @@ def get_forecast(ec_data, forecast_type): if forecast_type == "daily": half_days = ec_data.daily_forecasts + if not half_days: + return None today = { ATTR_FORECAST_TIME: dt.now().isoformat(), diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index ed698fde8dc..cf42c2bd24f 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -77,6 +77,7 @@ FLOWS = [ "emulated_roku", "enocean", "enphase_envoy", + "environment_canada", "epson", "esphome", "ezviz", diff --git a/requirements_all.txt b/requirements_all.txt index e91134d5d2f..12b83a61bc0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -591,7 +591,7 @@ enocean==0.50 enturclient==0.2.2 # homeassistant.components.environment_canada -env_canada==0.2.5 +env_canada==0.2.7 # homeassistant.components.envirophat # envirophat==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e6e464acb75..7341dcd518f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -356,6 +356,9 @@ emulated_roku==0.2.1 # homeassistant.components.enocean enocean==0.50 +# homeassistant.components.environment_canada +env_canada==0.2.7 + # homeassistant.components.enphase_envoy envoy_reader==0.20.0 diff --git a/tests/components/environment_canada/__init__.py b/tests/components/environment_canada/__init__.py new file mode 100644 index 00000000000..65b0ed16207 --- /dev/null +++ b/tests/components/environment_canada/__init__.py @@ -0,0 +1 @@ +"""Tests for the Environment Canada integration.""" diff --git a/tests/components/environment_canada/test_config_flow.py b/tests/components/environment_canada/test_config_flow.py new file mode 100644 index 00000000000..192efb05f40 --- /dev/null +++ b/tests/components/environment_canada/test_config_flow.py @@ -0,0 +1,178 @@ +"""Test the Environment Canada (EC) config flow.""" +from unittest.mock import MagicMock, Mock, patch +import xml.etree.ElementTree as et + +import aiohttp +import pytest + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.environment_canada.const import ( + CONF_LANGUAGE, + CONF_STATION, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE + +from tests.common import MockConfigEntry + +FAKE_CONFIG = { + CONF_STATION: "ON/s1234567", + CONF_LANGUAGE: "English", + CONF_LATITUDE: 42.42, + CONF_LONGITUDE: -42.42, +} +FAKE_TITLE = "Universal title!" + + +def mocked_ec( + station_id=FAKE_CONFIG[CONF_STATION], + lat=FAKE_CONFIG[CONF_LATITUDE], + lon=FAKE_CONFIG[CONF_LONGITUDE], + lang=FAKE_CONFIG[CONF_LANGUAGE], + update=None, + metadata={"location": FAKE_TITLE}, +): + """Mock the env_canada library.""" + ec_mock = MagicMock() + ec_mock.station_id = station_id + ec_mock.lat = lat + ec_mock.lon = lon + ec_mock.language = lang + ec_mock.metadata = metadata + + if update: + ec_mock.update = update + else: + ec_mock.update = Mock() + + return patch( + "homeassistant.components.environment_canada.config_flow.ECData", + return_value=ec_mock, + ) + + +async def test_create_entry(hass): + """Test creating an entry.""" + with mocked_ec(), patch( + "homeassistant.components.environment_canada.async_setup_entry", + return_value=True, + ): + flow = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + flow["flow_id"], FAKE_CONFIG + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == FAKE_CONFIG + assert result["title"] == FAKE_TITLE + + +async def test_create_same_entry_twice(hass): + """Test duplicate entries.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=FAKE_CONFIG, + unique_id="ON/s1234567-english", + ) + entry.add_to_hass(hass) + + with mocked_ec(), patch( + "homeassistant.components.environment_canada.async_setup_entry", + return_value=True, + ): + flow = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + flow["flow_id"], FAKE_CONFIG + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_too_many_attempts(hass): + """Test hitting rate limit.""" + with mocked_ec(metadata={}), patch( + "homeassistant.components.environment_canada.async_setup_entry", + return_value=True, + ): + flow = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + flow["flow_id"], + {}, + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "too_many_attempts"} + + +@pytest.mark.parametrize( + "error", + [ + (aiohttp.ClientResponseError(Mock(), (), status=404), "bad_station_id"), + (aiohttp.ClientResponseError(Mock(), (), status=400), "error_response"), + (aiohttp.ClientConnectionError, "cannot_connect"), + (et.ParseError, "bad_station_id"), + (ValueError, "unknown"), + ], +) +async def test_exception_handling(hass, error): + """Test exception handling.""" + exc, base_error = error + with patch( + "homeassistant.components.environment_canada.config_flow.ECData", + side_effect=exc, + ): + flow = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + flow["flow_id"], + {}, + ) + await hass.async_block_till_done() + assert result["type"] == "form" + assert result["errors"] == {"base": base_error} + + +async def test_lat_or_lon_not_specified(hass): + """Test that the import step works.""" + with mocked_ec(), patch( + "homeassistant.components.environment_canada.async_setup_entry", + return_value=True, + ): + fake_config = dict(FAKE_CONFIG) + del fake_config[CONF_LATITUDE] + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=fake_config + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == FAKE_CONFIG + assert result["title"] == FAKE_TITLE + + +async def test_async_step_import(hass): + """Test that the import step works.""" + with mocked_ec(), patch( + "homeassistant.components.environment_canada.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=FAKE_CONFIG + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == FAKE_CONFIG + assert result["title"] == FAKE_TITLE + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=FAKE_CONFIG + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT From 381301d9788562092b8953dd5ee626f355bfb791 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 11 Oct 2021 07:20:11 -1000 Subject: [PATCH 0248/1038] Add the switch platform to flux_led (#57444) --- homeassistant/components/flux_led/__init__.py | 11 ++- homeassistant/components/flux_led/entity.py | 92 +++++++++++++++++++ homeassistant/components/flux_led/light.py | 70 +------------- homeassistant/components/flux_led/switch.py | 42 +++++++++ tests/components/flux_led/__init__.py | 30 +++++- tests/components/flux_led/test_light.py | 28 +++--- tests/components/flux_led/test_switch.py | 62 +++++++++++++ 7 files changed, 249 insertions(+), 86 deletions(-) create mode 100644 homeassistant/components/flux_led/entity.py create mode 100644 homeassistant/components/flux_led/switch.py create mode 100644 tests/components/flux_led/test_switch.py diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index a933e127b61..248ce7261e9 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -5,6 +5,7 @@ from datetime import timedelta import logging from typing import Any, Final +from flux_led import DeviceType from flux_led.aio import AIOWifiLedBulb from flux_led.aioscanner import AIOBulbScanner @@ -34,7 +35,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS: Final = ["light"] +PLATFORMS_BY_TYPE: Final = {DeviceType.Bulb: ["light"], DeviceType.Switch: ["switch"]} DISCOVERY_INTERVAL: Final = timedelta(minutes=15) REQUEST_REFRESH_DELAY: Final = 1.5 @@ -149,7 +150,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) from ex coordinator = FluxLedUpdateCoordinator(hass, device) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + hass.config_entries.async_setup_platforms( + entry, PLATFORMS_BY_TYPE[device.device_type] + ) entry.async_on_unload(entry.add_update_listener(async_update_listener)) return True @@ -157,7 +160,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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): + device: AIOWifiLedBulb = hass.data[DOMAIN][entry.entry_id].device + platforms = PLATFORMS_BY_TYPE[device.device_type] + if unload_ok := await hass.config_entries.async_unload_platforms(entry, platforms): coordinator = hass.data[DOMAIN].pop(entry.entry_id) await coordinator.device.async_stop() return unload_ok diff --git a/homeassistant/components/flux_led/entity.py b/homeassistant/components/flux_led/entity.py new file mode 100644 index 00000000000..ae1525221de --- /dev/null +++ b/homeassistant/components/flux_led/entity.py @@ -0,0 +1,92 @@ +"""Support for FluxLED/MagicHome lights.""" +from __future__ import annotations + +from abc import abstractmethod +from typing import Any, cast + +from flux_led.aiodevice import AIOWifiLedBulb + +from homeassistant.const import ( + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + ATTR_SW_VERSION, +) +from homeassistant.core import callback +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import FluxLedUpdateCoordinator +from .const import SIGNAL_STATE_UPDATED + + +class FluxEntity(CoordinatorEntity): + """Representation of a Flux entity.""" + + coordinator: FluxLedUpdateCoordinator + + def __init__( + self, + coordinator: FluxLedUpdateCoordinator, + unique_id: str | None, + name: str, + ) -> None: + """Initialize the light.""" + super().__init__(coordinator) + self._device: AIOWifiLedBulb = coordinator.device + self._responding = True + self._attr_name = name + self._attr_unique_id = unique_id + if self.unique_id: + self._attr_device_info = { + "connections": {(dr.CONNECTION_NETWORK_MAC, self.unique_id)}, + ATTR_MODEL: f"0x{self._device.model_num:02X}", + ATTR_NAME: self.name, + ATTR_SW_VERSION: str(self._device.version_num), + ATTR_MANUFACTURER: "FluxLED/Magic Home", + } + + @property + def is_on(self) -> bool: + """Return true if device is on.""" + return cast(bool, self._device.is_on) + + @property + def extra_state_attributes(self) -> dict[str, str]: + """Return the attributes.""" + return {"ip_address": self._device.ipaddr} + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the specified device on.""" + await self._async_turn_on(**kwargs) + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + @abstractmethod + async def _async_turn_on(self, **kwargs: Any) -> None: + """Turn the specified device on.""" + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the specified device off.""" + await self._device.async_turn_off() + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + if self.coordinator.last_update_success != self._responding: + self.async_write_ha_state() + self._responding = self.coordinator.last_update_success + + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_STATE_UPDATED.format(self._device.ipaddr), + self.async_write_ha_state, + ) + ) + await super().async_added_to_hass() diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index c76e0f42b67..b587abcc7e6 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -6,7 +6,6 @@ import logging import random from typing import Any, Final, cast -from flux_led.aiodevice import AIOWifiLedBulb from flux_led.const import ( COLOR_MODE_CCT as FLUX_COLOR_MODE_CCT, COLOR_MODE_DIM as FLUX_COLOR_MODE_DIM, @@ -47,11 +46,7 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.const import ( - ATTR_MANUFACTURER, ATTR_MODE, - ATTR_MODEL, - ATTR_NAME, - ATTR_SW_VERSION, CONF_DEVICES, CONF_HOST, CONF_MAC, @@ -59,10 +54,9 @@ from homeassistant.const import ( CONF_NAME, CONF_PROTOCOL, ) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import device_registry as dr, entity_platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -92,11 +86,11 @@ from .const import ( MODE_RGB, MODE_RGBW, MODE_WHITE, - SIGNAL_STATE_UPDATED, TRANSITION_GRADUAL, TRANSITION_JUMP, TRANSITION_STROBE, ) +from .entity import FluxEntity _LOGGER = logging.getLogger(__name__) @@ -284,11 +278,9 @@ async def async_setup_entry( ) -class FluxLight(CoordinatorEntity, LightEntity): +class FluxLight(FluxEntity, CoordinatorEntity, LightEntity): """Representation of a Flux light.""" - coordinator: FluxLedUpdateCoordinator - def __init__( self, coordinator: FluxLedUpdateCoordinator, @@ -299,11 +291,7 @@ class FluxLight(CoordinatorEntity, LightEntity): custom_effect_transition: str, ) -> None: """Initialize the light.""" - super().__init__(coordinator) - self._device: AIOWifiLedBulb = coordinator.device - self._responding = True - self._attr_name = name - self._attr_unique_id = unique_id + super().__init__(coordinator, unique_id, name) self._attr_supported_features = SUPPORT_FLUX_LED self._attr_min_mireds = ( color_temperature_kelvin_to_mired(self._device.max_temp) + 1 @@ -319,19 +307,6 @@ class FluxLight(CoordinatorEntity, LightEntity): self._custom_effect_colors = custom_effect_colors self._custom_effect_speed_pct = custom_effect_speed_pct self._custom_effect_transition = custom_effect_transition - if self.unique_id: - self._attr_device_info = { - "connections": {(dr.CONNECTION_NETWORK_MAC, self.unique_id)}, - ATTR_MODEL: f"0x{self._device.model_num:02X}", - ATTR_NAME: self.name, - ATTR_SW_VERSION: str(self._device.version_num), - ATTR_MANUFACTURER: "FluxLED/Magic Home", - } - - @property - def is_on(self) -> bool: - """Return true if device is on.""" - return cast(bool, self._device.is_on) @property def brightness(self) -> int: @@ -382,17 +357,6 @@ class FluxLight(CoordinatorEntity, LightEntity): return EFFECT_CUSTOM return EFFECT_ID_NAME.get(current_mode) - @property - def extra_state_attributes(self) -> dict[str, str]: - """Return the attributes.""" - return {"ip_address": self._device.ipaddr} - - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the specified or all lights on.""" - await self._async_turn_on(**kwargs) - self.async_write_ha_state() - await self.coordinator.async_request_refresh() - async def _async_turn_on(self, **kwargs: Any) -> None: """Turn the specified or all lights on.""" if not self.is_on: @@ -506,27 +470,3 @@ class FluxLight(CoordinatorEntity, LightEntity): speed_pct, transition, ) - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn the specified or all lights off.""" - await self._device.async_turn_off() - self.async_write_ha_state() - await self.coordinator.async_request_refresh() - - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - if self.coordinator.last_update_success != self._responding: - self.async_write_ha_state() - self._responding = self.coordinator.last_update_success - - async def async_added_to_hass(self) -> None: - """Handle entity which will be added.""" - self.async_on_remove( - async_dispatcher_connect( - self.hass, - SIGNAL_STATE_UPDATED.format(self._device.ipaddr), - self.async_write_ha_state, - ) - ) - await super().async_added_to_hass() diff --git a/homeassistant/components/flux_led/switch.py b/homeassistant/components/flux_led/switch.py new file mode 100644 index 00000000000..0ca7a771c78 --- /dev/null +++ b/homeassistant/components/flux_led/switch.py @@ -0,0 +1,42 @@ +"""Support for FluxLED/MagicHome switches.""" +from __future__ import annotations + +from typing import Any + +from homeassistant import config_entries +from homeassistant.components.switch import SwitchEntity +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import FluxLedUpdateCoordinator +from .const import DOMAIN +from .entity import FluxEntity + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Flux lights.""" + coordinator: FluxLedUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + [ + FluxSwitch( + coordinator, + entry.unique_id, + entry.data[CONF_NAME], + ) + ] + ) + + +class FluxSwitch(FluxEntity, CoordinatorEntity, SwitchEntity): + """Representation of a Flux switch.""" + + async def _async_turn_on(self, **kwargs: Any) -> None: + """Turn the device on.""" + if not self.is_on: + await self._device.async_turn_on() diff --git a/tests/components/flux_led/__init__.py b/tests/components/flux_led/__init__.py index d705f0d43ff..3501d317d6c 100644 --- a/tests/components/flux_led/__init__.py +++ b/tests/components/flux_led/__init__.py @@ -5,6 +5,7 @@ import asyncio from typing import Callable from unittest.mock import AsyncMock, MagicMock, patch +from flux_led import DeviceType from flux_led.aio import AIOWifiLedBulb from flux_led.const import ( COLOR_MODE_CCT as FLUX_COLOR_MODE_CCT, @@ -43,6 +44,7 @@ def _mocked_bulb() -> AIOWifiLedBulb: async def _save_setup_callback(callback: Callable) -> None: bulb.data_receive_callback = callback + bulb.device_type = DeviceType.Bulb bulb.async_setup = AsyncMock(side_effect=_save_setup_callback) bulb.async_set_custom_pattern = AsyncMock() bulb.async_set_preset_pattern = AsyncMock() @@ -76,16 +78,36 @@ def _mocked_bulb() -> AIOWifiLedBulb: return bulb -async def async_mock_bulb_turn_off(hass: HomeAssistant, bulb: AIOWifiLedBulb) -> None: - """Mock the bulb being off.""" +def _mocked_switch() -> AIOWifiLedBulb: + switch = MagicMock(auto_spec=AIOWifiLedBulb) + + async def _save_setup_callback(callback: Callable) -> None: + switch.data_receive_callback = callback + + switch.device_type = DeviceType.Switch + switch.async_setup = AsyncMock(side_effect=_save_setup_callback) + switch.async_stop = AsyncMock() + switch.async_update = AsyncMock() + switch.async_turn_off = AsyncMock() + switch.async_turn_on = AsyncMock() + switch.model_num = 0x97 + switch.version_num = 0x97 + switch.raw_state = LEDENETRawState( + 0, 0x97, 0, 0x61, 0x97, 50, 255, 0, 0, 50, 8, 0, 0, 0 + ) + return switch + + +async def async_mock_device_turn_off(hass: HomeAssistant, bulb: AIOWifiLedBulb) -> None: + """Mock the device being off.""" bulb.is_on = False bulb.raw_state._replace(power_state=0x24) bulb.data_receive_callback() await hass.async_block_till_done() -async def async_mock_bulb_turn_on(hass: HomeAssistant, bulb: AIOWifiLedBulb) -> None: - """Mock the bulb being on.""" +async def async_mock_device_turn_on(hass: HomeAssistant, bulb: AIOWifiLedBulb) -> None: + """Mock the device being on.""" bulb.is_on = True bulb.raw_state._replace(power_state=0x23) bulb.data_receive_callback() diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index aa2ee650020..1ddd79070b3 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -64,8 +64,8 @@ from . import ( _mocked_bulb, _patch_discovery, _patch_wifibulb, - async_mock_bulb_turn_off, - async_mock_bulb_turn_on, + async_mock_device_turn_off, + async_mock_device_turn_on, ) from tests.common import MockConfigEntry, async_fire_time_changed @@ -206,7 +206,7 @@ async def test_rgb_light(hass: HomeAssistant) -> None: ) bulb.async_turn_off.assert_called_once() - await async_mock_bulb_turn_off(hass, bulb) + await async_mock_device_turn_off(hass, bulb) assert hass.states.get(entity_id).state == STATE_OFF await hass.services.async_call( @@ -291,7 +291,7 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None: LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) bulb.async_turn_off.assert_called_once() - await async_mock_bulb_turn_off(hass, bulb) + await async_mock_device_turn_off(hass, bulb) assert hass.states.get(entity_id).state == STATE_OFF @@ -343,7 +343,7 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None: bulb.raw_state = bulb.raw_state._replace( red=0, green=0, blue=0, warm_white=1, cool_white=2 ) - await async_mock_bulb_turn_on(hass, bulb) + await async_mock_device_turn_on(hass, bulb) state = hass.states.get(entity_id) assert state.state == STATE_ON attributes = state.attributes @@ -410,7 +410,7 @@ async def test_rgbw_light(hass: HomeAssistant) -> None: LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) bulb.async_turn_off.assert_called_once() - await async_mock_bulb_turn_off(hass, bulb) + await async_mock_device_turn_off(hass, bulb) assert hass.states.get(entity_id).state == STATE_OFF @@ -513,7 +513,7 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None: LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) bulb.async_turn_off.assert_called_once() - await async_mock_bulb_turn_off(hass, bulb) + await async_mock_device_turn_off(hass, bulb) assert hass.states.get(entity_id).state == STATE_OFF @@ -640,7 +640,7 @@ async def test_white_light(hass: HomeAssistant) -> None: LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) bulb.async_turn_off.assert_called_once() - await async_mock_bulb_turn_off(hass, bulb) + await async_mock_device_turn_off(hass, bulb) assert hass.states.get(entity_id).state == STATE_OFF @@ -705,7 +705,7 @@ async def test_rgb_light_custom_effects(hass: HomeAssistant) -> None: LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) bulb.async_turn_off.assert_called_once() - await async_mock_bulb_turn_off(hass, bulb) + await async_mock_device_turn_off(hass, bulb) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF @@ -720,7 +720,7 @@ async def test_rgb_light_custom_effects(hass: HomeAssistant) -> None: ) bulb.async_set_custom_pattern.reset_mock() bulb.preset_pattern_num = EFFECT_CUSTOM_CODE - await async_mock_bulb_turn_on(hass, bulb) + await async_mock_device_turn_on(hass, bulb) state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -738,7 +738,7 @@ async def test_rgb_light_custom_effects(hass: HomeAssistant) -> None: ) bulb.async_set_custom_pattern.reset_mock() bulb.preset_pattern_num = EFFECT_CUSTOM_CODE - await async_mock_bulb_turn_on(hass, bulb) + await async_mock_device_turn_on(hass, bulb) state = hass.states.get(entity_id) assert state.state == STATE_ON @@ -812,7 +812,7 @@ async def test_rgb_light_custom_effect_via_service( ) bulb.async_turn_off.assert_called_once() - await async_mock_bulb_turn_off(hass, bulb) + await async_mock_device_turn_off(hass, bulb) assert hass.states.get(entity_id).state == STATE_OFF await hass.services.async_call( @@ -911,7 +911,7 @@ async def test_addressable_light(hass: HomeAssistant) -> None: ) bulb.async_turn_off.assert_called_once() - await async_mock_bulb_turn_off(hass, bulb) + await async_mock_device_turn_off(hass, bulb) assert hass.states.get(entity_id).state == STATE_OFF await hass.services.async_call( @@ -919,7 +919,7 @@ async def test_addressable_light(hass: HomeAssistant) -> None: ) bulb.async_turn_on.assert_called_once() bulb.async_turn_on.reset_mock() - await async_mock_bulb_turn_on(hass, bulb) + await async_mock_device_turn_on(hass, bulb) with pytest.raises(ValueError): await hass.services.async_call( diff --git a/tests/components/flux_led/test_switch.py b/tests/components/flux_led/test_switch.py new file mode 100644 index 00000000000..e41a10807c7 --- /dev/null +++ b/tests/components/flux_led/test_switch.py @@ -0,0 +1,62 @@ +"""Tests for switch platform.""" +from homeassistant.components import flux_led +from homeassistant.components.flux_led.const import DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_HOST, + CONF_NAME, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from . import ( + DEFAULT_ENTRY_TITLE, + IP_ADDRESS, + MAC_ADDRESS, + _mocked_switch, + _patch_discovery, + _patch_wifibulb, + async_mock_device_turn_off, + async_mock_device_turn_on, +) + +from tests.common import MockConfigEntry + + +async def test_switch_on_off(hass: HomeAssistant) -> None: + """Test a switch light.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + switch = _mocked_switch() + with _patch_discovery(device=switch), _patch_wifibulb(device=switch): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "switch.az120444_aabbccddeeff" + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + switch.async_turn_off.assert_called_once() + + await async_mock_device_turn_off(hass, switch) + assert hass.states.get(entity_id).state == STATE_OFF + + await hass.services.async_call( + SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + switch.async_turn_on.assert_called_once() + switch.async_turn_on.reset_mock() + + await async_mock_device_turn_on(hass, switch) + assert hass.states.get(entity_id).state == STATE_ON From 02c30aed5e11719abf0c2fbe202351c1898c2b8c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 11 Oct 2021 07:20:24 -1000 Subject: [PATCH 0249/1038] Add DHCP discovery for additional Zengge devices, generic magichome strips (#57408) --- homeassistant/components/flux_led/manifest.json | 12 ++++++++++++ homeassistant/generated/dhcp.py | 15 +++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 28d4ecd772c..dbff7aaac89 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -15,14 +15,26 @@ "macaddress": "249494*", "hostname": "[ba][lk]*" }, + { + "macaddress": "7CB94C*", + "hostname": "[ba][lk]*" + }, { "macaddress": "B4E842*", "hostname": "[ba][lk]*" }, + { + "macaddress": "8CCE4E*", + "hostname": "lwip*" + }, { "macaddress": "2462AB*", "hostname": "zengge_35*" }, + { + "macaddress": "70039F*", + "hostname": "zengge_0e*" + }, { "macaddress": "C82E47*", "hostname": "sta*" diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 7fb65c45421..69ca9b422d7 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -81,16 +81,31 @@ DHCP = [ "macaddress": "249494*", "hostname": "[ba][lk]*" }, + { + "domain": "flux_led", + "macaddress": "7CB94C*", + "hostname": "[ba][lk]*" + }, { "domain": "flux_led", "macaddress": "B4E842*", "hostname": "[ba][lk]*" }, + { + "domain": "flux_led", + "macaddress": "8CCE4E*", + "hostname": "lwip*" + }, { "domain": "flux_led", "macaddress": "2462AB*", "hostname": "zengge_35*" }, + { + "domain": "flux_led", + "macaddress": "70039F*", + "hostname": "zengge_0e*" + }, { "domain": "flux_led", "macaddress": "C82E47*", From b155d2bbe59ebe81fdba0750e3e4a5dccceef502 Mon Sep 17 00:00:00 2001 From: chpego <38792705+chpego@users.noreply.github.com> Date: Mon, 11 Oct 2021 20:07:23 +0200 Subject: [PATCH 0250/1038] Bump youtube-dl to 2021.06.06 (#57490) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index d7dd665b9d9..69c4b3782c2 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -2,7 +2,7 @@ "domain": "media_extractor", "name": "Media Extractor", "documentation": "https://www.home-assistant.io/integrations/media_extractor", - "requirements": ["youtube_dl==2021.04.26"], + "requirements": ["youtube_dl==2021.06.06"], "dependencies": ["media_player"], "codeowners": [], "quality_scale": "internal", diff --git a/requirements_all.txt b/requirements_all.txt index 12b83a61bc0..7674aff0895 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2465,7 +2465,7 @@ yeelightsunflower==0.0.10 youless-api==0.14 # homeassistant.components.media_extractor -youtube_dl==2021.04.26 +youtube_dl==2021.06.06 # homeassistant.components.zengge zengge==0.2 From 48c2cfa6f8fe5ef1123141f4ce438632b59037cf Mon Sep 17 00:00:00 2001 From: RenierM26 <66512715+RenierM26@users.noreply.github.com> Date: Mon, 11 Oct 2021 20:09:19 +0200 Subject: [PATCH 0251/1038] Use entity description for Ezviz sensors (#56634) --- .coveragerc | 1 + .../components/ezviz/binary_sensor.py | 103 +++++------- homeassistant/components/ezviz/camera.py | 149 +++++++----------- homeassistant/components/ezviz/entity.py | 36 +++++ homeassistant/components/ezviz/manifest.json | 2 +- homeassistant/components/ezviz/sensor.py | 108 ++++++------- homeassistant/components/ezviz/switch.py | 71 +++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 9 files changed, 207 insertions(+), 267 deletions(-) create mode 100644 homeassistant/components/ezviz/entity.py diff --git a/.coveragerc b/.coveragerc index 829915400bd..a4288b278ca 100644 --- a/.coveragerc +++ b/.coveragerc @@ -301,6 +301,7 @@ omit = homeassistant/components/ezviz/camera.py homeassistant/components/ezviz/coordinator.py homeassistant/components/ezviz/const.py + homeassistant/components/ezviz/entity.py homeassistant/components/ezviz/binary_sensor.py homeassistant/components/ezviz/sensor.py homeassistant/components/ezviz/switch.py diff --git a/homeassistant/components/ezviz/binary_sensor.py b/homeassistant/components/ezviz/binary_sensor.py index bc343f06065..e7d8be80509 100644 --- a/homeassistant/components/ezviz/binary_sensor.py +++ b/homeassistant/components/ezviz/binary_sensor.py @@ -1,19 +1,36 @@ """Support for Ezviz binary sensors.""" -import logging +from __future__ import annotations -from pyezviz.constants import BinarySensorType - -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOTION, + DEVICE_CLASS_UPDATE, + BinarySensorEntity, + BinarySensorEntityDescription, +) 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.update_coordinator import CoordinatorEntity -from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER +from .const import DATA_COORDINATOR, DOMAIN from .coordinator import EzvizDataUpdateCoordinator +from .entity import EzvizEntity -_LOGGER = logging.getLogger(__name__) +PARALLEL_UPDATES = 1 + +BINARY_SENSOR_TYPES: dict[str, BinarySensorEntityDescription] = { + "Motion_Trigger": BinarySensorEntityDescription( + key="Motion_Trigger", + device_class=DEVICE_CLASS_MOTION, + ), + "alarm_schedules_enabled": BinarySensorEntityDescription( + key="alarm_schedules_enabled" + ), + "encrypted": BinarySensorEntityDescription(key="encrypted"), + "upgrade_available": BinarySensorEntityDescription( + key="upgrade_available", + device_class=DEVICE_CLASS_UPDATE, + ), +} async def async_setup_entry( @@ -23,24 +40,19 @@ async def async_setup_entry( coordinator: EzvizDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ DATA_COORDINATOR ] - sensors = [] - for idx, camera in enumerate(coordinator.data): - for name in camera: - # Only add sensor with value. - if camera.get(name) is None: - continue - - if name in BinarySensorType.__members__: - sensor_type_name = getattr(BinarySensorType, name).value - sensors.append( - EzvizBinarySensor(coordinator, idx, name, sensor_type_name) - ) - - async_add_entities(sensors) + async_add_entities( + [ + EzvizBinarySensor(coordinator, camera, binary_sensor) + for camera in coordinator.data + for binary_sensor, value in coordinator.data[camera].items() + if binary_sensor in BINARY_SENSOR_TYPES + if value is not None + ] + ) -class EzvizBinarySensor(CoordinatorEntity, BinarySensorEntity): +class EzvizBinarySensor(EzvizEntity, BinarySensorEntity): """Representation of a Ezviz sensor.""" coordinator: EzvizDataUpdateCoordinator @@ -48,46 +60,17 @@ class EzvizBinarySensor(CoordinatorEntity, BinarySensorEntity): def __init__( self, coordinator: EzvizDataUpdateCoordinator, - idx: int, - name: str, - sensor_type_name: str, + serial: str, + binary_sensor: str, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator) - self._idx = idx - self._camera_name = self.coordinator.data[self._idx]["name"] - self._name = name - self._sensor_name = f"{self._camera_name}.{self._name}" - self.sensor_type_name = sensor_type_name - self._serial = self.coordinator.data[self._idx]["serial"] - - @property - def name(self) -> str: - """Return the name of the Ezviz sensor.""" - return self._name + super().__init__(coordinator, serial) + self._sensor_name = binary_sensor + self._attr_name = f"{self._camera_name} {binary_sensor.title()}" + self._attr_unique_id = f"{serial}_{self._camera_name}.{binary_sensor}" + self.entity_description = BINARY_SENSOR_TYPES[binary_sensor] @property def is_on(self) -> bool: """Return the state of the sensor.""" - return self.coordinator.data[self._idx][self._name] - - @property - def unique_id(self) -> str: - """Return the unique ID of this sensor.""" - return f"{self._serial}_{self._sensor_name}" - - @property - def device_info(self) -> DeviceInfo: - """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN, self._serial)}, - "name": self.coordinator.data[self._idx]["name"], - "model": self.coordinator.data[self._idx]["device_sub_category"], - "manufacturer": MANUFACTURER, - "sw_version": self.coordinator.data[self._idx]["version"], - } - - @property - def device_class(self) -> str: - """Device class for the sensor.""" - return self.sensor_type_name + return self.data[self._sensor_name] diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index 44a90e2928f..89023b8902d 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -18,9 +18,7 @@ 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.entity import DeviceInfo from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTR_DIRECTION, @@ -40,7 +38,6 @@ from .const import ( DIR_RIGHT, DIR_UP, DOMAIN, - MANUFACTURER, SERVICE_ALARM_SOUND, SERVICE_ALARM_TRIGER, SERVICE_DETECTION_SENSITIVITY, @@ -48,6 +45,7 @@ from .const import ( SERVICE_WAKE_DEVICE, ) from .coordinator import EzvizDataUpdateCoordinator +from .entity import EzvizEntity CAMERA_SCHEMA = vol.Schema( {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} @@ -115,41 +113,37 @@ async def async_setup_entry( coordinator: EzvizDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ DATA_COORDINATOR ] - camera_config_entries = hass.config_entries.async_entries(DOMAIN) camera_entities = [] - for idx, camera in enumerate(coordinator.data): - - # There seem to be a bug related to localRtspPort in Ezviz API... - local_rtsp_port = DEFAULT_RTSP_PORT + for camera, value in coordinator.data.items(): camera_rtsp_entry = [ item - for item in camera_config_entries - if item.unique_id == camera[ATTR_SERIAL] + for item in hass.config_entries.async_entries(DOMAIN) + if item.unique_id == camera and item.source != SOURCE_IGNORE ] - if camera["local_rtsp_port"] != 0: - local_rtsp_port = camera["local_rtsp_port"] + # There seem to be a bug related to localRtspPort in Ezviz API. + local_rtsp_port = ( + value["local_rtsp_port"] + if value["local_rtsp_port"] != 0 + else DEFAULT_RTSP_PORT + ) if camera_rtsp_entry: - conf_cameras = camera_rtsp_entry[0] - # Skip ignored entities. - if conf_cameras.source == SOURCE_IGNORE: - continue + ffmpeg_arguments = camera_rtsp_entry[0].options[CONF_FFMPEG_ARGUMENTS] + camera_username = camera_rtsp_entry[0].data[CONF_USERNAME] + camera_password = camera_rtsp_entry[0].data[CONF_PASSWORD] - ffmpeg_arguments = conf_cameras.options.get( - CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS - ) - - camera_username = conf_cameras.data[CONF_USERNAME] - camera_password = conf_cameras.data[CONF_PASSWORD] - - camera_rtsp_stream = f"rtsp://{camera_username}:{camera_password}@{camera['local_ip']}:{local_rtsp_port}{ffmpeg_arguments}" + camera_rtsp_stream = f"rtsp://{camera_username}:{camera_password}@{value['local_ip']}:{local_rtsp_port}{ffmpeg_arguments}" _LOGGER.debug( - "Camera %s source stream: %s", camera[ATTR_SERIAL], camera_rtsp_stream + "Configuring Camera %s with ip: %s rtsp port: %s ffmpeg arguments: %s", + camera, + value["local_ip"], + local_rtsp_port, + ffmpeg_arguments, ) else: @@ -159,26 +153,27 @@ async def async_setup_entry( DOMAIN, context={"source": SOURCE_DISCOVERY}, data={ - ATTR_SERIAL: camera[ATTR_SERIAL], - CONF_IP_ADDRESS: camera["local_ip"], + ATTR_SERIAL: camera, + CONF_IP_ADDRESS: value["local_ip"], }, ) ) - camera_username = DEFAULT_CAMERA_USERNAME - camera_password = "" - camera_rtsp_stream = "" - ffmpeg_arguments = DEFAULT_FFMPEG_ARGUMENTS _LOGGER.warning( "Found camera with serial %s without configuration. Please go to integration to complete setup", - camera[ATTR_SERIAL], + camera, ) + ffmpeg_arguments = DEFAULT_FFMPEG_ARGUMENTS + camera_username = DEFAULT_CAMERA_USERNAME + camera_password = None + camera_rtsp_stream = "" + camera_entities.append( EzvizCamera( hass, coordinator, - idx, + camera, camera_username, camera_password, camera_rtsp_stream, @@ -230,7 +225,7 @@ async def async_setup_entry( ) -class EzvizCamera(CoordinatorEntity, Camera): +class EzvizCamera(EzvizEntity, Camera): """An implementation of a Ezviz security camera.""" coordinator: EzvizDataUpdateCoordinator @@ -239,69 +234,51 @@ class EzvizCamera(CoordinatorEntity, Camera): self, hass: HomeAssistant, coordinator: EzvizDataUpdateCoordinator, - idx: int, + serial: str, camera_username: str, - camera_password: str, + camera_password: str | None, camera_rtsp_stream: str | None, - local_rtsp_port: int | None, + local_rtsp_port: int, ffmpeg_arguments: str | None, ) -> None: """Initialize a Ezviz security camera.""" - super().__init__(coordinator) + super().__init__(coordinator, serial) Camera.__init__(self) self._username = camera_username self._password = camera_password self._rtsp_stream = camera_rtsp_stream - self._idx = idx - self._ffmpeg = hass.data[DATA_FFMPEG] self._local_rtsp_port = local_rtsp_port self._ffmpeg_arguments = ffmpeg_arguments - - self._serial = self.coordinator.data[self._idx]["serial"] - self._name = self.coordinator.data[self._idx]["name"] - self._local_ip = self.coordinator.data[self._idx]["local_ip"] + self._ffmpeg = hass.data[DATA_FFMPEG] + self._attr_unique_id = serial + self._attr_name = self.data["name"] @property def available(self) -> bool: """Return True if entity is available.""" - return self.coordinator.data[self._idx]["status"] != 2 + return self.data["status"] != 2 @property def supported_features(self) -> int: """Return supported features.""" - if self._rtsp_stream: + if self._password: return SUPPORT_STREAM return 0 - @property - def name(self) -> str: - """Return the name of this device.""" - return self._name - - @property - def model(self) -> str: - """Return the model of this device.""" - return self.coordinator.data[self._idx]["device_sub_category"] - - @property - def brand(self) -> str: - """Return the manufacturer of this device.""" - return MANUFACTURER - @property def is_on(self) -> bool: """Return true if on.""" - return bool(self.coordinator.data[self._idx]["status"]) + return bool(self.data["status"]) @property def is_recording(self) -> bool: """Return true if the device is recording.""" - return self.coordinator.data[self._idx]["alarm_notify"] + return self.data["alarm_notify"] @property def motion_detection_enabled(self) -> bool: """Camera Motion Detection Status.""" - return self.coordinator.data[self._idx]["alarm_notify"] + return self.data["alarm_notify"] def enable_motion_detection(self) -> None: """Enable motion detection in camera.""" @@ -319,11 +296,6 @@ class EzvizCamera(CoordinatorEntity, Camera): except InvalidHost as err: raise InvalidHost("Error disabling motion detection") from err - @property - def unique_id(self) -> str: - """Return the name of this camera.""" - return self._serial - async def async_camera_image( self, width: int | None = None, height: int | None = None ) -> bytes | None: @@ -334,31 +306,24 @@ class EzvizCamera(CoordinatorEntity, Camera): self.hass, self._rtsp_stream, width=width, height=height ) - @property - def device_info(self) -> DeviceInfo: - """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN, self._serial)}, - "name": self.coordinator.data[self._idx]["name"], - "model": self.coordinator.data[self._idx]["device_sub_category"], - "manufacturer": MANUFACTURER, - "sw_version": self.coordinator.data[self._idx]["version"], - } - async def stream_source(self) -> str | None: """Return the stream source.""" - local_ip = self.coordinator.data[self._idx]["local_ip"] - if self._local_rtsp_port: - rtsp_stream_source = ( - f"rtsp://{self._username}:{self._password}@" - f"{local_ip}:{self._local_rtsp_port}{self._ffmpeg_arguments}" - ) - _LOGGER.debug( - "Camera %s source stream: %s", self._serial, rtsp_stream_source - ) - self._rtsp_stream = rtsp_stream_source - return rtsp_stream_source - return None + if self._password is None: + return None + local_ip = self.data["local_ip"] + self._rtsp_stream = ( + f"rtsp://{self._username}:{self._password}@" + f"{local_ip}:{self._local_rtsp_port}{self._ffmpeg_arguments}" + ) + _LOGGER.debug( + "Configuring Camera %s with ip: %s rtsp port: %s ffmpeg arguments: %s", + self._serial, + local_ip, + self._local_rtsp_port, + self._ffmpeg_arguments, + ) + + return self._rtsp_stream def perform_ptz(self, direction: str, speed: int) -> None: """Perform a PTZ action on the camera.""" diff --git a/homeassistant/components/ezviz/entity.py b/homeassistant/components/ezviz/entity.py new file mode 100644 index 00000000000..e7aa7d5039a --- /dev/null +++ b/homeassistant/components/ezviz/entity.py @@ -0,0 +1,36 @@ +"""An abstract class common to all Ezviz entities.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, MANUFACTURER +from .coordinator import EzvizDataUpdateCoordinator + + +class EzvizEntity(CoordinatorEntity, Entity): + """Generic entity encapsulating common features of Ezviz device.""" + + def __init__( + self, + coordinator: EzvizDataUpdateCoordinator, + serial: str, + ) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + self._serial = serial + self._camera_name = self.data["name"] + self._attr_device_info: DeviceInfo = { + "identifiers": {(DOMAIN, serial)}, + "name": self.data["name"], + "model": self.data["device_sub_category"], + "manufacturer": MANUFACTURER, + "sw_version": self.data["version"], + } + + @property + def data(self) -> dict[str, Any]: + """Return coordinator data for this entity.""" + return self.coordinator.data[self._serial] diff --git a/homeassistant/components/ezviz/manifest.json b/homeassistant/components/ezviz/manifest.json index d5a38b17755..1108f1a6f83 100644 --- a/homeassistant/components/ezviz/manifest.json +++ b/homeassistant/components/ezviz/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/ezviz", "dependencies": ["ffmpeg"], "codeowners": ["@RenierM26", "@baqs"], - "requirements": ["pyezviz==0.1.8.9"], + "requirements": ["pyezviz==0.1.9.4"], "config_flow": true, "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ezviz/sensor.py b/homeassistant/components/ezviz/sensor.py index 512491a2548..3ea650154f0 100644 --- a/homeassistant/components/ezviz/sensor.py +++ b/homeassistant/components/ezviz/sensor.py @@ -1,21 +1,42 @@ """Support for Ezviz sensors.""" from __future__ import annotations -import logging - -from pyezviz.constants import SensorType - -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.binary_sensor import DEVICE_CLASS_MOTION +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE 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 DATA_COORDINATOR, DOMAIN, MANUFACTURER +from .const import DATA_COORDINATOR, DOMAIN from .coordinator import EzvizDataUpdateCoordinator +from .entity import EzvizEntity -_LOGGER = logging.getLogger(__name__) +PARALLEL_UPDATES = 1 + +SENSOR_TYPES: dict[str, SensorEntityDescription] = { + "sw_version": SensorEntityDescription(key="sw_version"), + "battery_level": SensorEntityDescription( + key="battery_level", + native_unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_BATTERY, + ), + "alarm_sound_mod": SensorEntityDescription(key="alarm_sound_mod"), + "detection_sensibility": SensorEntityDescription(key="detection_sensibility"), + "last_alarm_time": SensorEntityDescription(key="last_alarm_time"), + "Seconds_Last_Trigger": SensorEntityDescription( + key="Seconds_Last_Trigger", + entity_registry_enabled_default=False, + ), + "last_alarm_pic": SensorEntityDescription(key="last_alarm_pic"), + "supported_channels": SensorEntityDescription(key="supported_channels"), + "local_ip": SensorEntityDescription(key="local_ip"), + "wan_ip": SensorEntityDescription(key="wan_ip"), + "PIR_Status": SensorEntityDescription( + key="PIR_Status", + device_class=DEVICE_CLASS_MOTION, + ), +} async def async_setup_entry( @@ -25,69 +46,34 @@ async def async_setup_entry( coordinator: EzvizDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ DATA_COORDINATOR ] - sensors = [] - for idx, camera in enumerate(coordinator.data): - for name in camera: - # Only add sensor with value. - if camera.get(name) is None: - continue - - if name in SensorType.__members__: - sensor_type_name = getattr(SensorType, name).value - sensors.append(EzvizSensor(coordinator, idx, name, sensor_type_name)) - - async_add_entities(sensors) + async_add_entities( + [ + EzvizSensor(coordinator, camera, sensor) + for camera in coordinator.data + for sensor, value in coordinator.data[camera].items() + if sensor in SENSOR_TYPES + if value is not None + ] + ) -class EzvizSensor(CoordinatorEntity, SensorEntity): +class EzvizSensor(EzvizEntity, SensorEntity): """Representation of a Ezviz sensor.""" coordinator: EzvizDataUpdateCoordinator def __init__( - self, - coordinator: EzvizDataUpdateCoordinator, - idx: int, - name: str, - sensor_type_name: str, + self, coordinator: EzvizDataUpdateCoordinator, serial: str, sensor: str ) -> None: """Initialize the sensor.""" - super().__init__(coordinator) - self._idx = idx - self._camera_name = self.coordinator.data[self._idx]["name"] - self._name = name - self._sensor_name = f"{self._camera_name}.{self._name}" - self.sensor_type_name = sensor_type_name - self._serial = self.coordinator.data[self._idx]["serial"] - - @property - def name(self) -> str: - """Return the name of the Ezviz sensor.""" - return self._name + super().__init__(coordinator, serial) + self._sensor_name = sensor + self._attr_name = f"{self._camera_name} {sensor.title()}" + self._attr_unique_id = f"{serial}_{self._camera_name}.{sensor}" + self.entity_description = SENSOR_TYPES[sensor] @property def native_value(self) -> int | str: """Return the state of the sensor.""" - return self.coordinator.data[self._idx][self._name] - - @property - def unique_id(self) -> str: - """Return the unique ID of this sensor.""" - return f"{self._serial}_{self._sensor_name}" - - @property - def device_info(self) -> DeviceInfo: - """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN, self._serial)}, - "name": self.coordinator.data[self._idx]["name"], - "model": self.coordinator.data[self._idx]["device_sub_category"], - "manufacturer": MANUFACTURER, - "sw_version": self.coordinator.data[self._idx]["version"], - } - - @property - def device_class(self) -> str: - """Device class for the sensor.""" - return self.sensor_type_name + return self.data[self._sensor_name] diff --git a/homeassistant/components/ezviz/switch.py b/homeassistant/components/ezviz/switch.py index 9949dc18b23..0324d508f7f 100644 --- a/homeassistant/components/ezviz/switch.py +++ b/homeassistant/components/ezviz/switch.py @@ -1,7 +1,6 @@ """Support for Ezviz Switch sensors.""" from __future__ import annotations -import logging from typing import Any from pyezviz.constants import DeviceSwitchType @@ -10,14 +9,11 @@ from pyezviz.exceptions import HTTPError, PyEzvizError from homeassistant.components.switch import DEVICE_CLASS_SWITCH, 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.update_coordinator import CoordinatorEntity -from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER +from .const import DATA_COORDINATOR, DOMAIN from .coordinator import EzvizDataUpdateCoordinator - -_LOGGER = logging.getLogger(__name__) +from .entity import EzvizEntity async def async_setup_entry( @@ -27,51 +23,40 @@ async def async_setup_entry( coordinator: EzvizDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ DATA_COORDINATOR ] - switch_entities = [] + supported_switches = {switches.value for switches in DeviceSwitchType} - for idx, camera in enumerate(coordinator.data): - if not camera.get("switches"): - continue - for switch in camera["switches"]: - if switch not in supported_switches: - continue - switch_entities.append(EzvizSwitch(coordinator, idx, switch)) - - async_add_entities(switch_entities) + async_add_entities( + [ + EzvizSwitch(coordinator, camera, switch) + for camera in coordinator.data + for switch in coordinator.data[camera].get("switches") + if switch in supported_switches + ] + ) -class EzvizSwitch(CoordinatorEntity, SwitchEntity): +class EzvizSwitch(EzvizEntity, SwitchEntity): """Representation of a Ezviz sensor.""" coordinator: EzvizDataUpdateCoordinator + ATTR_DEVICE_CLASS = DEVICE_CLASS_SWITCH def __init__( - self, coordinator: EzvizDataUpdateCoordinator, idx: int, switch: str + self, coordinator: EzvizDataUpdateCoordinator, serial: str, switch: str ) -> None: """Initialize the switch.""" - super().__init__(coordinator) - self._idx = idx - self._camera_name = self.coordinator.data[self._idx]["name"] + super().__init__(coordinator, serial) self._name = switch - self._sensor_name = f"{self._camera_name}.{DeviceSwitchType(self._name).name}" - self._serial = self.coordinator.data[self._idx]["serial"] - self._device_class = DEVICE_CLASS_SWITCH - - @property - def name(self) -> str: - """Return the name of the Ezviz switch.""" - return f"{DeviceSwitchType(self._name).name}" + self._attr_name = f"{self._camera_name} {DeviceSwitchType(switch).name.title()}" + self._attr_unique_id = ( + f"{serial}_{self._camera_name}.{DeviceSwitchType(switch).name}" + ) @property def is_on(self) -> bool: """Return the state of the switch.""" - return self.coordinator.data[self._idx]["switches"][self._name] - - @property - def unique_id(self) -> str: - """Return the unique ID of this switch.""" - return f"{self._serial}_{self._sensor_name}" + return self.data["switches"][self._name] async def async_turn_on(self, **kwargs: Any) -> None: """Change a device switch on the camera.""" @@ -98,19 +83,3 @@ class EzvizSwitch(CoordinatorEntity, SwitchEntity): if update_ok: await self.coordinator.async_request_refresh() - - @property - def device_info(self) -> DeviceInfo: - """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN, self._serial)}, - "name": self.coordinator.data[self._idx]["name"], - "model": self.coordinator.data[self._idx]["device_sub_category"], - "manufacturer": MANUFACTURER, - "sw_version": self.coordinator.data[self._idx]["version"], - } - - @property - def device_class(self) -> str: - """Device class for the sensor.""" - return self._device_class diff --git a/requirements_all.txt b/requirements_all.txt index 7674aff0895..46d058ed4f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1465,7 +1465,7 @@ pyephember==0.3.1 pyeverlights==0.1.0 # homeassistant.components.ezviz -pyezviz==0.1.8.9 +pyezviz==0.1.9.4 # homeassistant.components.fido pyfido==2.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7341dcd518f..b78e1483200 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -853,7 +853,7 @@ pyefergy==0.1.2 pyeverlights==0.1.0 # homeassistant.components.ezviz -pyezviz==0.1.8.9 +pyezviz==0.1.9.4 # homeassistant.components.fido pyfido==2.1.1 From 6a39119ccca329a536b867e62cccaea49eb503a1 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 11 Oct 2021 20:26:36 +0200 Subject: [PATCH 0252/1038] Streamline modbus before 100% coverage. (#57478) --- homeassistant/components/modbus/modbus.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index e81afc968ca..e5c08b2e1ed 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -293,22 +293,18 @@ class ModbusHub: func = getattr(self._client, entry.func_name) self._pb_call[entry.call_type] = RunEntry(entry.attr, func) - await self.async_connect_task() - return True - - async def async_connect_task(self) -> None: - """Try to connect, and retry if needed.""" async with self._lock: if not await self.hass.async_add_executor_job(self._pymodbus_connect): err = f"{self.name} connect failed, retry in pymodbus" self._log_error(err, error_state=False) - return + return False # Start counting down to allow modbus requests. if self._config_delay: self._async_cancel_listener = async_call_later( self.hass, self._config_delay, self.async_end_delay ) + return True @callback def async_end_delay(self, args: Any) -> None: @@ -341,10 +337,8 @@ class ModbusHub: def _pymodbus_connect(self) -> bool: """Connect client.""" - if not self._client: - return False try: - self._client.connect() + self._client.connect() # type: ignore[union-attr] except ModbusException as exception_error: self._log_error(str(exception_error), error_state=False) return False From 0c04ca20c6571635860a4d1fc1618bf58b92797f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 11 Oct 2021 12:41:39 -0600 Subject: [PATCH 0253/1038] Add ability to re-auth WattTime (#56582) * Tests cleanup * Still store the abbreviation * Code review * Remove unused attribute * Add ability to re-auth WattTime * Consolidate logic for entry unique ID * Fix tests * Fix docstring --- homeassistant/components/watttime/__init__.py | 7 +- .../components/watttime/config_flow.py | 121 +++++++++++++----- .../components/watttime/strings.json | 10 +- .../components/watttime/translations/en.json | 10 +- tests/components/watttime/test_config_flow.py | 97 +++++++++++++- 5 files changed, 207 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/watttime/__init__.py b/homeassistant/components/watttime/__init__.py index 8b3a83aa8d1..a4e1acb4b7e 100644 --- a/homeassistant/components/watttime/__init__.py +++ b/homeassistant/components/watttime/__init__.py @@ -5,7 +5,7 @@ from datetime import timedelta from aiowatttime import Client from aiowatttime.emissions import RealTimeEmissionsResponseType -from aiowatttime.errors import WattTimeError +from aiowatttime.errors import InvalidCredentialsError, WattTimeError from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -15,6 +15,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -36,6 +37,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: client = await Client.async_login( entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], session=session ) + except InvalidCredentialsError as err: + raise ConfigEntryAuthFailed("Invalid username/password") from err except WattTimeError as err: LOGGER.error("Error while authenticating with WattTime: %s", err) return False @@ -46,6 +49,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await client.emissions.async_get_realtime_emissions( entry.data[CONF_LATITUDE], entry.data[CONF_LONGITUDE] ) + except InvalidCredentialsError as err: + raise ConfigEntryAuthFailed("Invalid username/password") from err except WattTimeError as err: raise UpdateFailed( f"Error while requesting data from WattTime: {err}" diff --git a/homeassistant/components/watttime/config_flow.py b/homeassistant/components/watttime/config_flow.py index 6c523f64331..c2db7847b56 100644 --- a/homeassistant/components/watttime/config_flow.py +++ b/homeassistant/components/watttime/config_flow.py @@ -14,8 +14,10 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, ) +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers.typing import ConfigType from .const import ( CONF_BALANCING_AUTHORITY, @@ -44,6 +46,12 @@ STEP_LOCATION_DATA_SCHEMA = vol.Schema( } ) +STEP_REAUTH_CONFIRM_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_PASSWORD): str, + } +) + STEP_USER_DATA_SCHEMA = vol.Schema( { vol.Required(CONF_USERNAME): str, @@ -52,6 +60,12 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) +@callback +def get_unique_id(data: dict[str, Any]) -> str: + """Get a unique ID from a data payload.""" + return f"{data[CONF_LATITUDE]}, {data[CONF_LONGITUDE]}" + + class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for WattTime.""" @@ -60,8 +74,49 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize.""" self._client: Client | None = None - self._password: str | None = None - self._username: str | None = None + self._data: dict[str, Any] = {} + + async def _async_validate_credentials( + self, username: str, password: str, error_step_id: str, error_schema: vol.Schema + ): + """Validate input credentials and proceed accordingly.""" + session = aiohttp_client.async_get_clientsession(self.hass) + + try: + self._client = await Client.async_login(username, password, session=session) + except InvalidCredentialsError: + return self.async_show_form( + step_id=error_step_id, + data_schema=error_schema, + errors={"base": "invalid_auth"}, + description_placeholders={CONF_USERNAME: username}, + ) + except Exception as err: # pylint: disable=broad-except + LOGGER.exception("Unexpected exception while logging in: %s", err) + return self.async_show_form( + step_id=error_step_id, + data_schema=error_schema, + errors={"base": "unknown"}, + description_placeholders={CONF_USERNAME: username}, + ) + + if CONF_LATITUDE in self._data: + # If coordinates already exist at this stage, we're in an existing flow and + # should reauth: + entry_unique_id = get_unique_id(self._data) + if existing_entry := await self.async_set_unique_id(entry_unique_id): + self.hass.config_entries.async_update_entry( + existing_entry, data=self._data + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(existing_entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") + + # ...otherwise, we're in a new flow: + self._data[CONF_USERNAME] = username + self._data[CONF_PASSWORD] = password + return await self.async_step_location() async def async_step_coordinates( self, user_input: dict[str, Any] | None = None @@ -75,7 +130,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if TYPE_CHECKING: assert self._client - unique_id = f"{user_input[CONF_LATITUDE]}, {user_input[CONF_LONGITUDE]}" + unique_id = get_unique_id(user_input) await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured() @@ -100,8 +155,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry( title=unique_id, data={ - CONF_USERNAME: self._username, - CONF_PASSWORD: self._password, + CONF_USERNAME: self._data[CONF_USERNAME], + CONF_PASSWORD: self._data[CONF_PASSWORD], CONF_LATITUDE: user_input[CONF_LATITUDE], CONF_LONGITUDE: user_input[CONF_LONGITUDE], CONF_BALANCING_AUTHORITY: grid_region["name"], @@ -127,6 +182,31 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return await self.async_step_coordinates() + async def async_step_reauth(self, config: ConfigType) -> FlowResult: + """Handle configuration by re-auth.""" + self._data = {**config} + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle re-auth completion.""" + if not user_input: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=STEP_REAUTH_CONFIRM_DATA_SCHEMA, + description_placeholders={CONF_USERNAME: self._data[CONF_USERNAME]}, + ) + + self._data[CONF_PASSWORD] = user_input[CONF_PASSWORD] + + return await self._async_validate_credentials( + self._data[CONF_USERNAME], + self._data[CONF_PASSWORD], + "reauth_confirm", + STEP_REAUTH_CONFIRM_DATA_SCHEMA, + ) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -136,28 +216,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=STEP_USER_DATA_SCHEMA ) - session = aiohttp_client.async_get_clientsession(self.hass) - - try: - self._client = await Client.async_login( - user_input[CONF_USERNAME], - user_input[CONF_PASSWORD], - session=session, - ) - except InvalidCredentialsError: - return self.async_show_form( - step_id="user", - data_schema=STEP_USER_DATA_SCHEMA, - errors={CONF_USERNAME: "invalid_auth"}, - ) - except Exception as err: # pylint: disable=broad-except - LOGGER.exception("Unexpected exception while logging in: %s", err) - return self.async_show_form( - step_id="user", - data_schema=STEP_USER_DATA_SCHEMA, - errors={"base": "unknown"}, - ) - - self._username = user_input[CONF_USERNAME] - self._password = user_input[CONF_PASSWORD] - return await self.async_step_location() + return await self._async_validate_credentials( + user_input[CONF_USERNAME], + user_input[CONF_PASSWORD], + "user", + STEP_USER_DATA_SCHEMA, + ) diff --git a/homeassistant/components/watttime/strings.json b/homeassistant/components/watttime/strings.json index 34dc253dcde..594848afce1 100644 --- a/homeassistant/components/watttime/strings.json +++ b/homeassistant/components/watttime/strings.json @@ -14,6 +14,13 @@ "location_type": "[%key:common::config_flow::data::location%]" } }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "Please re-enter the password for {username}:", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } + }, "user": { "description": "Input your username and password:", "data": { @@ -28,7 +35,8 @@ "unknown_coordinates": "No data for latitude/longitude" }, "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/watttime/translations/en.json b/homeassistant/components/watttime/translations/en.json index 44ae51fae53..e99af749031 100644 --- a/homeassistant/components/watttime/translations/en.json +++ b/homeassistant/components/watttime/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", @@ -22,6 +23,13 @@ }, "description": "Pick a location to monitor:" }, + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "Please re-enter the password for {username}.", + "title": "Reauthenticate Integration" + }, "user": { "data": { "password": "Password", diff --git a/tests/components/watttime/test_config_flow.py b/tests/components/watttime/test_config_flow.py index e50e89dfb26..249bd51c4da 100644 --- a/tests/components/watttime/test_config_flow.py +++ b/tests/components/watttime/test_config_flow.py @@ -42,9 +42,11 @@ def client_fixture(get_grid_region): @pytest.fixture(name="client_login") def client_login_fixture(client): """Define a fixture for patching the aiowatttime coroutine to get a client.""" - with patch("homeassistant.components.watttime.config_flow.Client.async_login") as m: - m.return_value = client - yield m + with patch( + "homeassistant.components.watttime.config_flow.Client.async_login" + ) as mock_client: + mock_client.return_value = client + yield mock_client @pytest.fixture(name="get_grid_region") @@ -162,7 +164,92 @@ async def test_step_coordinates_unknown_error( assert result["errors"] == {"base": "unknown"} -async def test_step_login_coordinates(hass: HomeAssistant, client_login) -> None: +async def test_step_reauth(hass: HomeAssistant, client_login) -> None: + """Test a full reauth flow.""" + MockConfigEntry( + domain=DOMAIN, + unique_id="51.528308, -0.3817765", + data={ + CONF_USERNAME: "user", + CONF_PASSWORD: "password", + CONF_LATITUDE: 51.528308, + CONF_LONGITUDE: -0.3817765, + CONF_BALANCING_AUTHORITY: "Authority 1", + CONF_BALANCING_AUTHORITY_ABBREV: "AUTH_1", + }, + ).add_to_hass(hass) + + await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( + "homeassistant.components.watttime.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data={ + CONF_USERNAME: "user", + CONF_PASSWORD: "password", + CONF_LATITUDE: 51.528308, + CONF_LONGITUDE: -0.3817765, + CONF_BALANCING_AUTHORITY: "Authority 1", + CONF_BALANCING_AUTHORITY_ABBREV: "AUTH_1", + }, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: "password"}, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + assert len(hass.config_entries.async_entries()) == 1 + + +async def test_step_reauth_invalid_credentials(hass: HomeAssistant) -> None: + """Test that invalid credentials during reauth are handled.""" + MockConfigEntry( + domain=DOMAIN, + unique_id="51.528308, -0.3817765", + data={ + CONF_USERNAME: "user", + CONF_PASSWORD: "password", + CONF_LATITUDE: 51.528308, + CONF_LONGITUDE: -0.3817765, + CONF_BALANCING_AUTHORITY: "Authority 1", + CONF_BALANCING_AUTHORITY_ABBREV: "AUTH_1", + }, + ).add_to_hass(hass) + + await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( + "homeassistant.components.watttime.config_flow.Client.async_login", + AsyncMock(side_effect=InvalidCredentialsError), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data={ + CONF_USERNAME: "user", + CONF_PASSWORD: "password", + CONF_LATITUDE: 51.528308, + CONF_LONGITUDE: -0.3817765, + CONF_BALANCING_AUTHORITY: "Authority 1", + CONF_BALANCING_AUTHORITY_ABBREV: "AUTH_1", + }, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: "password"}, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_step_user_coordinates(hass: HomeAssistant, client_login) -> None: """Test a full login flow (inputting custom coordinates).""" with patch( @@ -241,7 +328,7 @@ async def test_step_user_invalid_credentials(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {"username": "invalid_auth"} + assert result["errors"] == {"base": "invalid_auth"} @pytest.mark.parametrize("get_grid_region", [AsyncMock(side_effect=Exception)]) From d10b1d9fe0679256461e65985e0d65190d577358 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 11 Oct 2021 22:08:57 +0200 Subject: [PATCH 0254/1038] Fix watttime config flow and tests (#57498) --- homeassistant/components/watttime/config_flow.py | 2 +- tests/components/watttime/test_config_flow.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/watttime/config_flow.py b/homeassistant/components/watttime/config_flow.py index c2db7847b56..a00ba4c8c86 100644 --- a/homeassistant/components/watttime/config_flow.py +++ b/homeassistant/components/watttime/config_flow.py @@ -78,7 +78,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_validate_credentials( self, username: str, password: str, error_step_id: str, error_schema: vol.Schema - ): + ) -> FlowResult: """Validate input credentials and proceed accordingly.""" session = aiohttp_client.async_get_clientsession(self.hass) diff --git a/tests/components/watttime/test_config_flow.py b/tests/components/watttime/test_config_flow.py index 249bd51c4da..672f294c099 100644 --- a/tests/components/watttime/test_config_flow.py +++ b/tests/components/watttime/test_config_flow.py @@ -179,7 +179,6 @@ async def test_step_reauth(hass: HomeAssistant, client_login) -> None: }, ).add_to_hass(hass) - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.watttime.async_setup_entry", return_value=True, @@ -222,7 +221,6 @@ async def test_step_reauth_invalid_credentials(hass: HomeAssistant) -> None: }, ).add_to_hass(hass) - await setup.async_setup_component(hass, "persistent_notification", {}) with patch( "homeassistant.components.watttime.config_flow.Client.async_login", AsyncMock(side_effect=InvalidCredentialsError), From 13db867c1d2f215244f16c773f0302c5bad6ced8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Oct 2021 23:15:32 +0200 Subject: [PATCH 0255/1038] Move attribution attribute to Entity base class (#57492) --- .../components/accuweather/sensor.py | 5 +++-- .../components/air_quality/__init__.py | 11 +--------- homeassistant/components/met/const.py | 2 -- homeassistant/components/weather/__init__.py | 11 ---------- homeassistant/helpers/entity.py | 10 +++++++++ .../air_quality/test_air_quality.py | 8 ++----- .../homematicip_cloud/test_weather.py | 8 +++---- tests/components/smhi/test_weather.py | 9 +++----- tests/components/template/test_weather.py | 4 ++-- tests/components/weather/test_weather.py | 4 ++-- tests/helpers/test_entity.py | 21 ++++++++++++++++++- 11 files changed, 47 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index b5f979b45cf..77d604a8f6c 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -5,7 +5,7 @@ from typing import Any, cast from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, DEVICE_CLASS_TEMPERATURE +from homeassistant.const import CONF_NAME, DEVICE_CLASS_TEMPERATURE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -59,6 +59,7 @@ async def async_setup_entry( class AccuWeatherSensor(CoordinatorEntity, SensorEntity): """Define an AccuWeather entity.""" + _attr_attribution = ATTRIBUTION coordinator: AccuWeatherDataUpdateCoordinator entity_description: AccuWeatherSensorDescription @@ -75,7 +76,7 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity): self._sensor_data = _get_sensor_data( coordinator.data, forecast_day, description.key ) - self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} + self._attrs: dict[str, Any] = {} if forecast_day is not None: self._attr_name = f"{name} {description.name} {forecast_day}d" self._attr_unique_id = ( diff --git a/homeassistant/components/air_quality/__init__.py b/homeassistant/components/air_quality/__init__.py index 1b8ab5f9c30..1e38bad55a8 100644 --- a/homeassistant/components/air_quality/__init__.py +++ b/homeassistant/components/air_quality/__init__.py @@ -6,10 +6,7 @@ import logging from typing import Final, final from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, -) +from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER from homeassistant.core import HomeAssistant from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -41,7 +38,6 @@ SCAN_INTERVAL: Final = timedelta(seconds=30) PROP_TO_ATTR: Final[dict[str, str]] = { "air_quality_index": ATTR_AQI, - "attribution": ATTR_ATTRIBUTION, "carbon_dioxide": ATTR_CO2, "carbon_monoxide": ATTR_CO, "nitrogen_oxide": ATTR_N2O, @@ -114,11 +110,6 @@ class AirQualityEntity(Entity): """Return the CO2 (carbon dioxide) level.""" return None - @property - def attribution(self) -> StateType: - """Return the attribution.""" - return None - @property def sulphur_dioxide(self) -> StateType: """Return the SO2 (sulphur dioxide) level.""" diff --git a/homeassistant/components/met/const.py b/homeassistant/components/met/const.py index 0f4c22dbba3..93f9e3414dd 100644 --- a/homeassistant/components/met/const.py +++ b/homeassistant/components/met/const.py @@ -18,7 +18,6 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_SPEED, - ATTR_WEATHER_ATTRIBUTION, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, @@ -184,7 +183,6 @@ FORECAST_MAP = { } ATTR_MAP = { - ATTR_WEATHER_ATTRIBUTION: "attribution", ATTR_WEATHER_HUMIDITY: "humidity", ATTR_WEATHER_PRESSURE: "pressure", ATTR_WEATHER_TEMPERATURE: "temperature", diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 7d5f7a99d40..d4965be841d 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -47,7 +47,6 @@ ATTR_FORECAST_TEMP_LOW: Final = "templow" ATTR_FORECAST_TIME: Final = "datetime" ATTR_FORECAST_WIND_BEARING: Final = "wind_bearing" ATTR_FORECAST_WIND_SPEED: Final = "wind_speed" -ATTR_WEATHER_ATTRIBUTION = "attribution" ATTR_WEATHER_HUMIDITY = "humidity" ATTR_WEATHER_OZONE = "ozone" ATTR_WEATHER_PRESSURE = "pressure" @@ -107,7 +106,6 @@ class WeatherEntity(Entity): """ABC for weather data.""" entity_description: WeatherEntityDescription - _attr_attribution: str | None = None _attr_condition: str | None _attr_forecast: list[Forecast] | None = None _attr_humidity: float | None = None @@ -156,11 +154,6 @@ class WeatherEntity(Entity): """Return the ozone level.""" return self._attr_ozone - @property - def attribution(self) -> str | None: - """Return the attribution.""" - return self._attr_attribution - @property def visibility(self) -> float | None: """Return the visibility.""" @@ -216,10 +209,6 @@ class WeatherEntity(Entity): if visibility is not None: data[ATTR_WEATHER_VISIBILITY] = visibility - attribution = self.attribution - if attribution is not None: - data[ATTR_WEATHER_ATTRIBUTION] = attribution - if self.forecast is not None: forecast = [] for forecast_entry in self.forecast: diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index f04949c0caa..122d04b0bf9 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -16,6 +16,7 @@ from typing import Any, TypedDict, final from homeassistant.config import DATA_CUSTOMIZE from homeassistant.const import ( ATTR_ASSUMED_STATE, + ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME, @@ -232,6 +233,7 @@ class Entity(ABC): # Entity Properties _attr_assumed_state: bool = False + _attr_attribution: str | None = None _attr_available: bool = True _attr_context_recent_time: timedelta = timedelta(seconds=5) _attr_device_class: str | None @@ -397,6 +399,11 @@ class Entity(ABC): return self.entity_description.entity_registry_enabled_default return True + @property + def attribution(self) -> str | None: + """Return the attribution.""" + return self._attr_attribution + # DO NOT OVERWRITE # These properties and methods are either managed by Home Assistant or they # are used to perform a very specific function. Overwriting these may @@ -520,6 +527,9 @@ class Entity(ABC): if (device_class := self.device_class) is not None: attr[ATTR_DEVICE_CLASS] = str(device_class) + if (attribution := self.attribution) is not None: + attr[ATTR_ATTRIBUTION] = attribution + end = timer() if end - start > 0.4 and not self._slow_reported: diff --git a/tests/components/air_quality/test_air_quality.py b/tests/components/air_quality/test_air_quality.py index e0692931e1c..e0133e3ecc3 100644 --- a/tests/components/air_quality/test_air_quality.py +++ b/tests/components/air_quality/test_air_quality.py @@ -1,11 +1,7 @@ """The tests for the Air Quality component.""" -from homeassistant.components.air_quality import ( - ATTR_ATTRIBUTION, - ATTR_N2O, - ATTR_OZONE, - ATTR_PM_10, -) +from homeassistant.components.air_quality import ATTR_N2O, ATTR_OZONE, ATTR_PM_10 from homeassistant.const import ( + ATTR_ATTRIBUTION, ATTR_UNIT_OF_MEASUREMENT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ) diff --git a/tests/components/homematicip_cloud/test_weather.py b/tests/components/homematicip_cloud/test_weather.py index e3370e77ffe..4861a4d2696 100644 --- a/tests/components/homematicip_cloud/test_weather.py +++ b/tests/components/homematicip_cloud/test_weather.py @@ -1,13 +1,13 @@ """Tests for HomematicIP Cloud weather.""" from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.components.weather import ( - ATTR_WEATHER_ATTRIBUTION, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, DOMAIN as WEATHER_DOMAIN, ) +from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.setup import async_setup_component from .helper import async_manipulate_test_data, get_and_check_entity_basics @@ -38,7 +38,7 @@ async def test_hmip_weather_sensor(hass, default_mock_hap_factory): assert ha_state.attributes[ATTR_WEATHER_TEMPERATURE] == 4.3 assert ha_state.attributes[ATTR_WEATHER_HUMIDITY] == 97 assert ha_state.attributes[ATTR_WEATHER_WIND_SPEED] == 15.0 - assert ha_state.attributes[ATTR_WEATHER_ATTRIBUTION] == "Powered by Homematic IP" + assert ha_state.attributes[ATTR_ATTRIBUTION] == "Powered by Homematic IP" await async_manipulate_test_data(hass, hmip_device, "actualTemperature", 12.1) ha_state = hass.states.get(entity_id) @@ -63,7 +63,7 @@ async def test_hmip_weather_sensor_pro(hass, default_mock_hap_factory): assert ha_state.attributes[ATTR_WEATHER_HUMIDITY] == 65 assert ha_state.attributes[ATTR_WEATHER_WIND_SPEED] == 2.6 assert ha_state.attributes[ATTR_WEATHER_WIND_BEARING] == 295.0 - assert ha_state.attributes[ATTR_WEATHER_ATTRIBUTION] == "Powered by Homematic IP" + assert ha_state.attributes[ATTR_ATTRIBUTION] == "Powered by Homematic IP" await async_manipulate_test_data(hass, hmip_device, "actualTemperature", 12.1) ha_state = hass.states.get(entity_id) @@ -86,7 +86,7 @@ async def test_hmip_home_weather(hass, default_mock_hap_factory): assert ha_state.attributes[ATTR_WEATHER_HUMIDITY] == 54 assert ha_state.attributes[ATTR_WEATHER_WIND_SPEED] == 8.6 assert ha_state.attributes[ATTR_WEATHER_WIND_BEARING] == 294 - assert ha_state.attributes[ATTR_WEATHER_ATTRIBUTION] == "Powered by Homematic IP" + assert ha_state.attributes[ATTR_ATTRIBUTION] == "Powered by Homematic IP" await async_manipulate_test_data( hass, mock_hap.home.weather, "temperature", 28.3, fire_device=mock_hap.home diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index 33214be0ae3..c890ad62216 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -19,7 +19,6 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, - ATTR_WEATHER_ATTRIBUTION, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, @@ -27,7 +26,7 @@ from homeassistant.components.weather import ( ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, ) -from homeassistant.const import STATE_UNKNOWN +from homeassistant.const import ATTR_ATTRIBUTION, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.util.dt import utcnow @@ -60,7 +59,7 @@ async def test_setup_hass( assert state.attributes[ATTR_SMHI_CLOUDINESS] == 50 assert state.attributes[ATTR_SMHI_THUNDER_PROBABILITY] == 33 assert state.attributes[ATTR_SMHI_WIND_GUST_SPEED] == 17 - assert state.attributes[ATTR_WEATHER_ATTRIBUTION].find("SMHI") >= 0 + assert state.attributes[ATTR_ATTRIBUTION].find("SMHI") >= 0 assert state.attributes[ATTR_WEATHER_HUMIDITY] == 55 assert state.attributes[ATTR_WEATHER_PRESSURE] == 1024 assert state.attributes[ATTR_WEATHER_TEMPERATURE] == 17 @@ -94,9 +93,7 @@ async def test_properties_no_data(hass: HomeAssistant) -> None: assert state assert state.name == "test" assert state.state == STATE_UNKNOWN - assert ( - state.attributes[ATTR_WEATHER_ATTRIBUTION] == "Swedish weather institute (SMHI)" - ) + assert state.attributes[ATTR_ATTRIBUTION] == "Swedish weather institute (SMHI)" assert ATTR_WEATHER_HUMIDITY not in state.attributes assert ATTR_WEATHER_PRESSURE not in state.attributes assert ATTR_WEATHER_TEMPERATURE not in state.attributes diff --git a/tests/components/template/test_weather.py b/tests/components/template/test_weather.py index c112473ecd6..dbd3e5706c3 100644 --- a/tests/components/template/test_weather.py +++ b/tests/components/template/test_weather.py @@ -2,7 +2,6 @@ import pytest from homeassistant.components.weather import ( - ATTR_WEATHER_ATTRIBUTION, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_OZONE, ATTR_WEATHER_PRESSURE, @@ -12,6 +11,7 @@ from homeassistant.components.weather import ( ATTR_WEATHER_WIND_SPEED, DOMAIN, ) +from homeassistant.const import ATTR_ATTRIBUTION @pytest.mark.parametrize("count,domain", [(1, DOMAIN)]) @@ -44,7 +44,7 @@ async def test_template_state_text(hass, start_ha): for attr, v_attr, value in [ ( "sensor.attribution", - ATTR_WEATHER_ATTRIBUTION, + ATTR_ATTRIBUTION, "The custom attribution", ), ("sensor.temperature", ATTR_WEATHER_TEMPERATURE, 22.3), diff --git a/tests/components/weather/test_weather.py b/tests/components/weather/test_weather.py index c32c4d09523..3057532668a 100644 --- a/tests/components/weather/test_weather.py +++ b/tests/components/weather/test_weather.py @@ -7,7 +7,6 @@ from homeassistant.components.weather import ( ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, - ATTR_WEATHER_ATTRIBUTION, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_OZONE, ATTR_WEATHER_PRESSURE, @@ -15,6 +14,7 @@ from homeassistant.components.weather import ( ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, ) +from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.setup import async_setup_component from homeassistant.util.unit_system import METRIC_SYSTEM @@ -39,7 +39,7 @@ async def test_attributes(hass): assert data.get(ATTR_WEATHER_WIND_SPEED) == 0.5 assert data.get(ATTR_WEATHER_WIND_BEARING) is None assert data.get(ATTR_WEATHER_OZONE) is None - assert data.get(ATTR_WEATHER_ATTRIBUTION) == "Powered by Home Assistant" + assert data.get(ATTR_ATTRIBUTION) == "Powered by Home Assistant" assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_CONDITION) == "rainy" assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_PRECIPITATION) == 1 assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == 60 diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 21811c3bfdc..bdb7a2782a3 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -7,7 +7,12 @@ from unittest.mock import MagicMock, PropertyMock, patch import pytest -from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_DEVICE_CLASS, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) from homeassistant.core import Context, HomeAssistantError from homeassistant.helpers import entity, entity_registry @@ -790,3 +795,17 @@ async def test_float_conversion(hass): state = hass.states.get("hello.world") assert state is not None assert state.state == "3.6" + + +async def test_attribution_attribute(hass): + """Test attribution attribute.""" + mock_entity = entity.Entity() + mock_entity.hass = hass + mock_entity.entity_id = "hello.world" + mock_entity._attr_attribution = "Home Assistant" + + mock_entity.async_schedule_update_ha_state(True) + await hass.async_block_till_done() + + state = hass.states.get(mock_entity.entity_id) + assert state.attributes.get(ATTR_ATTRIBUTION) == "Home Assistant" From a36a765352747f13142d026197d339e3511b43be Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 11 Oct 2021 23:37:31 +0200 Subject: [PATCH 0256/1038] Allow MQTT discovery availability shortcut (#57479) * replace base in availability topic * add tests for availability shortcuts - fix import * group constants * simplified loop * Moving constants to .const * rename value to topic * move CONF_TOPIC to .const * move CONF_AVAILABILITY to .const * remove check for string * Silently ignore if no config topic is found. * CONF_TOPIC should be required --- homeassistant/components/mqtt/const.py | 2 ++ homeassistant/components/mqtt/discovery.py | 10 +++++++ homeassistant/components/mqtt/mixins.py | 7 ++--- tests/components/mqtt/test_discovery.py | 31 +++++++++++++++++++++- 4 files changed, 46 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 6c334eca311..6435670e92c 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -9,11 +9,13 @@ ATTR_QOS = "qos" ATTR_RETAIN = "retain" ATTR_TOPIC = "topic" +CONF_AVAILABILITY = "availability" CONF_BROKER = "broker" CONF_BIRTH_MESSAGE = "birth_message" CONF_QOS = ATTR_QOS CONF_RETAIN = ATTR_RETAIN CONF_STATE_TOPIC = "state_topic" +CONF_TOPIC = "topic" CONF_WILL_MESSAGE = "will_message" DATA_MQTT_CONFIG = "mqtt_config" diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 6996072d226..5678f31f5b2 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -22,6 +22,8 @@ from .const import ( ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_PAYLOAD, ATTR_DISCOVERY_TOPIC, + CONF_AVAILABILITY, + CONF_TOPIC, DOMAIN, ) @@ -138,6 +140,14 @@ async def async_start( # noqa: C901 payload[key] = f"{base}{value[1:]}" if value[-1] == TOPIC_BASE and key.endswith("topic"): payload[key] = f"{value[:-1]}{base}" + if payload.get(CONF_AVAILABILITY): + for availability_conf in payload[CONF_AVAILABILITY]: + topic = availability_conf.get(CONF_TOPIC) + if topic: + if topic[0] == TOPIC_BASE: + availability_conf[CONF_TOPIC] = f"{base}{topic[1:]}" + if topic[-1] == TOPIC_BASE: + availability_conf[CONF_TOPIC] = f"{topic[:-1]}{base}" # If present, the node_id will be included in the discovered object id discovery_id = " ".join((node_id, object_id)) if node_id else object_id diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index e21f3f5c280..6c29938c75d 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -18,12 +18,14 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType -from . import CONF_TOPIC, DATA_MQTT, debug_info, publish, subscription +from . import DATA_MQTT, debug_info, publish, subscription from .const import ( ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_PAYLOAD, ATTR_DISCOVERY_TOPIC, + CONF_AVAILABILITY, CONF_QOS, + CONF_TOPIC, DEFAULT_PAYLOAD_AVAILABLE, DEFAULT_PAYLOAD_NOT_AVAILABLE, DOMAIN, @@ -50,7 +52,6 @@ AVAILABILITY_LATEST = "latest" AVAILABILITY_MODES = [AVAILABILITY_ALL, AVAILABILITY_ANY, AVAILABILITY_LATEST] -CONF_AVAILABILITY = "availability" CONF_AVAILABILITY_MODE = "availability_mode" CONF_AVAILABILITY_TOPIC = "availability_topic" CONF_ENABLED_BY_DEFAULT = "enabled_by_default" @@ -108,7 +109,7 @@ MQTT_AVAILABILITY_LIST_SCHEMA = vol.Schema( cv.ensure_list, [ { - vol.Optional(CONF_TOPIC): valid_subscribe_topic, + vol.Required(CONF_TOPIC): valid_subscribe_topic, vol.Optional( CONF_PAYLOAD_AVAILABLE, default=DEFAULT_PAYLOAD_AVAILABLE ): cv.string, diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 907c3d398b6..8d2106c90fa 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -12,7 +12,12 @@ from homeassistant.components.mqtt.abbreviations import ( DEVICE_ABBREVIATIONS, ) from homeassistant.components.mqtt.discovery import ALREADY_DISCOVERED, async_start -from homeassistant.const import EVENT_STATE_CHANGED, STATE_OFF, STATE_ON +from homeassistant.const import ( + EVENT_STATE_CHANGED, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) import homeassistant.core as ha from tests.common import ( @@ -449,6 +454,18 @@ async def test_discovery_expansion(hass, mqtt_mock, caplog): ' "name": "DiscoveryExpansionTest1",' ' "stat_t": "test_topic/~",' ' "cmd_t": "~/test_topic",' + ' "availability": [' + " {" + ' "topic":"~/avail_item1",' + ' "payload_available": "available",' + ' "payload_not_available": "not_available"' + " }," + " {" + ' "topic":"avail_item2/~",' + ' "payload_available": "available",' + ' "payload_not_available": "not_available"' + " }" + " ]," ' "dev":{' ' "ids":["5706DF"],' ' "name":"DiscoveryExpansionTest1 Device",' @@ -463,6 +480,12 @@ async def test_discovery_expansion(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, "homeassistant/switch/bla/config", data) await hass.async_block_till_done() + state = hass.states.get("switch.DiscoveryExpansionTest1") + assert state.state == STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, "avail_item2/some/base/topic", "available") + await hass.async_block_till_done() + state = hass.states.get("switch.DiscoveryExpansionTest1") assert state is not None assert state.name == "DiscoveryExpansionTest1" @@ -474,6 +497,12 @@ async def test_discovery_expansion(hass, mqtt_mock, caplog): state = hass.states.get("switch.DiscoveryExpansionTest1") assert state.state == STATE_ON + async_fire_mqtt_message(hass, "some/base/topic/avail_item1", "not_available") + await hass.async_block_till_done() + + state = hass.states.get("switch.DiscoveryExpansionTest1") + assert state.state == STATE_UNAVAILABLE + ABBREVIATIONS_WHITE_LIST = [ # MQTT client/server/trigger settings From 7acb1b6eb9d84e3e4fd502233df7b45bd3c8e35a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 12 Oct 2021 00:12:42 +0200 Subject: [PATCH 0257/1038] Override the jinja2 int filter (#57470) --- homeassistant/helpers/template.py | 22 +++++++++++++++++++++- tests/helpers/test_template.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 019b3aaf5fb..b12ebca53c9 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1211,7 +1211,7 @@ def warn_no_default(function, value, default): ( "Template warning: '%s' got invalid input '%s' when %s template '%s' " "but no default was specified. Currently '%s' will return '%s', however this template will fail " - "to render in Home Assistant core 2021.12" + "to render in Home Assistant core 2022.1" ), function, value, @@ -1463,6 +1463,24 @@ def forgiving_float_filter(value, default=_SENTINEL): return default +def forgiving_int(value, default=_SENTINEL, base=10): + """Try to convert value to an int, and warn if it fails.""" + result = jinja2.filters.do_int(value, default=default, base=base) + if result is _SENTINEL: + warn_no_default("int", value, value) + return value + return result + + +def forgiving_int_filter(value, default=_SENTINEL, base=10): + """Try to convert value to an int, and warn if it fails.""" + result = jinja2.filters.do_int(value, default=default, base=base) + if result is _SENTINEL: + warn_no_default("int", value, 0) + return 0 + return result + + def is_number(value): """Try to convert value to a float.""" try: @@ -1693,6 +1711,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.filters["ord"] = ord self.filters["is_number"] = is_number self.filters["float"] = forgiving_float_filter + self.filters["int"] = forgiving_int_filter self.globals["log"] = logarithm self.globals["sin"] = sine self.globals["cos"] = cosine @@ -1716,6 +1735,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.globals["max"] = max self.globals["min"] = min self.globals["is_number"] = is_number + self.globals["int"] = forgiving_int self.tests["match"] = regex_match self.tests["search"] = regex_search diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 5522956c81c..4be9d527d31 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -240,6 +240,34 @@ def test_float_filter(hass): assert render(hass, "{{ 'bad' | float(default=1) }}") == 1 +def test_int_filter(hass): + """Test int filter.""" + hass.states.async_set("sensor.temperature", "12.2") + assert render(hass, "{{ states.sensor.temperature.state | int }}") == 12 + assert render(hass, "{{ states.sensor.temperature.state | int > 11 }}") is True + + hass.states.async_set("sensor.temperature", "0x10") + assert render(hass, "{{ states.sensor.temperature.state | int(base=16) }}") == 16 + + assert render(hass, "{{ 'bad' | int }}") == 0 + assert render(hass, "{{ 'bad' | int(1) }}") == 1 + assert render(hass, "{{ 'bad' | int(default=1) }}") == 1 + + +def test_int_function(hass): + """Test int filter.""" + hass.states.async_set("sensor.temperature", "12.2") + assert render(hass, "{{ int(states.sensor.temperature.state) }}") == 12 + assert render(hass, "{{ int(states.sensor.temperature.state) > 11 }}") is True + + hass.states.async_set("sensor.temperature", "0x10") + assert render(hass, "{{ int(states.sensor.temperature.state, base=16) }}") == 16 + + assert render(hass, "{{ int('bad') }}") == "bad" + assert render(hass, "{{ int('bad', 1) }}") == 1 + assert render(hass, "{{ int('bad', default=1) }}") == 1 + + @pytest.mark.parametrize( "value, expected", [ From 10b62370ff24205f5fe68a3d8670493e4c9d8c70 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 12 Oct 2021 00:12:28 +0000 Subject: [PATCH 0258/1038] [ci skip] Translation update --- .../alarm_control_panel/translations/he.json | 26 ++++++ .../binary_sensor/translations/he.json | 87 ++++++++++++++++++- .../components/cover/translations/he.json | 22 +++++ .../components/deconz/translations/he.json | 15 +++- .../components/demo/translations/he.json | 18 ++++ .../demo/translations/select.he.json | 4 +- .../components/efergy/translations/ca.json | 21 +++++ .../components/efergy/translations/de.json | 21 +++++ .../components/efergy/translations/et.json | 21 +++++ .../components/efergy/translations/hu.json | 21 +++++ .../components/efergy/translations/ru.json | 21 +++++ .../efergy/translations/zh-Hant.json | 21 +++++ .../environment_canada/translations/de.json | 23 +++++ .../environment_canada/translations/et.json | 23 +++++ .../environment_canada/translations/hu.json | 23 +++++ .../environment_canada/translations/ru.json | 23 +++++ .../components/flux_led/translations/hu.json | 2 +- .../geonetnz_quakes/translations/he.json | 7 ++ .../homekit_controller/translations/he.json | 9 +- .../homematicip_cloud/translations/he.json | 2 +- .../components/lock/translations/he.json | 7 ++ .../logi_circle/translations/he.json | 7 ++ .../components/mqtt/translations/he.json | 9 +- .../components/mysensors/translations/he.json | 7 ++ .../components/netatmo/translations/hu.json | 8 +- .../components/onvif/translations/he.json | 10 ++- .../components/plex/translations/he.json | 5 ++ .../components/select/translations/he.json | 11 +++ .../stookalert/translations/he.json | 14 +++ .../components/tplink/translations/he.json | 1 + .../components/watttime/translations/de.json | 10 ++- .../components/watttime/translations/en.json | 2 +- .../components/watttime/translations/hu.json | 10 ++- 33 files changed, 493 insertions(+), 18 deletions(-) create mode 100644 homeassistant/components/efergy/translations/ca.json create mode 100644 homeassistant/components/efergy/translations/de.json create mode 100644 homeassistant/components/efergy/translations/et.json create mode 100644 homeassistant/components/efergy/translations/hu.json create mode 100644 homeassistant/components/efergy/translations/ru.json create mode 100644 homeassistant/components/efergy/translations/zh-Hant.json create mode 100644 homeassistant/components/environment_canada/translations/de.json create mode 100644 homeassistant/components/environment_canada/translations/et.json create mode 100644 homeassistant/components/environment_canada/translations/hu.json create mode 100644 homeassistant/components/environment_canada/translations/ru.json create mode 100644 homeassistant/components/stookalert/translations/he.json diff --git a/homeassistant/components/alarm_control_panel/translations/he.json b/homeassistant/components/alarm_control_panel/translations/he.json index 836194caa80..9710be7c3c2 100644 --- a/homeassistant/components/alarm_control_panel/translations/he.json +++ b/homeassistant/components/alarm_control_panel/translations/he.json @@ -1,4 +1,30 @@ { + "device_automation": { + "action_type": { + "arm_away": "\u05d3\u05e8\u05d9\u05db\u05ea {entity_name} \u05dc\u05d0 \u05d1\u05d1\u05d9\u05ea", + "arm_home": "\u05d3\u05e8\u05d9\u05db\u05ea {entity_name} \u05d4\u05d1\u05d9\u05ea\u05d4", + "arm_night": "\u05d3\u05e8\u05d9\u05db\u05ea {entity_name} \u05dc\u05d9\u05dc\u05d4", + "arm_vacation": "\u05d3\u05e8\u05d9\u05db\u05ea {entity_name} \u05d7\u05d5\u05e4\u05e9\u05d4", + "disarm": "\u05e0\u05d9\u05d8\u05e8\u05d5\u05dc {entity_name}", + "trigger": "\u05d4\u05e4\u05e2\u05dc\u05ea {entity_name}" + }, + "condition_type": { + "is_armed_away": "{entity_name} \u05d3\u05e8\u05d5\u05da \u05dc\u05d0 \u05d1\u05d1\u05d9\u05ea", + "is_armed_home": "{entity_name} \u05d3\u05e8\u05d5\u05da \u05d1\u05d9\u05ea", + "is_armed_night": "{entity_name} \u05d3\u05e8\u05d5\u05da \u05dc\u05d9\u05dc\u05d4", + "is_armed_vacation": "{entity_name} \u05d1\u05d7\u05d5\u05e4\u05e9\u05d4 \u05d3\u05e8\u05d5\u05db\u05d4", + "is_disarmed": "{entity_name} \u05de\u05e0\u05d5\u05d8\u05e8\u05dc", + "is_triggered": "{entity_name} \u05de\u05d5\u05e4\u05e2\u05dc" + }, + "trigger_type": { + "armed_away": "{entity_name} \u05d3\u05e8\u05d5\u05da \u05dc\u05d0 \u05d1\u05d1\u05d9\u05ea", + "armed_home": "{entity_name} \u05d3\u05e8\u05d5\u05da \u05d1\u05d1\u05d9\u05ea", + "armed_night": "{entity_name} \u05d3\u05e8\u05d5\u05da \u05dc\u05d9\u05dc\u05d4", + "armed_vacation": "{entity_name} \u05d7\u05d5\u05e4\u05e9\u05d4 \u05d3\u05e8\u05d5\u05db\u05d4", + "disarmed": "{entity_name} \u05de\u05e0\u05d5\u05d8\u05e8\u05dc", + "triggered": "{entity_name} \u05de\u05d5\u05e4\u05e2\u05dc" + } + }, "state": { "_": { "armed": "\u05d3\u05e8\u05d5\u05da", diff --git a/homeassistant/components/binary_sensor/translations/he.json b/homeassistant/components/binary_sensor/translations/he.json index 65501c6e698..f6018cfe08a 100644 --- a/homeassistant/components/binary_sensor/translations/he.json +++ b/homeassistant/components/binary_sensor/translations/he.json @@ -1,16 +1,96 @@ { "device_automation": { "condition_type": { + "is_bat_low": "\u05e1\u05d5\u05dc\u05dc\u05ea {entity_name} \u05d7\u05dc\u05e9\u05d4", "is_cold": "{entity_name} \u05e7\u05e8", + "is_connected": "{entity_name} \u05de\u05d7\u05d5\u05d1\u05e8", + "is_gas": "{entity_name} \u05de\u05d6\u05d4\u05d4 \u05d2\u05d6", + "is_hot": "{entity_name} \u05d7\u05dd", "is_light": "{entity_name} \u05de\u05d6\u05d4\u05d4 \u05d0\u05d5\u05e8", + "is_locked": "{entity_name} \u05e0\u05e2\u05d5\u05dc", + "is_moist": "{entity_name} \u05dc\u05d7", + "is_motion": "{entity_name} \u05de\u05d6\u05d4\u05d4 \u05ea\u05e0\u05d5\u05e2\u05d4", + "is_moving": "{entity_name} \u05d6\u05d6", + "is_no_gas": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05de\u05d6\u05d4\u05d4 \u05d2\u05d6", "is_no_light": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05de\u05d6\u05d4\u05d4 \u05d0\u05d5\u05e8", - "is_not_cold": "{entity_name} \u05dc\u05d0 \u05e7\u05e8" + "is_no_motion": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05de\u05d6\u05d4\u05d4 \u05ea\u05e0\u05d5\u05e2\u05d4", + "is_no_problem": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05de\u05d6\u05d4\u05d4 \u05d1\u05e2\u05d9\u05d4", + "is_no_smoke": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05de\u05d6\u05d4\u05d4 \u05e2\u05e9\u05df", + "is_no_sound": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05de\u05d6\u05d4\u05d4 \u05e6\u05dc\u05d9\u05dc", + "is_no_update": "{entity_name} \u05de\u05e2\u05d5\u05d3\u05db\u05df", + "is_no_vibration": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05de\u05d6\u05d4\u05d4 \u05e8\u05d8\u05d8", + "is_not_bat_low": "\u05e1\u05d5\u05dc\u05dc\u05ea {entity_name} \u05ea\u05e7\u05d9\u05e0\u05d4", + "is_not_cold": "{entity_name} \u05dc\u05d0 \u05e7\u05e8", + "is_not_connected": "{entity_name} \u05de\u05e0\u05d5\u05ea\u05e7", + "is_not_hot": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05d7\u05dd", + "is_not_locked": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05e0\u05e2\u05d5\u05dc", + "is_not_moist": "{entity_name} \u05d9\u05d1\u05e9", + "is_not_moving": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05d6\u05d6", + "is_not_occupied": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05ea\u05e4\u05d5\u05e1", + "is_not_open": "{entity_name} \u05e1\u05d2\u05d5\u05e8", + "is_not_plugged_in": "{entity_name} \u05de\u05e0\u05d5\u05ea\u05e7", + "is_not_powered": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05de\u05d5\u05e4\u05e2\u05dc", + "is_not_present": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05e7\u05d9\u05d9\u05dd", + "is_not_unsafe": "{entity_name} \u05d1\u05d8\u05d5\u05d7", + "is_occupied": "{entity_name} \u05ea\u05e4\u05d5\u05e1", + "is_off": "{entity_name} \u05db\u05d1\u05d5\u05d9", + "is_on": "{entity_name} \u05de\u05d5\u05e4\u05e2\u05dc", + "is_open": "{entity_name} \u05e4\u05ea\u05d5\u05d7", + "is_plugged_in": "{entity_name} \u05de\u05d7\u05d5\u05d1\u05e8", + "is_powered": "{entity_name} \u05de\u05d5\u05e4\u05e2\u05dc", + "is_present": "{entity_name} \u05e0\u05d5\u05db\u05d7", + "is_problem": "{entity_name} \u05de\u05d6\u05d4\u05d4 \u05d1\u05e2\u05d9\u05d4", + "is_smoke": "{entity_name} \u05de\u05d6\u05d4\u05d4 \u05e2\u05e9\u05df", + "is_sound": "{entity_name} \u05de\u05d6\u05d4\u05d4 \u05e6\u05dc\u05d9\u05dc", + "is_unsafe": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05d1\u05d8\u05d5\u05d7", + "is_update": "\u05e2\u05d3\u05db\u05d5\u05df \u05d6\u05de\u05d9\u05df \u05e2\u05d1\u05d5\u05e8 {entity_name}", + "is_vibration": "{entity_name} \u05de\u05d6\u05d4\u05d4 \u05e8\u05d8\u05d8" }, "trigger_type": { + "bat_low": "\u05e1\u05d5\u05dc\u05dc\u05ea {entity_name} \u05d7\u05dc\u05e9\u05d4", "cold": "{entity_name} \u05e0\u05d4\u05d9\u05d4 \u05e7\u05e8", + "connected": "{entity_name} \u05de\u05d7\u05d5\u05d1\u05e8", + "gas": "{entity_name} \u05d4\u05d7\u05dc \u05dc\u05d6\u05d4\u05d5\u05ea \u05d2\u05d6", + "hot": "{entity_name} \u05e0\u05e2\u05e9\u05d4 \u05d7\u05dd", "light": "{entity_name} \u05d4\u05ea\u05d7\u05d9\u05dc \u05dc\u05d6\u05d4\u05d5\u05ea \u05d0\u05d5\u05e8", + "locked": "{entity_name} \u05e0\u05e2\u05d5\u05dc", + "moist": "{entity_name} \u05d4\u05e4\u05da \u05dc\u05d7", + "motion": "{entity_name} \u05d4\u05d7\u05dc \u05dc\u05d6\u05d4\u05d5\u05ea \u05ea\u05e0\u05d5\u05e2\u05d4", + "moving": "{entity_name} \u05d4\u05d7\u05dc \u05dc\u05e0\u05d5\u05e2", + "no_gas": "{entity_name} \u05d4\u05e4\u05e1\u05d9\u05e7 \u05dc\u05d6\u05d4\u05d5\u05ea \u05d2\u05d6", "no_light": "{entity_name} \u05d4\u05e4\u05e1\u05d9\u05e7 \u05dc\u05d6\u05d4\u05d5\u05ea \u05d0\u05d5\u05e8", - "not_cold": "{entity_name} \u05e0\u05d4\u05d9\u05d4 \u05dc\u05d0 \u05e7\u05e8" + "no_motion": "{entity_name} \u05d4\u05e4\u05e1\u05d9\u05e7 \u05dc\u05d6\u05d4\u05d5\u05ea \u05ea\u05e0\u05d5\u05e2\u05d4", + "no_problem": "{entity_name} \u05d4\u05e4\u05e1\u05d9\u05e7 \u05dc\u05d6\u05d4\u05d5\u05ea \u05d1\u05e2\u05d9\u05d4", + "no_smoke": "{entity_name} \u05d4\u05e4\u05e1\u05d9\u05e7 \u05dc\u05d6\u05d4\u05d5\u05ea \u05e2\u05e9\u05df", + "no_sound": "{entity_name} \u05d4\u05e4\u05e1\u05d9\u05e7 \u05dc\u05d6\u05d4\u05d5\u05ea \u05e6\u05dc\u05d9\u05dc", + "no_update": "{entity_name} \u05d4\u05e4\u05da \u05dc\u05de\u05e2\u05d5\u05d3\u05db\u05df", + "no_vibration": "{entity_name} \u05d4\u05e4\u05e1\u05d9\u05e7 \u05dc\u05d6\u05d4\u05d5\u05ea \u05e8\u05d8\u05d8", + "not_bat_low": "{entity_name} \u05e1\u05d5\u05dc\u05dc\u05d4 \u05e8\u05d2\u05d9\u05dc\u05d4", + "not_cold": "{entity_name} \u05e0\u05d4\u05d9\u05d4 \u05dc\u05d0 \u05e7\u05e8", + "not_connected": "{entity_name} \u05de\u05e0\u05d5\u05ea\u05e7", + "not_hot": "{entity_name} \u05d4\u05e4\u05da \u05dc\u05d0 \u05d7\u05dd", + "not_locked": "{entity_name} \u05dc\u05d0 \u05e0\u05e2\u05d5\u05dc", + "not_moist": "{entity_name} \u05d4\u05ea\u05d9\u05d9\u05d1\u05e9", + "not_moving": "{entity_name} \u05d4\u05e4\u05e1\u05d9\u05e7 \u05dc\u05d6\u05d5\u05d6", + "not_occupied": "{entity_name} \u05dc\u05d0 \u05e0\u05ea\u05e4\u05e1", + "not_opened": "{entity_name} \u05e1\u05d2\u05d5\u05e8", + "not_plugged_in": "{entity_name} \u05de\u05e0\u05d5\u05ea\u05e7", + "not_powered": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05de\u05d5\u05e4\u05e2\u05dc", + "not_present": "{entity_name} \u05d0\u05d9\u05e0\u05d5 \u05e7\u05d9\u05d9\u05dd", + "not_unsafe": "{entity_name} \u05d4\u05e4\u05da \u05dc\u05d1\u05d8\u05d5\u05d7", + "occupied": "{entity_name} \u05e0\u05ea\u05e4\u05e1", + "opened": "{entity_name} \u05e0\u05e4\u05ea\u05d7", + "plugged_in": "{entity_name} \u05de\u05d7\u05d5\u05d1\u05e8", + "powered": "{entity_name} \u05de\u05d5\u05e4\u05e2\u05dc", + "present": "{entity_name} \u05e0\u05d5\u05db\u05d7", + "problem": "{entity_name} \u05d4\u05d7\u05dc\u05d4 \u05dc\u05d6\u05d4\u05d5\u05ea \u05d1\u05e2\u05d9\u05d4", + "smoke": "{entity_name} \u05d4\u05d7\u05dc \u05dc\u05d6\u05d4\u05d5\u05ea \u05e2\u05e9\u05df", + "sound": "{entity_name} \u05d4\u05d7\u05dc \u05dc\u05d6\u05d4\u05d5\u05ea \u05e6\u05dc\u05d9\u05dc", + "turned_off": "{entity_name} \u05db\u05d5\u05d1\u05d4", + "turned_on": "{entity_name} \u05d4\u05d5\u05e4\u05e2\u05dc", + "unsafe": "{entity_name} \u05d4\u05e4\u05da \u05dc\u05dc\u05d0 \u05d1\u05d8\u05d5\u05d7", + "update": "{entity_name} \u05e7\u05d9\u05d1\u05dc \u05e2\u05d3\u05db\u05d5\u05df \u05d6\u05de\u05d9\u05df", + "vibration": "{entity_name} \u05d4\u05d7\u05dc \u05dc\u05d6\u05d4\u05d5\u05ea \u05e8\u05d8\u05d8" } }, "state": { @@ -79,7 +159,8 @@ "on": "\u05e4\u05ea\u05d5\u05d7" }, "plug": { - "off": "\u05de\u05e0\u05d5\u05ea\u05e7" + "off": "\u05de\u05e0\u05d5\u05ea\u05e7", + "on": "\u05de\u05d7\u05d5\u05d1\u05e8" }, "presence": { "off": "\u05d1\u05d7\u05d5\u05e5", diff --git a/homeassistant/components/cover/translations/he.json b/homeassistant/components/cover/translations/he.json index 5dad593467c..66f6b9f3bbe 100644 --- a/homeassistant/components/cover/translations/he.json +++ b/homeassistant/components/cover/translations/he.json @@ -1,7 +1,29 @@ { "device_automation": { "action_type": { + "close": "\u05e1\u05d2\u05d9\u05e8\u05ea {entity_name}", + "close_tilt": "\u05e1\u05d2\u05d9\u05e8\u05ea \u05d4\u05d8\u05d9\u05d4 \u05e9\u05dc {entity_name}", + "open": "\u05e4\u05ea\u05d9\u05d7\u05ea {entity_name}", + "open_tilt": "\u05e4\u05ea\u05d9\u05d7\u05ea \u05d4\u05d8\u05d9\u05d9\u05ea {entity_name}", + "set_position": "\u05d4\u05d2\u05d3\u05e8\u05ea \u05de\u05d9\u05e7\u05d5\u05dd {entity_name}", + "set_tilt_position": "\u05d4\u05d2\u05d3\u05e8\u05ea \u05de\u05d9\u05e7\u05d5\u05dd \u05d4\u05d8\u05d9\u05d9\u05ea {entity_name}", "stop": "\u05e2\u05e6\u05d5\u05e8 {entity_name}" + }, + "condition_type": { + "is_closed": "{entity_name} \u05e1\u05d2\u05d5\u05e8", + "is_closing": "{entity_name} \u05e0\u05e1\u05d2\u05e8", + "is_open": "{entity_name} \u05e4\u05ea\u05d5\u05d7", + "is_opening": "{entity_name} \u05e0\u05e4\u05ea\u05d7", + "is_position": "\u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05d4\u05e0\u05d5\u05db\u05d7\u05d9 {entity_name} \u05d4\u05d5\u05d0", + "is_tilt_position": "\u05de\u05d9\u05e7\u05d5\u05dd \u05d4\u05d4\u05d8\u05d9\u05d4 \u05d4\u05e0\u05d5\u05db\u05d7\u05d9 {entity_name} \u05d4\u05d5\u05d0" + }, + "trigger_type": { + "closed": "{entity_name} \u05e1\u05d2\u05d5\u05e8", + "closing": "{entity_name} \u05e0\u05e1\u05d2\u05e8", + "opened": "{entity_name} \u05e0\u05e4\u05ea\u05d7", + "opening": "\u05e4\u05ea\u05d9\u05d7\u05ea {entity_name}", + "position": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05de\u05d9\u05e7\u05d5\u05dd", + "tilt_position": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05de\u05d9\u05e7\u05d5\u05dd \u05d4\u05d8\u05d9\u05d4" } }, "state": { diff --git a/homeassistant/components/deconz/translations/he.json b/homeassistant/components/deconz/translations/he.json index 74a8e1ba54b..3e2b350a0d9 100644 --- a/homeassistant/components/deconz/translations/he.json +++ b/homeassistant/components/deconz/translations/he.json @@ -30,10 +30,23 @@ }, "device_automation": { "trigger_subtype": { + "both_buttons": "\u05e9\u05e0\u05d9 \u05d4\u05dc\u05d7\u05e6\u05e0\u05d9\u05dd", + "button_1": "\u05dc\u05d7\u05e6\u05df \u05e8\u05d0\u05e9\u05d5\u05df", + "button_2": "\u05dc\u05d7\u05e6\u05df \u05e9\u05e0\u05d9", + "button_3": "\u05dc\u05d7\u05e6\u05df \u05e9\u05dc\u05d9\u05e9\u05d9", + "button_4": "\u05dc\u05d7\u05e6\u05df \u05e8\u05d1\u05d9\u05e2\u05d9", "button_5": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05d7\u05de\u05d9\u05e9\u05d9", "button_6": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05e9\u05d9\u05e9\u05d9", "button_7": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05e9\u05d1\u05d9\u05e2\u05d9", - "button_8": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05e9\u05de\u05d9\u05e0\u05d9" + "button_8": "\u05db\u05e4\u05ea\u05d5\u05e8 \u05e9\u05de\u05d9\u05e0\u05d9", + "close": "\u05e1\u05d2\u05d5\u05e8", + "dim_down": "\u05e2\u05de\u05e2\u05d5\u05dd \u05dc\u05de\u05d8\u05d4", + "dim_up": "\u05e2\u05de\u05e2\u05d5\u05dd \u05dc\u05de\u05e2\u05dc\u05d4", + "left": "\u05e9\u05de\u05d0\u05dc", + "open": "\u05e4\u05ea\u05d5\u05d7", + "right": "\u05d9\u05de\u05d9\u05df", + "turn_off": "\u05db\u05d9\u05d1\u05d5\u05d9", + "turn_on": "\u05d4\u05e4\u05e2\u05dc\u05d4" } } } \ No newline at end of file diff --git a/homeassistant/components/demo/translations/he.json b/homeassistant/components/demo/translations/he.json index c3162b87a5e..7e3349d3abc 100644 --- a/homeassistant/components/demo/translations/he.json +++ b/homeassistant/components/demo/translations/he.json @@ -1,3 +1,21 @@ { + "options": { + "step": { + "options_1": { + "data": { + "bool": "\u05d1\u05d5\u05dc\u05d9\u05d0\u05e0\u05d9 \u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9", + "constant": "\u05e7\u05d1\u05d5\u05e2", + "int": "\u05e7\u05dc\u05d8 \u05de\u05e1\u05e4\u05e8\u05d9" + } + }, + "options_2": { + "data": { + "multi": "\u05d1\u05d7\u05d9\u05e8\u05d4 \u05de\u05e8\u05d5\u05d1\u05d4", + "select": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea", + "string": "\u05e2\u05e8\u05da \u05de\u05d7\u05e8\u05d5\u05d6\u05ea" + } + } + } + }, "title": "\u05d4\u05d3\u05d2\u05de\u05d4" } \ No newline at end of file diff --git a/homeassistant/components/demo/translations/select.he.json b/homeassistant/components/demo/translations/select.he.json index 0264a0021e2..bb4bb95d3d5 100644 --- a/homeassistant/components/demo/translations/select.he.json +++ b/homeassistant/components/demo/translations/select.he.json @@ -1,7 +1,9 @@ { "state": { "demo__speed": { - "light_speed": "\u05de\u05d4\u05d9\u05e8\u05d5\u05ea \u05d4\u05d0\u05d5\u05e8" + "light_speed": "\u05de\u05d4\u05d9\u05e8\u05d5\u05ea \u05d4\u05d0\u05d5\u05e8", + "ludicrous_speed": "\u05de\u05d4\u05d9\u05e8\u05d5\u05ea \u05de\u05d2\u05d5\u05d7\u05db\u05ea", + "ridiculous_speed": "\u05de\u05d4\u05d9\u05e8\u05d5\u05ea \u05de\u05d2\u05d5\u05d7\u05db\u05ea" } } } \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/ca.json b/homeassistant/components/efergy/translations/ca.json new file mode 100644 index 00000000000..298826e75e5 --- /dev/null +++ b/homeassistant/components/efergy/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API" + }, + "title": "Efergy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/de.json b/homeassistant/components/efergy/translations/de.json new file mode 100644 index 00000000000..3945d3da7d4 --- /dev/null +++ b/homeassistant/components/efergy/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "api_key": "API-Schl\u00fcssel" + }, + "title": "Efergy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/et.json b/homeassistant/components/efergy/translations/et.json new file mode 100644 index 00000000000..73c2f1a3547 --- /dev/null +++ b/homeassistant/components/efergy/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti" + }, + "title": "Efergy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/hu.json b/homeassistant/components/efergy/translations/hu.json new file mode 100644 index 00000000000..ad41d4025bb --- /dev/null +++ b/homeassistant/components/efergy/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "api_key": "API kulcs" + }, + "title": "Efergy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/ru.json b/homeassistant/components/efergy/translations/ru.json new file mode 100644 index 00000000000..6a659c9b7c6 --- /dev/null +++ b/homeassistant/components/efergy/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.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\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": { + "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "title": "Efergy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/zh-Hant.json b/homeassistant/components/efergy/translations/zh-Hant.json new file mode 100644 index 00000000000..7b51c998fb2 --- /dev/null +++ b/homeassistant/components/efergy/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "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", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "api_key": "API \u5bc6\u9470" + }, + "title": "Efergy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/de.json b/homeassistant/components/environment_canada/translations/de.json new file mode 100644 index 00000000000..573e006aa3d --- /dev/null +++ b/homeassistant/components/environment_canada/translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "bad_station_id": "Die Stations-ID ist ung\u00fcltig, fehlt oder wurde in der Stations-ID-Datenbank nicht gefunden", + "cannot_connect": "Verbindung fehlgeschlagen", + "error_response": "Fehlerhafte Antwort von Environment Canada", + "too_many_attempts": "Verbindungen zu Environment Canada sind in ihrer Geschwindigkeit begrenzt; versuches in 60 Sekunden erneut.", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "language": "Sprache der Wetterinformationen", + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad", + "station": "ID der Wetterstation" + }, + "description": "Es muss entweder eine Stations-ID oder der Breitengrad/L\u00e4ngengrad angegeben werden. Als Standardwerte f\u00fcr Breitengrad/L\u00e4ngengrad werden die in Ihrer Home Assistant-Installation konfigurierten Werte verwendet. Bei Angabe von Koordinaten wird die den Koordinaten am n\u00e4chsten gelegene Wetterstation verwendet. Wenn ein Stationscode verwendet wird, muss er dem Format entsprechen: PP/Code, wobei PP f\u00fcr die zweistellige Provinz und Code f\u00fcr die Stationskennung steht. Die Liste der Stations-IDs findest du hier: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. Die Wetterinformationen k\u00f6nnen entweder in Englisch oder Franz\u00f6sisch abgerufen werden.", + "title": "Environment Canada: Wetterstandort und Sprache" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/et.json b/homeassistant/components/environment_canada/translations/et.json new file mode 100644 index 00000000000..af93b060144 --- /dev/null +++ b/homeassistant/components/environment_canada/translations/et.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "bad_station_id": "Jaama ID ei sobi, puudub v\u00f5i seda ei leitud jaamade ID andmebaasist", + "cannot_connect": "\u00dchendamine nurjus", + "error_response": "Kanada keskkonnaameti ekslik vastus", + "too_many_attempts": "\u00dchendus Kanada keskkonnaametiga on piiratud; proovi uuesti 60 sekundi p\u00e4rast", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "language": "Ilmateabe keel", + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad", + "station": "Ilmajaama ID" + }, + "description": "Tuleb m\u00e4\u00e4rata kas jaama ID v\u00f5i laiuskraad/pikkuskraad. Vaikimisi kasutatakse laiuskraadi/pikkuskraadi v\u00e4\u00e4rtusi, mis on konfigureeritud teie Home Assistant'i paigalduses. Koordinaatidele l\u00e4himat ilmajaama kasutatakse koordinaatide m\u00e4\u00e4ramisel. Kui kasutatakse jaama koodi, peab see j\u00e4rgima formaati: PP/kood, kus PP on kahet\u00e4heline provints ja kood on jaama ID. Jaama ID-de nimekiri on leitav siit: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. Ilmateavet saab otsida kas inglise v\u00f5i prantsuse keeles.", + "title": "Kanada keskonnaamet: ilmateabe asukoht ja keel" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/hu.json b/homeassistant/components/environment_canada/translations/hu.json new file mode 100644 index 00000000000..8a920274c1a --- /dev/null +++ b/homeassistant/components/environment_canada/translations/hu.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "bad_station_id": "Az \u00e1llom\u00e1s azonos\u00edt\u00f3ja \u00e9rv\u00e9nytelen, hi\u00e1nyzik, vagy nem tal\u00e1lhat\u00f3 az \u00e1llom\u00e1s azonos\u00edt\u00f3 adatb\u00e1zisban.", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "error_response": "Az Environment Canada hib\u00e1val v\u00e1laszolt", + "too_many_attempts": "Az Environment Canadahoz a kapcsol\u00f3d\u00e1sok sz\u00e1ma korl\u00e1tozva van; Pr\u00f3b\u00e1lja \u00fajra 60 m\u00e1sodperc m\u00falva", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "language": "Id\u0151j\u00e1r\u00e1si inform\u00e1ci\u00f3k nyelve", + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g", + "station": "Id\u0151j\u00e1r\u00e1s \u00e1llom\u00e1s ID-ja" + }, + "description": "Adja meg \u00e1llom\u00e1s ID-t vagy a sz\u00e9less\u00e9gi/hossz\u00fas\u00e1gi fokot. Az alap\u00e9rtelmezett f\u00f6ldrajzi sz\u00e9less\u00e9g/hossz\u00fas\u00e1g a Home Assistant telep\u00edt\u00e9s\u00e9n\u00e9l be\u00e1ll\u00edtott \u00e9rt\u00e9kek. Koordin\u00e1t\u00e1k megad\u00e1sa eset\u00e9n a koordin\u00e1t\u00e1khoz legk\u00f6zelebbi id\u0151j\u00e1r\u00e1si \u00e1llom\u00e1s ker\u00fcl felhaszn\u00e1l\u00e1sra. Ha \u00e1llom\u00e1sk\u00f3dot haszn\u00e1l, annak a k\u00f6vetkez\u0151 form\u00e1tumot kell k\u00f6vetnie: PP/k\u00f3d, ahol PP a k\u00e9tbet\u0171s tartom\u00e1ny, a k\u00f3d pedig az \u00e1llom\u00e1s azonos\u00edt\u00f3ja. Az \u00e1llom\u00e1sazonos\u00edt\u00f3k list\u00e1ja itt tal\u00e1lhat\u00f3: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. Az id\u0151j\u00e1r\u00e1si inform\u00e1ci\u00f3k angol vagy francia nyelven k\u00e9rdezhet\u0151k le.", + "title": "Environment Canada: id\u0151j\u00e1r\u00e1s helysz\u00edne \u00e9s nyelv" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/ru.json b/homeassistant/components/environment_canada/translations/ru.json new file mode 100644 index 00000000000..26c0108ed3a --- /dev/null +++ b/homeassistant/components/environment_canada/translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "bad_station_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043c\u0435\u0442\u0435\u043e\u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d, \u043b\u0438\u0431\u043e \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "error_response": "\u041e\u0442\u0432\u0435\u0442 \u043e\u0442 Environment Canada \u043f\u043e \u043e\u0448\u0438\u0431\u043a\u0435.", + "too_many_attempts": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u043f\u044b\u0442\u043e\u043a \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a Environment Canada \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u043e. \u041f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u0447\u0435\u0440\u0435\u0437 60 \u0441\u0435\u043a\u0443\u043d\u0434.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "language": "\u042f\u0437\u044b\u043a, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0433\u043e\u0434\u0435", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "station": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043c\u0435\u0442\u0435\u043e\u0441\u0442\u0430\u043d\u0446\u0438\u0438" + }, + "description": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u043a\u0430\u043a \u043c\u0438\u043d\u0438\u043c\u0443\u043c \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043c\u0435\u0442\u0435\u043e\u0441\u0442\u0430\u043d\u0446\u0438\u0438, \u043b\u0438\u0431\u043e \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f. \u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442\u0441\u044f \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0448\u0438\u0440\u043e\u0442\u044b \u0438 \u0434\u043e\u043b\u0433\u043e\u0442\u044b, \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0412\u0430\u0448\u0435\u0433\u043e Home Assistant. \u041f\u0440\u0438 \u0443\u043a\u0430\u0437\u0430\u043d\u0438\u0438 \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0431\u043b\u0438\u0436\u0430\u0439\u0448\u0430\u044f \u043a \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u0430\u043c \u043c\u0435\u0442\u0435\u043e\u0441\u0442\u0430\u043d\u0446\u0438\u044f. \u0415\u0441\u043b\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043c\u0435\u0442\u0435\u043e\u0441\u0442\u0430\u043d\u0446\u0438\u0438, \u043e\u043d \u0434\u043e\u043b\u0436\u0435\u043d \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u043e\u0432\u0430\u0442\u044c \u0444\u043e\u0440\u043c\u0430\u0442\u0443: PP/\u043a\u043e\u0434, \u0433\u0434\u0435 PP \u2014 \u044d\u0442\u043e \u0438\u043d\u0434\u0435\u043a\u0441 \u043f\u0440\u043e\u0432\u0438\u043d\u0446\u0438\u0438, \u0430 \u043a\u043e\u0434 \u2014 \u044d\u0442\u043e \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043c\u0435\u0442\u0435\u043e\u0441\u0442\u0430\u043d\u0446\u0438\u0438. \u0421\u043f\u0438\u0441\u043e\u043a \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u043e\u0432 \u0441\u0442\u0430\u043d\u0446\u0438\u0439 \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0439\u0442\u0438 \u0437\u0434\u0435\u0441\u044c: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv.\n\u0418\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e \u043f\u043e\u0433\u043e\u0434\u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043d\u0430 \u0430\u043d\u0433\u043b\u0438\u0439\u0441\u043a\u043e\u043c \u0438\u043b\u0438 \u0444\u0440\u0430\u043d\u0446\u0443\u0437\u0441\u043a\u043e\u043c \u044f\u0437\u044b\u043a\u0430\u0445.", + "title": "Environment Canada: \u043f\u043e\u0433\u043e\u0434\u0430, \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0438 \u044f\u0437\u044b\u043a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/hu.json b/homeassistant/components/flux_led/translations/hu.json index 3cfef2c9eb6..4afa3d99f74 100644 --- a/homeassistant/components/flux_led/translations/hu.json +++ b/homeassistant/components/flux_led/translations/hu.json @@ -11,7 +11,7 @@ "flow_title": "{model} {id} ({ipaddr})", "step": { "discovery_confirm": { - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani a(z) {model} {id} ({ipaddr}) webhelyet?" + "description": "Be szeretn\u00e9 \u00e1ll\u00edtani: {model} {id} ({ipaddr}) ?" }, "user": { "data": { diff --git a/homeassistant/components/geonetnz_quakes/translations/he.json b/homeassistant/components/geonetnz_quakes/translations/he.json index 48a6eeeea33..7718605a588 100644 --- a/homeassistant/components/geonetnz_quakes/translations/he.json +++ b/homeassistant/components/geonetnz_quakes/translations/he.json @@ -2,6 +2,13 @@ "config": { "abort": { "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + }, + "step": { + "user": { + "data": { + "radius": "\u05e8\u05d3\u05d9\u05d5\u05e1" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/he.json b/homeassistant/components/homekit_controller/translations/he.json index 1028351a1bc..9593bbd90e4 100644 --- a/homeassistant/components/homekit_controller/translations/he.json +++ b/homeassistant/components/homekit_controller/translations/he.json @@ -3,6 +3,13 @@ "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}", + "step": { + "user": { + "data": { + "device": "\u05d4\u05ea\u05e7\u05df" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/translations/he.json b/homeassistant/components/homematicip_cloud/translations/he.json index 55260a82f9d..8e6f13544b9 100644 --- a/homeassistant/components/homematicip_cloud/translations/he.json +++ b/homeassistant/components/homematicip_cloud/translations/he.json @@ -21,7 +21,7 @@ "title": "\u05d1\u05d7\u05e8 \u05e0\u05e7\u05d5\u05d3\u05ea \u05d2\u05d9\u05e9\u05d4 HomematicIP" }, "link": { - "description": "\u05dc\u05d7\u05e5 \u05e2\u05dc \u05d4\u05db\u05e4\u05ea\u05d5\u05e8 \u05d4\u05db\u05d7\u05d5\u05dc \u05d1\u05e0\u05e7\u05d5\u05d3\u05ea \u05d2\u05d9\u05e9\u05d4 \u05d5\u05e2\u05dc \u05db\u05e4\u05ea\u05d5\u05e8 \u05d4\u05e9\u05dc\u05d9\u05d7\u05d4 \u05db\u05d3\u05d9 \u05dc\u05d7\u05d1\u05e8 \u05d0\u05ea HomematicIP \u05e2\u05ddHome Assistant.\n\n![\u05de\u05d9\u05e7\u05d5\u05dd \u05d4\u05db\u05e4\u05ea\u05d5\u05e8 \u05d1\u05de\u05d2\u05e9\u05e8](/static/images/config_flows/config_homematicip_cloud.png)", + "description": "\u05dc\u05d7\u05d9\u05e6\u05d4 \u05e2\u05dc \u05d4\u05db\u05e4\u05ea\u05d5\u05e8 \u05d4\u05db\u05d7\u05d5\u05dc \u05d1\u05e0\u05e7\u05d5\u05d3\u05ea \u05d2\u05d9\u05e9\u05d4 \u05d5\u05e2\u05dc \u05db\u05e4\u05ea\u05d5\u05e8 \u05d4\u05e9\u05dc\u05d9\u05d7\u05d4 \u05db\u05d3\u05d9 \u05dc\u05e8\u05e9\u05d5\u05dd \u05d0\u05ea HomematIP \u05e2\u05dd Home Assistant.\n\n![\u05de\u05d9\u05e7\u05d5\u05dd \u05d4\u05db\u05e4\u05ea\u05d5\u05e8 \u05d1\u05de\u05d2\u05e9\u05e8](/static/images/config_flows/config_homematicip_cloud.png)", "title": "\u05e0\u05e7\u05d5\u05d3\u05ea \u05d2\u05d9\u05e9\u05d4 \u05dc\u05e7\u05d9\u05e9\u05d5\u05e8" } } diff --git a/homeassistant/components/lock/translations/he.json b/homeassistant/components/lock/translations/he.json index b51566b4d0d..fdf3eec71a0 100644 --- a/homeassistant/components/lock/translations/he.json +++ b/homeassistant/components/lock/translations/he.json @@ -1,4 +1,11 @@ { + "device_automation": { + "action_type": { + "lock": "\u05e0\u05e2\u05d9\u05dc\u05ea {entity_name}", + "open": "\u05e4\u05ea\u05d9\u05d7\u05ea {entity_name}", + "unlock": "\u05e0\u05e2\u05d9\u05dc\u05ea {entity_name}" + } + }, "state": { "_": { "locked": "\u05e0\u05e2\u05d5\u05dc", diff --git a/homeassistant/components/logi_circle/translations/he.json b/homeassistant/components/logi_circle/translations/he.json index 425a68fee00..aaf82df64b4 100644 --- a/homeassistant/components/logi_circle/translations/he.json +++ b/homeassistant/components/logi_circle/translations/he.json @@ -7,6 +7,13 @@ "error": { "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_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "user": { + "data": { + "flow_impl": "\u05e1\u05e4\u05e7" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/he.json b/homeassistant/components/mqtt/translations/he.json index 36521ce6839..7dbc50e246f 100644 --- a/homeassistant/components/mqtt/translations/he.json +++ b/homeassistant/components/mqtt/translations/he.json @@ -52,6 +52,7 @@ "options": { "error": { "bad_birth": "\u05e0\u05d5\u05e9\u05d0 \u05dc\u05d9\u05d3\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9.", + "bad_will": "\u05e0\u05d5\u05e9\u05d0 \u05d7\u05d5\u05e7 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9.", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, "step": { @@ -69,9 +70,15 @@ "data": { "birth_enable": "\u05d0\u05e4\u05e9\u05e8 \u05d4\u05d5\u05d3\u05e2\u05ea \u05dc\u05d9\u05d3\u05d4", "birth_payload": "\u05de\u05d8\u05e2\u05df \u05d4\u05d5\u05d3\u05e2\u05ea \u05dc\u05d9\u05d3\u05d4", + "birth_qos": "\u05d4\u05d5\u05d3\u05e2\u05ea \u05dc\u05d9\u05d3\u05d4 QoS", "birth_retain": "\u05d4\u05d5\u05d3\u05e2\u05ea \u05dc\u05d9\u05d3\u05d4 \u05e0\u05e9\u05de\u05e8\u05ea", "birth_topic": "\u05e0\u05d5\u05e9\u05d0 \u05d4\u05d5\u05d3\u05e2\u05ea \u05dc\u05d9\u05d3\u05d4", - "discovery": "\u05d0\u05d9\u05e4\u05e9\u05d5\u05e8 \u05d2\u05d9\u05dc\u05d5\u05d9" + "discovery": "\u05d0\u05d9\u05e4\u05e9\u05d5\u05e8 \u05d2\u05d9\u05dc\u05d5\u05d9", + "will_enable": "\u05d4\u05e4\u05d9\u05db\u05ea \u05d4\u05d5\u05d3\u05e2\u05d4 \u05dc\u05d6\u05de\u05d9\u05e0\u05d4", + "will_payload": "\u05d4\u05d0\u05dd \u05ea\u05d5\u05db\u05df \u05de\u05e0\u05d4 \u05e9\u05dc \u05d4\u05d5\u05d3\u05e2\u05d4", + "will_qos": "\u05d4\u05d0\u05dd \u05d4\u05d5\u05d3\u05e2\u05ea QoS", + "will_retain": "\u05d4\u05d0\u05dd \u05d4\u05d4\u05d5\u05d3\u05e2\u05d4 \u05ea\u05d9\u05e9\u05de\u05e8", + "will_topic": "\u05d4\u05d0\u05dd \u05e0\u05d5\u05e9\u05d0 \u05d4\u05d5\u05d3\u05e2\u05d4" }, "description": "\u05d2\u05d9\u05dc\u05d5\u05d9 - \u05d0\u05dd \u05d2\u05d9\u05dc\u05d5\u05d9 \u05de\u05d5\u05e4\u05e2\u05dc (\u05de\u05d5\u05de\u05dc\u05e5), Home Assistant \u05d9\u05d2\u05dc\u05d4 \u05d1\u05d0\u05d5\u05e4\u05df \u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d5\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05d4\u05de\u05e4\u05e8\u05e1\u05de\u05d9\u05dd \u05d0\u05ea \u05ea\u05e6\u05d5\u05e8\u05ea\u05dd \u05d1\u05de\u05ea\u05d5\u05d5\u05da MQTT. \u05d0\u05dd \u05d4\u05d2\u05d9\u05dc\u05d5\u05d9 \u05d0\u05d9\u05e0\u05d5 \u05de\u05d5\u05e4\u05e2\u05dc, \u05db\u05dc \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05d7\u05d9\u05d9\u05d1\u05ea \u05dc\u05d4\u05d9\u05e2\u05e9\u05d5\u05ea \u05d1\u05d0\u05d5\u05e4\u05df \u05d9\u05d3\u05e0\u05d9.\n\u05d4\u05d5\u05d3\u05e2\u05ea \u05dc\u05d9\u05d3\u05d4 - \u05d4\u05d5\u05d3\u05e2\u05ea \u05d4\u05dc\u05d9\u05d3\u05d4 \u05ea\u05d9\u05e9\u05dc\u05d7 \u05d1\u05db\u05dc \u05e4\u05e2\u05dd \u05e9-Home Assistant \u05de\u05ea\u05d7\u05d1\u05e8 (\u05de\u05d7\u05d3\u05e9) \u05dc\u05de\u05ea\u05d5\u05d5\u05da MQTT.\n\u05d4\u05d5\u05d3\u05e2\u05ea \u05e8\u05e6\u05d5\u05df - \u05d4\u05d5\u05d3\u05e2\u05ea \u05d4\u05e8\u05e6\u05d5\u05df \u05ea\u05d9\u05e9\u05dc\u05d7 \u05d1\u05db\u05dc \u05e4\u05e2\u05dd \u05e9-Home Assistant \u05d9\u05d0\u05d1\u05d3 \u05d0\u05ea \u05d4\u05e7\u05e9\u05e8 \u05e9\u05dc\u05d5 \u05dc\u05de\u05ea\u05d5\u05d5\u05da, \u05d2\u05dd \u05d1\u05de\u05e7\u05e8\u05d4 \u05e9\u05dc \u05e0\u05d9\u05ea\u05d5\u05e7 \u05e0\u05e7\u05d9 (\u05dc\u05de\u05e9\u05dc \u05db\u05d9\u05d1\u05d5\u05d9 \u05e9\u05dc Home Assistant) \u05d5\u05d2\u05dd \u05d1\u05de\u05e7\u05e8\u05d4 \u05e9\u05dc \u05e0\u05d9\u05ea\u05d5\u05e7 \u05dc\u05d0 \u05e0\u05e7\u05d9 (\u05dc\u05de\u05e9\u05dc Home Assistant \u05de\u05ea\u05e8\u05e1\u05e7 \u05d0\u05d5 \u05de\u05d0\u05d1\u05d3 \u05d0\u05ea \u05d7\u05d9\u05d1\u05d5\u05e8 \u05d4\u05e8\u05e9\u05ea \u05e9\u05dc\u05d5).", "title": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea MQTT" diff --git a/homeassistant/components/mysensors/translations/he.json b/homeassistant/components/mysensors/translations/he.json index 3ade3fcdad4..bbe627fd878 100644 --- a/homeassistant/components/mysensors/translations/he.json +++ b/homeassistant/components/mysensors/translations/he.json @@ -15,6 +15,13 @@ }, "step": { "gw_mqtt": { + "data": { + "persistence_file": "\u05e7\u05d5\u05d1\u05e5 \u05d4\u05ea\u05de\u05d3\u05d4 (\u05d4\u05e9\u05d0\u05e8\u05d4 \u05e8\u05d9\u05e7 \u05dc\u05d9\u05e6\u05d9\u05e8\u05d4 \u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9\u05ea)", + "retain": "\u05e9\u05de\u05d9\u05e8\u05d4 \u05e2\u05dc mqtt", + "topic_in_prefix": "\u05e7\u05d9\u05d3\u05d5\u05de\u05ea \u05e2\u05d1\u05d5\u05e8 \u05e0\u05d5\u05e9\u05d0\u05d9 \u05e7\u05dc\u05d8 (topic_in_prefix)", + "topic_out_prefix": "\u05e7\u05d9\u05d3\u05d5\u05de\u05ea \u05e2\u05d1\u05d5\u05e8 \u05e0\u05d5\u05e9\u05d0\u05d9 \u05e4\u05dc\u05d8 (topic_out_prefix)", + "version": "\u05d2\u05d9\u05e8\u05e1\u05ea MySensors" + }, "description": "\u05d4\u05d2\u05d3\u05e8\u05ea \u05e9\u05e2\u05e8 MQTT" }, "gw_tcp": { diff --git a/homeassistant/components/netatmo/translations/hu.json b/homeassistant/components/netatmo/translations/hu.json index cb634547efc..5802e1a2679 100644 --- a/homeassistant/components/netatmo/translations/hu.json +++ b/homeassistant/components/netatmo/translations/hu.json @@ -42,10 +42,10 @@ "public_weather": { "data": { "area_name": "A ter\u00fclet neve", - "lat_ne": "Sz\u00e9less\u00e9g \u00c9szakkeleti sarok", - "lat_sw": "Sz\u00e9less\u00e9g D\u00e9lnyugati sarok", - "lon_ne": "Hossz\u00fas\u00e1g \u00c9szakkeleti sarok", - "lon_sw": "Hossz\u00fas\u00e1g D\u00e9lnyugati sarok", + "lat_ne": "\u00c9szakkeleti sarok sz\u00e9less\u00e9go fok", + "lat_sw": "D\u00e9lnyugati sarok sz\u00e9lees\u00e9gi fok", + "lon_ne": "\u00c9szakkeleti sarok hossz\u00fas\u00e1gi fok", + "lon_sw": "D\u00e9lnyugati sarok hossz\u00fas\u00e1ggi fok", "mode": "Sz\u00e1m\u00edt\u00e1s", "show_on_map": "Mutasd a t\u00e9rk\u00e9pen" }, diff --git a/homeassistant/components/onvif/translations/he.json b/homeassistant/components/onvif/translations/he.json index 6d1229f1a5d..5883d1afc53 100644 --- a/homeassistant/components/onvif/translations/he.json +++ b/homeassistant/components/onvif/translations/he.json @@ -4,7 +4,8 @@ "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_h264": "\u05dc\u05d0 \u05d4\u05d9\u05d5 \u05d6\u05e8\u05de\u05d9 H264 \u05d6\u05de\u05d9\u05e0\u05d9\u05dd. \u05d9\u05e9 \u05dc\u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea \u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e4\u05e8\u05d5\u05e4\u05d9\u05dc \u05d1\u05d4\u05ea\u05e7\u05df \u05e9\u05dc\u05da.", - "no_mac": "\u05dc\u05d0 \u05d4\u05d9\u05ea\u05d4 \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05e7\u05d1\u05d5\u05e2 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05de\u05d6\u05d4\u05d4 \u05d9\u05d9\u05d7\u05d5\u05d3\u05d9 \u05e2\u05d1\u05d5\u05e8 \u05d4\u05ea\u05e7\u05df ONVIF." + "no_mac": "\u05dc\u05d0 \u05d4\u05d9\u05ea\u05d4 \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05e7\u05d1\u05d5\u05e2 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05de\u05d6\u05d4\u05d4 \u05d9\u05d9\u05d7\u05d5\u05d3\u05d9 \u05e2\u05d1\u05d5\u05e8 \u05d4\u05ea\u05e7\u05df ONVIF.", + "onvif_error": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05d4\u05d2\u05d3\u05e8\u05ea \u05d4\u05ea\u05e7\u05df ONVIF. \u05e0\u05d0 \u05dc\u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea \u05d9\u05d5\u05de\u05e0\u05d9 \u05d4\u05e8\u05d9\u05e9\u05d5\u05dd \u05dc\u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3." }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" @@ -14,7 +15,8 @@ "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" - } + }, + "title": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05d0\u05d9\u05de\u05d5\u05ea" }, "configure": { "data": { @@ -30,6 +32,7 @@ "data": { "include": "\u05e6\u05d5\u05e8 \u05d9\u05e9\u05d5\u05ea \u05de\u05e6\u05dc\u05de\u05d4" }, + "description": "\u05d4\u05d0\u05dd \u05dc\u05d9\u05e6\u05d5\u05e8 \u05d9\u05e9\u05d5\u05ea \u05de\u05e6\u05dc\u05de\u05d4 \u05e2\u05d1\u05d5\u05e8 {profile} \u05d1\u05e8\u05d6\u05d5\u05dc\u05d5\u05e6\u05d9\u05d9\u05ea {resolution}?", "title": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05e4\u05e8\u05d5\u05e4\u05d9\u05dc\u05d9\u05dd" }, "device": { @@ -47,6 +50,9 @@ "title": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05d4\u05ea\u05e7\u05df ONVIF" }, "user": { + "data": { + "auto": "\u05d7\u05d9\u05e4\u05d5\u05e9 \u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9" + }, "description": "\u05d1\u05dc\u05d7\u05d9\u05e6\u05d4 \u05e2\u05dc \u05e9\u05dc\u05d7, \u05e0\u05d7\u05e4\u05e9 \u05d1\u05e8\u05e9\u05ea \u05e9\u05dc\u05da \u05de\u05db\u05e9\u05d9\u05e8\u05d9 ONVIF \u05d4\u05ea\u05d5\u05de\u05db\u05d9\u05dd \u05d1\u05e4\u05e8\u05d5\u05e4\u05d9\u05dc S.\n\n\u05d9\u05e6\u05e8\u05e0\u05d9\u05dd \u05de\u05e1\u05d5\u05d9\u05de\u05d9\u05dd \u05d4\u05d7\u05dc\u05d5 \u05dc\u05d4\u05e9\u05d1\u05d9\u05ea \u05d0\u05ea ONVIF \u05db\u05d1\u05e8\u05d9\u05e8\u05ea \u05de\u05d7\u05d3\u05dc. \u05e0\u05d0 \u05d5\u05d3\u05d0 \u05e9\u05ea\u05e6\u05d5\u05e8\u05ea ONVIF \u05d6\u05de\u05d9\u05e0\u05d4 \u05d1\u05de\u05e6\u05dc\u05de\u05d4 \u05e9\u05dc\u05da.", "title": "\u05d4\u05d2\u05d3\u05e8\u05ea \u05d4\u05ea\u05e7\u05df ONVIF" } diff --git a/homeassistant/components/plex/translations/he.json b/homeassistant/components/plex/translations/he.json index dafda36af4c..2d13d1ed0c8 100644 --- a/homeassistant/components/plex/translations/he.json +++ b/homeassistant/components/plex/translations/he.json @@ -17,6 +17,11 @@ "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL", "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" } + }, + "select_server": { + "data": { + "server": "\u05e9\u05e8\u05ea" + } } } } diff --git a/homeassistant/components/select/translations/he.json b/homeassistant/components/select/translations/he.json index 7f2ff684474..3cde3703009 100644 --- a/homeassistant/components/select/translations/he.json +++ b/homeassistant/components/select/translations/he.json @@ -1,3 +1,14 @@ { + "device_automation": { + "action_type": { + "select_option": "\u05e9\u05d9\u05e0\u05d5\u05d9 \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea {entity_name}" + }, + "condition_type": { + "selected_option": "\u05d4\u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05d4\u05e0\u05d5\u05db\u05d7\u05d9\u05ea {entity_name} \u05e9\u05e0\u05d1\u05d7\u05e8\u05d4" + }, + "trigger_type": { + "current_option_changed": "\u05d4\u05d0\u05e4\u05e9\u05e8\u05d5\u05ea {entity_name} \u05d4\u05e9\u05ea\u05e0\u05ea\u05d4" + } + }, "title": "\u05d1\u05d7\u05e8" } \ No newline at end of file diff --git a/homeassistant/components/stookalert/translations/he.json b/homeassistant/components/stookalert/translations/he.json new file mode 100644 index 00000000000..8506408c0f5 --- /dev/null +++ b/homeassistant/components/stookalert/translations/he.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + }, + "step": { + "user": { + "data": { + "province": "\u05de\u05d7\u05d5\u05d6" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/translations/he.json b/homeassistant/components/tplink/translations/he.json index 621ee6bebc9..6ac16c36476 100644 --- a/homeassistant/components/tplink/translations/he.json +++ b/homeassistant/components/tplink/translations/he.json @@ -8,6 +8,7 @@ "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, + "flow_title": "{name} {model} ({host})", "step": { "confirm": { "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d7\u05db\u05de\u05d9\u05dd \u05e9\u05dc TP-Link ?" diff --git a/homeassistant/components/watttime/translations/de.json b/homeassistant/components/watttime/translations/de.json index c6bf9641c13..65552140eca 100644 --- a/homeassistant/components/watttime/translations/de.json +++ b/homeassistant/components/watttime/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", @@ -22,6 +23,13 @@ }, "description": "W\u00e4hle einen Standort f\u00fcr die \u00dcberwachung:" }, + "reauth_confirm": { + "data": { + "password": "Passwort" + }, + "description": "Bitte gib das Passwort f\u00fcr {ame} erneut ein:", + "title": "Integration erneut authentifizieren" + }, "user": { "data": { "password": "Passwort", diff --git a/homeassistant/components/watttime/translations/en.json b/homeassistant/components/watttime/translations/en.json index e99af749031..49b264851f2 100644 --- a/homeassistant/components/watttime/translations/en.json +++ b/homeassistant/components/watttime/translations/en.json @@ -27,7 +27,7 @@ "data": { "password": "Password" }, - "description": "Please re-enter the password for {username}.", + "description": "Please re-enter the password for {username}:", "title": "Reauthenticate Integration" }, "user": { diff --git a/homeassistant/components/watttime/translations/hu.json b/homeassistant/components/watttime/translations/hu.json index a106416f4b9..8ef371d2691 100644 --- a/homeassistant/components/watttime/translations/hu.json +++ b/homeassistant/components/watttime/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", @@ -22,6 +23,13 @@ }, "description": "V\u00e1lasszon egy helyet a monitoroz\u00e1shoz:" }, + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "K\u00e9rem, adja meg \u00fajra a jelsz\u00f3t {username} r\u00e9sz\u00e9re:", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { "password": "Jelsz\u00f3", From 58362404eac68998ba2bcfdd274f77c1ee91d309 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 11 Oct 2021 17:30:23 -1000 Subject: [PATCH 0259/1038] Simplify yeelight setup to improve reliability (#57500) --- homeassistant/components/yeelight/__init__.py | 167 +++++------------- tests/components/yeelight/test_init.py | 56 ++++-- 2 files changed, 86 insertions(+), 137 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index fb908775d1b..64fa7b01f28 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -26,10 +26,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.event import async_call_later, async_track_time_interval from homeassistant.helpers.typing import ConfigType @@ -42,7 +39,6 @@ POWER_STATE_CHANGE_TIME = 1 # seconds DOMAIN = "yeelight" DATA_YEELIGHT = DOMAIN DATA_UPDATED = "yeelight_{}_data_updated" -DEVICE_INITIALIZED = "yeelight_{}_device_initialized" DEFAULT_NAME = "Yeelight" DEFAULT_TRANSITION = 350 @@ -203,24 +199,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def _async_initialize( hass: HomeAssistant, entry: ConfigEntry, - host: str, - device: YeelightDevice | None = None, + device: YeelightDevice, ) -> None: - entry_data = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][entry.entry_id] = { - DATA_PLATFORMS_LOADED: False - } - - @callback - def _async_load_platforms(): - if entry_data[DATA_PLATFORMS_LOADED]: - return - entry_data[DATA_PLATFORMS_LOADED] = True - hass.config_entries.async_setup_platforms(entry, PLATFORMS) - - if not device: - # get device and start listening for local pushes - device = await _async_get_device(hass, host, entry) - + entry_data = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][entry.entry_id] = {} await device.async_setup() entry_data[DATA_DEVICE] = device @@ -232,15 +213,9 @@ async def _async_initialize( entry, options={**entry.options, CONF_MODEL: device.capabilities["model"]} ) - entry.async_on_unload(entry.add_update_listener(_async_update_listener)) - entry.async_on_unload( - async_dispatcher_connect( - hass, DEVICE_INITIALIZED.format(host), _async_load_platforms - ) - ) - # fetch initial state - asyncio.create_task(device.async_update()) + await device.async_update() + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) @callback @@ -256,7 +231,7 @@ def _async_normalize_config_entry(hass: HomeAssistant, entry: ConfigEntry) -> No entry, data={ CONF_HOST: entry.data.get(CONF_HOST), - CONF_ID: entry.data.get(CONF_ID, entry.unique_id), + CONF_ID: entry.data.get(CONF_ID) or entry.unique_id, }, options={ CONF_NAME: entry.data.get(CONF_NAME, ""), @@ -270,68 +245,44 @@ def _async_normalize_config_entry(hass: HomeAssistant, entry: ConfigEntry) -> No CONF_NIGHTLIGHT_SWITCH, DEFAULT_NIGHTLIGHT_SWITCH ), }, + unique_id=entry.unique_id or entry.data.get(CONF_ID), ) elif entry.unique_id and not entry.data.get(CONF_ID): hass.config_entries.async_update_entry( entry, data={CONF_HOST: entry.data.get(CONF_HOST), CONF_ID: entry.unique_id}, ) + elif entry.data.get(CONF_ID) and not entry.unique_id: + hass.config_entries.async_update_entry( + entry, + unique_id=entry.data[CONF_ID], + ) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Yeelight from a config entry.""" _async_normalize_config_entry(hass, entry) - if entry.data.get(CONF_HOST): - try: - device = await _async_get_device(hass, entry.data[CONF_HOST], entry) - except BULB_EXCEPTIONS as ex: - # Always retry later since bulbs can stop responding to SSDP - # sometimes even though they are online. If it has changed - # IP we will update it via discovery to the config flow - raise ConfigEntryNotReady from ex - else: - # Since device is passed this cannot throw an exception anymore - await _async_initialize(hass, entry, entry.data[CONF_HOST], device=device) - return True + if not entry.data.get(CONF_HOST): + bulb_id = async_format_id(entry.data.get(CONF_ID, entry.unique_id)) + raise ConfigEntryNotReady(f"Waiting for {bulb_id} to be discovered") - async def _async_from_discovery(capabilities: dict[str, str]) -> None: - host = urlparse(capabilities["location"]).hostname - try: - await _async_initialize(hass, entry, host) - except BULB_EXCEPTIONS: - _LOGGER.exception("Failed to connect to bulb at %s", host) + try: + device = await _async_get_device(hass, entry.data[CONF_HOST], entry) + await _async_initialize(hass, entry, device) + except BULB_EXCEPTIONS as ex: + raise ConfigEntryNotReady from ex + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) - scanner = YeelightScanner.async_get(hass) - await scanner.async_register_callback(entry.data[CONF_ID], _async_from_discovery) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - if entry.data.get(CONF_ID): - # discovery - scanner = YeelightScanner.async_get(hass) - scanner.async_unregister_callback(entry.data[CONF_ID]) - data_config_entries = hass.data[DOMAIN][DATA_CONFIG_ENTRIES] - if entry.entry_id not in data_config_entries: - # Device not online - return True - - entry_data = data_config_entries[entry.entry_id] - unload_ok = True - if entry_data[DATA_PLATFORMS_LOADED]: - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - if DATA_DEVICE in entry_data: - device = entry_data[DATA_DEVICE] - _LOGGER.debug("Shutting down Yeelight Listener") - await device.bulb.async_stop_listening() - _LOGGER.debug("Yeelight Listener stopped") - data_config_entries.pop(entry.entry_id) - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) @callback @@ -380,7 +331,6 @@ class YeelightScanner: def __init__(self, hass: HomeAssistant) -> None: """Initialize class.""" self._hass = hass - self._callbacks = {} self._host_discovered_events = {} self._unique_id_capabilities = {} self._host_capabilities = {} @@ -391,7 +341,7 @@ class YeelightScanner: async def async_setup(self): """Set up the scanner.""" if self._connected_events: - await asyncio.gather(*(event.wait() for event in self._connected_events)) + await self._async_wait_connected() return for idx, source_ip in enumerate(await self._async_build_source_set()): @@ -434,9 +384,16 @@ class YeelightScanner: for listener in failed_listeners: self._listeners.remove(listener) - await asyncio.gather(*(event.wait() for event in self._connected_events)) + await self._async_wait_connected() + self._track_interval = async_track_time_interval( + self._hass, self.async_scan, DISCOVERY_INTERVAL + ) self.async_scan() + async def _async_wait_connected(self): + """Wait for the listeners to be up and connected.""" + await asyncio.gather(*(event.wait() for event in self._connected_events)) + async def _async_build_source_set(self) -> set[IPv4Address]: """Build the list of ssdp sources.""" adapters = await network.async_get_adapters(self._hass) @@ -453,6 +410,7 @@ class YeelightScanner: async def async_discover(self): """Discover bulbs.""" + _LOGGER.debug("Yeelight discover with interval %s", DISCOVERY_SEARCH_INTERVAL) await self.async_setup() for _ in range(DISCOVERY_ATTEMPTS): self.async_scan() @@ -513,45 +471,6 @@ class YeelightScanner: self._unique_id_capabilities[unique_id] = response for event in self._host_discovered_events.get(host, []): event.set() - if unique_id in self._callbacks: - self._hass.async_create_task(self._callbacks[unique_id](response)) - self._callbacks.pop(unique_id) - if not self._callbacks: - self._async_stop_scan() - - async def _async_start_scan(self): - """Start scanning for Yeelight devices.""" - _LOGGER.debug("Start scanning") - await self.async_setup() - if not self._track_interval: - self._track_interval = async_track_time_interval( - self._hass, self.async_scan, DISCOVERY_INTERVAL - ) - self.async_scan() - - @callback - def _async_stop_scan(self): - """Stop scanning.""" - if self._track_interval is None: - return - _LOGGER.debug("Stop scanning interval") - self._track_interval() - self._track_interval = None - - async def async_register_callback(self, unique_id, callback_func): - """Register callback function.""" - if capabilities := self._unique_id_capabilities.get(unique_id): - self._hass.async_create_task(callback_func(capabilities)) - return - self._callbacks[unique_id] = callback_func - await self._async_start_scan() - - @callback - def async_unregister_callback(self, unique_id): - """Unregister callback function.""" - self._callbacks.pop(unique_id, None) - if not self._callbacks: - self._async_stop_scan() def update_needs_bg_power_workaround(data): @@ -675,7 +594,6 @@ class YeelightDevice: self._available = True if not self._initialized: self._initialized = True - async_dispatcher_send(self._hass, DEVICE_INITIALIZED.format(self._host)) except BULB_NETWORK_EXCEPTIONS as ex: if self._available: # just inform once _LOGGER.error( @@ -725,9 +643,6 @@ class YeelightDevice: ): # On reconnect the properties may be out of sync # - # We need to make sure the DEVICE_INITIALIZED dispatcher is setup - # before we can update on reconnect by checking self._did_first_update - # # If the device drops the connection right away, we do not want to # do a property resync via async_update since its about # to be called when async_setup_entry reaches the end of the @@ -743,10 +658,7 @@ class YeelightEntity(Entity): def __init__(self, device: YeelightDevice, entry: ConfigEntry) -> None: """Initialize the entity.""" self._device = device - self._unique_id = entry.entry_id - if entry.unique_id is not None: - # Use entry unique id (device id) whenever possible - self._unique_id = entry.unique_id + self._unique_id = entry.unique_id or entry.entry_id @property def unique_id(self) -> str: @@ -794,12 +706,19 @@ async def _async_get_device( # register stop callback to shutdown listening for local pushes async def async_stop_listen_task(event): - """Stop listen thread.""" - _LOGGER.debug("Shutting down Yeelight Listener") + """Stop listen task.""" + _LOGGER.debug("Shutting down Yeelight Listener (stop event)") await device.bulb.async_stop_listening() + @callback + def _async_stop_listen_on_unload(): + """Stop listen task.""" + _LOGGER.debug("Shutting down Yeelight Listener (unload)") + hass.async_create_task(device.bulb.async_stop_listening()) + entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_listen_task) ) + entry.async_on_unload(_async_stop_listen_on_unload) return device diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index 3ad99fa34ac..7ddb2845ac8 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -111,7 +111,9 @@ async def test_ip_changes_id_missing_cannot_fallback(hass: HomeAssistant): async def test_setup_discovery(hass: HomeAssistant): """Test setting up Yeelight by discovery.""" - config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA) + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS, **CONFIG_ENTRY_DATA} + ) config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() @@ -151,7 +153,9 @@ async def test_setup_discovery_with_manually_configured_network_adapter( hass: HomeAssistant, ): """Test setting up Yeelight by discovery with a manually configured network adapter.""" - config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA) + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS, **CONFIG_ENTRY_DATA} + ) config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() @@ -205,7 +209,9 @@ async def test_setup_discovery_with_manually_configured_network_adapter_one_fail hass: HomeAssistant, caplog ): """Test setting up Yeelight by discovery with a manually configured network adapter with one that fails to bind.""" - config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA) + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS, **CONFIG_ENTRY_DATA} + ) config_entry.add_to_hass(hass) mocked_bulb = _mocked_bulb() @@ -268,7 +274,7 @@ async def test_unique_ids_device(hass: HomeAssistant): """Test Yeelight unique IDs from yeelight device IDs.""" config_entry = MockConfigEntry( domain=DOMAIN, - data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: True}, + data={CONF_HOST: IP_ADDRESS, **CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: True}, unique_id=ID, ) config_entry.add_to_hass(hass) @@ -292,7 +298,8 @@ async def test_unique_ids_device(hass: HomeAssistant): async def test_unique_ids_entry(hass: HomeAssistant): """Test Yeelight unique IDs from entry IDs.""" config_entry = MockConfigEntry( - domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: True} + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NIGHTLIGHT_SWITCH: True}, ) config_entry.add_to_hass(hass) @@ -357,18 +364,16 @@ async def test_async_listen_error_late_discovery(hass, caplog): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state is ConfigEntryState.LOADED - assert "Failed to connect to bulb at" in caplog.text - await hass.config_entries.async_unload(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.SETUP_RETRY await hass.async_block_till_done() - - caplog.clear() + assert "Waiting for 0x15243f to be discovered" in caplog.text with _patch_discovery(), patch(f"{MODULE}.AsyncBulb", return_value=_mocked_bulb()): - await hass.config_entries.async_setup(config_entry.entry_id) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) await hass.async_block_till_done() - assert "Failed to connect to bulb at" not in caplog.text assert config_entry.state is ConfigEntryState.LOADED assert config_entry.options[CONF_MODEL] == MODEL @@ -386,7 +391,7 @@ async def test_unload_before_discovery(hass, caplog): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state is ConfigEntryState.LOADED + assert config_entry.state is ConfigEntryState.SETUP_RETRY await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() @@ -451,6 +456,31 @@ async def test_async_setup_with_missing_id(hass: HomeAssistant): assert config_entry.state is ConfigEntryState.LOADED +async def test_async_setup_with_missing_unique_id(hass: HomeAssistant): + """Test that setting adds the missing unique_id from CONF_ID.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "127.0.0.1", CONF_ID: ID}, + options={CONF_NAME: "Test name"}, + ) + config_entry.add_to_hass(hass) + + with _patch_discovery(), _patch_discovery_timeout(), _patch_discovery_interval(), patch( + f"{MODULE}.AsyncBulb", return_value=_mocked_bulb(cannot_connect=True) + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.SETUP_RETRY + assert config_entry.unique_id == ID + + with _patch_discovery(), _patch_discovery_timeout(), _patch_discovery_interval(), patch( + f"{MODULE}.AsyncBulb", return_value=_mocked_bulb() + ): + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=2)) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + async def test_connection_dropped_resyncs_properties(hass: HomeAssistant): """Test handling a connection drop results in a property resync.""" config_entry = MockConfigEntry( From f0abd5b815ccbbe4ac2d9617a66f45b38409bd35 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 12 Oct 2021 05:32:15 +0200 Subject: [PATCH 0260/1038] Fix Tuya error when removing device (#57512) --- homeassistant/components/tuya/__init__.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 28c43d8df46..3b358e8889a 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -134,12 +134,17 @@ async def cleanup_device_registry(hass: HomeAssistant, entry: ConfigEntry) -> No @callback -def async_remove_hass_device(hass: HomeAssistant, device_id: str) -> None: +def async_remove_hass_device( + hass: HomeAssistant, entry: ConfigEntry, device_id: str +) -> None: """Remove device from hass cache.""" device_registry_object = device_registry.async_get(hass) - for device_entry in list(device_registry_object.devices.values()): - if device_id in list(device_entry.identifiers)[0]: - device_registry_object.async_remove_device(device_entry.id) + device_entry = device_registry_object.async_get_device( + identifiers={(DOMAIN, device_id)} + ) + if device_entry is not None: + device_registry_object.async_remove_device(device_entry.id) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].discard(device_id) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -188,7 +193,9 @@ class DeviceListener(TuyaDeviceListener): *self.hass.data[DOMAIN][self.entry.entry_id][TUYA_HA_TUYA_MAP].values() ): ha_tuya_map = self.hass.data[DOMAIN][self.entry.entry_id][TUYA_HA_TUYA_MAP] - self.hass.add_job(async_remove_hass_device, self.hass, device.id) + self.hass.add_job( + async_remove_hass_device, self.hass, self.entry, device.id + ) for domain, tuya_list in ha_tuya_map.items(): if device.category in tuya_list: @@ -219,4 +226,4 @@ class DeviceListener(TuyaDeviceListener): def remove_device(self, device_id: str) -> None: """Add device removed listener.""" _LOGGER.debug("tuya remove device:%s", device_id) - self.hass.add_job(async_remove_hass_device, self.hass, device_id) + self.hass.add_job(async_remove_hass_device, self.hass, self.entry, device_id) From 71a3daa8ce93ea62b416c661a9f25faa9f5e4692 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 12 Oct 2021 05:36:46 +0200 Subject: [PATCH 0261/1038] Pass device manager directly in Tuya registry cleanup (#57511) --- homeassistant/components/tuya/__init__.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 3b358e8889a..326cacfebbf 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -111,7 +111,7 @@ async def _init_tuya_sdk(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE_MANAGER] = device_manager # Clean up device entities - await cleanup_device_registry(hass, entry) + await cleanup_device_registry(hass, device_manager) _LOGGER.debug("init support type->%s", PLATFORMS) @@ -120,12 +120,11 @@ async def _init_tuya_sdk(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def cleanup_device_registry(hass: HomeAssistant, entry: ConfigEntry) -> None: +async def cleanup_device_registry( + hass: HomeAssistant, device_manager: TuyaDeviceManager +) -> None: """Remove deleted device registry entry if there are no remaining entities.""" - device_registry_object = device_registry.async_get(hass) - device_manager = hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE_MANAGER] - for dev_id, device_entry in list(device_registry_object.devices.items()): for item in device_entry.identifiers: if DOMAIN == item[0] and item[1] not in device_manager.device_map: @@ -171,7 +170,6 @@ class DeviceListener(TuyaDeviceListener): def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Init DeviceListener.""" - self.hass = hass self.entry = entry From 580b5fb8124e34392872aa794de875c8809c4e6c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 12 Oct 2021 05:37:18 +0200 Subject: [PATCH 0262/1038] Entity attributes cleanup in Tuya base entity (#57510) --- homeassistant/components/tuya/base.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index a1f65227e95..2ddfe484ffd 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -14,10 +14,11 @@ from .const import DOMAIN, TUYA_HA_SIGNAL_UPDATE_ENTITY class TuyaHaEntity(Entity): """Tuya base device.""" + _attr_should_poll = False + def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> None: """Init TuyaHaEntity.""" - super().__init__() - + self._attr_unique_id = f"tuya.{device.id}" self.tuya_device = device self.tuya_device_manager = device_manager @@ -28,16 +29,6 @@ class TuyaHaEntity(Entity): new_max - new_min ) + new_min - @property - def should_poll(self) -> bool: - """Hass should not poll.""" - return False - - @property - def unique_id(self) -> str | None: - """Return a unique ID.""" - return f"tuya.{self.tuya_device.id}" - @property def name(self) -> str | None: """Return Tuya device name.""" From e94bebdf7b2b145de81b427f1f6c21485d2adbbe Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 12 Oct 2021 05:39:51 +0200 Subject: [PATCH 0263/1038] Migrate attribution attribute for Picnic (#57507) --- homeassistant/components/picnic/sensor.py | 3 +-- tests/components/picnic/test_sensor.py | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/picnic/sensor.py b/homeassistant/components/picnic/sensor.py index e0f0d943663..7eafef17982 100644 --- a/homeassistant/components/picnic/sensor.py +++ b/homeassistant/components/picnic/sensor.py @@ -5,7 +5,6 @@ from typing import Any 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.typing import StateType from homeassistant.helpers.update_coordinator import ( @@ -41,8 +40,8 @@ async def async_setup_entry( class PicnicSensor(SensorEntity, CoordinatorEntity): """The CoordinatorEntity subclass representing Picnic sensors.""" + _attr_attribution = ATTRIBUTION entity_description: PicnicSensorEntityDescription - _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} def __init__( self, diff --git a/tests/components/picnic/test_sensor.py b/tests/components/picnic/test_sensor.py index 36aa06443df..2f8fb4cec53 100644 --- a/tests/components/picnic/test_sensor.py +++ b/tests/components/picnic/test_sensor.py @@ -138,6 +138,8 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase): if unit: assert sensor.attributes["unit_of_measurement"] == unit + assert sensor.attributes["attribution"] == "Data provided by Picnic" + async def _setup_platform( self, use_default_responses=False, enable_all_sensors=True ): From 813e27a46a71f14f9095efcd348b5fbd7a5092b7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 12 Oct 2021 05:40:08 +0200 Subject: [PATCH 0264/1038] Migrate attribution attribute for AmberElectric (#57505) --- .../components/amberelectric/binary_sensor.py | 5 ++--- homeassistant/components/amberelectric/sensor.py | 10 ++++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/amberelectric/binary_sensor.py b/homeassistant/components/amberelectric/binary_sensor.py index aff19c6f695..fe6edea18f8 100644 --- a/homeassistant/components/amberelectric/binary_sensor.py +++ b/homeassistant/components/amberelectric/binary_sensor.py @@ -10,7 +10,6 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) 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 @@ -28,6 +27,8 @@ PRICE_SPIKE_ICONS = { class AmberPriceGridSensor(CoordinatorEntity, BinarySensorEntity): """Sensor to show single grid binary values.""" + _attr_attribution = ATTRIBUTION + def __init__( self, coordinator: AmberUpdateCoordinator, @@ -38,7 +39,6 @@ class AmberPriceGridSensor(CoordinatorEntity, BinarySensorEntity): self.site_id = coordinator.site_id self.entity_description = description self._attr_unique_id = f"{coordinator.site_id}-{description.key}" - self._attr_device_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} @property def is_on(self) -> bool | None: @@ -67,7 +67,6 @@ class AmberPriceSpikeBinarySensor(AmberPriceGridSensor): spike_status = self.coordinator.data["grid"]["price_spike"] return { "spike_status": spike_status, - ATTR_ATTRIBUTION: ATTRIBUTION, } diff --git a/homeassistant/components/amberelectric/sensor.py b/homeassistant/components/amberelectric/sensor.py index 974de2d5c15..a1644fb7924 100644 --- a/homeassistant/components/amberelectric/sensor.py +++ b/homeassistant/components/amberelectric/sensor.py @@ -20,7 +20,7 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, CURRENCY_DOLLAR, ENERGY_KILO_WATT_HOUR +from homeassistant.const import CURRENCY_DOLLAR, ENERGY_KILO_WATT_HOUR from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -54,6 +54,8 @@ def friendly_channel_type(channel_type: str) -> str: class AmberSensor(CoordinatorEntity, SensorEntity): """Amber Base Sensor.""" + _attr_attribution = ATTRIBUTION + def __init__( self, coordinator: AmberUpdateCoordinator, @@ -88,7 +90,7 @@ class AmberPriceSensor(AmberSensor): """Return additional pieces of information about the price.""" interval = self.coordinator.data[self.entity_description.key][self.channel_type] - data: dict[str, Any] = {ATTR_ATTRIBUTION: ATTRIBUTION} + data: dict[str, Any] = {} if interval is None: return data @@ -143,7 +145,6 @@ class AmberForecastSensor(AmberSensor): data = { "forecasts": [], "channel_type": intervals[0].channel_type.value, - ATTR_ATTRIBUTION: ATTRIBUTION, } for interval in intervals: @@ -172,6 +173,8 @@ class AmberForecastSensor(AmberSensor): class AmberGridSensor(CoordinatorEntity, SensorEntity): """Sensor to show single grid specific values.""" + _attr_attribution = ATTRIBUTION + def __init__( self, coordinator: AmberUpdateCoordinator, @@ -181,7 +184,6 @@ class AmberGridSensor(CoordinatorEntity, SensorEntity): super().__init__(coordinator) self.site_id = coordinator.site_id self.entity_description = description - self._attr_device_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} self._attr_unique_id = f"{coordinator.site_id}-{description.key}" @property From 1a68784852472c85a5fbb3ae7001283bd7a43b23 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 12 Oct 2021 05:40:21 +0200 Subject: [PATCH 0265/1038] Migrate attribution attribute for UptimeRobot (#57508) --- homeassistant/components/uptimerobot/entity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/uptimerobot/entity.py b/homeassistant/components/uptimerobot/entity.py index 89ff7680eae..6944750ab66 100644 --- a/homeassistant/components/uptimerobot/entity.py +++ b/homeassistant/components/uptimerobot/entity.py @@ -3,7 +3,6 @@ from __future__ import annotations from pyuptimerobot import UptimeRobotMonitor -from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -16,6 +15,8 @@ from .const import ATTR_TARGET, ATTRIBUTION, DOMAIN class UptimeRobotEntity(CoordinatorEntity): """Base UptimeRobot entity.""" + _attr_attribution = ATTRIBUTION + def __init__( self, coordinator: DataUpdateCoordinator, @@ -34,7 +35,6 @@ class UptimeRobotEntity(CoordinatorEntity): "model": self.monitor.type.name, } self._attr_extra_state_attributes = { - ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_TARGET: self.monitor.url, } self._attr_unique_id = str(self.monitor.id) From f561543e929f06486a4f59baf6764a390cf450bf Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 12 Oct 2021 05:40:40 +0200 Subject: [PATCH 0266/1038] Migrate attribution attribute for Arlo (#57504) --- homeassistant/components/arlo/sensor.py | 4 ++-- tests/components/arlo/test_sensor.py | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/arlo/sensor.py b/homeassistant/components/arlo/sensor.py index 57c897cfd59..0cbb7c95f65 100644 --- a/homeassistant/components/arlo/sensor.py +++ b/homeassistant/components/arlo/sensor.py @@ -12,7 +12,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.const import ( - ATTR_ATTRIBUTION, CONCENTRATION_PARTS_PER_MILLION, CONF_MONITORED_CONDITIONS, DEVICE_CLASS_BATTERY, @@ -123,6 +122,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class ArloSensor(SensorEntity): """An implementation of a Netgear Arlo IP sensor.""" + _attr_attribution = ATTRIBUTION + def __init__(self, device, sensor_entry): """Initialize an Arlo sensor.""" self.entity_description = sensor_entry @@ -212,7 +213,6 @@ class ArloSensor(SensorEntity): """Return the device state attributes.""" attrs = {} - attrs[ATTR_ATTRIBUTION] = ATTRIBUTION attrs["brand"] = DEFAULT_BRAND if self.entity_description.key != "total_cameras": diff --git a/tests/components/arlo/test_sensor.py b/tests/components/arlo/test_sensor.py index 2c1f3e26b54..4d6e8f6f228 100644 --- a/tests/components/arlo/test_sensor.py +++ b/tests/components/arlo/test_sensor.py @@ -7,7 +7,6 @@ import pytest from homeassistant.components.arlo import DATA_ARLO, sensor as arlo from homeassistant.components.arlo.sensor import SENSOR_TYPES from homeassistant.const import ( - ATTR_ATTRIBUTION, DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, @@ -179,6 +178,13 @@ def test_device_class(default_sensor, temperature_sensor, humidity_sensor): assert humidity_sensor.device_class == DEVICE_CLASS_HUMIDITY +def test_attribution(default_sensor, temperature_sensor, humidity_sensor): + """Test the device_class property.""" + assert default_sensor.attribution == "Data provided by arlo.netgear.com" + assert temperature_sensor.attribution == "Data provided by arlo.netgear.com" + assert humidity_sensor.attribution == "Data provided by arlo.netgear.com" + + def test_update_total_cameras(cameras_sensor): """Test update method for total_cameras sensor type.""" cameras_sensor.update() @@ -195,7 +201,6 @@ def _test_attributes(hass, sensor_type): data = _get_named_tuple({"model_id": "TEST123"}) sensor = _get_sensor(hass, "test", sensor_type, data) attrs = sensor.extra_state_attributes - assert attrs.get(ATTR_ATTRIBUTION) == "Data provided by arlo.netgear.com" assert attrs.get("brand") == "Netgear Arlo" assert attrs.get("model") == "TEST123" @@ -212,7 +217,6 @@ def test_state_attributes(hass): def test_attributes_total_cameras(cameras_sensor): """Test attributes for total cameras sensor type.""" attrs = cameras_sensor.extra_state_attributes - assert attrs.get(ATTR_ATTRIBUTION) == "Data provided by arlo.netgear.com" assert attrs.get("brand") == "Netgear Arlo" assert attrs.get("model") is None From 3ff30f53a7aaa8ba649aec84d27ed5eca5890640 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 12 Oct 2021 06:08:36 +0200 Subject: [PATCH 0267/1038] Migrate attribution attribute for Stookalert (#57503) --- homeassistant/components/stookalert/binary_sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/stookalert/binary_sensor.py b/homeassistant/components/stookalert/binary_sensor.py index 91634bfffa0..07e89f3c97e 100644 --- a/homeassistant/components/stookalert/binary_sensor.py +++ b/homeassistant/components/stookalert/binary_sensor.py @@ -13,7 +13,6 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_IDENTIFIERS, ATTR_MANUFACTURER, ATTR_MODEL, @@ -83,12 +82,12 @@ async def async_setup_entry( class StookalertBinarySensor(BinarySensorEntity): """Defines a Stookalert binary sensor.""" + _attr_attribution = ATTRIBUTION _attr_device_class = DEVICE_CLASS_SAFETY def __init__(self, client: stookalert.stookalert, entry: ConfigEntry) -> None: """Initialize a Stookalert device.""" self._client = client - self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} self._attr_name = f"Stookalert {entry.data[CONF_PROVINCE]}" self._attr_unique_id = entry.unique_id self._attr_device_info = { From 0139bfa7497cfed160eeaf6016c50cb99e953361 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 12 Oct 2021 06:17:18 +0200 Subject: [PATCH 0268/1038] Detect if mysql and sqlite support row_number (#57475) --- homeassistant/components/recorder/__init__.py | 2 + .../components/recorder/statistics.py | 103 +++++++++++++----- homeassistant/components/recorder/util.py | 28 ++++- tests/components/recorder/test_util.py | 80 +++++++++++--- tests/components/sensor/test_recorder.py | 13 ++- 5 files changed, 180 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 1b090c331a7..7e9bab0ed4e 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -413,6 +413,7 @@ class Recorder(threading.Thread): self.async_migration_event = asyncio.Event() self.migration_in_progress = False self._queue_watcher = None + self._db_supports_row_number = True self.enabled = True @@ -972,6 +973,7 @@ class Recorder(threading.Thread): def setup_recorder_connection(dbapi_connection, connection_record): """Dbapi specific connection settings.""" setup_connection_for_dialect( + self, self.engine.dialect.name, dbapi_connection, not self._completed_first_database_setup, diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index d253d1e2275..200da8d192d 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -89,6 +89,13 @@ QUERY_STATISTICS_SUMMARY_SUM = [ .label("rownum"), ] +QUERY_STATISTICS_SUMMARY_SUM_LEGACY = [ + StatisticsShortTerm.metadata_id, + StatisticsShortTerm.last_reset, + StatisticsShortTerm.state, + StatisticsShortTerm.sum, +] + QUERY_STATISTIC_META = [ StatisticsMeta.id, StatisticsMeta.statistic_id, @@ -275,37 +282,81 @@ def compile_hourly_statistics( } # Get last hour's last sum - subquery = ( - session.query(*QUERY_STATISTICS_SUMMARY_SUM) - .filter(StatisticsShortTerm.start >= bindparam("start_time")) - .filter(StatisticsShortTerm.start < bindparam("end_time")) - .subquery() - ) - query = ( - session.query(subquery) - .filter(subquery.c.rownum == 1) - .order_by(subquery.c.metadata_id) - ) - stats = execute(query.params(start_time=start_time, end_time=end_time)) + if instance._db_supports_row_number: # pylint: disable=[protected-access] + subquery = ( + session.query(*QUERY_STATISTICS_SUMMARY_SUM) + .filter(StatisticsShortTerm.start >= bindparam("start_time")) + .filter(StatisticsShortTerm.start < bindparam("end_time")) + .subquery() + ) + query = ( + session.query(subquery) + .filter(subquery.c.rownum == 1) + .order_by(subquery.c.metadata_id) + ) + stats = execute(query.params(start_time=start_time, end_time=end_time)) - if stats: - for stat in stats: - metadata_id, start, last_reset, state, _sum, _ = stat - if metadata_id in summary: - summary[metadata_id].update( - { + if stats: + for stat in stats: + metadata_id, start, last_reset, state, _sum, _ = stat + if metadata_id in summary: + summary[metadata_id].update( + { + "last_reset": process_timestamp(last_reset), + "state": state, + "sum": _sum, + } + ) + else: + summary[metadata_id] = { + "start": start_time, + "last_reset": process_timestamp(last_reset), + "state": state, + "sum": _sum, + } + else: + baked_query = instance.hass.data[STATISTICS_SHORT_TERM_BAKERY]( + lambda session: session.query(*QUERY_STATISTICS_SUMMARY_SUM_LEGACY) + ) + + baked_query += lambda q: q.filter( + StatisticsShortTerm.start >= bindparam("start_time") + ) + baked_query += lambda q: q.filter( + StatisticsShortTerm.start < bindparam("end_time") + ) + baked_query += lambda q: q.order_by( + StatisticsShortTerm.metadata_id, StatisticsShortTerm.start.desc() + ) + + stats = execute( + baked_query(session).params(start_time=start_time, end_time=end_time) + ) + + if stats: + for metadata_id, group in groupby(stats, lambda stat: stat["metadata_id"]): # type: ignore + ( + metadata_id, + last_reset, + state, + _sum, + ) = next(group) + if metadata_id in summary: + summary[metadata_id].update( + { + "start": start_time, + "last_reset": process_timestamp(last_reset), + "state": state, + "sum": _sum, + } + ) + else: + summary[metadata_id] = { + "start": start_time, "last_reset": process_timestamp(last_reset), "state": state, "sum": _sum, } - ) - else: - summary[metadata_id] = { - "start": start_time, - "last_reset": process_timestamp(last_reset), - "state": state, - "sum": _sum, - } # Insert compiled hourly statistics in the database for metadata_id, stat in summary.items(): diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 101915c7117..567164d4325 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -266,7 +266,18 @@ def execute_on_connection(dbapi_connection, statement): cursor.close() -def setup_connection_for_dialect(dialect_name, dbapi_connection, first_connection): +def query_on_connection(dbapi_connection, statement): + """Execute a single statement with a dbapi connection and return the result.""" + cursor = dbapi_connection.cursor() + cursor.execute(statement) + result = cursor.fetchall() + cursor.close() + return result + + +def setup_connection_for_dialect( + instance, dialect_name, dbapi_connection, first_connection +): """Execute statements needed for dialect connection.""" # Returns False if the the connection needs to be setup # on the next connection, returns True if the connection @@ -280,6 +291,13 @@ def setup_connection_for_dialect(dialect_name, dbapi_connection, first_connectio # WAL mode only needs to be setup once # instead of every time we open the sqlite connection # as its persistent and isn't free to call every time. + result = query_on_connection(dbapi_connection, "SELECT sqlite_version()") + version = result[0][0] + major, minor, _patch = version.split(".", 2) + if int(major) == 3 and int(minor) < 25: + instance._db_supports_row_number = ( # pylint: disable=[protected-access] + False + ) # approximately 8MiB of memory execute_on_connection(dbapi_connection, "PRAGMA cache_size = -8192") @@ -289,6 +307,14 @@ def setup_connection_for_dialect(dialect_name, dbapi_connection, first_connectio if dialect_name == "mysql": execute_on_connection(dbapi_connection, "SET session wait_timeout=28800") + if first_connection: + result = query_on_connection(dbapi_connection, "SELECT VERSION()") + version = result[0][0] + major, minor, _patch = version.split(".", 2) + if int(major) == 5 and int(minor) < 8: + instance._db_supports_row_number = ( # pylint: disable=[protected-access] + False + ) def end_incomplete_runs(session, start_time): diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index f193993ffe5..8b5de5cff16 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -122,44 +122,88 @@ async def test_last_run_was_recently_clean(hass): ) -def test_setup_connection_for_dialect_mysql(): +@pytest.mark.parametrize( + "mysql_version, db_supports_row_number", + [ + ("10.0.0", True), + ("5.8.0", True), + ("5.7.0", False), + ], +) +def test_setup_connection_for_dialect_mysql(mysql_version, db_supports_row_number): """Test setting up the connection for a mysql dialect.""" - execute_mock = MagicMock() + instance_mock = MagicMock(_db_supports_row_number=True) + execute_args = [] close_mock = MagicMock() + def execute_mock(statement): + nonlocal execute_args + execute_args.append(statement) + + def fetchall_mock(): + nonlocal execute_args + if execute_args[-1] == "SELECT VERSION()": + return [[mysql_version]] + return None + def _make_cursor_mock(*_): - return MagicMock(execute=execute_mock, close=close_mock) + return MagicMock(execute=execute_mock, close=close_mock, fetchall=fetchall_mock) dbapi_connection = MagicMock(cursor=_make_cursor_mock) - util.setup_connection_for_dialect("mysql", dbapi_connection, True) + util.setup_connection_for_dialect(instance_mock, "mysql", dbapi_connection, True) - assert execute_mock.call_args[0][0] == "SET session wait_timeout=28800" + assert len(execute_args) == 2 + assert execute_args[0] == "SET session wait_timeout=28800" + assert execute_args[1] == "SELECT VERSION()" + + assert instance_mock._db_supports_row_number == db_supports_row_number -def test_setup_connection_for_dialect_sqlite(): +@pytest.mark.parametrize( + "sqlite_version, db_supports_row_number", + [ + ("3.25.0", True), + ("3.24.0", False), + ], +) +def test_setup_connection_for_dialect_sqlite(sqlite_version, db_supports_row_number): """Test setting up the connection for a sqlite dialect.""" - execute_mock = MagicMock() + instance_mock = MagicMock(_db_supports_row_number=True) + execute_args = [] close_mock = MagicMock() + def execute_mock(statement): + nonlocal execute_args + execute_args.append(statement) + + def fetchall_mock(): + nonlocal execute_args + if execute_args[-1] == "SELECT sqlite_version()": + return [[sqlite_version]] + return None + def _make_cursor_mock(*_): - return MagicMock(execute=execute_mock, close=close_mock) + return MagicMock(execute=execute_mock, close=close_mock, fetchall=fetchall_mock) dbapi_connection = MagicMock(cursor=_make_cursor_mock) - util.setup_connection_for_dialect("sqlite", dbapi_connection, True) + util.setup_connection_for_dialect(instance_mock, "sqlite", dbapi_connection, True) - assert len(execute_mock.call_args_list) == 3 - assert execute_mock.call_args_list[0][0][0] == "PRAGMA journal_mode=WAL" - assert execute_mock.call_args_list[1][0][0] == "PRAGMA cache_size = -8192" - assert execute_mock.call_args_list[2][0][0] == "PRAGMA foreign_keys=ON" + assert len(execute_args) == 4 + assert execute_args[0] == "PRAGMA journal_mode=WAL" + assert execute_args[1] == "SELECT sqlite_version()" + assert execute_args[2] == "PRAGMA cache_size = -8192" + assert execute_args[3] == "PRAGMA foreign_keys=ON" - execute_mock.reset_mock() - util.setup_connection_for_dialect("sqlite", dbapi_connection, False) + execute_args = [] + util.setup_connection_for_dialect(instance_mock, "sqlite", dbapi_connection, False) - assert len(execute_mock.call_args_list) == 2 - assert execute_mock.call_args_list[0][0][0] == "PRAGMA cache_size = -8192" - assert execute_mock.call_args_list[1][0][0] == "PRAGMA foreign_keys=ON" + assert len(execute_args) == 2 + assert execute_args[0] == "PRAGMA cache_size = -8192" + assert execute_args[1] == "PRAGMA foreign_keys=ON" + + assert instance_mock._db_supports_row_number == db_supports_row_number def test_basic_sanity_check(hass_recorder): diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 9ae4b467da5..8a0da39cde3 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -1806,7 +1806,13 @@ def test_compile_hourly_statistics_changing_statistics( assert "Error while processing event StatisticsTask" not in caplog.text -def test_compile_statistics_hourly_summary(hass_recorder, caplog): +@pytest.mark.parametrize( + "db_supports_row_number,in_log,not_in_log", + [(True, "row_number", None), (False, None, "row_number")], +) +def test_compile_statistics_hourly_summary( + hass_recorder, caplog, db_supports_row_number, in_log, not_in_log +): """Test compiling hourly statistics.""" zero = dt_util.utcnow() zero = zero.replace(minute=0, second=0, microsecond=0) @@ -1815,6 +1821,7 @@ def test_compile_statistics_hourly_summary(hass_recorder, caplog): zero += timedelta(hours=1) hass = hass_recorder() recorder = hass.data[DATA_INSTANCE] + recorder._db_supports_row_number = db_supports_row_number setup_component(hass, "sensor", {}) attributes = { "device_class": None, @@ -2052,6 +2059,10 @@ def test_compile_statistics_hourly_summary(hass_recorder, caplog): end += timedelta(hours=1) assert stats == expected_stats assert "Error while processing event StatisticsTask" not in caplog.text + if in_log: + assert in_log in caplog.text + if not_in_log: + assert not_in_log not in caplog.text def record_states(hass, zero, entity_id, attributes, seq=None): From 70aa8b6f00e547b30e513f42014999e61d1c8d01 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 12 Oct 2021 10:15:45 +0200 Subject: [PATCH 0269/1038] Upgrade flake8-comprehensions to 3.7.0 (#57520) --- .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 0a9424e53b8..18bb8aa58c3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,7 @@ repos: - pyflakes==2.3.1 - flake8-docstrings==1.6.0 - pydocstyle==6.0.0 - - flake8-comprehensions==3.5.0 + - flake8-comprehensions==3.7.0 - flake8-noqa==1.1.0 - mccabe==0.6.1 files: ^(homeassistant|script|tests)/.+\.py$ diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index aa3279cea18..ad9e168f3dd 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -3,7 +3,7 @@ bandit==1.7.0 black==21.9b0 codespell==2.0.0 -flake8-comprehensions==3.5.0 +flake8-comprehensions==3.7.0 flake8-docstrings==1.6.0 flake8-noqa==1.1.0 flake8==3.9.2 From 1b71eafeba4cbf2613bf1581aaeff57a1bac7aea Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 12 Oct 2021 10:47:34 +0200 Subject: [PATCH 0270/1038] Upgrade coverage to 6.0.2 (#57518) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 27d9a29ad74..36b8f41fea1 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.0.1 +coverage==6.0.2 jsonpickle==1.4.1 mock-open==1.4.0 mypy==0.910 From d0cc890d2b7722d7d7e466e492092c9512bb622d Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 12 Oct 2021 01:56:57 -0700 Subject: [PATCH 0271/1038] Add statistics support to nest sensors (#57393) Co-authored-by: Franck Nijhof --- homeassistant/components/nest/sensor_sdm.py | 54 +++++---------------- tests/components/nest/sensor_sdm_test.py | 20 ++++++++ 2 files changed, 31 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py index 0034acff3af..52d81ab7dd9 100644 --- a/homeassistant/components/nest/sensor_sdm.py +++ b/homeassistant/components/nest/sensor_sdm.py @@ -7,7 +7,7 @@ from google_nest_sdm.device import Device from google_nest_sdm.device_traits import HumidityTrait, TemperatureTrait from google_nest_sdm.exceptions import GoogleNestException -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, @@ -17,7 +17,6 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_SUBSCRIBER, DOMAIN @@ -58,26 +57,15 @@ async def async_setup_sdm_entry( class SensorBase(SensorEntity): """Representation of a dynamically updated Sensor.""" + _attr_shoud_poll = False + _attr_state_class = STATE_CLASS_MEASUREMENT + def __init__(self, device: Device) -> None: """Initialize the sensor.""" self._device = device self._device_info = NestDeviceInfo(device) - - @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.""" - # The API "name" field is a unique device identifier. - return f"{self._device.name}-{self.device_class}" - - @property - def device_info(self) -> DeviceInfo: - """Return device specific attributes.""" - return self._device_info.device_info + self._attr_unique_id = f"{device.name}-{self.device_class}" + self._attr_device_info = self._device_info.device_info async def async_added_to_hass(self) -> None: """Run when entity is added to register update signal handler.""" @@ -89,6 +77,9 @@ class SensorBase(SensorEntity): class TemperatureSensor(SensorBase): """Representation of a Temperature Sensor.""" + _attr_device_class = DEVICE_CLASS_TEMPERATURE + _attr_native_unit_of_measurement = TEMP_CELSIUS + @property def name(self) -> str: """Return the name of the sensor.""" @@ -100,25 +91,12 @@ class TemperatureSensor(SensorBase): trait: TemperatureTrait = self._device.traits[TemperatureTrait.NAME] return trait.ambient_temperature_celsius - @property - def native_unit_of_measurement(self) -> str: - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def device_class(self) -> str: - """Return the class of this device.""" - return DEVICE_CLASS_TEMPERATURE - class HumiditySensor(SensorBase): """Representation of a Humidity Sensor.""" - @property - def unique_id(self) -> str | None: - """Return a unique ID.""" - # The API returns the identifier under the name field. - return f"{self._device.name}-humidity" + _attr_device_class = DEVICE_CLASS_HUMIDITY + _attr_native_unit_of_measurement = PERCENTAGE @property def name(self) -> str: @@ -130,13 +108,3 @@ class HumiditySensor(SensorBase): """Return the state of the sensor.""" trait: HumidityTrait = self._device.traits[HumidityTrait.NAME] return trait.ambient_humidity_percent - - @property - def native_unit_of_measurement(self) -> str: - """Return the unit of measurement.""" - return PERCENTAGE - - @property - def device_class(self) -> str: - """Return the class of this device.""" - return DEVICE_CLASS_HUMIDITY diff --git a/tests/components/nest/sensor_sdm_test.py b/tests/components/nest/sensor_sdm_test.py index dfdfd58d546..bfac288742d 100644 --- a/tests/components/nest/sensor_sdm_test.py +++ b/tests/components/nest/sensor_sdm_test.py @@ -8,6 +8,15 @@ pubsub subscriber. from google_nest_sdm.device import Device from google_nest_sdm.event import EventMessage +from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, + TEMP_CELSIUS, +) from homeassistant.helpers import device_registry as dr, entity_registry as er from .common import async_setup_sdm_platform @@ -49,10 +58,16 @@ async def test_thermostat_device(hass): temperature = hass.states.get("sensor.my_sensor_temperature") assert temperature is not None assert temperature.state == "25.1" + assert temperature.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert temperature.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert temperature.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT humidity = hass.states.get("sensor.my_sensor_humidity") assert humidity is not None assert humidity.state == "35.0" + assert humidity.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + assert humidity.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY + assert humidity.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT registry = er.async_get(hass) entry = registry.async_get("sensor.my_sensor_temperature") @@ -60,6 +75,11 @@ async def test_thermostat_device(hass): assert entry.original_name == "My Sensor Temperature" assert entry.domain == "sensor" + entry = registry.async_get("sensor.my_sensor_humidity") + assert entry.unique_id == "some-device-id-humidity" + assert entry.original_name == "My Sensor Humidity" + assert entry.domain == "sensor" + device_registry = dr.async_get(hass) device = device_registry.async_get(entry.device_id) assert device.name == "My Sensor" From 931032667c4994b38c51b9ad03009567668c29f3 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 12 Oct 2021 11:23:49 +0200 Subject: [PATCH 0272/1038] Bump `accuweather` library to version 0.3.0 (#57497) --- 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 04b1b4b39c6..fd391a81bad 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.2.0"], + "requirements": ["accuweather==0.3.0"], "codeowners": ["@bieniu"], "config_flow": true, "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 46d058ed4f1..67cd3c16fcb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -88,7 +88,7 @@ WazeRouteCalculator==0.13 abodepy==1.2.0 # homeassistant.components.accuweather -accuweather==0.2.0 +accuweather==0.3.0 # homeassistant.components.bmp280 adafruit-circuitpython-bmp280==3.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b78e1483200..f018c8b40d5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -48,7 +48,7 @@ WazeRouteCalculator==0.13 abodepy==1.2.0 # homeassistant.components.accuweather -accuweather==0.2.0 +accuweather==0.3.0 # homeassistant.components.adax adax==0.1.1 From 879144b48d33640bab959a9e6ce796f6474e655e Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 12 Oct 2021 11:26:38 +0200 Subject: [PATCH 0273/1038] MQTT rework constants (#57529) * reference CONF_TOPIC to .const * Organize common mqtt constants --- homeassistant/components/mqtt/__init__.py | 4 +--- homeassistant/components/mqtt/alarm_control_panel.py | 11 ++--------- homeassistant/components/mqtt/binary_sensor.py | 3 ++- homeassistant/components/mqtt/camera.py | 4 ++-- homeassistant/components/mqtt/climate.py | 10 ++-------- homeassistant/components/mqtt/const.py | 1 + homeassistant/components/mqtt/cover.py | 11 ++--------- homeassistant/components/mqtt/device_trigger.py | 12 +++++++++--- homeassistant/components/mqtt/fan.py | 11 ++--------- homeassistant/components/mqtt/humidifier.py | 11 ++--------- homeassistant/components/mqtt/light/schema_basic.py | 3 ++- homeassistant/components/mqtt/light/schema_json.py | 3 ++- .../components/mqtt/light/schema_template.py | 3 ++- homeassistant/components/mqtt/lock.py | 11 ++--------- homeassistant/components/mqtt/number.py | 11 ++--------- homeassistant/components/mqtt/scene.py | 3 ++- homeassistant/components/mqtt/select.py | 11 ++--------- homeassistant/components/mqtt/sensor.py | 3 ++- homeassistant/components/mqtt/switch.py | 11 ++--------- homeassistant/components/mqtt/tag.py | 10 ++++++++-- homeassistant/components/mqtt/trigger.py | 3 +-- .../components/mqtt/vacuum/schema_legacy.py | 11 ++++++----- homeassistant/components/mqtt/vacuum/schema_state.py | 3 ++- tests/components/mqtt/test_cover.py | 2 +- 24 files changed, 61 insertions(+), 105 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 36402380b33..9ddf50aad43 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -56,6 +56,7 @@ from .const import ( ATTR_TOPIC, CONF_BIRTH_MESSAGE, CONF_BROKER, + CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, @@ -98,9 +99,6 @@ CONF_CLIENT_CERT = "client_cert" CONF_TLS_INSECURE = "tls_insecure" CONF_TLS_VERSION = "tls_version" -CONF_COMMAND_TOPIC = "command_topic" -CONF_TOPIC = "topic" - PROTOCOL_31 = "3.1" DEFAULT_PORT = 1883 diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index f3e8e112f1a..0966497c024 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -33,16 +33,9 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType -from . import ( - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_RETAIN, - CONF_STATE_TOPIC, - DOMAIN, - PLATFORMS, - subscription, -) +from . import PLATFORMS, subscription from .. import mqtt +from .const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN from .debug_info import log_messages from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 66dea3e3aa0..70638f5cc76 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -26,8 +26,9 @@ from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util -from . import CONF_QOS, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, subscription +from . import PLATFORMS, subscription from .. import mqtt +from .const import CONF_QOS, CONF_STATE_TOPIC, DOMAIN from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index ebd6956e8fd..cc331c0008c 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -13,12 +13,12 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType -from . import CONF_QOS, DOMAIN, PLATFORMS, subscription +from . import PLATFORMS, subscription from .. import mqtt +from .const import CONF_QOS, CONF_TOPIC, DOMAIN from .debug_info import log_messages from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper -CONF_TOPIC = "topic" DEFAULT_NAME = "MQTT Camera" MQTT_CAMERA_ATTRIBUTES_BLOCKED = frozenset( diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index ccaa7c65176..fb19c5e7038 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -51,15 +51,9 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType -from . import ( - CONF_QOS, - CONF_RETAIN, - DOMAIN, - MQTT_BASE_PLATFORM_SCHEMA, - PLATFORMS, - subscription, -) +from . import MQTT_BASE_PLATFORM_SCHEMA, PLATFORMS, subscription from .. import mqtt +from .const import CONF_QOS, CONF_RETAIN, DOMAIN from .debug_info import log_messages from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 6435670e92c..c626592b0a3 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -12,6 +12,7 @@ ATTR_TOPIC = "topic" CONF_AVAILABILITY = "availability" CONF_BROKER = "broker" CONF_BIRTH_MESSAGE = "birth_message" +CONF_COMMAND_TOPIC = "command_topic" CONF_QOS = ATTR_QOS CONF_RETAIN = ATTR_RETAIN CONF_STATE_TOPIC = "state_topic" diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index d920d12662f..b48f39908fa 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -36,16 +36,9 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType -from . import ( - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_RETAIN, - CONF_STATE_TOPIC, - DOMAIN, - PLATFORMS, - subscription, -) +from . import PLATFORMS, subscription from .. import mqtt +from .const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN from .debug_info import log_messages from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index fe1bd608305..42bd4d19132 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -30,9 +30,16 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.helpers.typing import ConfigType -from . import CONF_PAYLOAD, CONF_QOS, DOMAIN, debug_info, trigger as mqtt_trigger +from . import debug_info, trigger as mqtt_trigger from .. import mqtt -from .const import ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_TOPIC +from .const import ( + ATTR_DISCOVERY_HASH, + ATTR_DISCOVERY_TOPIC, + CONF_PAYLOAD, + CONF_QOS, + CONF_TOPIC, + DOMAIN, +) from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_UPDATED, clear_discovery_hash from .mixins import ( CONF_CONNECTIONS, @@ -48,7 +55,6 @@ _LOGGER = logging.getLogger(__name__) CONF_AUTOMATION_TYPE = "automation_type" CONF_DISCOVERY_ID = "discovery_id" CONF_SUBTYPE = "subtype" -CONF_TOPIC = "topic" DEFAULT_ENCODING = "utf-8" DEVICE = "device" diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index ee328c1eda5..28a187941bc 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -36,16 +36,9 @@ from homeassistant.util.percentage import ( ranged_value_to_percentage, ) -from . import ( - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_RETAIN, - CONF_STATE_TOPIC, - DOMAIN, - PLATFORMS, - subscription, -) +from . import PLATFORMS, subscription from .. import mqtt +from .const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN from .debug_info import log_messages from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 2f76b416184..c1db0a6d4ee 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -27,16 +27,9 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType -from . import ( - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_RETAIN, - CONF_STATE_TOPIC, - DOMAIN, - PLATFORMS, - subscription, -) +from . import PLATFORMS, subscription from .. import mqtt +from .const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN from .debug_info import log_messages from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index c4af68d3044..9a900c296d3 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -50,8 +50,9 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.color as color_util -from .. import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, subscription +from .. import subscription from ... import mqtt +from ..const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity from .schema import MQTT_LIGHT_SCHEMA_SCHEMA diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 1223b79148a..8538b1169cc 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -56,8 +56,9 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType import homeassistant.util.color as color_util -from .. import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, subscription +from .. import subscription from ... import mqtt +from ..const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity from .schema import MQTT_LIGHT_SCHEMA_SCHEMA diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 9a23d9ea2f9..0c6522efc4f 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -32,8 +32,9 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.color as color_util -from .. import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, subscription +from .. import subscription from ... import mqtt +from ..const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity from .schema import MQTT_LIGHT_SCHEMA_SCHEMA diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 45703fc0cc3..158e3547737 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -11,16 +11,9 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType -from . import ( - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_RETAIN, - CONF_STATE_TOPIC, - DOMAIN, - PLATFORMS, - subscription, -) +from . import PLATFORMS, subscription from .. import mqtt +from .const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN from .debug_info import log_messages from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index d7bce567976..a54fd55adb8 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -18,16 +18,9 @@ from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType -from . import ( - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_STATE_TOPIC, - DOMAIN, - PLATFORMS, - subscription, -) +from . import PLATFORMS, subscription from .. import mqtt -from .const import CONF_RETAIN +from .const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN from .debug_info import log_messages from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index 1d84d1cecae..5d9f7ba376e 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -11,8 +11,9 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType -from . import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, DOMAIN, PLATFORMS +from . import PLATFORMS from .. import mqtt +from .const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, DOMAIN from .mixins import ( MQTT_AVAILABILITY_SCHEMA, MqttAvailability, diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 4f4d3fbb663..439aaccdc3b 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -13,16 +13,9 @@ from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType -from . import ( - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_STATE_TOPIC, - DOMAIN, - PLATFORMS, - subscription, -) +from . import PLATFORMS, subscription from .. import mqtt -from .const import CONF_RETAIN +from .const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN from .debug_info import log_messages from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 0e5cca03ceb..0c0e1e700b7 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -28,8 +28,9 @@ from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util -from . import CONF_QOS, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, subscription +from . import PLATFORMS, subscription from .. import mqtt +from .const import CONF_QOS, CONF_STATE_TOPIC, DOMAIN from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index f383a2ae310..52addc19d12 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -19,16 +19,9 @@ from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType -from . import ( - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_RETAIN, - CONF_STATE_TOPIC, - DOMAIN, - PLATFORMS, - subscription, -) +from . import PLATFORMS, subscription from .. import mqtt +from .const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN from .debug_info import log_messages from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index 4960ff50fb5..908895f180c 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -12,9 +12,15 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) -from . import CONF_QOS, CONF_TOPIC, DOMAIN, subscription +from . import subscription from .. import mqtt -from .const import ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_TOPIC +from .const import ( + ATTR_DISCOVERY_HASH, + ATTR_DISCOVERY_TOPIC, + CONF_QOS, + CONF_TOPIC, + DOMAIN, +) from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_UPDATED, clear_discovery_hash from .mixins import ( CONF_CONNECTIONS, diff --git a/homeassistant/components/mqtt/trigger.py b/homeassistant/components/mqtt/trigger.py index 6be19a1b43a..f9c035dea85 100644 --- a/homeassistant/components/mqtt/trigger.py +++ b/homeassistant/components/mqtt/trigger.py @@ -10,12 +10,11 @@ from homeassistant.core import HassJob, callback from homeassistant.helpers import config_validation as cv, template from .. import mqtt +from .const import CONF_QOS, CONF_TOPIC # mypy: allow-untyped-defs CONF_ENCODING = "encoding" -CONF_QOS = "qos" -CONF_TOPIC = "topic" DEFAULT_ENCODING = "utf-8" DEFAULT_QOS = 0 diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index 009cfe18016..8543fa177c4 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -25,6 +25,7 @@ from homeassistant.helpers.icon import icon_for_battery_level from .. import subscription from ... import mqtt +from ..const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED @@ -147,8 +148,8 @@ PLATFORM_SCHEMA_LEGACY = ( vol.Optional( CONF_SUPPORTED_FEATURES, default=DEFAULT_SERVICE_STRINGS ): vol.All(cv.ensure_list, [vol.In(STRING_TO_SERVICE.keys())]), - vol.Optional(mqtt.CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(mqtt.CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + vol.Optional(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, } ) .extend(MQTT_ENTITY_COMMON_SCHEMA.schema) @@ -192,10 +193,10 @@ class MqttVacuum(MqttEntity, VacuumEntity): supported_feature_strings, STRING_TO_SERVICE ) self._fan_speed_list = config[CONF_FAN_SPEED_LIST] - self._qos = config[mqtt.CONF_QOS] - self._retain = config[mqtt.CONF_RETAIN] + self._qos = config[CONF_QOS] + self._retain = config[CONF_RETAIN] - self._command_topic = config.get(mqtt.CONF_COMMAND_TOPIC) + self._command_topic = config.get(CONF_COMMAND_TOPIC) self._set_fan_speed_topic = config.get(CONF_SET_FAN_SPEED_TOPIC) self._send_command_topic = config.get(CONF_SEND_COMMAND_TOPIC) diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 2c222af28d8..3bc30d9d5cc 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -26,8 +26,9 @@ from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from .. import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, subscription +from .. import subscription from ... import mqtt +from ..const import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 0ee2557fbd6..cd2966f6853 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -10,7 +10,7 @@ from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, ) -from homeassistant.components.mqtt.const import CONF_STATE_TOPIC +from homeassistant.components.mqtt import CONF_STATE_TOPIC from homeassistant.components.mqtt.cover import ( CONF_GET_POSITION_TEMPLATE, CONF_GET_POSITION_TOPIC, From e23d35c6f067e898244905c4912ae6e0a92409e7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 12 Oct 2021 11:32:57 +0200 Subject: [PATCH 0274/1038] Move all Tuya device handling into device listener class (#57523) --- homeassistant/components/tuya/__init__.py | 33 +++++++++++------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 326cacfebbf..8dd9979f3e7 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -132,20 +132,6 @@ async def cleanup_device_registry( break -@callback -def async_remove_hass_device( - hass: HomeAssistant, entry: ConfigEntry, device_id: str -) -> None: - """Remove device from hass cache.""" - device_registry_object = device_registry.async_get(hass) - device_entry = device_registry_object.async_get_device( - identifiers={(DOMAIN, device_id)} - ) - if device_entry is not None: - device_registry_object.async_remove_device(device_entry.id) - hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].discard(device_id) - - async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unloading the Tuya platforms.""" _LOGGER.debug("integration unload") @@ -191,9 +177,7 @@ class DeviceListener(TuyaDeviceListener): *self.hass.data[DOMAIN][self.entry.entry_id][TUYA_HA_TUYA_MAP].values() ): ha_tuya_map = self.hass.data[DOMAIN][self.entry.entry_id][TUYA_HA_TUYA_MAP] - self.hass.add_job( - async_remove_hass_device, self.hass, self.entry, device.id - ) + self.hass.add_job(self.async_remove_device, device.id) for domain, tuya_list in ha_tuya_map.items(): if device.category in tuya_list: @@ -224,4 +208,17 @@ class DeviceListener(TuyaDeviceListener): def remove_device(self, device_id: str) -> None: """Add device removed listener.""" _LOGGER.debug("tuya remove device:%s", device_id) - self.hass.add_job(async_remove_hass_device, self.hass, self.entry, device_id) + self.hass.add_job(self.async_remove_device, device_id) + + @callback + def async_remove_device(self, device_id: str) -> None: + """Remove device from Home Assistant.""" + device_registry_object = device_registry.async_get(self.hass) + device_entry = device_registry_object.async_get_device( + identifiers={(DOMAIN, device_id)} + ) + if device_entry is not None: + device_registry_object.async_remove_device(device_entry.id) + self.hass.data[DOMAIN][self.entry.entry_id][TUYA_HA_DEVICES].discard( + device_id + ) From d44e323e951bfc3851d8a4156b5e4639ec6c2729 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 12 Oct 2021 11:34:18 +0200 Subject: [PATCH 0275/1038] Move Tuya remap method from base to light entity class (#57527) --- homeassistant/components/tuya/base.py | 7 ------- homeassistant/components/tuya/light.py | 7 +++++++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index 2ddfe484ffd..a8bb492f17b 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -22,13 +22,6 @@ class TuyaHaEntity(Entity): self.tuya_device = device self.tuya_device_manager = device_manager - @staticmethod - def remap(old_value, old_min, old_max, new_min, new_max): - """Remap old_value to new_value.""" - return ((old_value - old_min) / (old_max - old_min)) * ( - new_max - new_min - ) + new_min - @property def name(self) -> str | None: """Return Tuya device name.""" diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 6a119e71ba9..f2136da431f 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -384,3 +384,10 @@ class TuyaHaLight(TuyaHaEntity, LightEntity): ): color_modes.append(COLOR_MODE_HS) return set(color_modes) + + @staticmethod + def remap(old_value, old_min, old_max, new_min, new_max): + """Remap old_value to new_value.""" + return ((old_value - old_min) / (old_max - old_min)) * ( + new_max - new_min + ) + new_min From c4f8c52df9efc7e14822d7c40e68f307bed4c627 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 12 Oct 2021 11:46:23 +0200 Subject: [PATCH 0276/1038] Use EntityDescription - bmw_connected_drive (#56861) --- .../bmw_connected_drive/binary_sensor.py | 131 ++++++++++-------- 1 file changed, 73 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index 225ec5f7f99..d2d2aa9d42f 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -1,4 +1,6 @@ """Reads vehicle status from BMW connected drive portal.""" +from __future__ import annotations + import logging from bimmer_connected.state import ChargingState, LockState @@ -8,6 +10,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_PLUG, DEVICE_CLASS_PROBLEM, BinarySensorEntity, + BinarySensorEntityDescription, ) from homeassistant.const import LENGTH_KILOMETERS @@ -16,89 +19,101 @@ from .const import CONF_ACCOUNT, DATA_ENTRIES _LOGGER = logging.getLogger(__name__) -SENSOR_TYPES = { - "lids": ["Doors", DEVICE_CLASS_OPENING, "mdi:car-door-lock"], - "windows": ["Windows", DEVICE_CLASS_OPENING, "mdi:car-door"], - "door_lock_state": ["Door lock state", "lock", "mdi:car-key"], - "lights_parking": ["Parking lights", "light", "mdi:car-parking-lights"], - "condition_based_services": [ - "Condition based services", - DEVICE_CLASS_PROBLEM, - "mdi:wrench", - ], - "check_control_messages": [ - "Control messages", - DEVICE_CLASS_PROBLEM, - "mdi:car-tire-alert", - ], -} - -SENSOR_TYPES_ELEC = { - "charging_status": ["Charging status", "power", "mdi:ev-station"], - "connection_status": ["Connection status", DEVICE_CLASS_PLUG, "mdi:car-electric"], -} - -SENSOR_TYPES_ELEC.update(SENSOR_TYPES) +SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( + BinarySensorEntityDescription( + key="lids", + name="Doors", + device_class=DEVICE_CLASS_OPENING, + icon="mdi:car-door-lock", + ), + BinarySensorEntityDescription( + key="windows", + name="Windows", + device_class=DEVICE_CLASS_OPENING, + icon="mdi:car-door", + ), + BinarySensorEntityDescription( + key="door_lock_state", + name="Door lock state", + device_class="lock", + icon="mdi:car-key", + ), + BinarySensorEntityDescription( + key="lights_parking", + name="Parking lights", + device_class="light", + icon="mdi:car-parking-lights", + ), + BinarySensorEntityDescription( + key="condition_based_services", + name="Condition based services", + device_class=DEVICE_CLASS_PROBLEM, + icon="mdi:wrench", + ), + BinarySensorEntityDescription( + key="check_control_messages", + name="Control messages", + device_class=DEVICE_CLASS_PROBLEM, + icon="mdi:car-tire-alert", + ), + # electric + BinarySensorEntityDescription( + key="charging_status", + name="Charging status", + device_class="power", + icon="mdi:ev-station", + ), + BinarySensorEntityDescription( + key="connection_status", + name="Connection status", + device_class=DEVICE_CLASS_PLUG, + icon="mdi:car-electric", + ), +) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the BMW ConnectedDrive binary sensors from config entry.""" account = hass.data[BMW_DOMAIN][DATA_ENTRIES][config_entry.entry_id][CONF_ACCOUNT] - entities = [] - for vehicle in account.account.vehicles: - if vehicle.has_hv_battery: - _LOGGER.debug("BMW with a high voltage battery") - for key, value in sorted(SENSOR_TYPES_ELEC.items()): - if key in vehicle.available_attributes: - device = BMWConnectedDriveSensor( - account, vehicle, key, value[0], value[1], value[2] - ) - entities.append(device) - elif vehicle.has_internal_combustion_engine: - _LOGGER.debug("BMW with an internal combustion engine") - for key, value in sorted(SENSOR_TYPES.items()): - if key in vehicle.available_attributes: - device = BMWConnectedDriveSensor( - account, vehicle, key, value[0], value[1], value[2] - ) - entities.append(device) + entities = [ + BMWConnectedDriveSensor(account, vehicle, description) + for vehicle in account.account.vehicles + for description in SENSOR_TYPES + if description.key in vehicle.available_attributes + ] async_add_entities(entities, True) class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity): """Representation of a BMW vehicle binary sensor.""" - def __init__( - self, account, vehicle, attribute: str, sensor_name, device_class, icon - ): + def __init__(self, account, vehicle, description: BinarySensorEntityDescription): """Initialize sensor.""" super().__init__(account, vehicle) + self.entity_description = description - self._attribute = attribute - self._attr_name = f"{vehicle.name} {attribute}" - self._attr_unique_id = f"{vehicle.vin}-{attribute}" - self._sensor_name = sensor_name - self._attr_device_class = device_class - self._attr_icon = icon + self._attr_name = f"{vehicle.name} {description.key}" + self._attr_unique_id = f"{vehicle.vin}-{description.key}" def update(self): """Read new state data from the library.""" + sensor_type = self.entity_description.key vehicle_state = self._vehicle.state result = self._attrs.copy() # device class opening: On means open, Off means closed - if self._attribute == "lids": + if sensor_type == "lids": _LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed) self._attr_is_on = not vehicle_state.all_lids_closed for lid in vehicle_state.lids: result[lid.name] = lid.state.value - elif self._attribute == "windows": + elif sensor_type == "windows": self._attr_is_on = not vehicle_state.all_windows_closed for window in vehicle_state.windows: result[window.name] = window.state.value # device class lock: On means unlocked, Off means locked - elif self._attribute == "door_lock_state": + elif sensor_type == "door_lock_state": # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED self._attr_is_on = vehicle_state.door_lock_state not in [ LockState.LOCKED, @@ -107,15 +122,15 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity): result["door_lock_state"] = vehicle_state.door_lock_state.value result["last_update_reason"] = vehicle_state.last_update_reason # device class light: On means light detected, Off means no light - elif self._attribute == "lights_parking": + elif sensor_type == "lights_parking": self._attr_is_on = vehicle_state.are_parking_lights_on result["lights_parking"] = vehicle_state.parking_lights.value # device class problem: On means problem detected, Off means no problem - elif self._attribute == "condition_based_services": + elif sensor_type == "condition_based_services": self._attr_is_on = not vehicle_state.are_all_cbs_ok for report in vehicle_state.condition_based_services: result.update(self._format_cbs_report(report)) - elif self._attribute == "check_control_messages": + elif sensor_type == "check_control_messages": self._attr_is_on = vehicle_state.has_check_control_messages check_control_messages = vehicle_state.check_control_messages has_check_control_messages = vehicle_state.has_check_control_messages @@ -127,13 +142,13 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity): else: result["check_control_messages"] = "OK" # device class power: On means power detected, Off means no power - elif self._attribute == "charging_status": + elif sensor_type == "charging_status": self._attr_is_on = vehicle_state.charging_status in [ChargingState.CHARGING] result["charging_status"] = vehicle_state.charging_status.value result["last_charging_end_result"] = vehicle_state.last_charging_end_result # device class plug: On means device is plugged in, # Off means device is unplugged - elif self._attribute == "connection_status": + elif sensor_type == "connection_status": self._attr_is_on = vehicle_state.connection_status == "CONNECTED" result["connection_status"] = vehicle_state.connection_status From bdbedd0f069da7a233b8c6455d8f65125de3d4a4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 12 Oct 2021 12:08:59 +0200 Subject: [PATCH 0277/1038] Alphabetize parts of device registry code (#57532) --- .../components/config/device_registry.py | 14 +-- homeassistant/helpers/device_registry.py | 96 +++++++++---------- homeassistant/helpers/entity.py | 10 +- homeassistant/helpers/entity_platform.py | 12 +-- 4 files changed, 66 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index 4363fbbbe4d..1dd8fbe4167 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -67,17 +67,17 @@ async def websocket_update_device(hass, connection, msg): def _entry_dict(entry): """Convert entry to API format.""" return { + "area_id": entry.area_id, "config_entries": list(entry.config_entries), "connections": list(entry.connections), - "manufacturer": entry.manufacturer, - "model": entry.model, - "name": entry.name, - "sw_version": entry.sw_version, + "disabled_by": entry.disabled_by, "entry_type": entry.entry_type, "id": entry.id, "identifiers": list(entry.identifiers), - "via_device_id": entry.via_device_id, - "area_id": entry.area_id, + "manufacturer": entry.manufacturer, + "model": entry.model, "name_by_user": entry.name_by_user, - "disabled_by": entry.disabled_by, + "name": entry.name, + "sw_version": entry.sw_version, + "via_device_id": entry.via_device_id, } diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index f3051e9dfd7..9348c112942 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -53,20 +53,9 @@ class _DeviceIndex(NamedTuple): class DeviceEntry: """Device Registry Entry.""" + area_id: str | None = attr.ib(default=None) config_entries: set[str] = attr.ib(converter=set, factory=set) connections: set[tuple[str, str]] = attr.ib(converter=set, factory=set) - identifiers: set[tuple[str, str]] = attr.ib(converter=set, factory=set) - manufacturer: str | None = attr.ib(default=None) - model: str | None = attr.ib(default=None) - name: str | None = attr.ib(default=None) - sw_version: str | None = attr.ib(default=None) - via_device_id: str | None = attr.ib(default=None) - area_id: str | None = attr.ib(default=None) - name_by_user: str | None = attr.ib(default=None) - entry_type: str | None = attr.ib(default=None) - id: str = attr.ib(factory=uuid_util.random_uuid_hex) - # This value is not stored, just used to keep track of events to fire. - is_new: bool = attr.ib(default=False) disabled_by: str | None = attr.ib( default=None, validator=attr.validators.in_( @@ -78,7 +67,18 @@ class DeviceEntry: ) ), ) + entry_type: str | None = attr.ib(default=None) + id: str = attr.ib(factory=uuid_util.random_uuid_hex) + identifiers: set[tuple[str, str]] = attr.ib(converter=set, factory=set) + manufacturer: str | None = attr.ib(default=None) + model: str | None = attr.ib(default=None) + name_by_user: str | None = attr.ib(default=None) + name: str | None = attr.ib(default=None) suggested_area: str | None = attr.ib(default=None) + sw_version: str | None = attr.ib(default=None) + via_device_id: str | None = attr.ib(default=None) + # This value is not stored, just used to keep track of events to fire. + is_new: bool = attr.ib(default=False) @property def disabled(self) -> bool: @@ -245,19 +245,19 @@ class DeviceRegistry: *, config_entry_id: str, connections: set[tuple[str, str]] | None = None, + default_manufacturer: str | None | UndefinedType = UNDEFINED, + default_model: str | None | UndefinedType = UNDEFINED, + default_name: str | None | UndefinedType = UNDEFINED, + # To disable a device if it gets created + disabled_by: str | None | UndefinedType = UNDEFINED, + entry_type: str | None | UndefinedType = UNDEFINED, identifiers: set[tuple[str, str]] | None = None, manufacturer: str | None | UndefinedType = UNDEFINED, model: str | None | UndefinedType = UNDEFINED, name: str | None | UndefinedType = UNDEFINED, - default_manufacturer: str | None | UndefinedType = UNDEFINED, - default_model: str | None | UndefinedType = UNDEFINED, - default_name: str | None | UndefinedType = UNDEFINED, - sw_version: str | None | UndefinedType = UNDEFINED, - entry_type: str | None | UndefinedType = UNDEFINED, - via_device: tuple[str, str] | None = None, - # To disable a device if it gets created - disabled_by: str | None | UndefinedType = UNDEFINED, suggested_area: str | None | UndefinedType = UNDEFINED, + sw_version: str | None | UndefinedType = UNDEFINED, + via_device: tuple[str, str] | None = None, ) -> DeviceEntry: """Get device. Create if it doesn't exist.""" if not identifiers and not connections: @@ -302,16 +302,16 @@ class DeviceRegistry: device = self._async_update_device( device.id, add_config_entry_id=config_entry_id, - via_device_id=via_device_id, + disabled_by=disabled_by, + entry_type=entry_type, + manufacturer=manufacturer, merge_connections=connections or UNDEFINED, merge_identifiers=identifiers or UNDEFINED, - manufacturer=manufacturer, model=model, name=name, - sw_version=sw_version, - entry_type=entry_type, - disabled_by=disabled_by, suggested_area=suggested_area, + sw_version=sw_version, + via_device_id=via_device_id, ) # This is safe because _async_update_device will always return a device @@ -324,34 +324,34 @@ class DeviceRegistry: self, device_id: str, *, + add_config_entry_id: str | UndefinedType = UNDEFINED, area_id: str | None | UndefinedType = UNDEFINED, + disabled_by: str | None | UndefinedType = UNDEFINED, manufacturer: str | None | UndefinedType = UNDEFINED, model: str | None | UndefinedType = UNDEFINED, - name: str | None | UndefinedType = UNDEFINED, name_by_user: str | None | UndefinedType = UNDEFINED, + name: str | None | UndefinedType = UNDEFINED, new_identifiers: set[tuple[str, str]] | UndefinedType = UNDEFINED, + remove_config_entry_id: str | UndefinedType = UNDEFINED, + suggested_area: str | None | UndefinedType = UNDEFINED, sw_version: str | None | UndefinedType = UNDEFINED, via_device_id: str | None | UndefinedType = UNDEFINED, - add_config_entry_id: str | UndefinedType = UNDEFINED, - remove_config_entry_id: str | UndefinedType = UNDEFINED, - disabled_by: str | None | UndefinedType = UNDEFINED, - suggested_area: str | None | UndefinedType = UNDEFINED, ) -> DeviceEntry | None: """Update properties of a device.""" return self._async_update_device( device_id, + add_config_entry_id=add_config_entry_id, area_id=area_id, + disabled_by=disabled_by, manufacturer=manufacturer, model=model, - name=name, name_by_user=name_by_user, + name=name, new_identifiers=new_identifiers, + remove_config_entry_id=remove_config_entry_id, + suggested_area=suggested_area, sw_version=sw_version, via_device_id=via_device_id, - add_config_entry_id=add_config_entry_id, - remove_config_entry_id=remove_config_entry_id, - disabled_by=disabled_by, - suggested_area=suggested_area, ) @callback @@ -360,20 +360,20 @@ class DeviceRegistry: device_id: str, *, add_config_entry_id: str | UndefinedType = UNDEFINED, - remove_config_entry_id: str | UndefinedType = UNDEFINED, + area_id: str | None | UndefinedType = UNDEFINED, + disabled_by: str | None | UndefinedType = UNDEFINED, + entry_type: str | None | UndefinedType = UNDEFINED, + manufacturer: str | None | UndefinedType = UNDEFINED, merge_connections: set[tuple[str, str]] | UndefinedType = UNDEFINED, merge_identifiers: set[tuple[str, str]] | UndefinedType = UNDEFINED, - new_identifiers: set[tuple[str, str]] | UndefinedType = UNDEFINED, - manufacturer: str | None | UndefinedType = UNDEFINED, model: str | None | UndefinedType = UNDEFINED, - name: str | None | UndefinedType = UNDEFINED, - sw_version: str | None | UndefinedType = UNDEFINED, - entry_type: str | None | UndefinedType = UNDEFINED, - via_device_id: str | None | UndefinedType = UNDEFINED, - area_id: str | None | UndefinedType = UNDEFINED, name_by_user: str | None | UndefinedType = UNDEFINED, - disabled_by: str | None | UndefinedType = UNDEFINED, + name: str | None | UndefinedType = UNDEFINED, + new_identifiers: set[tuple[str, str]] | UndefinedType = UNDEFINED, + remove_config_entry_id: str | UndefinedType = UNDEFINED, suggested_area: str | None | UndefinedType = UNDEFINED, + sw_version: str | None | UndefinedType = UNDEFINED, + via_device_id: str | None | UndefinedType = UNDEFINED, ) -> DeviceEntry | None: """Update device attributes.""" old = self.devices[device_id] @@ -424,14 +424,14 @@ class DeviceRegistry: changes["identifiers"] = new_identifiers for attr_name, value in ( + ("disabled_by", disabled_by), + ("entry_type", entry_type), ("manufacturer", manufacturer), ("model", model), ("name", name), - ("sw_version", sw_version), - ("entry_type", entry_type), - ("via_device_id", via_device_id), - ("disabled_by", disabled_by), ("suggested_area", suggested_area), + ("sw_version", sw_version), + ("via_device_id", via_device_id), ): if value is not UNDEFINED and value != getattr(old, attr_name): changes[attr_name] = value diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 122d04b0bf9..1d3b331b8ab 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -158,18 +158,18 @@ def get_unit_of_measurement(hass: HomeAssistant, entity_id: str) -> str | None: class DeviceInfo(TypedDict, total=False): """Entity device information for device registry.""" - name: str | None connections: set[tuple[str, str]] + default_manufacturer: str + default_model: str + default_name: str + entry_type: str | None identifiers: set[tuple[str, str]] manufacturer: str | None model: str | None + name: str | None suggested_area: str | None sw_version: str | None via_device: tuple[str, str] - entry_type: str | None - default_name: str - default_manufacturer: str - default_model: str @dataclass diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index c212645325c..831f6c9a4b6 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -461,17 +461,17 @@ class EntityPlatform: processed_dev_info = {"config_entry_id": config_entry_id} for key in ( "connections", + "default_manufacturer", + "default_model", + "default_name", + "entry_type", "identifiers", "manufacturer", "model", "name", - "default_manufacturer", - "default_model", - "default_name", - "sw_version", - "entry_type", - "via_device", "suggested_area", + "sw_version", + "via_device", ): if key in device_info: processed_dev_info[key] = device_info[key] # type: ignore[misc] From d90d804260b93dece10ba75258b1534c82f10141 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 12 Oct 2021 12:09:12 +0200 Subject: [PATCH 0278/1038] Bump Mill library to 0.6.2 (#57533) --- homeassistant/components/mill/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index e5dbbdfc1e8..55aeec305fb 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -2,7 +2,7 @@ "domain": "mill", "name": "Mill", "documentation": "https://www.home-assistant.io/integrations/mill", - "requirements": ["millheater==0.6.1"], + "requirements": ["millheater==0.6.2"], "codeowners": ["@danielhiversen"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 67cd3c16fcb..1fe2fe190f0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1002,7 +1002,7 @@ micloud==0.3 miflora==0.7.0 # homeassistant.components.mill -millheater==0.6.1 +millheater==0.6.2 # homeassistant.components.minio minio==4.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f018c8b40d5..49ac409ba10 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -591,7 +591,7 @@ mficlient==0.3.0 micloud==0.3 # homeassistant.components.mill -millheater==0.6.1 +millheater==0.6.2 # homeassistant.components.minio minio==4.0.9 From c943677675b21fd2b79312592246d6f3c35c1edc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 12 Oct 2021 12:25:03 +0200 Subject: [PATCH 0279/1038] Tweaks to Tuya base entity (#57526) --- homeassistant/components/tuya/base.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index a8bb492f17b..0b2ca643e12 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -6,7 +6,7 @@ from typing import Any from tuya_iot import TuyaDevice, TuyaDeviceManager from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN, TUYA_HA_SIGNAL_UPDATE_ENTITY @@ -28,21 +28,21 @@ class TuyaHaEntity(Entity): return self.tuya_device.name @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" - return { - "identifiers": {(DOMAIN, f"{self.tuya_device.id}")}, - "manufacturer": "Tuya", - "name": self.tuya_device.name, - "model": self.tuya_device.product_name, - } + return DeviceInfo( + identifiers={(DOMAIN, self.tuya_device.id)}, + manufacturer="Tuya", + name=self.tuya_device.name, + model=self.tuya_device.product_name, + ) @property def available(self) -> bool: """Return if the device is available.""" return self.tuya_device.online - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" self.async_on_remove( async_dispatcher_connect( From f82af47f6a5e5ea1a9a2f259f63317c83797df3f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 12 Oct 2021 14:33:19 +0200 Subject: [PATCH 0280/1038] Use Tuya endpoints values from upstream library (#57537) --- homeassistant/components/tuya/const.py | 87 +++++++++++------------ tests/components/tuya/test_config_flow.py | 4 +- 2 files changed, 43 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 44b66b576e3..e4ecfbd4bf3 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -1,6 +1,8 @@ """Constants for the Tuya integration.""" from dataclasses import dataclass +from tuya_iot import TuyaCloudOpenAPIEndpoint + DOMAIN = "tuya" CONF_AUTH_TYPE = "auth_type" @@ -31,13 +33,6 @@ TUYA_HA_SIGNAL_UPDATE_ENTITY = "tuya_entry_update" TUYA_SMART_APP = "tuyaSmart" SMARTLIFE_APP = "smartlife" -ENDPOINT_AMERICA = "https://openapi.tuyaus.com" -ENDPOINT_CHINA = "https://openapi.tuyacn.com" -ENDPOINT_EASTERN_AMERICA = "https://openapi-ueaz.tuyaus.com" -ENDPOINT_EUROPE = "https://openapi.tuyaeu.com" -ENDPOINT_INDIA = "https://openapi.tuyain.com" -ENDPOINT_WESTERN_EUROPE = "https://openapi-weaz.tuyaeu.com" - PLATFORMS = ["climate", "fan", "light", "scene", "switch"] @@ -47,7 +42,7 @@ class Country: name: str country_code: str - endpoint: str = ENDPOINT_AMERICA + endpoint: str = TuyaCloudOpenAPIEndpoint.AMERICA # https://developer.tuya.com/en/docs/iot/oem-app-data-center-distributed?id=Kafi0ku9l07qb#title-4-China%20Data%20Center @@ -61,18 +56,18 @@ TUYA_COUNTRIES = [ Country("Anguilla", "1-264"), Country("Antarctica", "672"), Country("Antigua and Barbuda", "1-268"), - Country("Argentina", "54", ENDPOINT_EUROPE), + Country("Argentina", "54", TuyaCloudOpenAPIEndpoint.EUROPE), Country("Armenia", "374"), Country("Aruba", "297"), Country("Australia", "61"), - Country("Austria", "43", ENDPOINT_EUROPE), + Country("Austria", "43", TuyaCloudOpenAPIEndpoint.EUROPE), Country("Azerbaijan", "994"), Country("Bahamas", "1-242"), Country("Bahrain", "973"), Country("Bangladesh", "880"), Country("Barbados", "1-246"), Country("Belarus", "375"), - Country("Belgium", "32", ENDPOINT_EUROPE), + Country("Belgium", "32", TuyaCloudOpenAPIEndpoint.EUROPE), Country("Belize", "501"), Country("Benin", "229"), Country("Bermuda", "1-441"), @@ -80,7 +75,7 @@ TUYA_COUNTRIES = [ Country("Bolivia", "591"), Country("Bosnia and Herzegovina", "387"), Country("Botswana", "267"), - Country("Brazil", "55", ENDPOINT_EUROPE), + Country("Brazil", "55", TuyaCloudOpenAPIEndpoint.EUROPE), Country("British Indian Ocean Territory", "246"), Country("British Virgin Islands", "1-284"), Country("Brunei", "673"), @@ -89,26 +84,26 @@ TUYA_COUNTRIES = [ Country("Burundi", "257"), Country("Cambodia", "855"), Country("Cameroon", "237"), - Country("Canada", "1", ENDPOINT_AMERICA), + Country("Canada", "1", TuyaCloudOpenAPIEndpoint.AMERICA), Country("Cape Verde", "238"), Country("Cayman Islands", "1-345"), Country("Central African Republic", "236"), Country("Chad", "235"), Country("Chile", "56"), - Country("China", "86", ENDPOINT_CHINA), + Country("China", "86", TuyaCloudOpenAPIEndpoint.CHINA), Country("Christmas Island", "61"), Country("Cocos Islands", "61"), Country("Colombia", "57"), Country("Comoros", "269"), Country("Cook Islands", "682"), Country("Costa Rica", "506"), - Country("Croatia", "385", ENDPOINT_EUROPE), + Country("Croatia", "385", TuyaCloudOpenAPIEndpoint.EUROPE), Country("Cuba", "53"), Country("Curacao", "599"), - Country("Cyprus", "357", ENDPOINT_EUROPE), - Country("Czech Republic", "420", ENDPOINT_EUROPE), + Country("Cyprus", "357", TuyaCloudOpenAPIEndpoint.EUROPE), + Country("Czech Republic", "420", TuyaCloudOpenAPIEndpoint.EUROPE), Country("Democratic Republic of the Congo", "243"), - Country("Denmark", "45", ENDPOINT_EUROPE), + Country("Denmark", "45", TuyaCloudOpenAPIEndpoint.EUROPE), Country("Djibouti", "253"), Country("Dominica", "1-767"), Country("Dominican Republic", "1-809"), @@ -118,21 +113,21 @@ TUYA_COUNTRIES = [ Country("El Salvador", "503"), Country("Equatorial Guinea", "240"), Country("Eritrea", "291"), - Country("Estonia", "372", ENDPOINT_EUROPE), + Country("Estonia", "372", TuyaCloudOpenAPIEndpoint.EUROPE), Country("Ethiopia", "251"), Country("Falkland Islands", "500"), Country("Faroe Islands", "298"), Country("Fiji", "679"), - Country("Finland", "358", ENDPOINT_EUROPE), - Country("France", "33", ENDPOINT_EUROPE), + Country("Finland", "358", TuyaCloudOpenAPIEndpoint.EUROPE), + Country("France", "33", TuyaCloudOpenAPIEndpoint.EUROPE), Country("French Polynesia", "689"), Country("Gabon", "241"), Country("Gambia", "220"), Country("Georgia", "995"), - Country("Germany", "49", ENDPOINT_EUROPE), + Country("Germany", "49", TuyaCloudOpenAPIEndpoint.EUROPE), Country("Ghana", "233"), Country("Gibraltar", "350"), - Country("Greece", "30", ENDPOINT_EUROPE), + Country("Greece", "30", TuyaCloudOpenAPIEndpoint.EUROPE), Country("Greenland", "299"), Country("Grenada", "1-473"), Country("Guam", "1-671"), @@ -144,19 +139,19 @@ TUYA_COUNTRIES = [ Country("Haiti", "509"), Country("Honduras", "504"), Country("Hong Kong", "852"), - Country("Hungary", "36", ENDPOINT_EUROPE), - Country("Iceland", "354", ENDPOINT_EUROPE), - Country("India", "91", ENDPOINT_INDIA), + Country("Hungary", "36", TuyaCloudOpenAPIEndpoint.EUROPE), + Country("Iceland", "354", TuyaCloudOpenAPIEndpoint.EUROPE), + Country("India", "91", TuyaCloudOpenAPIEndpoint.INDIA), Country("Indonesia", "62"), Country("Iran", "98"), Country("Iraq", "964"), - Country("Ireland", "353", ENDPOINT_EUROPE), + Country("Ireland", "353", TuyaCloudOpenAPIEndpoint.EUROPE), Country("Isle of Man", "44-1624"), Country("Israel", "972"), - Country("Italy", "39", ENDPOINT_EUROPE), + Country("Italy", "39", TuyaCloudOpenAPIEndpoint.EUROPE), Country("Ivory Coast", "225"), Country("Jamaica", "1-876"), - Country("Japan", "81", ENDPOINT_EUROPE), + Country("Japan", "81", TuyaCloudOpenAPIEndpoint.EUROPE), Country("Jersey", "44-1534"), Country("Jordan", "962"), Country("Kazakhstan", "7"), @@ -166,14 +161,14 @@ TUYA_COUNTRIES = [ Country("Kuwait", "965"), Country("Kyrgyzstan", "996"), Country("Laos", "856"), - Country("Latvia", "371", ENDPOINT_EUROPE), + Country("Latvia", "371", TuyaCloudOpenAPIEndpoint.EUROPE), Country("Lebanon", "961"), Country("Lesotho", "266"), Country("Liberia", "231"), Country("Libya", "218"), - Country("Liechtenstein", "423", ENDPOINT_EUROPE), - Country("Lithuania", "370", ENDPOINT_EUROPE), - Country("Luxembourg", "352", ENDPOINT_EUROPE), + Country("Liechtenstein", "423", TuyaCloudOpenAPIEndpoint.EUROPE), + Country("Lithuania", "370", TuyaCloudOpenAPIEndpoint.EUROPE), + Country("Luxembourg", "352", TuyaCloudOpenAPIEndpoint.EUROPE), Country("Macau", "853"), Country("Macedonia", "389"), Country("Madagascar", "261"), @@ -181,7 +176,7 @@ TUYA_COUNTRIES = [ Country("Malaysia", "60"), Country("Maldives", "960"), Country("Mali", "223"), - Country("Malta", "356", ENDPOINT_EUROPE), + Country("Malta", "356", TuyaCloudOpenAPIEndpoint.EUROPE), Country("Marshall Islands", "692"), Country("Mauritania", "222"), Country("Mauritius", "230"), @@ -199,7 +194,7 @@ TUYA_COUNTRIES = [ Country("Namibia", "264"), Country("Nauru", "674"), Country("Nepal", "977"), - Country("Netherlands", "31", ENDPOINT_EUROPE), + Country("Netherlands", "31", TuyaCloudOpenAPIEndpoint.EUROPE), Country("Netherlands Antilles", "599"), Country("New Caledonia", "687"), Country("New Zealand", "64"), @@ -220,14 +215,14 @@ TUYA_COUNTRIES = [ Country("Peru", "51"), Country("Philippines", "63"), Country("Pitcairn", "64"), - Country("Poland", "48", ENDPOINT_EUROPE), - Country("Portugal", "351", ENDPOINT_EUROPE), + Country("Poland", "48", TuyaCloudOpenAPIEndpoint.EUROPE), + Country("Portugal", "351", TuyaCloudOpenAPIEndpoint.EUROPE), Country("Puerto Rico", "1-787, 1-939"), Country("Qatar", "974"), Country("Republic of the Congo", "242"), Country("Reunion", "262"), - Country("Romania", "40", ENDPOINT_EUROPE), - Country("Russia", "7", ENDPOINT_EUROPE), + Country("Romania", "40", TuyaCloudOpenAPIEndpoint.EUROPE), + Country("Russia", "7", TuyaCloudOpenAPIEndpoint.EUROPE), Country("Rwanda", "250"), Country("Saint Barthelemy", "590"), Country("Saint Helena", "290"), @@ -246,20 +241,20 @@ TUYA_COUNTRIES = [ Country("Sierra Leone", "232"), Country("Singapore", "65"), Country("Sint Maarten", "1-721"), - Country("Slovakia", "421", ENDPOINT_EUROPE), - Country("Slovenia", "386", ENDPOINT_EUROPE), + Country("Slovakia", "421", TuyaCloudOpenAPIEndpoint.EUROPE), + Country("Slovenia", "386", TuyaCloudOpenAPIEndpoint.EUROPE), Country("Solomon Islands", "677"), Country("Somalia", "252"), Country("South Africa", "27"), Country("South Korea", "82"), Country("South Sudan", "211"), - Country("Spain", "34", ENDPOINT_EUROPE), + Country("Spain", "34", TuyaCloudOpenAPIEndpoint.EUROPE), Country("Sri Lanka", "94"), Country("Sudan", "249"), Country("Suriname", "597"), - Country("Svalbard and Jan Mayen", "47", ENDPOINT_EUROPE), + Country("Svalbard and Jan Mayen", "47", TuyaCloudOpenAPIEndpoint.EUROPE), Country("Swaziland", "268"), - Country("Sweden", "46", ENDPOINT_EUROPE), + Country("Sweden", "46", TuyaCloudOpenAPIEndpoint.EUROPE), Country("Switzerland", "41"), Country("Syria", "963"), Country("Taiwan", "886"), @@ -279,8 +274,8 @@ TUYA_COUNTRIES = [ Country("Uganda", "256"), Country("Ukraine", "380"), Country("United Arab Emirates", "971"), - Country("United Kingdom", "44", ENDPOINT_EUROPE), - Country("United States", "1", ENDPOINT_AMERICA), + Country("United Kingdom", "44", TuyaCloudOpenAPIEndpoint.EUROPE), + Country("United States", "1", TuyaCloudOpenAPIEndpoint.AMERICA), Country("Uruguay", "598"), Country("Uzbekistan", "998"), Country("Vanuatu", "678"), diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index a5aee459bd7..46db598e1d8 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -5,6 +5,7 @@ from typing import Any from unittest.mock import MagicMock, patch import pytest +from tuya_iot import TuyaCloudOpenAPIEndpoint from homeassistant import config_entries, data_entry_flow from homeassistant.components.tuya.const import ( @@ -17,7 +18,6 @@ from homeassistant.components.tuya.const import ( CONF_PASSWORD, CONF_USERNAME, DOMAIN, - ENDPOINT_INDIA, SMARTLIFE_APP, TUYA_COUNTRIES, TUYA_SMART_APP, @@ -32,7 +32,7 @@ MOCK_ACCESS_ID = "myAccessId" MOCK_ACCESS_SECRET = "myAccessSecret" MOCK_USERNAME = "myUsername" MOCK_PASSWORD = "myPassword" -MOCK_ENDPOINT = ENDPOINT_INDIA +MOCK_ENDPOINT = TuyaCloudOpenAPIEndpoint.INDIA TUYA_INPUT_DATA = { CONF_COUNTRY_CODE: MOCK_COUNTRY, From 4afb4d907609be9a139c857410fb823b2522745f Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 12 Oct 2021 15:07:20 +0200 Subject: [PATCH 0281/1038] Bump pytradfri to 7.0.7 (#57543) --- homeassistant/components/tradfri/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index 7ffad04074d..d26093c32ed 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -3,7 +3,7 @@ "name": "IKEA TR\u00c5DFRI", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tradfri", - "requirements": ["pytradfri[async]==7.0.6"], + "requirements": ["pytradfri[async]==7.0.7"], "homekit": { "models": ["TRADFRI"] }, diff --git a/requirements_all.txt b/requirements_all.txt index 1fe2fe190f0..1628e027ed0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1970,7 +1970,7 @@ pytouchline==0.7 pytraccar==0.9.0 # homeassistant.components.tradfri -pytradfri[async]==7.0.6 +pytradfri[async]==7.0.7 # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 49ac409ba10..27f5a1ed220 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1139,7 +1139,7 @@ pytile==5.2.3 pytraccar==0.9.0 # homeassistant.components.tradfri -pytradfri[async]==7.0.6 +pytradfri[async]==7.0.7 # homeassistant.components.usb pyudev==0.22.0 From bf240904635d7d170c2b6d71e9c7bb2ec717bcc4 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 12 Oct 2021 15:33:08 +0200 Subject: [PATCH 0282/1038] Update flake8 related packages (#57538) --- .pre-commit-config.yaml | 12 ++++++------ requirements_test_pre_commit.txt | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 18bb8aa58c3..ef58733100b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,17 +22,17 @@ repos: - --quiet-level=2 exclude_types: [csv, json] exclude: ^tests/fixtures/ - - repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.2 + - repo: https://github.com/PyCQA/flake8 + rev: 4.0.1 hooks: - id: flake8 additional_dependencies: - - pycodestyle==2.7.0 - - pyflakes==2.3.1 + - pycodestyle==2.8.0 + - pyflakes==2.4.0 - flake8-docstrings==1.6.0 - - pydocstyle==6.0.0 + - pydocstyle==6.1.1 - flake8-comprehensions==3.7.0 - - flake8-noqa==1.1.0 + - flake8-noqa==1.2.0 - 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 ad9e168f3dd..ffa11a0fc30 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -5,12 +5,12 @@ black==21.9b0 codespell==2.0.0 flake8-comprehensions==3.7.0 flake8-docstrings==1.6.0 -flake8-noqa==1.1.0 -flake8==3.9.2 +flake8-noqa==1.2.0 +flake8==4.0.1 isort==5.9.3 mccabe==0.6.1 -pycodestyle==2.7.0 -pydocstyle==6.0.0 -pyflakes==2.3.1 +pycodestyle==2.8.0 +pydocstyle==6.1.1 +pyflakes==2.4.0 pyupgrade==2.27.0 yamllint==1.26.1 From fb18c108d11a3bfac31e87bd30cc3e896c04ed1f Mon Sep 17 00:00:00 2001 From: shbatm Date: Tue, 12 Oct 2021 08:40:46 -0500 Subject: [PATCH 0283/1038] Add service to Rainmachine to push weather data from Home Assistant (#57354) --- .../components/rainmachine/__init__.py | 69 ++++++++- .../components/rainmachine/manifest.json | 4 +- .../components/rainmachine/services.yaml | 134 ++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 205 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 0d25b6c41c7..72951223d09 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -46,8 +46,6 @@ from .const import ( LOGGER, ) -CONF_SECONDS = "seconds" - DEFAULT_ATTRIBUTION = "Data provided by Green Electronics LLC" DEFAULT_ICON = "mdi:water" DEFAULT_SSL = True @@ -57,7 +55,34 @@ CONFIG_SCHEMA = cv.deprecated(DOMAIN) PLATFORMS = ["binary_sensor", "sensor", "switch"] +# Constants expected by the RainMachine API for Service Data +CONF_CONDITION = "condition" +CONF_DEWPOINT = "dewpoint" +CONF_ET = "et" +CONF_MAXRH = "maxrh" +CONF_MAXTEMP = "maxtemp" +CONF_MINRH = "minrh" +CONF_MINTEMP = "mintemp" +CONF_PRESSURE = "pressure" +CONF_QPF = "qpf" +CONF_RAIN = "rain" +CONF_SECONDS = "seconds" +CONF_SOLARRAD = "solarrad" +CONF_TEMPERATURE = "temperature" +CONF_TIMESTAMP = "timestamp" +CONF_WEATHER = "weather" +CONF_WIND = "wind" + +# Config Validators for Weather Service Data +CV_WX_DATA_VALID_PERCENTAGE = vol.All(vol.Coerce(int), vol.Range(min=0, max=100)) +CV_WX_DATA_VALID_TEMP_RANGE = vol.All(vol.Coerce(float), vol.Range(min=-40.0, max=40.0)) +CV_WX_DATA_VALID_RAIN_RANGE = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=1000.0)) +CV_WX_DATA_VALID_WIND_SPEED = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=65.0)) +CV_WX_DATA_VALID_PRESSURE = vol.All(vol.Coerce(float), vol.Range(min=60.0, max=110.0)) +CV_WX_DATA_VALID_SOLARRAD = vol.All(vol.Coerce(float), vol.Range(min=0.0, max=5.0)) + SERVICE_NAME_PAUSE_WATERING = "pause_watering" +SERVICE_NAME_PUSH_WEATHER_DATA = "push_weather_data" SERVICE_NAME_STOP_ALL = "stop_all" SERVICE_NAME_UNPAUSE_WATERING = "unpause_watering" @@ -73,6 +98,25 @@ SERVICE_PAUSE_WATERING_SCHEMA = SERVICE_SCHEMA.extend( } ) +SERVICE_PUSH_WEATHER_DATA_SCHEMA = SERVICE_SCHEMA.extend( + { + vol.Optional(CONF_TIMESTAMP): cv.positive_float, + vol.Optional(CONF_MINTEMP): CV_WX_DATA_VALID_TEMP_RANGE, + vol.Optional(CONF_MAXTEMP): CV_WX_DATA_VALID_TEMP_RANGE, + vol.Optional(CONF_TEMPERATURE): CV_WX_DATA_VALID_TEMP_RANGE, + vol.Optional(CONF_WIND): CV_WX_DATA_VALID_WIND_SPEED, + vol.Optional(CONF_SOLARRAD): CV_WX_DATA_VALID_SOLARRAD, + vol.Optional(CONF_QPF): CV_WX_DATA_VALID_RAIN_RANGE, + vol.Optional(CONF_RAIN): CV_WX_DATA_VALID_RAIN_RANGE, + vol.Optional(CONF_ET): CV_WX_DATA_VALID_RAIN_RANGE, + vol.Optional(CONF_MINRH): CV_WX_DATA_VALID_PERCENTAGE, + vol.Optional(CONF_MAXRH): CV_WX_DATA_VALID_PERCENTAGE, + vol.Optional(CONF_CONDITION): cv.string, + vol.Optional(CONF_PRESSURE): CV_WX_DATA_VALID_PRESSURE, + vol.Optional(CONF_DEWPOINT): CV_WX_DATA_VALID_TEMP_RANGE, + } +) + @callback def async_get_controller_for_service_call( @@ -201,6 +245,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await controller.watering.pause_all(call.data[CONF_SECONDS]) await async_update_programs_and_zones(hass, entry) + async def async_push_weather_data(call: ServiceCall) -> None: + """Push weather data to the device.""" + controller = async_get_controller_for_service_call(hass, call) + await controller.parsers.post_data( + { + CONF_WEATHER: [ + { + key: value + for key, value in call.data.items() + if key != CONF_DEVICE_ID + } + ] + } + ) + async def async_stop_all(call: ServiceCall) -> None: """Stop all watering.""" controller = async_get_controller_for_service_call(hass, call) @@ -219,6 +278,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: SERVICE_PAUSE_WATERING_SCHEMA, async_pause_watering, ), + ( + SERVICE_NAME_PUSH_WEATHER_DATA, + SERVICE_PUSH_WEATHER_DATA_SCHEMA, + async_push_weather_data, + ), (SERVICE_NAME_STOP_ALL, SERVICE_SCHEMA, async_stop_all), (SERVICE_NAME_UNPAUSE_WATERING, SERVICE_SCHEMA, async_unpause_watering), ): @@ -240,6 +304,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # during integration setup: for service_name in ( SERVICE_NAME_PAUSE_WATERING, + SERVICE_NAME_PUSH_WEATHER_DATA, SERVICE_NAME_STOP_ALL, SERVICE_NAME_UNPAUSE_WATERING, ): diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index 03fedcf8c57..307cd7681f5 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,12 +3,12 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==3.1.5"], + "requirements": ["regenmaschine==3.2.0"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { "models": ["Touch HD", "SPK5"] - }, + }, "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/homeassistant/components/rainmachine/services.yaml b/homeassistant/components/rainmachine/services.yaml index fc6a71f8842..1709e32c261 100644 --- a/homeassistant/components/rainmachine/services.yaml +++ b/homeassistant/components/rainmachine/services.yaml @@ -107,3 +107,137 @@ unpause_watering: selector: device: integration: rainmachine +push_weather_data: + name: Push Weather Data + description: >- + Push Weather Data from Home Assistant to the RainMachine device. + + Local Weather Push service should be enabled from Settings > Weather > Developer tab for RainMachine to consider the values being sent. + Units must be sent in metric; no conversions are performed by the integraion. + + See details of RainMachine API Here: https://rainmachine.docs.apiary.io/#reference/weather-services/parserdata/post + fields: + device_id: + name: Controller + description: The controller for the weather data to be pushed. + required: true + selector: + device: + integration: rainmachine + timestamp: + name: Timestamp + description: UNIX Timestamp for the Weather Data. If omitted, the RainMachine device's local time at the time of the call is used. + selector: + text: + mintemp: + name: Min Temp + description: Minimum Temperature (°C). + selector: + number: + min: -40 + max: 40 + step: 0.1 + unit_of_measurement: '°C' + maxtemp: + name: Max Temp + description: Maximum Temperature (°C). + selector: + number: + min: -40 + max: 40 + step: 0.1 + unit_of_measurement: '°C' + temperature: + name: Temperature + description: Current Temperature (°C). + selector: + number: + min: -40 + max: 40 + step: 0.1 + unit_of_measurement: '°C' + wind: + name: Wind Speed + description: Wind Speed (m/s) + selector: + number: + min: 0 + max: 65 + unit_of_measurement: 'm/s' + solarrad: + name: Solar Radiation + description: Solar Radiation (MJ/m²/h) + selector: + number: + min: 0 + max: 5 + step: 0.1 + unit_of_measurement: 'MJ/m²/h' + et: + name: Evapotranspiration + description: Evapotranspiration (mm) + selector: + number: + min: 0 + max: 1000 + unit_of_measurement: 'mm' + qpf: + name: Quantitative Precipitation Forecast + description: >- + Quantitative Precipitation Forecast (mm), or QPF. Note: QPF values shouldn't + be send as cumulative values but the measured/forecasted values for each hour or day. + The RainMachine Mixer will sum all QPF values in the current day to have the day total QPF. + selector: + number: + min: 0 + max: 1000 + unit_of_measurement: 'mm' + rain: + name: Measured Rainfall + description: >- + Measured Rainfail (mm). Note: RAIN values shouldn't be send as cumulative values but the + measured/forecasted values for each hour or day. The RainMachine Mixer will sum all RAIN values + in the current day to have the day total RAIN. + selector: + number: + min: 0 + max: 1000 + unit_of_measurement: 'mm' + minrh: + name: Min Relative Humidity + description: Min Relative Humidity (%RH) + selector: + number: + min: 0 + max: 100 + unit_of_measurement: '%' + maxrh: + name: Max Relative Humidity + description: Max Relative Humidity (%RH) + selector: + number: + min: 0 + max: 100 + unit_of_measurement: '%' + condition: + name: Weather Condition Code + description: Current weather condition code (WNUM). + selector: + text: + pressure: + name: Barametric Pressure + description: Barametric Pressure (kPa) + selector: + number: + min: 60 + max: 110 + unit_of_measurement: "kPa" + dewpoint: + name: Dew Point + description: Dew Point (°C). + selector: + number: + min: -40 + max: 40 + step: 0.1 + unit_of_measurement: '°C' diff --git a/requirements_all.txt b/requirements_all.txt index 1628e027ed0..9512f9bf75f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2043,7 +2043,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==3.1.5 +regenmaschine==3.2.0 # homeassistant.components.renault renault-api==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 27f5a1ed220..18211445831 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1175,7 +1175,7 @@ pyzerproc==0.4.8 rachiopy==1.0.3 # homeassistant.components.rainmachine -regenmaschine==3.1.5 +regenmaschine==3.2.0 # homeassistant.components.renault renault-api==0.1.4 From 8ec38ef034f4d09686076ae3616e7c29f06706d0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 12 Oct 2021 16:49:46 +0200 Subject: [PATCH 0284/1038] Refactor Tuya device handling (#57545) * Refactor Tuya device handling * Tweak --- homeassistant/components/tuya/__init__.py | 104 ++++++++++------------ homeassistant/components/tuya/climate.py | 61 ++++--------- homeassistant/components/tuya/const.py | 32 +++++-- homeassistant/components/tuya/fan.py | 55 +++--------- homeassistant/components/tuya/light.py | 60 +++---------- homeassistant/components/tuya/scene.py | 42 ++++----- homeassistant/components/tuya/switch.py | 91 ++++++++----------- 7 files changed, 164 insertions(+), 281 deletions(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 8dd9979f3e7..1c7908ab986 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -1,7 +1,8 @@ """Support for Tuya Smart devices.""" +from __future__ import annotations -import itertools import logging +from typing import NamedTuple from tuya_iot import ( AuthType, @@ -30,24 +31,25 @@ from .const import ( CONF_USERNAME, DOMAIN, PLATFORMS, - TUYA_DEVICE_MANAGER, TUYA_DISCOVERY_NEW, - TUYA_HA_DEVICES, TUYA_HA_SIGNAL_UPDATE_ENTITY, - TUYA_HA_TUYA_MAP, - TUYA_HOME_MANAGER, - TUYA_MQTT_LISTENER, + TUYA_SUPPORTED_PRODUCT_CATEGORIES, ) _LOGGER = logging.getLogger(__name__) +class HomeAssistantTuyaData(NamedTuple): + """Tuya data stored in the Home Assistant data object.""" + + device_listener: TuyaDeviceListener + device_manager: TuyaDeviceManager + home_manager: TuyaHomeManager + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Async setup hass config entry.""" - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { - TUYA_HA_TUYA_MAP: {}, - TUYA_HA_DEVICES: set(), - } + hass.data.setdefault(DOMAIN, {}) # Project type has been renamed to auth type in the upstream Tuya IoT SDK. # This migrates existing config entries to reflect that name change. @@ -98,25 +100,28 @@ async def _init_tuya_sdk(hass: HomeAssistant, entry: ConfigEntry) -> bool: tuya_mq = TuyaOpenMQ(api) tuya_mq.start() + device_ids: set[str] = set() device_manager = TuyaDeviceManager(api, tuya_mq) - - # Get device list home_manager = TuyaHomeManager(api, tuya_mq, device_manager) - await hass.async_add_executor_job(home_manager.update_device_cache) - hass.data[DOMAIN][entry.entry_id][TUYA_HOME_MANAGER] = home_manager - - listener = DeviceListener(hass, entry) - hass.data[DOMAIN][entry.entry_id][TUYA_MQTT_LISTENER] = listener + listener = DeviceListener(hass, device_manager, device_ids) device_manager.add_device_listener(listener) - hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE_MANAGER] = device_manager - # Clean up device entities + hass.data[DOMAIN][entry.entry_id] = HomeAssistantTuyaData( + device_listener=listener, + device_manager=device_manager, + home_manager=home_manager, + ) + + # Get devices & clean up device entities + await hass.async_add_executor_job(home_manager.update_device_cache) await cleanup_device_registry(hass, device_manager) - _LOGGER.debug("init support type->%s", PLATFORMS) + # Register known device IDs + for device in device_manager.device_map.values(): + if device.category in TUYA_SUPPORTED_PRODUCT_CATEGORIES: + device_ids.add(device.id) hass.config_entries.async_setup_platforms(entry, PLATFORMS) - return True @@ -134,17 +139,13 @@ async def cleanup_device_registry( async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unloading the Tuya platforms.""" - _LOGGER.debug("integration unload") unload = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload: - device_manager = hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE_MANAGER] - device_manager.mq.stop() - device_manager.remove_device_listener( - hass.data[DOMAIN][entry.entry_id][TUYA_MQTT_LISTENER] - ) + hass_data: HomeAssistantTuyaData = hass.data[DOMAIN][entry.entry_id] + hass_data.device_manager.mq.stop() + hass_data.device_manager.remove_device_listener(hass_data.device_listener) hass.data[DOMAIN].pop(entry.entry_id) - if not hass.data[DOMAIN]: hass.data.pop(DOMAIN) @@ -154,14 +155,20 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class DeviceListener(TuyaDeviceListener): """Device Update Listener.""" - def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + def __init__( + self, + hass: HomeAssistant, + device_manager: TuyaDeviceManager, + device_ids: set[str], + ) -> None: """Init DeviceListener.""" self.hass = hass - self.entry = entry + self.device_manager = device_manager + self.device_ids = device_ids def update_device(self, device: TuyaDevice) -> None: """Update device status.""" - if device.id in self.hass.data[DOMAIN][self.entry.entry_id][TUYA_HA_DEVICES]: + if device.id in self.device_ids: _LOGGER.debug( "_update-->%s;->>%s", self, @@ -171,33 +178,14 @@ class DeviceListener(TuyaDeviceListener): def add_device(self, device: TuyaDevice) -> None: """Add device added listener.""" - device_add = False - - if device.category in itertools.chain( - *self.hass.data[DOMAIN][self.entry.entry_id][TUYA_HA_TUYA_MAP].values() - ): - ha_tuya_map = self.hass.data[DOMAIN][self.entry.entry_id][TUYA_HA_TUYA_MAP] + if device.category in TUYA_SUPPORTED_PRODUCT_CATEGORIES: + # Ensure the device isn't present stale self.hass.add_job(self.async_remove_device, device.id) - for domain, tuya_list in ha_tuya_map.items(): - if device.category in tuya_list: - device_add = True - _LOGGER.debug( - "Add device category->%s; domain-> %s", - device.category, - domain, - ) - self.hass.data[DOMAIN][self.entry.entry_id][TUYA_HA_DEVICES].add( - device.id - ) - dispatcher_send( - self.hass, TUYA_DISCOVERY_NEW.format(domain), [device.id] - ) + self.device_ids.add(device.id) + dispatcher_send(self.hass, TUYA_DISCOVERY_NEW, [device.id]) - if device_add: - device_manager = self.hass.data[DOMAIN][self.entry.entry_id][ - TUYA_DEVICE_MANAGER - ] + device_manager = self.device_manager device_manager.mq.stop() tuya_mq = TuyaOpenMQ(device_manager.api) tuya_mq.start() @@ -207,18 +195,16 @@ class DeviceListener(TuyaDeviceListener): def remove_device(self, device_id: str) -> None: """Add device removed listener.""" - _LOGGER.debug("tuya remove device:%s", device_id) self.hass.add_job(self.async_remove_device, device_id) @callback def async_remove_device(self, device_id: str) -> None: """Remove device from Home Assistant.""" + _LOGGER.debug("Remove device: %s", device_id) device_registry_object = device_registry.async_get(self.hass) device_entry = device_registry_object.async_get_device( identifiers={(DOMAIN, device_id)} ) if device_entry is not None: device_registry_object.async_remove_device(device_entry.id) - self.hass.data[DOMAIN][self.entry.entry_id][TUYA_HA_DEVICES].discard( - device_id - ) + self.device_ids.discard(device_id) diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index 810e8ad8aab..ac309464054 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -8,7 +8,7 @@ from typing import Any from tuya_iot import TuyaDevice, TuyaDeviceManager -from homeassistant.components.climate import DOMAIN as DEVICE_DOMAIN, ClimateEntity +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, HVAC_MODE_COOL, @@ -25,17 +25,11 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import HomeAssistantTuyaData from .base import TuyaHaEntity -from .const import ( - DOMAIN, - TUYA_DEVICE_MANAGER, - TUYA_DISCOVERY_NEW, - TUYA_HA_DEVICES, - TUYA_HA_TUYA_MAP, -) +from .const import DOMAIN, TUYA_DISCOVERY_NEW _LOGGER = logging.getLogger(__name__) @@ -88,50 +82,25 @@ TUYA_SUPPORT_TYPE = { async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up tuya climate dynamically through tuya discovery.""" - _LOGGER.debug("climate init") - - hass.data[DOMAIN][entry.entry_id][TUYA_HA_TUYA_MAP][ - DEVICE_DOMAIN - ] = TUYA_SUPPORT_TYPE + """Set up Tuya climate dynamically through Tuya discovery.""" + hass_data: HomeAssistantTuyaData = hass.data[DOMAIN][entry.entry_id] @callback - def async_discover_device(dev_ids: list[str]) -> None: - """Discover and add a discovered tuya climate.""" - _LOGGER.debug("climate add-> %s", dev_ids) - if not dev_ids: - return - entities = _setup_entities(hass, entry, dev_ids) + def async_discover_device(device_ids: list[str]) -> None: + """Discover and add a discovered Tuya climate.""" + entities: list[TuyaHaClimate] = [] + for device_id in device_ids: + device = hass_data.device_manager.device_map[device_id] + if device and device.category in TUYA_SUPPORT_TYPE: + entities.append(TuyaHaClimate(device, hass_data.device_manager)) async_add_entities(entities) + async_discover_device([*hass_data.device_manager.device_map]) + entry.async_on_unload( - async_dispatcher_connect( - hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device - ) + async_dispatcher_connect(hass, TUYA_DISCOVERY_NEW, async_discover_device) ) - device_manager = hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE_MANAGER] - device_ids = [] - for (device_id, device) in device_manager.device_map.items(): - if device.category in TUYA_SUPPORT_TYPE: - device_ids.append(device_id) - async_discover_device(device_ids) - - -def _setup_entities( - hass: HomeAssistant, entry: ConfigEntry, device_ids: list[str] -) -> list[Entity]: - """Set up Tuya Climate.""" - device_manager = hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE_MANAGER] - entities: list[Entity] = [] - for device_id in device_ids: - device = device_manager.device_map[device_id] - if device is None: - continue - entities.append(TuyaHaClimate(device, device_manager)) - hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) - return entities - class TuyaHaClimate(TuyaHaEntity, ClimateEntity): """Tuya Switch Device.""" diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index e4ecfbd4bf3..6d92383a871 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -15,12 +15,8 @@ CONF_PASSWORD = "password" CONF_COUNTRY_CODE = "country_code" CONF_APP_TYPE = "tuya_app_type" -TUYA_DISCOVERY_NEW = "tuya_discovery_new_{}" -TUYA_DEVICE_MANAGER = "tuya_device_manager" -TUYA_HOME_MANAGER = "tuya_home_manager" -TUYA_MQTT_LISTENER = "tuya_mqtt_listener" -TUYA_HA_TUYA_MAP = "tuya_ha_tuya_map" -TUYA_HA_DEVICES = "tuya_ha_devices" +TUYA_DISCOVERY_NEW = "tuya_discovery_new" +TUYA_HA_SIGNAL_UPDATE_ENTITY = "tuya_entry_update" TUYA_RESPONSE_CODE = "code" TUYA_RESPONSE_RESULT = "result" @@ -28,7 +24,29 @@ TUYA_RESPONSE_MSG = "msg" TUYA_RESPONSE_SUCCESS = "success" TUYA_RESPONSE_PLATFROM_URL = "platform_url" -TUYA_HA_SIGNAL_UPDATE_ENTITY = "tuya_entry_update" +TUYA_SUPPORTED_PRODUCT_CATEGORIES = ( + "bh", # Smart Kettle + "cwysj", # Pet Water Feeder + "cz", # Socket + "dc", # Light string + "dd", # Light strip + "dj", # Light + "dlq", # Breaker + "fs", # Fan + "fs", # Fan + "fwl", # Ambient light + "jsq", # Humidifier's light + "kg", # Switch + "kj", # Air Purifier + "kj", # Air Purifier + "kt", # Air conditioner + "pc", # Power Strip + "qn", # Heater + "wk", # Thermostat + "xdd", # Ceiling Light + "xxj", # Diffuser + "xxj", # Diffuser's light +) TUYA_SMART_APP = "tuyaSmart" SMARTLIFE_APP = "smartlife" diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index 15a8e553a10..88aa1ab53b9 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -10,7 +10,6 @@ from tuya_iot import TuyaDevice, TuyaDeviceManager from homeassistant.components.fan import ( DIRECTION_FORWARD, DIRECTION_REVERSE, - DOMAIN as DEVICE_DOMAIN, SUPPORT_DIRECTION, SUPPORT_OSCILLATE, SUPPORT_PRESET_MODE, @@ -26,14 +25,9 @@ from homeassistant.util.percentage import ( percentage_to_ordered_list_item, ) +from . import HomeAssistantTuyaData from .base import TuyaHaEntity -from .const import ( - DOMAIN, - TUYA_DEVICE_MANAGER, - TUYA_DISCOVERY_NEW, - TUYA_HA_DEVICES, - TUYA_HA_TUYA_MAP, -) +from .const import DOMAIN, TUYA_DISCOVERY_NEW _LOGGER = logging.getLogger(__name__) @@ -61,49 +55,24 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ): """Set up tuya fan dynamically through tuya discovery.""" - _LOGGER.debug("fan init") - - hass.data[DOMAIN][entry.entry_id][TUYA_HA_TUYA_MAP][ - DEVICE_DOMAIN - ] = TUYA_SUPPORT_TYPE + hass_data: HomeAssistantTuyaData = hass.data[DOMAIN][entry.entry_id] @callback - def async_discover_device(dev_ids: list[str]) -> None: + def async_discover_device(device_ids: list[str]) -> None: """Discover and add a discovered tuya fan.""" - _LOGGER.debug("fan add-> %s", dev_ids) - if not dev_ids: - return - entities = _setup_entities(hass, entry, dev_ids) + entities: list[TuyaHaFan] = [] + for device_id in device_ids: + device = hass_data.device_manager.device_map[device_id] + if device and device.category in TUYA_SUPPORT_TYPE: + entities.append(TuyaHaFan(device, hass_data.device_manager)) async_add_entities(entities) + async_discover_device([*hass_data.device_manager.device_map]) + entry.async_on_unload( - async_dispatcher_connect( - hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device - ) + async_dispatcher_connect(hass, TUYA_DISCOVERY_NEW, async_discover_device) ) - device_manager = hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE_MANAGER] - device_ids = [] - for (device_id, device) in device_manager.device_map.items(): - if device.category in TUYA_SUPPORT_TYPE: - device_ids.append(device_id) - async_discover_device(device_ids) - - -def _setup_entities( - hass: HomeAssistant, entry: ConfigEntry, device_ids: list[str] -) -> list[TuyaHaFan]: - """Set up Tuya Fan.""" - device_manager = hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE_MANAGER] - entities = [] - for device_id in device_ids: - device = device_manager.device_map[device_id] - if device is None: - continue - entities.append(TuyaHaFan(device, device_manager)) - hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) - return entities - class TuyaHaFan(TuyaHaEntity, FanEntity): """Tuya Fan Device.""" diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index f2136da431f..1970ba4c312 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -15,7 +15,6 @@ from homeassistant.components.light import ( COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS, COLOR_MODE_ONOFF, - DOMAIN as DEVICE_DOMAIN, LightEntity, ) from homeassistant.config_entries import ConfigEntry @@ -23,14 +22,9 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import HomeAssistantTuyaData from .base import TuyaHaEntity -from .const import ( - DOMAIN, - TUYA_DEVICE_MANAGER, - TUYA_DISCOVERY_NEW, - TUYA_HA_DEVICES, - TUYA_HA_TUYA_MAP, -) +from .const import DOMAIN, TUYA_DISCOVERY_NEW _LOGGER = logging.getLogger(__name__) @@ -84,54 +78,24 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up tuya light dynamically through tuya discovery.""" - _LOGGER.debug("light init") - - hass.data[DOMAIN][entry.entry_id][TUYA_HA_TUYA_MAP][ - DEVICE_DOMAIN - ] = TUYA_SUPPORT_TYPE + hass_data: HomeAssistantTuyaData = hass.data[DOMAIN][entry.entry_id] @callback - def async_discover_device(dev_ids: list[str]): + def async_discover_device(device_ids: list[str]): """Discover and add a discovered tuya light.""" - _LOGGER.debug("light add-> %s", dev_ids) - if not dev_ids: - return - entities = _setup_entities(hass, entry, dev_ids) + entities: list[TuyaHaLight] = [] + for device_id in device_ids: + device = hass_data.device_manager.device_map[device_id] + if device and device.category in TUYA_SUPPORT_TYPE: + entities.append(TuyaHaLight(device, hass_data.device_manager)) async_add_entities(entities) + async_discover_device([*hass_data.device_manager.device_map]) + entry.async_on_unload( - async_dispatcher_connect( - hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device - ) + async_dispatcher_connect(hass, TUYA_DISCOVERY_NEW, async_discover_device) ) - device_manager = hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE_MANAGER] - device_ids = [] - for (device_id, device) in device_manager.device_map.items(): - if device.category in TUYA_SUPPORT_TYPE: - device_ids.append(device_id) - async_discover_device(device_ids) - - -def _setup_entities( - hass, entry: ConfigEntry, device_ids: list[str] -) -> list[TuyaHaLight]: - """Set up Tuya Light device.""" - device_manager = hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE_MANAGER] - entities = [] - for device_id in device_ids: - device = device_manager.device_map[device_id] - if device is None: - continue - - tuya_ha_light = TuyaHaLight(device, device_manager) - entities.append(tuya_ha_light) - hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add( - tuya_ha_light.tuya_device.id - ) - - return entities - class TuyaHaLight(TuyaHaEntity, LightEntity): """Tuya light device.""" diff --git a/homeassistant/components/tuya/scene.py b/homeassistant/components/tuya/scene.py index c90c6798b9b..f008d5a5b7b 100644 --- a/homeassistant/components/tuya/scene.py +++ b/homeassistant/components/tuya/scene.py @@ -1,7 +1,6 @@ """Support for Tuya scenes.""" from __future__ import annotations -import logging from typing import Any from tuya_iot import TuyaHomeManager, TuyaScene @@ -9,55 +8,48 @@ from tuya_iot import TuyaHomeManager, TuyaScene from homeassistant.components.scene import Scene 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, TUYA_HOME_MANAGER - -_LOGGER = logging.getLogger(__name__) +from . import HomeAssistantTuyaData +from .const import DOMAIN async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up tuya scenes.""" - home_manager = hass.data[DOMAIN][entry.entry_id][TUYA_HOME_MANAGER] - scenes = await hass.async_add_executor_job(home_manager.query_scenes) - async_add_entities(TuyaHAScene(home_manager, scene) for scene in scenes) + """Set up Tuya scenes.""" + hass_data: HomeAssistantTuyaData = hass.data[DOMAIN][entry.entry_id] + scenes = await hass.async_add_executor_job(hass_data.home_manager.query_scenes) + async_add_entities(TuyaHAScene(hass_data.home_manager, scene) for scene in scenes) class TuyaHAScene(Scene): """Tuya Scene Remote.""" + _should_poll = False + def __init__(self, home_manager: TuyaHomeManager, scene: TuyaScene) -> None: """Init Tuya Scene.""" super().__init__() + self._attr_unique_id = f"tys{scene.scene_id}" self.home_manager = home_manager self.scene = scene - @property - def should_poll(self) -> bool: - """Hass should not poll.""" - return False - - @property - def unique_id(self) -> str | None: - """Return a unique ID.""" - return f"tys{self.scene.scene_id}" - @property def name(self) -> str | None: """Return Tuya scene name.""" return self.scene.name @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" - return { - "identifiers": {(DOMAIN, f"{self.unique_id}")}, - "manufacturer": "tuya", - "name": self.scene.name, - "model": "Tuya Scene", - } + return DeviceInfo( + identifiers={(DOMAIN, f"{self.unique_id}")}, + manufacturer="tuya", + name=self.scene.name, + model="Tuya Scene", + ) @property def available(self) -> bool: diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 5bafbe1b7f6..af723a3d4f4 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -6,21 +6,15 @@ from typing import Any from tuya_iot import TuyaDevice, TuyaDeviceManager -from homeassistant.components.switch import DOMAIN as DEVICE_DOMAIN, SwitchEntity +from homeassistant.components.switch import SwitchEntity 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 Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import HomeAssistantTuyaData from .base import TuyaHaEntity -from .const import ( - DOMAIN, - TUYA_DEVICE_MANAGER, - TUYA_DISCOVERY_NEW, - TUYA_HA_DEVICES, - TUYA_HA_TUYA_MAP, -) +from .const import DOMAIN, TUYA_DISCOVERY_NEW _LOGGER = logging.getLogger(__name__) @@ -62,45 +56,36 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up tuya sensors dynamically through tuya discovery.""" - _LOGGER.debug("switch init") + hass_data: HomeAssistantTuyaData = hass.data[DOMAIN][entry.entry_id] - hass.data[DOMAIN][entry.entry_id][TUYA_HA_TUYA_MAP][ - DEVICE_DOMAIN - ] = TUYA_SUPPORT_TYPE - - async def async_discover_device(dev_ids): + @callback + def async_discover_device(device_ids: list[str]) -> None: """Discover and add a discovered tuya sensor.""" - _LOGGER.debug("switch add-> %s", dev_ids) - if not dev_ids: - return - entities = _setup_entities(hass, entry, dev_ids) - async_add_entities(entities) + async_add_entities( + _setup_entities(hass, entry, hass_data.device_manager, device_ids) + ) + + async_discover_device([*hass_data.device_manager.device_map]) entry.async_on_unload( - async_dispatcher_connect( - hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device - ) + async_dispatcher_connect(hass, TUYA_DISCOVERY_NEW, async_discover_device) ) - device_manager = hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE_MANAGER] - device_ids = [] - for (device_id, device) in device_manager.device_map.items(): - if device.category in TUYA_SUPPORT_TYPE: - device_ids.append(device_id) - await async_discover_device(device_ids) - -def _setup_entities(hass, entry: ConfigEntry, device_ids: list[str]) -> list[Entity]: +def _setup_entities( + hass: HomeAssistant, + entry: ConfigEntry, + device_manager: TuyaDeviceManager, + device_ids: list[str], +) -> list[TuyaHaSwitch]: """Set up Tuya Switch device.""" - device_manager = hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE_MANAGER] - entities: list[Entity] = [] + entities: list[TuyaHaSwitch] = [] for device_id in device_ids: device = device_manager.device_map[device_id] - if device is None: + if device is None or device.category not in TUYA_SUPPORT_TYPE: continue for function in device.function: - tuya_ha_switch = None if device.category == "kj": if function in [ DPCODE_ANION, @@ -110,26 +95,26 @@ def _setup_entities(hass, entry: ConfigEntry, device_ids: list[str]) -> list[Ent DPCODE_UV, DPCODE_WET, ]: - tuya_ha_switch = TuyaHaSwitch(device, device_manager, function) - # Main device switch is handled by the Fan object + entities.append(TuyaHaSwitch(device, device_manager, function)) + elif device.category == "cwysj": - if function in [DPCODE_FRESET, DPCODE_UV, DPCODE_PRESET, DPCODE_WRESET]: - tuya_ha_switch = TuyaHaSwitch(device, device_manager, function) + if ( + function + in [ + DPCODE_FRESET, + DPCODE_UV, + DPCODE_PRESET, + DPCODE_WRESET, + ] + or function.startswith(DPCODE_SWITCH) + ): + entities.append(TuyaHaSwitch(device, device_manager, function)) - if function.startswith(DPCODE_SWITCH): - # Main device switch - tuya_ha_switch = TuyaHaSwitch(device, device_manager, function) - else: - if function.startswith(DPCODE_START): - tuya_ha_switch = TuyaHaSwitch(device, device_manager, function) - if function.startswith(DPCODE_SWITCH): - tuya_ha_switch = TuyaHaSwitch(device, device_manager, function) + elif function.startswith(DPCODE_START) or function.startswith( + DPCODE_SWITCH + ): + entities.append(TuyaHaSwitch(device, device_manager, function)) - if tuya_ha_switch is not None: - entities.append(tuya_ha_switch) - hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add( - tuya_ha_switch.tuya_device.id - ) return entities From 007af4a7aa2344f1960f07c5959378d17e4ec8da Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 12 Oct 2021 17:49:07 +0200 Subject: [PATCH 0285/1038] Search for areas, devices and entities in script choose actions (#57554) --- homeassistant/helpers/script.py | 50 +++++++++++++----- tests/helpers/test_script.py | 92 ++++++++++++++++++++++++++++++--- 2 files changed, 123 insertions(+), 19 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index c43b918c59d..289b649e90c 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1063,9 +1063,13 @@ class Script: if self._referenced_areas is not None: return self._referenced_areas - referenced: set[str] = set() + self._referenced_areas: set[str] = set() + Script._find_referenced_areas(self._referenced_areas, self.sequence) + return self._referenced_areas - for step in self.sequence: + @staticmethod + def _find_referenced_areas(referenced, sequence): + for step in sequence: action = cv.determine_script_action(step) if action == cv.SCRIPT_ACTION_CALL_SERVICE: @@ -1076,8 +1080,11 @@ class Script: ): _referenced_extract_ids(data, ATTR_AREA_ID, referenced) - self._referenced_areas = referenced - return referenced + elif action == cv.SCRIPT_ACTION_CHOOSE: + for choice in step[CONF_CHOOSE]: + Script._find_referenced_areas(referenced, choice[CONF_SEQUENCE]) + if CONF_DEFAULT in step: + Script._find_referenced_areas(referenced, step[CONF_DEFAULT]) @property def referenced_devices(self): @@ -1085,9 +1092,13 @@ class Script: if self._referenced_devices is not None: return self._referenced_devices - referenced: set[str] = set() + self._referenced_devices: set[str] = set() + Script._find_referenced_devices(self._referenced_devices, self.sequence) + return self._referenced_devices - for step in self.sequence: + @staticmethod + def _find_referenced_devices(referenced, sequence): + for step in sequence: action = cv.determine_script_action(step) if action == cv.SCRIPT_ACTION_CALL_SERVICE: @@ -1104,8 +1115,13 @@ class Script: elif action == cv.SCRIPT_ACTION_DEVICE_AUTOMATION: referenced.add(step[CONF_DEVICE_ID]) - self._referenced_devices = referenced - return referenced + elif action == cv.SCRIPT_ACTION_CHOOSE: + for choice in step[CONF_CHOOSE]: + for cond in choice[CONF_CONDITIONS]: + referenced |= condition.async_extract_devices(cond) + Script._find_referenced_devices(referenced, choice[CONF_SEQUENCE]) + if CONF_DEFAULT in step: + Script._find_referenced_devices(referenced, step[CONF_DEFAULT]) @property def referenced_entities(self): @@ -1113,9 +1129,13 @@ class Script: if self._referenced_entities is not None: return self._referenced_entities - referenced: set[str] = set() + self._referenced_entities: set[str] = set() + Script._find_referenced_entities(self._referenced_entities, self.sequence) + return self._referenced_entities - for step in self.sequence: + @staticmethod + def _find_referenced_entities(referenced, sequence): + for step in sequence: action = cv.determine_script_action(step) if action == cv.SCRIPT_ACTION_CALL_SERVICE: @@ -1133,8 +1153,14 @@ class Script: elif action == cv.SCRIPT_ACTION_ACTIVATE_SCENE: referenced.add(step[CONF_SCENE]) - self._referenced_entities = referenced - return referenced + elif action == cv.SCRIPT_ACTION_CHOOSE: + for choice in step[CONF_CHOOSE]: + for cond in choice[CONF_CONDITIONS]: + _LOGGER.error("Extracting entities from: %s", cond) + referenced |= condition.async_extract_entities(cond) + Script._find_referenced_entities(referenced, choice[CONF_SEQUENCE]) + if CONF_DEFAULT in step: + Script._find_referenced_entities(referenced, step[CONF_DEFAULT]) def run( self, variables: _VarsType | None = None, context: Context | None = None diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index dfa5ce34ce7..f8d6c0c6e6b 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -2282,6 +2282,38 @@ async def test_referenced_entities(hass): }, {"service": "test.script", "data": {"without": "entity_id"}}, {"scene": "scene.hello"}, + { + "choose": [ + { + "conditions": "{{ states.light.choice_1_cond == 'on' }}", + "sequence": [ + { + "service": "test.script", + "data": {"entity_id": "light.choice_1_seq"}, + } + ], + }, + { + "conditions": { + "condition": "state", + "entity_id": "light.choice_2_cond", + "state": "on", + }, + "sequence": [ + { + "service": "test.script", + "data": {"entity_id": "light.choice_2_seq"}, + } + ], + }, + ], + "default": [ + { + "service": "test.script", + "data": {"entity_id": "light.default_seq"}, + } + ], + }, {"event": "test_event"}, {"delay": "{{ delay_period }}"}, ] @@ -2290,13 +2322,18 @@ async def test_referenced_entities(hass): "test_domain", ) assert script_obj.referenced_entities == { - "light.service_not_list", - "light.service_list", - "sensor.condition", - "scene.hello", + # "light.choice_1_cond", # no entity extraction from template conditions + "light.choice_1_seq", + "light.choice_2_cond", + "light.choice_2_seq", + "light.default_seq", "light.direct_entity_referenced", - "light.entity_in_target", "light.entity_in_data_template", + "light.entity_in_target", + "light.service_list", + "light.service_not_list", + "scene.hello", + "sensor.condition", } # Test we cache results. assert script_obj.referenced_entities is script_obj.referenced_entities @@ -2330,19 +2367,60 @@ async def test_referenced_devices(hass): "service": "test.script", "target": {"device_id": ["target-list-id-1", "target-list-id-2"]}, }, + { + "choose": [ + { + "conditions": "{{ is_device_attr('choice-2-cond-dev-id', 'model', 'blah') }}", + "sequence": [ + { + "service": "test.script", + "target": { + "device_id": "choice-1-seq-device-target" + }, + } + ], + }, + { + "conditions": { + "condition": "device", + "device_id": "choice-2-cond-dev-id", + "domain": "switch", + }, + "sequence": [ + { + "service": "test.script", + "target": { + "device_id": "choice-2-seq-device-target" + }, + } + ], + }, + ], + "default": [ + { + "service": "test.script", + "target": {"device_id": "default-device-target"}, + } + ], + }, ] ), "Test Name", "test_domain", ) assert script_obj.referenced_devices == { - "script-dev-id", + # 'choice-1-cond-dev-id', # no device extraction from template conditions + "choice-1-seq-device-target", + "choice-2-cond-dev-id", + "choice-2-seq-device-target", "condition-dev-id", "data-string-id", "data-template-string-id", - "target-string-id", + "default-device-target", + "script-dev-id", "target-list-id-1", "target-list-id-2", + "target-string-id", } # Test we cache results. assert script_obj.referenced_devices is script_obj.referenced_devices From ffeb73a4f6cee53efcaa1e166a4881aab42b2622 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 12 Oct 2021 18:09:32 +0200 Subject: [PATCH 0286/1038] Add statistics tests for sensor with changing device class (#57317) --- homeassistant/components/sensor/recorder.py | 3 +- tests/components/sensor/test_recorder.py | 179 ++++++++++++++++++++ 2 files changed, 181 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 30b5a4605ef..00f5ed4453f 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -463,9 +463,10 @@ def _compile_statistics( # noqa: C901 if entity_id not in hass.data[WARN_UNSTABLE_UNIT]: hass.data[WARN_UNSTABLE_UNIT].add(entity_id) _LOGGER.warning( - "The unit of %s (%s) does not match the unit of already " + "The %sunit of %s (%s) does not match the unit of already " "compiled statistics (%s). Generation of long term statistics " "will be suppressed unless the unit changes back to %s", + "normalized " if device_class in DEVICE_CLASS_UNITS else "", entity_id, unit, old_metadata[1]["unit_of_measurement"], diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 8a0da39cde3..6fe90a26ded 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -1705,6 +1705,185 @@ def test_compile_hourly_statistics_changing_units_3( assert "Error while processing event StatisticsTask" not in caplog.text +@pytest.mark.parametrize( + "device_class,state_unit,statistic_unit,mean,min,max", + [ + ("power", "kW", "W", 13.050847, -10, 30), + ], +) +def test_compile_hourly_statistics_changing_device_class_1( + hass_recorder, caplog, device_class, state_unit, statistic_unit, mean, min, max +): + """Test compiling hourly statistics where device class changes from one hour to the next.""" + zero = dt_util.utcnow() + hass = hass_recorder() + recorder = hass.data[DATA_INSTANCE] + setup_component(hass, "sensor", {}) + + # Record some states for an initial period, the entity has no device class + attributes = { + "state_class": "measurement", + "unit_of_measurement": state_unit, + } + four, states = record_states(hass, zero, "sensor.test1", attributes) + + recorder.do_adhoc_statistics(start=zero) + wait_recording_done(hass) + assert "does not match the unit of already compiled" not in caplog.text + statistic_ids = list_statistic_ids(hass) + assert statistic_ids == [ + {"statistic_id": "sensor.test1", "unit_of_measurement": state_unit} + ] + stats = statistics_during_period(hass, zero, period="5minute") + assert stats == { + "sensor.test1": [ + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "mean": approx(mean), + "min": approx(min), + "max": approx(max), + "last_reset": None, + "state": None, + "sum": None, + } + ] + } + + # Update device class and record additional states + attributes["device_class"] = device_class + four, _states = record_states( + hass, zero + timedelta(minutes=5), "sensor.test1", attributes + ) + states["sensor.test1"] += _states["sensor.test1"] + four, _states = record_states( + hass, zero + timedelta(minutes=10), "sensor.test1", attributes + ) + states["sensor.test1"] += _states["sensor.test1"] + hist = history.get_significant_states(hass, zero, four) + assert dict(states) == dict(hist) + + # Run statistics again, we get a warning, and no additional statistics is generated + recorder.do_adhoc_statistics(start=zero + timedelta(minutes=10)) + wait_recording_done(hass) + assert ( + f"The normalized unit of sensor.test1 ({statistic_unit}) does not match the " + f"unit of already compiled statistics ({state_unit})" in caplog.text + ) + statistic_ids = list_statistic_ids(hass) + assert statistic_ids == [ + {"statistic_id": "sensor.test1", "unit_of_measurement": state_unit} + ] + stats = statistics_during_period(hass, zero, period="5minute") + assert stats == { + "sensor.test1": [ + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "mean": approx(mean), + "min": approx(min), + "max": approx(max), + "last_reset": None, + "state": None, + "sum": None, + } + ] + } + assert "Error while processing event StatisticsTask" not in caplog.text + + +@pytest.mark.parametrize( + "device_class,state_unit,statistic_unit,mean,min,max", + [ + ("power", "kW", "W", 13050.847, -10000, 30000), + ], +) +def test_compile_hourly_statistics_changing_device_class_2( + hass_recorder, caplog, device_class, state_unit, statistic_unit, mean, min, max +): + """Test compiling hourly statistics where device class changes from one hour to the next.""" + zero = dt_util.utcnow() + hass = hass_recorder() + recorder = hass.data[DATA_INSTANCE] + setup_component(hass, "sensor", {}) + + # Record some states for an initial period, the entity has a device class + attributes = { + "device_class": device_class, + "state_class": "measurement", + "unit_of_measurement": state_unit, + } + four, states = record_states(hass, zero, "sensor.test1", attributes) + + recorder.do_adhoc_statistics(start=zero) + wait_recording_done(hass) + assert "does not match the unit of already compiled" not in caplog.text + statistic_ids = list_statistic_ids(hass) + assert statistic_ids == [ + {"statistic_id": "sensor.test1", "unit_of_measurement": statistic_unit} + ] + stats = statistics_during_period(hass, zero, period="5minute") + assert stats == { + "sensor.test1": [ + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "mean": approx(mean), + "min": approx(min), + "max": approx(max), + "last_reset": None, + "state": None, + "sum": None, + } + ] + } + + # Remove device class and record additional states + attributes.pop("device_class") + four, _states = record_states( + hass, zero + timedelta(minutes=5), "sensor.test1", attributes + ) + states["sensor.test1"] += _states["sensor.test1"] + four, _states = record_states( + hass, zero + timedelta(minutes=10), "sensor.test1", attributes + ) + states["sensor.test1"] += _states["sensor.test1"] + hist = history.get_significant_states(hass, zero, four) + assert dict(states) == dict(hist) + + # Run statistics again, we get a warning, and no additional statistics is generated + recorder.do_adhoc_statistics(start=zero + timedelta(minutes=10)) + wait_recording_done(hass) + assert ( + f"The unit of sensor.test1 ({state_unit}) does not match the " + f"unit of already compiled statistics ({statistic_unit})" in caplog.text + ) + statistic_ids = list_statistic_ids(hass) + assert statistic_ids == [ + {"statistic_id": "sensor.test1", "unit_of_measurement": statistic_unit} + ] + stats = statistics_during_period(hass, zero, period="5minute") + assert stats == { + "sensor.test1": [ + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "mean": approx(mean), + "min": approx(min), + "max": approx(max), + "last_reset": None, + "state": None, + "sum": None, + } + ] + } + assert "Error while processing event StatisticsTask" not in caplog.text + + @pytest.mark.parametrize( "device_class,unit,native_unit,mean,min,max", [ From c55e9136ee9c86dcd4088ba416043dbff7e65eac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mark=20M=C3=A4kinen?= Date: Tue, 12 Oct 2021 19:35:35 +0300 Subject: [PATCH 0287/1038] Fix Fast.com autoupdate (#57552) --- homeassistant/components/fastdotcom/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fastdotcom/__init__.py b/homeassistant/components/fastdotcom/__init__.py index f2424332a01..34c8bda2c6c 100644 --- a/homeassistant/components/fastdotcom/__init__.py +++ b/homeassistant/components/fastdotcom/__init__.py @@ -1,7 +1,7 @@ """Support for testing internet speed via Fast.com.""" from __future__ import annotations -from datetime import timedelta +from datetime import datetime, timedelta import logging from typing import Any @@ -67,7 +67,7 @@ class SpeedtestData: self.data: dict[str, Any] | None = None self._hass = hass - def update(self) -> None: + def update(self, now: datetime | None = None) -> None: """Get the latest data from fast.com.""" _LOGGER.debug("Executing fast.com speedtest") From a4357fdb957bcad2827597a440fab9f0dc31567e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 12 Oct 2021 19:36:14 +0200 Subject: [PATCH 0288/1038] Replace all Tuya device property constants with an Enum (#57559) --- homeassistant/components/tuya/climate.py | 140 ++++++++++------------- homeassistant/components/tuya/const.py | 40 +++++++ homeassistant/components/tuya/fan.py | 73 +++++------- homeassistant/components/tuya/light.py | 56 ++++----- homeassistant/components/tuya/switch.py | 68 ++++------- 5 files changed, 171 insertions(+), 206 deletions(-) diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index ac309464054..fd1b48f865b 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -29,33 +29,10 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantTuyaData from .base import TuyaHaEntity -from .const import DOMAIN, TUYA_DISCOVERY_NEW +from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode _LOGGER = logging.getLogger(__name__) - -# Air Conditioner -# https://developer.tuya.com/en/docs/iot/f?id=K9gf46qujdmwb -DPCODE_SWITCH = "switch" -DPCODE_TEMP_SET = "temp_set" -DPCODE_TEMP_SET_F = "temp_set_f" -DPCODE_MODE = "mode" -DPCODE_HUMIDITY_SET = "humidity_set" -DPCODE_FAN_SPEED_ENUM = "fan_speed_enum" - -# Temperature unit -DPCODE_TEMP_UNIT_CONVERT = "temp_unit_convert" -DPCODE_C_F = "c_f" - -# swing flap switch -DPCODE_SWITCH_HORIZONTAL = "switch_horizontal" -DPCODE_SWITCH_VERTICAL = "switch_vertical" - -# status -DPCODE_TEMP_CURRENT = "temp_current" -DPCODE_TEMP_CURRENT_F = "temp_current_f" -DPCODE_HUMIDITY_CURRENT = "humidity_current" - SWING_OFF = "swing_off" SWING_VERTICAL = "swing_vertical" SWING_HORIZONTAL = "swing_horizontal" @@ -72,6 +49,7 @@ TUYA_HVAC_TO_HA = { "auto": HVAC_MODE_AUTO, } +# https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq TUYA_SUPPORT_TYPE = { "kt", # Air conditioner "qn", # Heater @@ -108,14 +86,14 @@ class TuyaHaClimate(TuyaHaEntity, ClimateEntity): def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> None: """Init Tuya Ha Climate.""" super().__init__(device, device_manager) - if DPCODE_C_F in self.tuya_device.status: - self.dp_temp_unit = DPCODE_C_F + if DPCode.C_F in self.tuya_device.status: + self.dp_temp_unit = DPCode.C_F else: - self.dp_temp_unit = DPCODE_TEMP_UNIT_CONVERT + self.dp_temp_unit = DPCode.TEMP_UNIT_CONVERT def get_temp_set_scale(self) -> int | None: """Get temperature set scale.""" - dp_temp_set = DPCODE_TEMP_SET if self.is_celsius() else DPCODE_TEMP_SET_F + dp_temp_set = DPCode.TEMP_SET if self.is_celsius() else DPCode.TEMP_SET_F temp_set_value_range_item = self.tuya_device.status_range.get(dp_temp_set) if not temp_set_value_range_item: return None @@ -126,7 +104,7 @@ class TuyaHaClimate(TuyaHaEntity, ClimateEntity): def get_temp_current_scale(self) -> int | None: """Get temperature current scale.""" dp_temp_current = ( - DPCODE_TEMP_CURRENT if self.is_celsius() else DPCODE_TEMP_CURRENT_F + DPCode.TEMP_CURRENT if self.is_celsius() else DPCode.TEMP_CURRENT_F ) temp_current_value_range_item = self.tuya_device.status_range.get( dp_temp_current @@ -143,46 +121,46 @@ class TuyaHaClimate(TuyaHaEntity, ClimateEntity): """Set new target hvac mode.""" commands = [] if hvac_mode == HVAC_MODE_OFF: - commands.append({"code": DPCODE_SWITCH, "value": False}) + commands.append({"code": DPCode.SWITCH, "value": False}) else: - commands.append({"code": DPCODE_SWITCH, "value": True}) + commands.append({"code": DPCode.SWITCH, "value": True}) for tuya_mode, ha_mode in TUYA_HVAC_TO_HA.items(): if ha_mode == hvac_mode: - commands.append({"code": DPCODE_MODE, "value": tuya_mode}) + commands.append({"code": DPCode.MODE, "value": tuya_mode}) break self._send_command(commands) def set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" - self._send_command([{"code": DPCODE_FAN_SPEED_ENUM, "value": fan_mode}]) + self._send_command([{"code": DPCode.FAN_SPEED_ENUM, "value": fan_mode}]) def set_humidity(self, humidity: float) -> None: """Set new target humidity.""" - self._send_command([{"code": DPCODE_HUMIDITY_SET, "value": int(humidity)}]) + self._send_command([{"code": DPCode.HUMIDITY_SET, "value": int(humidity)}]) def set_swing_mode(self, swing_mode: str) -> None: """Set new target swing operation.""" if swing_mode == SWING_BOTH: commands = [ - {"code": DPCODE_SWITCH_VERTICAL, "value": True}, - {"code": DPCODE_SWITCH_HORIZONTAL, "value": True}, + {"code": DPCode.SWITCH_VERTICAL, "value": True}, + {"code": DPCode.SWITCH_HORIZONTAL, "value": True}, ] elif swing_mode == SWING_HORIZONTAL: commands = [ - {"code": DPCODE_SWITCH_VERTICAL, "value": False}, - {"code": DPCODE_SWITCH_HORIZONTAL, "value": True}, + {"code": DPCode.SWITCH_VERTICAL, "value": False}, + {"code": DPCode.SWITCH_HORIZONTAL, "value": True}, ] elif swing_mode == SWING_VERTICAL: commands = [ - {"code": DPCODE_SWITCH_VERTICAL, "value": True}, - {"code": DPCODE_SWITCH_HORIZONTAL, "value": False}, + {"code": DPCode.SWITCH_VERTICAL, "value": True}, + {"code": DPCode.SWITCH_HORIZONTAL, "value": False}, ] else: commands = [ - {"code": DPCODE_SWITCH_VERTICAL, "value": False}, - {"code": DPCODE_SWITCH_HORIZONTAL, "value": False}, + {"code": DPCode.SWITCH_VERTICAL, "value": False}, + {"code": DPCode.SWITCH_HORIZONTAL, "value": False}, ] self._send_command(commands) @@ -190,7 +168,7 @@ class TuyaHaClimate(TuyaHaEntity, ClimateEntity): def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" _LOGGER.debug("climate temp-> %s", kwargs) - code = DPCODE_TEMP_SET if self.is_celsius() else DPCODE_TEMP_SET_F + code = DPCode.TEMP_SET if self.is_celsius() else DPCode.TEMP_SET_F temp_set_scale = self.get_temp_set_scale() if not temp_set_scale: return @@ -212,8 +190,8 @@ class TuyaHaClimate(TuyaHaEntity, ClimateEntity): ): return True if ( - DPCODE_TEMP_SET in self.tuya_device.status - or DPCODE_TEMP_CURRENT in self.tuya_device.status + DPCode.TEMP_SET in self.tuya_device.status + or DPCode.TEMP_CURRENT in self.tuya_device.status ): return True return False @@ -229,8 +207,8 @@ class TuyaHaClimate(TuyaHaEntity, ClimateEntity): def current_temperature(self) -> float | None: """Return the current temperature.""" if ( - DPCODE_TEMP_CURRENT not in self.tuya_device.status - and DPCODE_TEMP_CURRENT_F not in self.tuya_device.status + DPCode.TEMP_CURRENT not in self.tuya_device.status + and DPCode.TEMP_CURRENT_F not in self.tuya_device.status ): return None @@ -239,12 +217,12 @@ class TuyaHaClimate(TuyaHaEntity, ClimateEntity): return None if self.is_celsius(): - temperature = self.tuya_device.status.get(DPCODE_TEMP_CURRENT) + temperature = self.tuya_device.status.get(DPCode.TEMP_CURRENT) if not temperature: return None return temperature * 1.0 / (10 ** temp_current_scale) - temperature = self.tuya_device.status.get(DPCODE_TEMP_CURRENT_F) + temperature = self.tuya_device.status.get(DPCode.TEMP_CURRENT_F) if not temperature: return None return temperature * 1.0 / (10 ** temp_current_scale) @@ -252,7 +230,7 @@ class TuyaHaClimate(TuyaHaEntity, ClimateEntity): @property def current_humidity(self) -> int: """Return the current humidity.""" - return int(self.tuya_device.status.get(DPCODE_HUMIDITY_CURRENT, 0)) + return int(self.tuya_device.status.get(DPCode.HUMIDITY_CURRENT, 0)) @property def target_temperature(self) -> float | None: @@ -261,7 +239,7 @@ class TuyaHaClimate(TuyaHaEntity, ClimateEntity): if temp_set_scale is None: return None - dpcode_temp_set = self.tuya_device.status.get(DPCODE_TEMP_SET) + dpcode_temp_set = self.tuya_device.status.get(DPCode.TEMP_SET) if dpcode_temp_set is None: return None @@ -275,10 +253,10 @@ class TuyaHaClimate(TuyaHaEntity, ClimateEntity): return DEFAULT_MAX_TEMP if self.is_celsius(): - if DPCODE_TEMP_SET not in self.tuya_device.function: + if DPCode.TEMP_SET not in self.tuya_device.function: return DEFAULT_MAX_TEMP - function_item = self.tuya_device.function.get(DPCODE_TEMP_SET) + function_item = self.tuya_device.function.get(DPCode.TEMP_SET) if function_item is None: return DEFAULT_MAX_TEMP @@ -288,10 +266,10 @@ class TuyaHaClimate(TuyaHaEntity, ClimateEntity): if temp_max is None: return DEFAULT_MAX_TEMP return temp_max * 1.0 / (10 ** scale) - if DPCODE_TEMP_SET_F not in self.tuya_device.function: + if DPCode.TEMP_SET_F not in self.tuya_device.function: return DEFAULT_MAX_TEMP - function_item_f = self.tuya_device.function.get(DPCODE_TEMP_SET_F) + function_item_f = self.tuya_device.function.get(DPCode.TEMP_SET_F) if function_item_f is None: return DEFAULT_MAX_TEMP @@ -310,10 +288,10 @@ class TuyaHaClimate(TuyaHaEntity, ClimateEntity): return DEFAULT_MIN_TEMP if self.is_celsius(): - if DPCODE_TEMP_SET not in self.tuya_device.function: + if DPCode.TEMP_SET not in self.tuya_device.function: return DEFAULT_MIN_TEMP - function_temp_item = self.tuya_device.function.get(DPCODE_TEMP_SET) + function_temp_item = self.tuya_device.function.get(DPCode.TEMP_SET) if function_temp_item is None: return DEFAULT_MIN_TEMP temp_value = json.loads(function_temp_item.values) @@ -322,10 +300,10 @@ class TuyaHaClimate(TuyaHaEntity, ClimateEntity): return DEFAULT_MIN_TEMP return temp_min * 1.0 / (10 ** temp_set_scal) - if DPCODE_TEMP_SET_F not in self.tuya_device.function: + if DPCode.TEMP_SET_F not in self.tuya_device.function: return DEFAULT_MIN_TEMP - temp_value_temp_f = self.tuya_device.function.get(DPCODE_TEMP_SET_F) + temp_value_temp_f = self.tuya_device.function.get(DPCode.TEMP_SET_F) if temp_value_temp_f is None: return DEFAULT_MIN_TEMP temp_value_f = json.loads(temp_value_temp_f.values) @@ -340,13 +318,13 @@ class TuyaHaClimate(TuyaHaEntity, ClimateEntity): def target_temperature_step(self) -> float | None: """Return target temperature setp.""" if ( - DPCODE_TEMP_SET not in self.tuya_device.status_range - and DPCODE_TEMP_SET_F not in self.tuya_device.status_range + DPCode.TEMP_SET not in self.tuya_device.status_range + and DPCode.TEMP_SET_F not in self.tuya_device.status_range ): return 1.0 temp_set_value_range = json.loads( self.tuya_device.status_range.get( - DPCODE_TEMP_SET if self.is_celsius() else DPCODE_TEMP_SET_F + DPCode.TEMP_SET if self.is_celsius() else DPCode.TEMP_SET_F ).values ) step = temp_set_value_range.get("step") @@ -362,25 +340,25 @@ class TuyaHaClimate(TuyaHaEntity, ClimateEntity): @property def target_humidity(self) -> int: """Return target humidity.""" - return int(self.tuya_device.status.get(DPCODE_HUMIDITY_SET, 0)) + return int(self.tuya_device.status.get(DPCode.HUMIDITY_SET, 0)) @property def hvac_mode(self) -> str: """Return hvac mode.""" - if not self.tuya_device.status.get(DPCODE_SWITCH, False): + if not self.tuya_device.status.get(DPCode.SWITCH, False): return HVAC_MODE_OFF - if DPCODE_MODE not in self.tuya_device.status: + if DPCode.MODE not in self.tuya_device.status: return HVAC_MODE_OFF - if self.tuya_device.status.get(DPCODE_MODE) is not None: - return TUYA_HVAC_TO_HA[self.tuya_device.status[DPCODE_MODE]] + if self.tuya_device.status.get(DPCode.MODE) is not None: + return TUYA_HVAC_TO_HA[self.tuya_device.status[DPCode.MODE]] return HVAC_MODE_OFF @property def hvac_modes(self) -> list[str]: """Return hvac modes for select.""" - if DPCODE_MODE not in self.tuya_device.function: + if DPCode.MODE not in self.tuya_device.function: return [] - modes = json.loads(self.tuya_device.function.get(DPCODE_MODE, {}).values).get( + modes = json.loads(self.tuya_device.function.get(DPCode.MODE, {}).values).get( "range" ) @@ -394,12 +372,12 @@ class TuyaHaClimate(TuyaHaEntity, ClimateEntity): @property def fan_mode(self) -> str | None: """Return fan mode.""" - return self.tuya_device.status.get(DPCODE_FAN_SPEED_ENUM) + return self.tuya_device.status.get(DPCode.FAN_SPEED_ENUM) @property def fan_modes(self) -> list[str]: """Return fan modes for select.""" - fan_speed_device_function = self.tuya_device.function.get(DPCODE_FAN_SPEED_ENUM) + fan_speed_device_function = self.tuya_device.function.get(DPCode.FAN_SPEED_ENUM) if not fan_speed_device_function: return [] return json.loads(fan_speed_device_function.values).get("range", []) @@ -409,13 +387,13 @@ class TuyaHaClimate(TuyaHaEntity, ClimateEntity): """Return swing mode.""" mode = 0 if ( - DPCODE_SWITCH_HORIZONTAL in self.tuya_device.status - and self.tuya_device.status.get(DPCODE_SWITCH_HORIZONTAL) + DPCode.SWITCH_HORIZONTAL in self.tuya_device.status + and self.tuya_device.status.get(DPCode.SWITCH_HORIZONTAL) ): mode += 1 if ( - DPCODE_SWITCH_VERTICAL in self.tuya_device.status - and self.tuya_device.status.get(DPCODE_SWITCH_VERTICAL) + DPCode.SWITCH_VERTICAL in self.tuya_device.status + and self.tuya_device.status.get(DPCode.SWITCH_VERTICAL) ): mode += 2 @@ -437,17 +415,17 @@ class TuyaHaClimate(TuyaHaEntity, ClimateEntity): """Flag supported features.""" supports = 0 if ( - DPCODE_TEMP_SET in self.tuya_device.status - or DPCODE_TEMP_SET_F in self.tuya_device.status + DPCode.TEMP_SET in self.tuya_device.status + or DPCode.TEMP_SET_F in self.tuya_device.status ): supports |= SUPPORT_TARGET_TEMPERATURE - if DPCODE_FAN_SPEED_ENUM in self.tuya_device.status: + if DPCode.FAN_SPEED_ENUM in self.tuya_device.status: supports |= SUPPORT_FAN_MODE - if DPCODE_HUMIDITY_SET in self.tuya_device.status: + if DPCode.HUMIDITY_SET in self.tuya_device.status: supports |= SUPPORT_TARGET_HUMIDITY if ( - DPCODE_SWITCH_HORIZONTAL in self.tuya_device.status - or DPCODE_SWITCH_VERTICAL in self.tuya_device.status + DPCode.SWITCH_HORIZONTAL in self.tuya_device.status + or DPCode.SWITCH_VERTICAL in self.tuya_device.status ): supports |= SUPPORT_SWING_MODE return supports diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 6d92383a871..359c1740c75 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -1,5 +1,6 @@ """Constants for the Tuya integration.""" from dataclasses import dataclass +from enum import Enum from tuya_iot import TuyaCloudOpenAPIEndpoint @@ -54,6 +55,45 @@ SMARTLIFE_APP = "smartlife" PLATFORMS = ["climate", "fan", "light", "scene", "switch"] +class DPCode(str, Enum): + """Device Property Codes used by Tuya. + + https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq + """ + + ANION = "anion" # Ionizer unit + BRIGHT_VALUE = "bright_value" # Brightness + C_F = "c_f" # Temperature unit switching + COLOUR_DATA = "colour_data" # Colored light mode + COLOUR_DATA_V2 = "colour_data_v2" # Colored light mode + FAN_DIRECTION = "fan_direction" # Fan direction + FAN_SPEED_ENUM = "fan_speed_enum" # Speed mode + FAN_SPEED_PERCENT = "fan_speed_percent" # Stepless speed + FILTER_RESET = "filter_reset" # Filter (cartridge) reset + HUMIDITY_CURRENT = "humidity_current" # Current humidity + HUMIDITY_SET = "humidity_set" # Humidity setting + LIGHT = "light" # Light + LOCK = "lock" # Lock / Child lock + MODE = "mode" # Working mode / Mode + PUMP_RESET = "pump_reset" # Water pump reset + SPEED = "speed" # Speed level + START = "start" # Start + SWITCH = "switch" # Switch + SWITCH_HORIZONTAL = "switch_horizontal" # Horizontal swing flap switch + SWITCH_LED = "switch_led" # Switch + SWITCH_VERTICAL = "switch_vertical" # Vertical swing flap switch + TEMP_CURRENT = "temp_current" # Current temperature in °C + TEMP_CURRENT_F = "temp_current_f" # Current temperature in °F + TEMP_SET = "temp_set" # Set the temperature in °C + TEMP_SET_F = "temp_set_f" # Set the temperature in °F + TEMP_UNIT_CONVERT = "temp_unit_convert" # Temperature unit switching + TEMP_VALUE = "temp_value" # Color temperature + UV = "uv" # UV sterilization + WATER_RESET = "water_reset" # Resetting of water usage days + WET = "wet" # Humidification + WORK_MODE = "work_mode" # Working mode + + @dataclass class Country: """Describe a supported country.""" diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index 88aa1ab53b9..eedc9020374 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -2,7 +2,6 @@ from __future__ import annotations import json -import logging from typing import Any from tuya_iot import TuyaDevice, TuyaDeviceManager @@ -27,23 +26,7 @@ from homeassistant.util.percentage import ( from . import HomeAssistantTuyaData from .base import TuyaHaEntity -from .const import DOMAIN, TUYA_DISCOVERY_NEW - -_LOGGER = logging.getLogger(__name__) - - -# Fan -# https://developer.tuya.com/en/docs/iot/f?id=K9gf45vs7vkge -DPCODE_SWITCH = "switch" -DPCODE_FAN_SPEED = "fan_speed_percent" -DPCODE_MODE = "mode" -DPCODE_SWITCH_HORIZONTAL = "switch_horizontal" -DPCODE_FAN_DIRECTION = "fan_direction" - -# Air Purifier -# https://developer.tuya.com/en/docs/iot/s?id=K9gf48r41mn81 -DPCODE_AP_FAN_SPEED = "speed" -DPCODE_AP_FAN_SPEED_ENUM = "fan_speed_enum" +from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode TUYA_SUPPORT_TYPE = { "fs", # Fan @@ -82,9 +65,9 @@ class TuyaHaFan(TuyaHaEntity, FanEntity): super().__init__(device, device_manager) self.ha_preset_modes = [] - if DPCODE_MODE in self.tuya_device.function: + if DPCode.MODE in self.tuya_device.function: self.ha_preset_modes = json.loads( - self.tuya_device.function[DPCODE_MODE].values + self.tuya_device.function[DPCode.MODE].values ).get("range", []) # Air purifier fan can be controlled either via the ranged values or via the enum. @@ -94,13 +77,13 @@ class TuyaHaFan(TuyaHaEntity, FanEntity): self.air_purifier_speed_range_len = 0 self.air_purifier_speed_range_enum = [] if self.tuya_device.category == "kj" and ( - DPCODE_AP_FAN_SPEED_ENUM in self.tuya_device.function - or DPCODE_AP_FAN_SPEED in self.tuya_device.function + DPCode.FAN_SPEED_ENUM in self.tuya_device.function + or DPCode.SPEED in self.tuya_device.function ): - if DPCODE_AP_FAN_SPEED_ENUM in self.tuya_device.function: - self.dp_code_speed_enum = DPCODE_AP_FAN_SPEED_ENUM + if DPCode.FAN_SPEED_ENUM in self.tuya_device.function: + self.dp_code_speed_enum = DPCode.FAN_SPEED_ENUM else: - self.dp_code_speed_enum = DPCODE_AP_FAN_SPEED + self.dp_code_speed_enum = DPCode.SPEED data = json.loads( self.tuya_device.function[self.dp_code_speed_enum].values @@ -111,11 +94,11 @@ class TuyaHaFan(TuyaHaEntity, FanEntity): def set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode of the fan.""" - self._send_command([{"code": DPCODE_MODE, "value": preset_mode}]) + self._send_command([{"code": DPCode.MODE, "value": preset_mode}]) def set_direction(self, direction: str) -> None: """Set the direction of the fan.""" - self._send_command([{"code": DPCODE_FAN_DIRECTION, "value": direction}]) + self._send_command([{"code": DPCode.FAN_DIRECTION, "value": direction}]) def set_percentage(self, percentage: int) -> None: """Set the speed of the fan, as a percentage.""" @@ -132,11 +115,13 @@ class TuyaHaFan(TuyaHaEntity, FanEntity): ] ) else: - self._send_command([{"code": DPCODE_FAN_SPEED, "value": percentage}]) + self._send_command( + [{"code": DPCode.FAN_SPEED_PERCENT, "value": percentage}] + ) def turn_off(self, **kwargs: Any) -> None: """Turn the fan off.""" - self._send_command([{"code": DPCODE_SWITCH, "value": False}]) + self._send_command([{"code": DPCode.SWITCH, "value": False}]) def turn_on( self, @@ -146,28 +131,28 @@ class TuyaHaFan(TuyaHaEntity, FanEntity): **kwargs: Any, ) -> None: """Turn on the fan.""" - self._send_command([{"code": DPCODE_SWITCH, "value": True}]) + self._send_command([{"code": DPCode.SWITCH, "value": True}]) def oscillate(self, oscillating: bool) -> None: """Oscillate the fan.""" - self._send_command([{"code": DPCODE_SWITCH_HORIZONTAL, "value": oscillating}]) + self._send_command([{"code": DPCode.SWITCH_HORIZONTAL, "value": oscillating}]) @property def is_on(self) -> bool: """Return true if fan is on.""" - return self.tuya_device.status.get(DPCODE_SWITCH, False) + return self.tuya_device.status.get(DPCode.SWITCH, False) @property def current_direction(self) -> str: """Return the current direction of the fan.""" - if self.tuya_device.status[DPCODE_FAN_DIRECTION]: + if self.tuya_device.status[DPCode.FAN_DIRECTION]: return DIRECTION_FORWARD return DIRECTION_REVERSE @property def oscillating(self) -> bool: """Return true if the fan is oscillating.""" - return self.tuya_device.status.get(DPCODE_SWITCH_HORIZONTAL, False) + return self.tuya_device.status.get(DPCode.SWITCH_HORIZONTAL, False) @property def preset_modes(self) -> list[str]: @@ -177,7 +162,7 @@ class TuyaHaFan(TuyaHaEntity, FanEntity): @property def preset_mode(self) -> str: """Return the current preset_mode.""" - return self.tuya_device.status[DPCODE_MODE] + return self.tuya_device.status[DPCode.MODE] @property def percentage(self) -> int | None: @@ -189,16 +174,16 @@ class TuyaHaFan(TuyaHaEntity, FanEntity): self.tuya_device.category == "kj" and self.air_purifier_speed_range_len > 1 and not self.air_purifier_speed_range_enum - and DPCODE_AP_FAN_SPEED_ENUM in self.tuya_device.status + and DPCode.FAN_SPEED_ENUM in self.tuya_device.status ): # if air-purifier speed enumeration is supported we will prefer it. return ordered_list_item_to_percentage( self.air_purifier_speed_range_enum, - self.tuya_device.status[DPCODE_AP_FAN_SPEED_ENUM], + self.tuya_device.status[DPCode.FAN_SPEED_ENUM], ) # some type may not have the fan_speed_percent key - return self.tuya_device.status.get(DPCODE_FAN_SPEED) + return self.tuya_device.status.get(DPCode.FAN_SPEED_PERCENT) @property def speed_count(self) -> int: @@ -211,19 +196,19 @@ class TuyaHaFan(TuyaHaEntity, FanEntity): def supported_features(self): """Flag supported features.""" supports = 0 - if DPCODE_MODE in self.tuya_device.status: + if DPCode.MODE in self.tuya_device.status: supports |= SUPPORT_PRESET_MODE - if DPCODE_FAN_SPEED in self.tuya_device.status: + if DPCode.FAN_SPEED_PERCENT in self.tuya_device.status: supports |= SUPPORT_SET_SPEED - if DPCODE_SWITCH_HORIZONTAL in self.tuya_device.status: + if DPCode.SWITCH_HORIZONTAL in self.tuya_device.status: supports |= SUPPORT_OSCILLATE - if DPCODE_FAN_DIRECTION in self.tuya_device.status: + if DPCode.FAN_DIRECTION in self.tuya_device.status: supports |= SUPPORT_DIRECTION # Air Purifier specific if ( - DPCODE_AP_FAN_SPEED in self.tuya_device.status - or DPCODE_AP_FAN_SPEED_ENUM in self.tuya_device.status + DPCode.SPEED in self.tuya_device.status + or DPCode.FAN_SPEED_ENUM in self.tuya_device.status ): supports |= SUPPORT_SET_SPEED return supports diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 1970ba4c312..40f1628f2a7 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -24,21 +24,10 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantTuyaData from .base import TuyaHaEntity -from .const import DOMAIN, TUYA_DISCOVERY_NEW +from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode _LOGGER = logging.getLogger(__name__) - -# Light(dj) -# https://developer.tuya.com/en/docs/iot/f?id=K9i5ql3v98hn3 -DPCODE_SWITCH = "switch_led" -DPCODE_WORK_MODE = "work_mode" -DPCODE_BRIGHT_VALUE = "bright_value" -DPCODE_TEMP_VALUE = "temp_value" -DPCODE_COLOUR_DATA = "colour_data" -DPCODE_COLOUR_DATA_V2 = "colour_data_v2" -DPCODE_LIGHT = "light" - MIREDS_MAX = 500 MIREDS_MIN = 153 @@ -50,6 +39,7 @@ HSV_HA_SATURATION_MAX = 100 WORK_MODE_WHITE = "white" WORK_MODE_COLOUR = "colour" +# https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq TUYA_SUPPORT_TYPE = { "dj", # Light "dd", # Light strip @@ -102,16 +92,16 @@ class TuyaHaLight(TuyaHaEntity, LightEntity): def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> None: """Init TuyaHaLight.""" - self.dp_code_bright = DPCODE_BRIGHT_VALUE - self.dp_code_temp = DPCODE_TEMP_VALUE - self.dp_code_colour = DPCODE_COLOUR_DATA + self.dp_code_bright = DPCode.BRIGHT_VALUE + self.dp_code_temp = DPCode.TEMP_VALUE + self.dp_code_colour = DPCode.COLOUR_DATA for key in device.function: - if key.startswith(DPCODE_BRIGHT_VALUE): + if key.startswith(DPCode.BRIGHT_VALUE): self.dp_code_bright = key - elif key.startswith(DPCODE_TEMP_VALUE): + elif key.startswith(DPCode.TEMP_VALUE): self.dp_code_temp = key - elif key.startswith(DPCODE_COLOUR_DATA): + elif key.startswith(DPCode.COLOUR_DATA): self.dp_code_colour = key super().__init__(device, device_manager) @@ -119,7 +109,7 @@ class TuyaHaLight(TuyaHaEntity, LightEntity): @property def is_on(self) -> bool: """Return true if light is on.""" - return self.tuya_device.status.get(DPCODE_SWITCH, False) + return self.tuya_device.status.get(DPCode.SWITCH_LED, False) def turn_on(self, **kwargs: Any) -> None: """Turn on or control the light.""" @@ -127,12 +117,12 @@ class TuyaHaLight(TuyaHaEntity, LightEntity): _LOGGER.debug("light kwargs-> %s", kwargs) if ( - DPCODE_LIGHT in self.tuya_device.status - and DPCODE_SWITCH not in self.tuya_device.status + DPCode.LIGHT in self.tuya_device.status + and DPCode.SWITCH_LED not in self.tuya_device.status ): - commands += [{"code": DPCODE_LIGHT, "value": True}] + commands += [{"code": DPCode.LIGHT, "value": True}] else: - commands += [{"code": DPCODE_SWITCH, "value": True}] + commands += [{"code": DPCode.SWITCH_LED, "value": True}] if ATTR_BRIGHTNESS in kwargs: if self._work_mode().startswith(WORK_MODE_COLOUR): @@ -177,8 +167,8 @@ class TuyaHaLight(TuyaHaEntity, LightEntity): commands += [ {"code": self.dp_code_colour, "value": json.dumps(colour_data)} ] - if self.tuya_device.status[DPCODE_WORK_MODE] != "colour": - commands += [{"code": DPCODE_WORK_MODE, "value": "colour"}] + if self.tuya_device.status[DPCode.WORK_MODE] != "colour": + commands += [{"code": DPCode.WORK_MODE, "value": "colour"}] if ATTR_COLOR_TEMP in kwargs: # temp color @@ -200,20 +190,20 @@ class TuyaHaLight(TuyaHaEntity, LightEntity): ) commands += [{"code": self.dp_code_bright, "value": int(tuya_brightness)}] - if self.tuya_device.status[DPCODE_WORK_MODE] != "white": - commands += [{"code": DPCODE_WORK_MODE, "value": "white"}] + if self.tuya_device.status[DPCode.WORK_MODE] != "white": + commands += [{"code": DPCode.WORK_MODE, "value": "white"}] self._send_command(commands) def turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" if ( - DPCODE_LIGHT in self.tuya_device.status - and DPCODE_SWITCH not in self.tuya_device.status + DPCode.LIGHT in self.tuya_device.status + and DPCode.SWITCH_LED not in self.tuya_device.status ): - commands = [{"code": DPCODE_LIGHT, "value": False}] + commands = [{"code": DPCode.LIGHT, "value": False}] else: - commands = [{"code": DPCODE_SWITCH, "value": False}] + commands = [{"code": DPCode.SWITCH_LED, "value": False}] self._send_command(commands) @property @@ -319,7 +309,7 @@ class TuyaHaLight(TuyaHaEntity, LightEntity): return None colour_data = json.loads(colour_json) if ( - self.dp_code_colour == DPCODE_COLOUR_DATA_V2 + self.dp_code_colour == DPCode.COLOUR_DATA_V2 or colour_data.get("v", 0) > 255 or colour_data.get("s", 0) > 255 ): @@ -327,7 +317,7 @@ class TuyaHaLight(TuyaHaEntity, LightEntity): return DEFAULT_HSV def _work_mode(self) -> str: - return self.tuya_device.status.get(DPCODE_WORK_MODE, "") + return self.tuya_device.status.get(DPCode.WORK_MODE, "") def _get_hsv(self) -> dict[str, int]: return json.loads(self.tuya_device.status[self.dp_code_colour]) diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index af723a3d4f4..663603d2727 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -1,7 +1,6 @@ """Support for Tuya switches.""" from __future__ import annotations -import logging from typing import Any from tuya_iot import TuyaDevice, TuyaDeviceManager @@ -14,10 +13,9 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantTuyaData from .base import TuyaHaEntity -from .const import DOMAIN, TUYA_DISCOVERY_NEW - -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode +# https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq TUYA_SUPPORT_TYPE = { "kg", # Switch "cz", # Socket @@ -29,28 +27,6 @@ TUYA_SUPPORT_TYPE = { "xxj", # Diffuser } -# Switch(kg), Socket(cz), Power Strip(pc) -# https://developer.tuya.com/en/docs/iot/categorykgczpc?id=Kaiuz08zj1l4y -DPCODE_SWITCH = "switch" - -# Air Purifier -# https://developer.tuya.com/en/docs/iot/categorykj?id=Kaiuz1atqo5l7 -# Pet Water Feeder -# https://developer.tuya.com/en/docs/iot/f?id=K9gf46aewxem5 -DPCODE_ANION = "anion" # Air Purifier - Ionizer unit -# Air Purifier - Filter cartridge resetting; Pet Water Feeder - Filter cartridge resetting -DPCODE_FRESET = "filter_reset" -DPCODE_LIGHT = "light" # Air Purifier - Light -DPCODE_LOCK = "lock" # Air Purifier - Child lock -# Air Purifier - UV sterilization; Pet Water Feeder - UV sterilization -DPCODE_UV = "uv" -DPCODE_WET = "wet" # Air Purifier - Humidification unit -DPCODE_PRESET = "pump_reset" # Pet Water Feeder - Water pump resetting -DPCODE_WRESET = "water_reset" # Pet Water Feeder - Resetting of water usage days - - -DPCODE_START = "start" - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -88,12 +64,12 @@ def _setup_entities( for function in device.function: if device.category == "kj": if function in [ - DPCODE_ANION, - DPCODE_FRESET, - DPCODE_LIGHT, - DPCODE_LOCK, - DPCODE_UV, - DPCODE_WET, + DPCode.ANION, + DPCode.FILTER_RESET, + DPCode.LIGHT, + DPCode.LOCK, + DPCode.UV, + DPCode.WET, ]: entities.append(TuyaHaSwitch(device, device_manager, function)) @@ -101,17 +77,17 @@ def _setup_entities( if ( function in [ - DPCODE_FRESET, - DPCODE_UV, - DPCODE_PRESET, - DPCODE_WRESET, + DPCode.FILTER_RESET, + DPCode.UV, + DPCode.PUMP_RESET, + DPCode.WATER_RESET, ] - or function.startswith(DPCODE_SWITCH) + or function.startswith(DPCode.SWITCH) ): entities.append(TuyaHaSwitch(device, device_manager, function)) - elif function.startswith(DPCODE_START) or function.startswith( - DPCODE_SWITCH + elif function.startswith(DPCode.START) or function.startswith( + DPCode.SWITCH ): entities.append(TuyaHaSwitch(device, device_manager, function)) @@ -121,8 +97,8 @@ def _setup_entities( class TuyaHaSwitch(TuyaHaEntity, SwitchEntity): """Tuya Switch Device.""" - dp_code_switch = DPCODE_SWITCH - dp_code_start = DPCODE_START + dp_code_switch = DPCode.SWITCH + dp_code_start = DPCode.START def __init__( self, device: TuyaDevice, device_manager: TuyaDeviceManager, dp_code: str = "" @@ -132,15 +108,11 @@ class TuyaHaSwitch(TuyaHaEntity, SwitchEntity): self.dp_code = dp_code self.channel = ( - dp_code.replace(DPCODE_SWITCH, "") - if dp_code.startswith(DPCODE_SWITCH) + dp_code.replace(DPCode.SWITCH, "") + if dp_code.startswith(DPCode.SWITCH) else dp_code ) - - @property - def unique_id(self) -> str | None: - """Return a unique ID.""" - return f"{super().unique_id}{self.channel}" + self._attr_unique_id = f"{super().unique_id}{self.channel}" @property def name(self) -> str | None: From ee98849360a284447aa6238e20c670f098300b46 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 12 Oct 2021 19:39:36 +0200 Subject: [PATCH 0289/1038] Always include start point for statistics (#57182) --- .../components/recorder/statistics.py | 65 +++++++++++++++++-- tests/components/sensor/test_recorder.py | 23 ++++++- 2 files changed, 80 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 200da8d192d..2dc18d3aecb 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -5,7 +5,7 @@ from collections import defaultdict from collections.abc import Callable, Iterable import dataclasses from datetime import datetime, timedelta -from itertools import groupby +from itertools import chain, groupby import logging from typing import TYPE_CHECKING, Any, Literal @@ -629,7 +629,7 @@ def statistics_during_period( return {} # Return statistics combined with metadata return _sorted_statistics_to_dict( - hass, stats, statistic_ids, metadata, True, table.duration + hass, session, stats, statistic_ids, metadata, True, table, start_time ) @@ -668,25 +668,64 @@ def get_last_statistics( # Return statistics combined with metadata return _sorted_statistics_to_dict( hass, + session, stats, statistic_ids, metadata, convert_units, - StatisticsShortTerm.duration, + StatisticsShortTerm, + None, ) +def _statistics_at_time( + session: scoped_session, + metadata_ids: set[int], + table: type[Statistics | StatisticsShortTerm], + start_time: datetime, +) -> list | None: + """Return last known statics, earlier than start_time, for the metadata_ids.""" + # Fetch metadata for the given (or all) statistic_ids + if table == StatisticsShortTerm: + base_query = QUERY_STATISTICS_SHORT_TERM + else: + base_query = QUERY_STATISTICS + + query = session.query(*base_query) + + most_recent_statistic_ids = ( + session.query( + func.max(table.id).label("max_id"), + ) + .filter(table.start < start_time) + .filter(table.metadata_id.in_(metadata_ids)) + ) + most_recent_statistic_ids = most_recent_statistic_ids.group_by(table.metadata_id) + most_recent_statistic_ids = most_recent_statistic_ids.subquery() + query = query.join( + most_recent_statistic_ids, + table.id == most_recent_statistic_ids.c.max_id, + ) + + return execute(query) + + def _sorted_statistics_to_dict( hass: HomeAssistant, + session: scoped_session, stats: list, statistic_ids: list[str] | None, _metadata: dict[str, tuple[int, StatisticMetaData]], convert_units: bool, - duration: timedelta, + table: type[Statistics | StatisticsShortTerm], + start_time: datetime | None, ) -> dict[str, list[dict]]: """Convert SQL results into JSON friendly data structure.""" result: dict = defaultdict(list) units = hass.config.units + metadata = dict(_metadata.values()) + need_stat_at_start_time = set() + stats_at_start_time = {} def no_conversion(val: Any, _: Any) -> float | None: """Return x.""" @@ -697,7 +736,19 @@ def _sorted_statistics_to_dict( for stat_id in statistic_ids: result[stat_id] = [] - metadata = dict(_metadata.values()) + # Identify metadata IDs for which no data was available at the requested start time + for meta_id, group in groupby(stats, lambda stat: stat.metadata_id): # type: ignore + first_start_time = process_timestamp(next(group).start) + if start_time and first_start_time > start_time: + need_stat_at_start_time.add(meta_id) + + # Fetch last known statistics for the needed metadata IDs + if need_stat_at_start_time: + assert start_time # Can not be None if need_stat_at_start_time is not empty + tmp = _statistics_at_time(session, need_stat_at_start_time, table, start_time) + if tmp: + for stat in tmp: + stats_at_start_time[stat.metadata_id] = (stat,) # Append all statistic entries, and optionally do unit conversion for meta_id, group in groupby(stats, lambda stat: stat.metadata_id): # type: ignore @@ -709,9 +760,9 @@ def _sorted_statistics_to_dict( else: convert = no_conversion ent_results = result[meta_id] - for db_state in group: + for db_state in chain(stats_at_start_time.get(meta_id, ()), group): start = process_timestamp(db_state.start) - end = start + duration + end = start + table.duration ent_results.append( { "statistic_id": statistic_id, diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 6fe90a26ded..96335d435da 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -337,7 +337,7 @@ def test_compile_hourly_sum_statistics_amount( {"statistic_id": "sensor.test1", "unit_of_measurement": display_unit} ] stats = statistics_during_period(hass, period0, period="5minute") - assert stats == { + expected_stats = { "sensor.test1": [ { "statistic_id": "sensor.test1", @@ -374,6 +374,27 @@ def test_compile_hourly_sum_statistics_amount( }, ] } + assert stats == expected_stats + + # With an offset of 1 minute, we expect to get all periods + stats = statistics_during_period( + hass, period0 + timedelta(minutes=1), period="5minute" + ) + assert stats == expected_stats + + # With an offset of 5 minutes, we expect to get the 2nd and 3rd periods + stats = statistics_during_period( + hass, period0 + timedelta(minutes=5), period="5minute" + ) + expected_stats["sensor.test1"] = expected_stats["sensor.test1"][1:3] + assert stats == expected_stats + + # With an offset of 6 minutes, we expect to get the 2nd and 3rd periods + stats = statistics_during_period( + hass, period0 + timedelta(minutes=6), period="5minute" + ) + assert stats == expected_stats + assert "Error while processing event StatisticsTask" not in caplog.text assert "Detected new cycle for sensor.test1, last_reset set to" in caplog.text assert "Compiling initial sum statistics for sensor.test1" in caplog.text From deec3dfae4b595525b9d3c93ae28d3e1abab6b9b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 12 Oct 2021 12:35:10 -0600 Subject: [PATCH 0290/1038] Bump simplisafe-python to 11.0.7 (#57573) --- homeassistant/components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index c6bc3ae61fa..8b610c6c28c 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==11.0.6"], + "requirements": ["simplisafe-python==11.0.7"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 9512f9bf75f..57874bea5fd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2146,7 +2146,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==11.0.6 +simplisafe-python==11.0.7 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 18211445831..89e78f0af10 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1230,7 +1230,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==11.0.6 +simplisafe-python==11.0.7 # homeassistant.components.slack slackclient==2.5.0 From 282300f3e4639f9a059db08f951808c341054060 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 13 Oct 2021 00:11:33 +0000 Subject: [PATCH 0291/1038] [ci skip] Translation update --- .../environment_canada/translations/ca.json | 19 +++++++++++++++ .../translations/zh-Hant.json | 23 +++++++++++++++++++ .../components/watttime/translations/ca.json | 10 +++++++- .../components/watttime/translations/et.json | 10 +++++++- .../components/watttime/translations/ru.json | 10 +++++++- .../watttime/translations/zh-Hant.json | 10 +++++++- 6 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/environment_canada/translations/ca.json create mode 100644 homeassistant/components/environment_canada/translations/zh-Hant.json diff --git a/homeassistant/components/environment_canada/translations/ca.json b/homeassistant/components/environment_canada/translations/ca.json new file mode 100644 index 00000000000..d1dd85556eb --- /dev/null +++ b/homeassistant/components/environment_canada/translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "bad_station_id": "L'ID d'estaci\u00f3 no \u00e9s v\u00e0lid, no est\u00e0 present o no es troba a la base de dades d'IDs d'estacions", + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "language": "Idioma de la informaci\u00f3 meteorol\u00f2gica", + "latitude": "Latitud", + "longitude": "Longitud", + "station": "ID de l'estaci\u00f3 meteorol\u00f2gica" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/zh-Hant.json b/homeassistant/components/environment_canada/translations/zh-Hant.json new file mode 100644 index 00000000000..59fe99e8ead --- /dev/null +++ b/homeassistant/components/environment_canada/translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "bad_station_id": "\u6c23\u8c61\u7ad9 ID \u7121\u6548\u3001\u907a\u5931\u6216\u8cc7\u6599\u5eab\u4e2d\u627e\u4e0d\u5230\u8a72 ID", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "error_response": "\u4f86\u81ea Environment Canada \u56de\u8986\u932f\u8aa4", + "too_many_attempts": "\u8207 Environment Canada \u9023\u7dda\u6b21\u6578\u70ba\u6709\u9650\u6b21\u6578\uff1b\u8acb\u65bc 60 \u79d2\u5f8c\u518d\u8a66\u4e00\u6b21", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "language": "\u6c23\u8c61\u8cc7\u8a0a\u8a9e\u8a00", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6", + "station": "\u6c23\u8c61\u7ad9 ID" + }, + "description": "\u5fc5\u9808\u6307\u5b9a\u6c23\u8c61\u7ad9 ID \u6216\u7d93\u5ea6/\u7def\u5ea6\u3002\u5c07\u4f7f\u7528 Home Assistant \u5b89\u88dd\u4e2d\u8a2d\u5b9a\u4e4b\u7d93\u5ea6/\u7def\u5ea6\u70ba\u9810\u8a2d\u503c\uff0c\u4e26\u4f7f\u7528\u6700\u9760\u8fd1\u7684\u6c23\u8c61\u7ad9\u8cc7\u6599\u3002\u5047\u5982\u4f7f\u7528\u6c23\u8c61\u7ad9\u4ee3\u78bc\u5247\u5fc5\u9808\u8ddf\u96a8\u4ee5\u4e0b\u683c\u5f0f\uff1aPP/\u4ee3\u78bc\uff0cPP \u70ba\u5169\u4f4d\u5b57\u6bcd\u8868\u793a\u7701/\u5dde\u3001\u800c\u4ee3\u78bc\u5247\u70ba\u6c23\u8c61\u7ad9 ID\u3002\u53ef\u4ee5\u65bc\u6b64\u8655\u627e\u5230\u6c23\u8c61\u7ad9 ID \u5217\u8868\uff1ahttps://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv\u3002\u6c23\u8c61\u8cc7\u8a0a\u5247\u53ef\u8a2d\u5b9a\u70ba\u82f1\u6587\u6216\u6cd5\u6587\u3002", + "title": "Environment Canada\uff1a\u6c23\u8c61\u7ad9\u4f4d\u7f6e\u8207\u8a9e\u8a00" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/ca.json b/homeassistant/components/watttime/translations/ca.json index 09a0360fab3..ec86979a9fe 100644 --- a/homeassistant/components/watttime/translations/ca.json +++ b/homeassistant/components/watttime/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", @@ -22,6 +23,13 @@ }, "description": "Tria una ubicaci\u00f3 a monitoritzar:" }, + "reauth_confirm": { + "data": { + "password": "Contrasenya" + }, + "description": "Torna a introduir la contrasenya de {username}:", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, "user": { "data": { "password": "Contrasenya", diff --git a/homeassistant/components/watttime/translations/et.json b/homeassistant/components/watttime/translations/et.json index c9f47756021..d9e494e7112 100644 --- a/homeassistant/components/watttime/translations/et.json +++ b/homeassistant/components/watttime/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", @@ -22,6 +23,13 @@ }, "description": "Vali j\u00e4lgiv asukoht:" }, + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "Sisesta uuesti {username} salas\u00f5na:", + "title": "Taastuvasta sidumine" + }, "user": { "data": { "password": "Salas\u00f5na", diff --git a/homeassistant/components/watttime/translations/ru.json b/homeassistant/components/watttime/translations/ru.json index d7e67187d9d..5b2b80bddd8 100644 --- a/homeassistant/components/watttime/translations/ru.json +++ b/homeassistant/components/watttime/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.", @@ -22,6 +23,13 @@ }, "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430:" }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f {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/watttime/translations/zh-Hant.json b/homeassistant/components/watttime/translations/zh-Hant.json index 898dfc05dd7..1c405ee0a87 100644 --- a/homeassistant/components/watttime/translations/zh-Hant.json +++ b/homeassistant/components/watttime/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", @@ -22,6 +23,13 @@ }, "description": "\u9078\u64c7\u6240\u8981\u76e3\u63a7\u7684\u4f4d\u7f6e\uff1a" }, + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "\u8acb\u91cd\u65b0\u8f38\u5165 {username} \u5bc6\u78bc\uff1a", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, "user": { "data": { "password": "\u5bc6\u78bc", From 2adb9a8becbabfe3d017c11487c71298a46a430d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 12 Oct 2021 18:39:46 -1000 Subject: [PATCH 0292/1038] Refresh the bond token if it has changed and available (#57583) --- homeassistant/components/bond/__init__.py | 17 +++++++- homeassistant/components/bond/config_flow.py | 41 ++++++++++++----- tests/components/bond/test_config_flow.py | 46 +++++++++++++++++++- tests/components/bond/test_init.py | 32 ++++++++++++-- 4 files changed, 118 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index 93a927d21f3..90e2838ff36 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -1,11 +1,17 @@ """The Bond integration.""" from asyncio import TimeoutError as AsyncIOTimeoutError +import logging -from aiohttp import ClientError, ClientTimeout +from aiohttp import ClientError, ClientResponseError, ClientTimeout from bond_api import Bond, BPUPSubscriptions, start_bpup from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_HOST, + EVENT_HOMEASSISTANT_STOP, + HTTP_UNAUTHORIZED, +) from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -19,6 +25,8 @@ PLATFORMS = ["cover", "fan", "light", "switch"] _API_TIMEOUT = SLOW_UPDATE_WARNING - 1 _STOP_CANCEL = "stop_cancel" +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Bond from a config entry.""" @@ -35,6 +43,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hub = BondHub(bond) try: await hub.setup() + except ClientResponseError as ex: + if ex.status == HTTP_UNAUTHORIZED: + _LOGGER.error("Bond token no longer valid: %s", ex) + return False + raise ConfigEntryNotReady from ex except (ClientError, AsyncIOTimeoutError, OSError) as error: raise ConfigEntryNotReady from error diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index 9285b580851..da8f51227dd 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -9,6 +9,7 @@ from bond_api import Bond import voluptuous as vol from homeassistant import config_entries, exceptions +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_ACCESS_TOKEN, CONF_HOST, @@ -16,7 +17,7 @@ from homeassistant.const import ( HTTP_UNAUTHORIZED, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult +from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import DiscoveryInfoType @@ -33,6 +34,16 @@ DISCOVERY_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str}) TOKEN_SCHEMA = vol.Schema({}) +async def async_get_token(hass: HomeAssistant, host: str) -> str | None: + """Try to fetch the token from the bond device.""" + bond = Bond(host, "", session=async_get_clientsession(hass)) + try: + response: dict[str, str] = await bond.token() + except ClientConnectionError: + return None + return response.get("token") + + async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> tuple[str, str]: """Validate the user input allows us to connect.""" @@ -75,16 +86,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): online longer then the allowed setup period, and we will instead ask them to manually enter the token. """ - bond = Bond( - self._discovered[CONF_HOST], "", session=async_get_clientsession(self.hass) - ) - try: - response = await bond.token() - except ClientConnectionError: - return - - token = response.get("token") - if token is None: + host = self._discovered[CONF_HOST] + if not (token := await async_get_token(self.hass, host)): return self._discovered[CONF_ACCESS_TOKEN] = token @@ -99,7 +102,21 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): host: str = discovery_info[CONF_HOST] bond_id = name.partition(".")[0] await self.async_set_unique_id(bond_id) - self._abort_if_unique_id_configured({CONF_HOST: host}) + for entry in self._async_current_entries(): + if entry.unique_id != bond_id: + continue + updates = {CONF_HOST: host} + if entry.state == ConfigEntryState.SETUP_ERROR and ( + token := await async_get_token(self.hass, host) + ): + updates[CONF_ACCESS_TOKEN] = token + self.hass.config_entries.async_update_entry( + entry, data={**entry.data, **updates} + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + raise AbortFlow("already_configured") self._discovered = {CONF_HOST: host, CONF_NAME: bond_id} await self._async_try_automatic_configure() diff --git a/tests/components/bond/test_config_flow.py b/tests/components/bond/test_config_flow.py index 8dd379ed3a7..db9652bf0be 100644 --- a/tests/components/bond/test_config_flow.py +++ b/tests/components/bond/test_config_flow.py @@ -2,12 +2,13 @@ from __future__ import annotations from typing import Any -from unittest.mock import Mock, patch +from unittest.mock import MagicMock, Mock, patch from aiohttp import ClientConnectionError, ClientResponseError from homeassistant import config_entries, core from homeassistant.components.bond.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST from .common import ( @@ -308,6 +309,49 @@ async def test_zeroconf_already_configured(hass: core.HomeAssistant): assert len(mock_setup_entry.mock_calls) == 0 +async def test_zeroconf_already_configured_refresh_token(hass: core.HomeAssistant): + """Test starting a flow from zeroconf when already configured and the token is out of date.""" + entry2 = MockConfigEntry( + domain=DOMAIN, + unique_id="not-the-same-bond-id", + data={CONF_HOST: "stored-host", CONF_ACCESS_TOKEN: "correct-token"}, + ) + entry2.add_to_hass(hass) + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="already-registered-bond-id", + data={CONF_HOST: "stored-host", CONF_ACCESS_TOKEN: "incorrect-token"}, + ) + entry.add_to_hass(hass) + + with patch_bond_version( + side_effect=ClientResponseError(MagicMock(), MagicMock(), status=401) + ): + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.SETUP_ERROR + + with _patch_async_setup_entry() as mock_setup_entry, patch_bond_token( + return_value={"token": "discovered-token"} + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data={ + "name": "already-registered-bond-id.some-other-tail-info", + "host": "updated-host", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data["host"] == "updated-host" + assert entry.data[CONF_ACCESS_TOKEN] == "discovered-token" + # entry2 should not get changed + assert entry2.data[CONF_ACCESS_TOKEN] == "correct-token" + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_zeroconf_form_unexpected_error(hass: core.HomeAssistant): """Test we handle unexpected error gracefully.""" await _help_test_form_unexpected_error( diff --git a/tests/components/bond/test_init.py b/tests/components/bond/test_init.py index 4ba105248df..42eca44dfa7 100644 --- a/tests/components/bond/test_init.py +++ b/tests/components/bond/test_init.py @@ -1,8 +1,10 @@ """Tests for the Bond module.""" -from unittest.mock import Mock +import asyncio +from unittest.mock import MagicMock, Mock from aiohttp import ClientConnectionError, ClientResponseError from bond_api import DeviceType +import pytest from homeassistant.components.bond.const import DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -33,7 +35,16 @@ async def test_async_setup_no_domain_config(hass: HomeAssistant): assert result is True -async def test_async_setup_raises_entry_not_ready(hass: HomeAssistant): +@pytest.mark.parametrize( + "exc", + [ + ClientConnectionError, + ClientResponseError(MagicMock(), MagicMock(), status=404), + asyncio.TimeoutError, + OSError, + ], +) +async def test_async_setup_raises_entry_not_ready(hass: HomeAssistant, exc: Exception): """Test that it throws ConfigEntryNotReady when exception occurs during setup.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -41,11 +52,26 @@ async def test_async_setup_raises_entry_not_ready(hass: HomeAssistant): ) config_entry.add_to_hass(hass) - with patch_bond_version(side_effect=ClientConnectionError()): + with patch_bond_version(side_effect=exc): await hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.state is ConfigEntryState.SETUP_RETRY +async def test_async_setup_raises_fails_if_auth_fails(hass: HomeAssistant): + """Test that setup fails if auth fails during setup.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, + ) + config_entry.add_to_hass(hass) + + with patch_bond_version( + side_effect=ClientResponseError(MagicMock(), MagicMock(), status=401) + ): + await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.state is ConfigEntryState.SETUP_ERROR + + async def test_async_setup_entry_sets_up_hub_and_supported_domains(hass: HomeAssistant): """Test that configuring entry sets up cover domain.""" config_entry = MockConfigEntry( From f41aedc0f9ea35dd8f5b129ef4be8c1ce804ac7e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 12 Oct 2021 18:40:18 -1000 Subject: [PATCH 0293/1038] Fix single channel controllers with flux_led (#57458) --- homeassistant/components/flux_led/light.py | 12 +++--------- homeassistant/components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/flux_led/test_light.py | 14 ++------------ 5 files changed, 8 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index b587abcc7e6..885145c4b5c 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -30,14 +30,12 @@ from homeassistant.components.light import ( ATTR_RGB_COLOR, ATTR_RGBW_COLOR, ATTR_RGBWW_COLOR, - ATTR_WHITE, COLOR_MODE_BRIGHTNESS, COLOR_MODE_COLOR_TEMP, COLOR_MODE_ONOFF, COLOR_MODE_RGB, COLOR_MODE_RGBW, COLOR_MODE_RGBWW, - COLOR_MODE_WHITE, EFFECT_COLORLOOP, EFFECT_RANDOM, PLATFORM_SCHEMA, @@ -102,7 +100,7 @@ FLUX_COLOR_MODE_TO_HASS: Final = { FLUX_COLOR_MODE_RGBW: COLOR_MODE_RGBW, FLUX_COLOR_MODE_RGBWW: COLOR_MODE_RGBWW, FLUX_COLOR_MODE_CCT: COLOR_MODE_COLOR_TEMP, - FLUX_COLOR_MODE_DIM: COLOR_MODE_WHITE, + FLUX_COLOR_MODE_DIM: COLOR_MODE_BRIGHTNESS, } @@ -406,10 +404,6 @@ class FluxLight(FluxEntity, CoordinatorEntity, LightEntity): rgbcw = kwargs[ATTR_RGBWW_COLOR] await self._device.async_set_levels(*rgbcw_to_rgbwc(rgbcw)) return - # Handle switch to White Color Mode - if ATTR_WHITE in kwargs: - await self._device.async_set_levels(w=kwargs[ATTR_WHITE]) - return if ATTR_EFFECT in kwargs: effect = kwargs[ATTR_EFFECT] # Random color effect @@ -455,8 +449,8 @@ class FluxLight(FluxEntity, CoordinatorEntity, LightEntity): rgbwc = self.rgbwc_color await self._device.async_set_levels(*rgbww_brightness(rgbwc, brightness)) return - # Handle White Color Mode and Brightness Only Color Mode - if self.color_mode in (COLOR_MODE_WHITE, COLOR_MODE_BRIGHTNESS): + # Handle Brightness Only Color Mode + if self.color_mode == COLOR_MODE_BRIGHTNESS: await self._device.async_set_levels(w=brightness) return raise ValueError(f"Unsupported color mode {self.color_mode}") diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index dbff7aaac89..81648541106 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.24.3"], + "requirements": ["flux_led==0.24.4"], "codeowners": ["@icemanch"], "iot_class": "local_polling", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 57874bea5fd..874157d517b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -649,7 +649,7 @@ fjaraskupan==1.0.1 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.3 +flux_led==0.24.4 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 89e78f0af10..9a55702ad97 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -381,7 +381,7 @@ fjaraskupan==1.0.1 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.3 +flux_led==0.24.4 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index 1ddd79070b3..2931944343f 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -38,7 +38,6 @@ from homeassistant.components.light import ( ATTR_RGBW_COLOR, ATTR_RGBWW_COLOR, ATTR_SUPPORTED_COLOR_MODES, - ATTR_WHITE, DOMAIN as LIGHT_DOMAIN, ) from homeassistant.const import ( @@ -633,8 +632,8 @@ async def test_white_light(hass: HomeAssistant) -> None: assert state.state == STATE_ON attributes = state.attributes assert attributes[ATTR_BRIGHTNESS] == 128 - assert attributes[ATTR_COLOR_MODE] == "white" - assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["white"] + assert attributes[ATTR_COLOR_MODE] == "brightness" + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["brightness"] await hass.services.async_call( LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True @@ -659,15 +658,6 @@ async def test_white_light(hass: HomeAssistant) -> None: bulb.async_set_levels.assert_called_with(w=100) bulb.async_set_levels.reset_mock() - await hass.services.async_call( - LIGHT_DOMAIN, - "turn_on", - {ATTR_ENTITY_ID: entity_id, ATTR_WHITE: 100}, - blocking=True, - ) - bulb.async_set_levels.assert_called_with(w=100) - bulb.async_set_levels.reset_mock() - async def test_rgb_light_custom_effects(hass: HomeAssistant) -> None: """Test an rgb light with a custom effect.""" From abcacd2a00c1e35f1f0ada53be39149382f6dfb3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 12 Oct 2021 18:40:49 -1000 Subject: [PATCH 0294/1038] Use a human readable model name in flux_led (#57519) --- homeassistant/components/flux_led/entity.py | 2 +- tests/components/flux_led/__init__.py | 2 ++ tests/components/flux_led/test_light.py | 14 +++++++++----- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/flux_led/entity.py b/homeassistant/components/flux_led/entity.py index ae1525221de..0918d6af62a 100644 --- a/homeassistant/components/flux_led/entity.py +++ b/homeassistant/components/flux_led/entity.py @@ -41,7 +41,7 @@ class FluxEntity(CoordinatorEntity): if self.unique_id: self._attr_device_info = { "connections": {(dr.CONNECTION_NETWORK_MAC, self.unique_id)}, - ATTR_MODEL: f"0x{self._device.model_num:02X}", + ATTR_MODEL: self._device.model, ATTR_NAME: self.name, ATTR_SW_VERSION: str(self._device.version_num), ATTR_MANUFACTURER: "FluxLED/Magic Home", diff --git a/tests/components/flux_led/__init__.py b/tests/components/flux_led/__init__.py index 3501d317d6c..8f484ac1989 100644 --- a/tests/components/flux_led/__init__.py +++ b/tests/components/flux_led/__init__.py @@ -68,6 +68,7 @@ def _mocked_bulb() -> AIOWifiLedBulb: bulb.getWhiteTemperature = MagicMock(return_value=(2700, 128)) bulb.brightness = 128 bulb.model_num = 0x35 + bulb.model = "Smart Bulb (0x35)" bulb.version_num = 8 bulb.rgbwcapable = True bulb.color_modes = {FLUX_COLOR_MODE_RGB, FLUX_COLOR_MODE_CCT} @@ -91,6 +92,7 @@ def _mocked_switch() -> AIOWifiLedBulb: switch.async_turn_off = AsyncMock() switch.async_turn_on = AsyncMock() switch.model_num = 0x97 + switch.model = "Smart Switch (0x97)" switch.version_num = 0x97 switch.raw_state = LEDENETRawState( 0, 0x97, 0, 0x61, 0x97, 50, 255, 0, 0, 50, 8, 0, 0, 0 diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index 2931944343f..2a1e004556a 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -143,10 +143,14 @@ async def test_light_no_unique_id(hass: HomeAssistant) -> None: @pytest.mark.parametrize( - "protocol,sw_version,model", [("LEDENET_ORIGINAL", 1, 0x35), ("LEDENET", 8, 0x33)] + "protocol,sw_version,model_num,model", + [ + ("LEDENET_ORIGINAL", 1, 0x01, "Original LEDEDNET (0x35)"), + ("LEDENET", 8, 0x33, "Magic Home Branded RGB Controller (0x33)"), + ], ) async def test_light_device_registry( - hass: HomeAssistant, protocol: str, sw_version: int, model: int + hass: HomeAssistant, protocol: str, sw_version: int, model_num: int, model: str ) -> None: """Test a light device registry entry.""" config_entry = MockConfigEntry( @@ -158,8 +162,8 @@ async def test_light_device_registry( bulb = _mocked_bulb() bulb.version_num = sw_version bulb.protocol = protocol - bulb.raw_state = bulb.raw_state._replace(model_num=model, version_number=sw_version) - bulb.model_num = model + bulb.model_num = model_num + bulb.model = model with _patch_discovery(no_device=True), _patch_wifibulb(device=bulb): await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) @@ -170,7 +174,7 @@ async def test_light_device_registry( identifiers={}, connections={(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)} ) assert device.sw_version == str(sw_version) - assert device.model == f"0x{model:02X}" + assert device.model == model async def test_rgb_light(hass: HomeAssistant) -> None: From 8d7744a74f6f28657941dc095fb5a197e726ffff Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 13 Oct 2021 07:16:55 +0200 Subject: [PATCH 0295/1038] Warn user if Gateway is already paired (#57530) * Warn user if Gateway is already paired. Co-authored-by: Martin Hjelmare --- .../components/tradfri/config_flow.py | 3 ++- homeassistant/components/tradfri/strings.json | 3 ++- tests/components/tradfri/test_config_flow.py | 22 ++++++++++++++++++- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index e45bd36753f..11a56200eda 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -182,7 +182,8 @@ async def authenticate( raise AuthError("timeout") from err finally: await api_factory.shutdown() - + if key is None: + raise AuthError("cannot_authenticate") return await get_gateway_info(hass, host, identity, key) diff --git a/homeassistant/components/tradfri/strings.json b/homeassistant/components/tradfri/strings.json index 45850fd639a..34d7e89929a 100644 --- a/homeassistant/components/tradfri/strings.json +++ b/homeassistant/components/tradfri/strings.json @@ -13,7 +13,8 @@ "error": { "invalid_key": "Failed to register with provided key. If this keeps happening, try restarting the gateway.", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "timeout": "Timeout validating the code." + "timeout": "Timeout validating the code.", + "cannot_authenticate": "Cannot authenticate, is Gateway paired with another server like e.g. Homekit?" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index ca6380a9310..c8c5323d6e4 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -1,5 +1,5 @@ """Test the Tradfri config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import pytest @@ -18,6 +18,26 @@ def mock_auth(): yield mock_auth +async def test_already_paired(hass, mock_entry_setup): + """Test Gateway already paired.""" + with patch( + "homeassistant.components.tradfri.config_flow.APIFactory", + autospec=True, + ) as mock_lib: + mx = AsyncMock() + mx.generate_psk.return_value = None + mock_lib.init.return_value = mx + result = await hass.config_entries.flow.async_init( + "tradfri", context={"source": config_entries.SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "123.123.123.123", "security_code": "abcd"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_authenticate"} + + async def test_user_connection_successful(hass, mock_auth, mock_entry_setup): """Test a successful connection.""" mock_auth.side_effect = lambda hass, host, code: {"host": host, "gateway_id": "bla"} From 1fa6329c2e2c4d3f0a3a0907210aaa75521d2b06 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 13 Oct 2021 03:28:52 -0700 Subject: [PATCH 0296/1038] Add Nest WebRTC and support Nest Battery Camera and Nest Battery Doorbell (#57299) * Add WebSocket API for intiting a WebRTC stream See https://github.com/home-assistant/architecture/discussions/640 * Add nest support for initiating webrtc streams Add an implementation of async_handle_web_rtc_offer in nest, with test coverage. Issue #55302 * Rename offer variable to match overriden variable name * Remove unnecessary checks covered by websocket function * Update homeassistant/components/camera/__init__.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/nest/camera_sdm.py | 22 +++++- tests/components/nest/camera_sdm_test.py | 83 +++++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index 0a917e8cbdc..9c6ac7070e4 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -12,6 +12,7 @@ from google_nest_sdm.camera_traits import ( CameraLiveStreamTrait, EventImageGenerator, RtspStream, + StreamingProtocol, ) from google_nest_sdm.device import Device from google_nest_sdm.event import ImageEventBase @@ -19,6 +20,7 @@ from google_nest_sdm.exceptions import GoogleNestException from haffmpeg.tools import IMAGE_JPEG from homeassistant.components.camera import SUPPORT_STREAM, Camera +from homeassistant.components.camera.const import STREAM_TYPE_HLS, STREAM_TYPE_WEB_RTC from homeassistant.components.ffmpeg import async_get_image from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -114,9 +116,21 @@ class NestCamera(Camera): supported_features |= SUPPORT_STREAM return supported_features + @property + def stream_type(self) -> str | None: + """Return the type of stream supported by this camera.""" + if CameraLiveStreamTrait.NAME not in self._device.traits: + return None + trait = self._device.traits[CameraLiveStreamTrait.NAME] + if StreamingProtocol.WEB_RTC in trait.supported_protocols: + return STREAM_TYPE_WEB_RTC + return STREAM_TYPE_HLS + async def stream_source(self) -> str | None: """Return the source of the stream.""" - if CameraLiveStreamTrait.NAME not in self._device.traits: + if not self.supported_features & SUPPORT_STREAM: + return None + if self.stream_type != STREAM_TYPE_HLS: return None trait = self._device.traits[CameraLiveStreamTrait.NAME] if not self._stream: @@ -252,3 +266,9 @@ class NestCamera(Camera): self._event_id = None self._event_image_bytes = None self._event_image_cleanup_unsub = None + + async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str: + """Return the source of the stream.""" + trait: CameraLiveStreamTrait = self._device.traits[CameraLiveStreamTrait.NAME] + stream = await trait.generate_web_rtc_stream(offer_sdp) + return stream.answer_sdp diff --git a/tests/components/nest/camera_sdm_test.py b/tests/components/nest/camera_sdm_test.py index 3c0c0fdb4db..df36ae762df 100644 --- a/tests/components/nest/camera_sdm_test.py +++ b/tests/components/nest/camera_sdm_test.py @@ -15,6 +15,7 @@ import pytest from homeassistant.components import camera from homeassistant.components.camera import STATE_IDLE +from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component @@ -603,3 +604,85 @@ async def test_multiple_event_images(hass, auth): image = await async_get_image(hass) assert image.content == b"updated image bytes" + + +async def test_camera_web_rtc(hass, auth, hass_ws_client): + """Test a basic camera that supports web rtc.""" + expiration = utcnow() + datetime.timedelta(seconds=100) + auth.responses = [ + aiohttp.web.json_response( + { + "results": { + "answerSdp": "v=0\r\ns=-\r\n", + "mediaSessionId": "yP2grqz0Y1V_wgiX9KEbMWHoLd...", + "expiresAt": expiration.isoformat(timespec="seconds"), + }, + } + ) + ] + device_traits = { + "sdm.devices.traits.Info": { + "customName": "My Camera", + }, + "sdm.devices.traits.CameraLiveStream": { + "maxVideoResolution": { + "width": 640, + "height": 480, + }, + "videoCodecs": ["H264"], + "audioCodecs": ["AAC"], + "supportedProtocols": ["WEB_RTC"], + }, + } + await async_setup_camera(hass, device_traits, auth=auth) + + assert len(hass.states.async_all()) == 1 + cam = hass.states.get("camera.my_camera") + assert cam is not None + assert cam.state == STATE_IDLE + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 5, + "type": "camera/web_rtc_offer", + "entity_id": "camera.my_camera", + "offer": "a=recvonly", + } + ) + + msg = await client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + assert msg["result"]["answer"] == "v=0\r\ns=-\r\n" + + # Nest WebRTC cameras do not support a still image + with pytest.raises(HomeAssistantError): + await async_get_image(hass) + + +async def test_camera_web_rtc_unsupported(hass, auth, hass_ws_client): + """Test a basic camera that supports web rtc.""" + await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) + + assert len(hass.states.async_all()) == 1 + cam = hass.states.get("camera.my_camera") + assert cam is not None + assert cam.state == STATE_IDLE + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 5, + "type": "camera/web_rtc_offer", + "entity_id": "camera.my_camera", + "offer": "a=recvonly", + } + ) + + msg = await client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == TYPE_RESULT + assert not msg["success"] + assert msg["error"]["code"] == "web_rtc_offer_failed" From c470a03a4e6412a6de679cc31d374604b53c3df2 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 13 Oct 2021 13:36:02 +0200 Subject: [PATCH 0297/1038] Add a use_time sensor for Xiaomi_miio humidifiers (#57560) Co-authored-by: Maciej Bieniek --- .../components/xiaomi_miio/sensor.py | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index e7516bbca65..764976820bb 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -112,6 +112,7 @@ ATTR_POWER = "power" ATTR_PRESSURE = "pressure" ATTR_PURIFY_VOLUME = "purify_volume" ATTR_SENSOR_STATE = "sensor_state" +ATTR_USE_TIME = "use_time" ATTR_WATER_LEVEL = "water_level" ATTR_DND_START = "start" ATTR_DND_END = "end" @@ -193,6 +194,14 @@ SENSOR_TYPES = { icon="mdi:fast-forward", state_class=STATE_CLASS_MEASUREMENT, ), + ATTR_USE_TIME: XiaomiMiioSensorDescription( + key=ATTR_USE_TIME, + name="Use Time", + native_unit_of_measurement=TIME_SECONDS, + icon="mdi:progress-clock", + state_class=STATE_CLASS_TOTAL_INCREASING, + entity_registry_enabled_default=False, + ), ATTR_ILLUMINANCE: XiaomiMiioSensorDescription( key=ATTR_ILLUMINANCE, name="Illuminance", @@ -259,20 +268,27 @@ SENSOR_TYPES = { ), } -HUMIDIFIER_MIIO_SENSORS = (ATTR_HUMIDITY, ATTR_TEMPERATURE, ATTR_WATER_LEVEL) +HUMIDIFIER_MIIO_SENSORS = ( + ATTR_HUMIDITY, + ATTR_TEMPERATURE, + ATTR_USE_TIME, + ATTR_WATER_LEVEL, +) HUMIDIFIER_CA1_CB1_SENSORS = ( ATTR_HUMIDITY, ATTR_TEMPERATURE, ATTR_MOTOR_SPEED, + ATTR_USE_TIME, ATTR_WATER_LEVEL, ) HUMIDIFIER_MIOT_SENSORS = ( ATTR_ACTUAL_SPEED, ATTR_HUMIDITY, ATTR_TEMPERATURE, + ATTR_USE_TIME, ATTR_WATER_LEVEL, ) -HUMIDIFIER_MJJSQ_SENSORS = (ATTR_HUMIDITY, ATTR_TEMPERATURE) +HUMIDIFIER_MJJSQ_SENSORS = (ATTR_HUMIDITY, ATTR_TEMPERATURE, ATTR_USE_TIME) PURIFIER_MIIO_SENSORS = ( ATTR_FILTER_LIFE_REMAINING, From 3b1938d5ec4d153976274c97ebd741924c0df7f8 Mon Sep 17 00:00:00 2001 From: Lukas Kempf Date: Wed, 13 Oct 2021 15:33:37 +0200 Subject: [PATCH 0298/1038] Add unique_id support for eq3btsmart (#57603) --- homeassistant/components/eq3btsmart/climate.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/eq3btsmart/climate.py b/homeassistant/components/eq3btsmart/climate.py index f803c9c0bd5..3c3d41d090c 100644 --- a/homeassistant/components/eq3btsmart/climate.py +++ b/homeassistant/components/eq3btsmart/climate.py @@ -24,6 +24,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import format_mac _LOGGER = logging.getLogger(__name__) @@ -82,6 +83,7 @@ class EQ3BTSmartThermostat(ClimateEntity): """Initialize the thermostat.""" # We want to avoid name clash with this module. self._name = _name + self._mac = _mac self._thermostat = eq3.Thermostat(_mac) @property @@ -183,6 +185,11 @@ class EQ3BTSmartThermostat(ClimateEntity): """ 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): """Set new preset mode.""" if preset_mode == PRESET_NONE: From c97acf87137ddc28beabd10362a834f2d19628bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Wed, 13 Oct 2021 14:45:02 +0100 Subject: [PATCH 0299/1038] Add support for multiple Whirlpool airconditioners (#57588) --- homeassistant/components/whirlpool/climate.py | 4 +- tests/components/whirlpool/conftest.py | 63 ++- tests/components/whirlpool/test_climate.py | 524 ++++++++++-------- 3 files changed, 333 insertions(+), 258 deletions(-) diff --git a/homeassistant/components/whirlpool/climate.py b/homeassistant/components/whirlpool/climate.py index bd8cb0505fa..ccfa11b3ee4 100644 --- a/homeassistant/components/whirlpool/climate.py +++ b/homeassistant/components/whirlpool/climate.py @@ -71,8 +71,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): # 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 - aircon = AirConEntity(said_list[0], auth) - async_add_entities([aircon], True) + aircons = [AirConEntity(said, auth) for said in said_list] + async_add_entities(aircons, True) class AirConEntity(ClimateEntity): diff --git a/tests/components/whirlpool/conftest.py b/tests/components/whirlpool/conftest.py index e3919c118e2..3a5fd0e3d2e 100644 --- a/tests/components/whirlpool/conftest.py +++ b/tests/components/whirlpool/conftest.py @@ -4,8 +4,10 @@ from unittest.mock import AsyncMock import pytest import whirlpool +import whirlpool.aircon -MOCK_SAID = "said1" +MOCK_SAID1 = "said1" +MOCK_SAID2 = "said2" @pytest.fixture(name="mock_auth_api") @@ -14,28 +16,53 @@ def fixture_mock_auth_api(): 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_SAID] + mock_auth.return_value.get_said_list.return_value = [MOCK_SAID1, MOCK_SAID2] yield mock_auth -@pytest.fixture(name="mock_aircon_api", autouse=True) -def fixture_mock_aircon_api(mock_auth_api): +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 + mock_aircon.get_fanspeed.return_value = whirlpool.aircon.FanSpeed.Auto + mock_aircon.get_current_temp.return_value = 15 + mock_aircon.get_temp.return_value = 20 + mock_aircon.get_current_humidity.return_value = 80 + mock_aircon.get_humidity.return_value = 50 + mock_aircon.get_h_louver_swing.return_value = True + + mock_aircon.set_power_on = AsyncMock() + mock_aircon.set_mode = AsyncMock() + mock_aircon.set_temp = AsyncMock() + mock_aircon.set_humidity = AsyncMock() + mock_aircon.set_mode = AsyncMock() + mock_aircon.set_fanspeed = AsyncMock() + mock_aircon.set_h_louver_swing = AsyncMock() + + return mock_aircon + + +@pytest.fixture(name="mock_aircon1_api", autouse=True) +def fixture_mock_aircon1_api(mock_auth_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): + """Set up air conditioner API fixture.""" + yield get_aircon_mock(MOCK_SAID2) + + +@pytest.fixture(name="mock_aircon_api_instances", autouse=True) +def fixture_mock_aircon_api_instances(mock_aircon1_api, mock_aircon2_api): """Set up air conditioner API fixture.""" with mock.patch( "homeassistant.components.whirlpool.climate.Aircon" ) as mock_aircon_api: - mock_aircon_api.return_value.connect = AsyncMock() - mock_aircon_api.return_value.fetch_name = AsyncMock(return_value="TestZone") - mock_aircon_api.return_value.said = MOCK_SAID - mock_aircon_api.return_value.get_online.return_value = True - mock_aircon_api.return_value.get_power_on.return_value = True - mock_aircon_api.return_value.get_mode.return_value = whirlpool.aircon.Mode.Cool - mock_aircon_api.return_value.get_fanspeed.return_value = ( - whirlpool.aircon.FanSpeed.Auto - ) - mock_aircon_api.return_value.get_current_temp.return_value = 15 - mock_aircon_api.return_value.get_temp.return_value = 20 - mock_aircon_api.return_value.get_current_humidity.return_value = 80 - mock_aircon_api.return_value.get_humidity.return_value = 50 - mock_aircon_api.return_value.get_h_louver_swing.return_value = True + mock_aircon_api.side_effect = [mock_aircon1_api, mock_aircon2_api] yield mock_aircon_api diff --git a/tests/components/whirlpool/test_climate.py b/tests/components/whirlpool/test_climate.py index 314c6c2685b..befcd650b64 100644 --- a/tests/components/whirlpool/test_climate.py +++ b/tests/components/whirlpool/test_climate.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock, MagicMock import aiohttp +from attr import dataclass import pytest import whirlpool @@ -55,12 +56,19 @@ from homeassistant.helpers import entity_registry as er from . import init_integration -async def update_ac_state(hass: HomeAssistant, mock_aircon_api: MagicMock): +async def update_ac_state( + hass: HomeAssistant, + entity_id: str, + mock_aircon_api_instances: MagicMock, + mock_instance_idx: int, +): """Simulate an update trigger from the API.""" - update_ha_state_cb = mock_aircon_api.call_args.args[2] + update_ha_state_cb = mock_aircon_api_instances.call_args_list[ + mock_instance_idx + ].args[2] update_ha_state_cb() await hass.async_block_till_done() - return hass.states.get("climate.said1") + return hass.states.get(entity_id) async def test_no_appliances(hass: HomeAssistant, mock_auth_api: MagicMock): @@ -71,297 +79,337 @@ async def test_no_appliances(hass: HomeAssistant, mock_auth_api: MagicMock): async def test_name_fallback_on_exception( - hass: HomeAssistant, mock_aircon_api: MagicMock + hass: HomeAssistant, mock_aircon1_api: MagicMock ): """Test name property.""" - mock_aircon_api.return_value.fetch_name = AsyncMock( - side_effect=aiohttp.ClientError() - ) + 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_aircon_api: MagicMock): +async def test_static_attributes(hass: HomeAssistant, mock_aircon1_api: MagicMock): """Test static climate attributes.""" await init_integration(hass) - entry = er.async_get(hass).async_get("climate.said1") - assert entry - assert entry.unique_id == "said1" + for entity_id in ("climate.said1", "climate.said2"): + entry = er.async_get(hass).async_get(entity_id) + assert entry + assert entry.unique_id == entity_id.split(".")[1] - state = hass.states.get("climate.said1") - assert state is not None - assert state.state != STATE_UNAVAILABLE - assert state.state == HVAC_MODE_COOL + state = hass.states.get(entity_id) + assert state is not None + assert state.state != STATE_UNAVAILABLE + assert state.state == HVAC_MODE_COOL - attributes = state.attributes - assert attributes[ATTR_FRIENDLY_NAME] == "TestZone" + attributes = state.attributes + assert attributes[ATTR_FRIENDLY_NAME] == "TestZone" - assert ( - attributes[ATTR_SUPPORTED_FEATURES] - == SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | SUPPORT_SWING_MODE - ) - assert attributes[ATTR_HVAC_MODES] == [ - HVAC_MODE_COOL, - HVAC_MODE_HEAT, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_OFF, - ] - assert attributes[ATTR_FAN_MODES] == [ - FAN_AUTO, - FAN_HIGH, - FAN_MEDIUM, - FAN_LOW, - FAN_OFF, - ] - assert attributes[ATTR_SWING_MODES] == [SWING_HORIZONTAL, SWING_OFF] - assert attributes[ATTR_TARGET_TEMP_STEP] == 1 - assert attributes[ATTR_MIN_TEMP] == 16 - assert attributes[ATTR_MAX_TEMP] == 30 + assert ( + attributes[ATTR_SUPPORTED_FEATURES] + == SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | SUPPORT_SWING_MODE + ) + assert attributes[ATTR_HVAC_MODES] == [ + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_OFF, + ] + assert attributes[ATTR_FAN_MODES] == [ + FAN_AUTO, + FAN_HIGH, + FAN_MEDIUM, + FAN_LOW, + FAN_OFF, + ] + assert attributes[ATTR_SWING_MODES] == [SWING_HORIZONTAL, SWING_OFF] + assert attributes[ATTR_TARGET_TEMP_STEP] == 1 + assert attributes[ATTR_MIN_TEMP] == 16 + assert attributes[ATTR_MAX_TEMP] == 30 -async def test_dynamic_attributes(hass: HomeAssistant, mock_aircon_api: MagicMock): +async def test_dynamic_attributes( + hass: HomeAssistant, + mock_aircon_api_instances: MagicMock, + mock_aircon1_api: MagicMock, + mock_aircon2_api: MagicMock, +): """Test dynamic attributes.""" await init_integration(hass) - state = hass.states.get("climate.said1") - assert state is not None - assert state.state == HVAC_MODE_COOL + @dataclass + class ClimateTestInstance: + """Helper class for multiple climate and mock instances.""" - mock_aircon_api.return_value.get_power_on.return_value = False - state = await update_ac_state(hass, mock_aircon_api) - assert state.state == HVAC_MODE_OFF + entity_id: str + mock_instance: MagicMock + mock_instance_idx: int - mock_aircon_api.return_value.get_online.return_value = False - state = await update_ac_state(hass, mock_aircon_api) - assert state.state == STATE_UNAVAILABLE + for clim_test_instance in ( + ClimateTestInstance("climate.said1", mock_aircon1_api, 0), + ClimateTestInstance("climate.said2", mock_aircon2_api, 1), + ): + entity_id = clim_test_instance.entity_id + mock_instance = clim_test_instance.mock_instance + mock_instance_idx = clim_test_instance.mock_instance_idx + state = hass.states.get(entity_id) + assert state is not None + assert state.state == HVAC_MODE_COOL - mock_aircon_api.return_value.get_power_on.return_value = True - mock_aircon_api.return_value.get_online.return_value = True - state = await update_ac_state(hass, mock_aircon_api) - assert state.state == HVAC_MODE_COOL + mock_instance.get_power_on.return_value = False + state = await update_ac_state( + hass, entity_id, mock_aircon_api_instances, mock_instance_idx + ) + assert state.state == HVAC_MODE_OFF - mock_aircon_api.return_value.get_mode.return_value = whirlpool.aircon.Mode.Heat - state = await update_ac_state(hass, mock_aircon_api) - assert state.state == HVAC_MODE_HEAT + mock_instance.get_online.return_value = False + state = await update_ac_state( + hass, entity_id, mock_aircon_api_instances, mock_instance_idx + ) + assert state.state == STATE_UNAVAILABLE - mock_aircon_api.return_value.get_mode.return_value = whirlpool.aircon.Mode.Fan - state = await update_ac_state(hass, mock_aircon_api) - assert state.state == HVAC_MODE_FAN_ONLY + mock_instance.get_power_on.return_value = True + mock_instance.get_online.return_value = True + state = await update_ac_state( + hass, entity_id, mock_aircon_api_instances, mock_instance_idx + ) + assert state.state == HVAC_MODE_COOL - mock_aircon_api.return_value.get_fanspeed.return_value = ( - whirlpool.aircon.FanSpeed.Auto - ) - state = await update_ac_state(hass, mock_aircon_api) - assert state.attributes[ATTR_FAN_MODE] == HVAC_MODE_AUTO + mock_instance.get_mode.return_value = whirlpool.aircon.Mode.Heat + state = await update_ac_state( + hass, entity_id, mock_aircon_api_instances, mock_instance_idx + ) + assert state.state == HVAC_MODE_HEAT - mock_aircon_api.return_value.get_fanspeed.return_value = ( - whirlpool.aircon.FanSpeed.Low - ) - state = await update_ac_state(hass, mock_aircon_api) - assert state.attributes[ATTR_FAN_MODE] == FAN_LOW + mock_instance.get_mode.return_value = whirlpool.aircon.Mode.Fan + state = await update_ac_state( + hass, entity_id, mock_aircon_api_instances, mock_instance_idx + ) + assert state.state == HVAC_MODE_FAN_ONLY - mock_aircon_api.return_value.get_fanspeed.return_value = ( - whirlpool.aircon.FanSpeed.Medium - ) - state = await update_ac_state(hass, mock_aircon_api) - assert state.attributes[ATTR_FAN_MODE] == FAN_MEDIUM + mock_instance.get_fanspeed.return_value = whirlpool.aircon.FanSpeed.Auto + state = await update_ac_state( + hass, entity_id, mock_aircon_api_instances, mock_instance_idx + ) + assert state.attributes[ATTR_FAN_MODE] == HVAC_MODE_AUTO - mock_aircon_api.return_value.get_fanspeed.return_value = ( - whirlpool.aircon.FanSpeed.High - ) - state = await update_ac_state(hass, mock_aircon_api) - assert state.attributes[ATTR_FAN_MODE] == FAN_HIGH + mock_instance.get_fanspeed.return_value = whirlpool.aircon.FanSpeed.Low + state = await update_ac_state( + hass, entity_id, mock_aircon_api_instances, mock_instance_idx + ) + assert state.attributes[ATTR_FAN_MODE] == FAN_LOW - mock_aircon_api.return_value.get_fanspeed.return_value = ( - whirlpool.aircon.FanSpeed.Off - ) - state = await update_ac_state(hass, mock_aircon_api) - assert state.attributes[ATTR_FAN_MODE] == FAN_OFF + mock_instance.get_fanspeed.return_value = whirlpool.aircon.FanSpeed.Medium + state = await update_ac_state( + hass, entity_id, mock_aircon_api_instances, mock_instance_idx + ) + assert state.attributes[ATTR_FAN_MODE] == FAN_MEDIUM - mock_aircon_api.return_value.get_current_temp.return_value = 15 - mock_aircon_api.return_value.get_temp.return_value = 20 - mock_aircon_api.return_value.get_current_humidity.return_value = 80 - mock_aircon_api.return_value.get_h_louver_swing.return_value = True - attributes = (await update_ac_state(hass, mock_aircon_api)).attributes - assert attributes[ATTR_CURRENT_TEMPERATURE] == 15 - assert attributes[ATTR_TEMPERATURE] == 20 - assert attributes[ATTR_CURRENT_HUMIDITY] == 80 - assert attributes[ATTR_SWING_MODE] == SWING_HORIZONTAL + mock_instance.get_fanspeed.return_value = whirlpool.aircon.FanSpeed.High + state = await update_ac_state( + hass, entity_id, mock_aircon_api_instances, mock_instance_idx + ) + assert state.attributes[ATTR_FAN_MODE] == FAN_HIGH - mock_aircon_api.return_value.get_current_temp.return_value = 16 - mock_aircon_api.return_value.get_temp.return_value = 21 - mock_aircon_api.return_value.get_current_humidity.return_value = 70 - mock_aircon_api.return_value.get_h_louver_swing.return_value = False - attributes = (await update_ac_state(hass, mock_aircon_api)).attributes - assert attributes[ATTR_CURRENT_TEMPERATURE] == 16 - assert attributes[ATTR_TEMPERATURE] == 21 - assert attributes[ATTR_CURRENT_HUMIDITY] == 70 - assert attributes[ATTR_SWING_MODE] == SWING_OFF + mock_instance.get_fanspeed.return_value = whirlpool.aircon.FanSpeed.Off + state = await update_ac_state( + hass, entity_id, mock_aircon_api_instances, mock_instance_idx + ) + assert state.attributes[ATTR_FAN_MODE] == FAN_OFF + + mock_instance.get_current_temp.return_value = 15 + mock_instance.get_temp.return_value = 20 + mock_instance.get_current_humidity.return_value = 80 + mock_instance.get_h_louver_swing.return_value = True + attributes = ( + await update_ac_state( + hass, entity_id, mock_aircon_api_instances, mock_instance_idx + ) + ).attributes + assert attributes[ATTR_CURRENT_TEMPERATURE] == 15 + assert attributes[ATTR_TEMPERATURE] == 20 + assert attributes[ATTR_CURRENT_HUMIDITY] == 80 + assert attributes[ATTR_SWING_MODE] == SWING_HORIZONTAL + + mock_instance.get_current_temp.return_value = 16 + mock_instance.get_temp.return_value = 21 + mock_instance.get_current_humidity.return_value = 70 + mock_instance.get_h_louver_swing.return_value = False + attributes = ( + await update_ac_state( + hass, entity_id, mock_aircon_api_instances, mock_instance_idx + ) + ).attributes + assert attributes[ATTR_CURRENT_TEMPERATURE] == 16 + assert attributes[ATTR_TEMPERATURE] == 21 + assert attributes[ATTR_CURRENT_HUMIDITY] == 70 + assert attributes[ATTR_SWING_MODE] == SWING_OFF -async def test_service_calls(hass: HomeAssistant, mock_aircon_api: MagicMock): +async def test_service_calls( + hass: HomeAssistant, mock_aircon1_api: MagicMock, mock_aircon2_api: MagicMock +): """Test controlling the entity through service calls.""" await init_integration(hass) - mock_aircon_api.return_value.set_power_on = AsyncMock() - mock_aircon_api.return_value.set_mode = AsyncMock() - mock_aircon_api.return_value.set_temp = AsyncMock() - mock_aircon_api.return_value.set_humidity = AsyncMock() - mock_aircon_api.return_value.set_mode = AsyncMock() - mock_aircon_api.return_value.set_fanspeed = AsyncMock() - mock_aircon_api.return_value.set_h_louver_swing = AsyncMock() - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "climate.said1"}, - blocking=True, - ) - mock_aircon_api.return_value.set_power_on.assert_called_once_with(False) + @dataclass + class ClimateInstancesData: + """Helper class for multiple climate and mock instances.""" - mock_aircon_api.return_value.set_power_on.reset_mock() - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "climate.said1"}, - blocking=True, - ) - mock_aircon_api.return_value.set_power_on.assert_called_once_with(True) + entity_id: str + mock_instance: MagicMock - mock_aircon_api.return_value.set_power_on.reset_mock() - mock_aircon_api.return_value.get_power_on.return_value = False - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: "climate.said1", ATTR_HVAC_MODE: HVAC_MODE_COOL}, - blocking=True, - ) - mock_aircon_api.return_value.set_power_on.assert_called_once_with(True) + for clim_test_instance in ( + ClimateInstancesData("climate.said1", mock_aircon1_api), + ClimateInstancesData("climate.said2", mock_aircon2_api), + ): + mock_instance = clim_test_instance.mock_instance + entity_id = clim_test_instance.entity_id - mock_aircon_api.return_value.set_temp.reset_mock() - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: "climate.said1", ATTR_TEMPERATURE: 15}, - blocking=True, - ) - mock_aircon_api.return_value.set_temp.assert_called_once_with(15) + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_instance.set_power_on.assert_called_once_with(False) - mock_aircon_api.return_value.set_mode.reset_mock() - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: "climate.said1", ATTR_HVAC_MODE: HVAC_MODE_COOL}, - blocking=True, - ) - mock_aircon_api.return_value.set_mode.assert_called_once_with( - whirlpool.aircon.Mode.Cool - ) + mock_instance.set_power_on.reset_mock() + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mock_instance.set_power_on.assert_called_once_with(True) - mock_aircon_api.return_value.set_mode.reset_mock() - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: "climate.said1", ATTR_HVAC_MODE: HVAC_MODE_HEAT}, - blocking=True, - ) - mock_aircon_api.return_value.set_mode.assert_called_once_with( - whirlpool.aircon.Mode.Heat - ) - - mock_aircon_api.return_value.set_mode.reset_mock() - # HVAC_MODE_DRY is not supported - with pytest.raises(ValueError): + mock_instance.set_power_on.reset_mock() + mock_instance.get_power_on.return_value = False await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: "climate.said1", ATTR_HVAC_MODE: HVAC_MODE_DRY}, + {ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: HVAC_MODE_COOL}, blocking=True, ) - mock_aircon_api.return_value.set_mode.assert_not_called() + mock_instance.set_power_on.assert_called_once_with(True) - mock_aircon_api.return_value.set_mode.reset_mock() - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: "climate.said1", ATTR_HVAC_MODE: HVAC_MODE_FAN_ONLY}, - blocking=True, - ) - mock_aircon_api.return_value.set_mode.assert_called_once_with( - whirlpool.aircon.Mode.Fan - ) + mock_instance.set_temp.reset_mock() + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 15}, + blocking=True, + ) + mock_instance.set_temp.assert_called_once_with(15) - mock_aircon_api.return_value.set_fanspeed.reset_mock() - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: "climate.said1", ATTR_FAN_MODE: FAN_AUTO}, - blocking=True, - ) - mock_aircon_api.return_value.set_fanspeed.assert_called_once_with( - whirlpool.aircon.FanSpeed.Auto - ) + mock_instance.set_mode.reset_mock() + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: HVAC_MODE_COOL}, + blocking=True, + ) + mock_instance.set_mode.assert_called_once_with(whirlpool.aircon.Mode.Cool) - mock_aircon_api.return_value.set_fanspeed.reset_mock() - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: "climate.said1", ATTR_FAN_MODE: FAN_LOW}, - blocking=True, - ) - mock_aircon_api.return_value.set_fanspeed.assert_called_once_with( - whirlpool.aircon.FanSpeed.Low - ) + mock_instance.set_mode.reset_mock() + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: HVAC_MODE_HEAT}, + blocking=True, + ) + mock_instance.set_mode.assert_called_once_with(whirlpool.aircon.Mode.Heat) - mock_aircon_api.return_value.set_fanspeed.reset_mock() - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: "climate.said1", ATTR_FAN_MODE: FAN_MEDIUM}, - blocking=True, - ) - mock_aircon_api.return_value.set_fanspeed.assert_called_once_with( - whirlpool.aircon.FanSpeed.Medium - ) + mock_instance.set_mode.reset_mock() + # HVAC_MODE_DRY is not supported + with pytest.raises(ValueError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: HVAC_MODE_DRY}, + blocking=True, + ) + mock_instance.set_mode.assert_not_called() - mock_aircon_api.return_value.set_fanspeed.reset_mock() - # FAN_MIDDLE is not supported - with pytest.raises(ValueError): + mock_instance.set_mode.reset_mock() + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: HVAC_MODE_FAN_ONLY}, + blocking=True, + ) + mock_instance.set_mode.assert_called_once_with(whirlpool.aircon.Mode.Fan) + + mock_instance.set_fanspeed.reset_mock() await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: "climate.said1", ATTR_FAN_MODE: FAN_MIDDLE}, + {ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_AUTO}, blocking=True, ) - mock_aircon_api.return_value.set_fanspeed.assert_not_called() + mock_instance.set_fanspeed.assert_called_once_with( + whirlpool.aircon.FanSpeed.Auto + ) - mock_aircon_api.return_value.set_fanspeed.reset_mock() - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: "climate.said1", ATTR_FAN_MODE: FAN_HIGH}, - blocking=True, - ) - mock_aircon_api.return_value.set_fanspeed.assert_called_once_with( - whirlpool.aircon.FanSpeed.High - ) + mock_instance.set_fanspeed.reset_mock() + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_LOW}, + blocking=True, + ) + mock_instance.set_fanspeed.assert_called_once_with( + whirlpool.aircon.FanSpeed.Low + ) - mock_aircon_api.return_value.set_h_louver_swing.reset_mock() - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_SWING_MODE, - {ATTR_ENTITY_ID: "climate.said1", ATTR_SWING_MODE: SWING_HORIZONTAL}, - blocking=True, - ) - mock_aircon_api.return_value.set_h_louver_swing.assert_called_with(True) + mock_instance.set_fanspeed.reset_mock() + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_MEDIUM}, + blocking=True, + ) + mock_instance.set_fanspeed.assert_called_once_with( + whirlpool.aircon.FanSpeed.Medium + ) - mock_aircon_api.return_value.set_h_louver_swing.reset_mock() - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_SWING_MODE, - {ATTR_ENTITY_ID: "climate.said1", ATTR_SWING_MODE: SWING_OFF}, - blocking=True, - ) - mock_aircon_api.return_value.set_h_louver_swing.assert_called_with(False) + mock_instance.set_fanspeed.reset_mock() + # FAN_MIDDLE is not supported + with pytest.raises(ValueError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_MIDDLE}, + blocking=True, + ) + mock_instance.set_fanspeed.assert_not_called() + + mock_instance.set_fanspeed.reset_mock() + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_HIGH}, + blocking=True, + ) + mock_instance.set_fanspeed.assert_called_once_with( + whirlpool.aircon.FanSpeed.High + ) + + mock_instance.set_h_louver_swing.reset_mock() + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_SWING_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_SWING_MODE: SWING_HORIZONTAL}, + blocking=True, + ) + mock_instance.set_h_louver_swing.assert_called_with(True) + + mock_instance.set_h_louver_swing.reset_mock() + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_SWING_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_SWING_MODE: SWING_OFF}, + blocking=True, + ) + mock_instance.set_h_louver_swing.assert_called_with(False) From a5603c007695b60a7251d0a9e7b4e196fa9fbf37 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 13 Oct 2021 17:15:04 +0200 Subject: [PATCH 0300/1038] Improve warning prints for sensor statistics (#57605) --- homeassistant/components/energy/sensor.py | 1 + homeassistant/components/sensor/recorder.py | 37 +++++++++++++-------- tests/components/sensor/test_recorder.py | 17 +++++++--- 3 files changed, 37 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index 0c4c5eeb3b9..462c3f3215d 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -330,6 +330,7 @@ class EnergyCostSensor(SensorEntity): cast(str, self._config[self._adapter.entity_energy_key]), energy, float(self._last_energy_sensor_state.state), + self._last_energy_sensor_state, ): # Energy meter was reset, reset cost sensor too energy_state_copy = copy.copy(energy_state) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 00f5ed4453f..3ac4cf4e53b 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -287,7 +287,7 @@ def _suggest_report_issue(hass: HomeAssistant, entity_id: str) -> str: return report_issue -def warn_dip(hass: HomeAssistant, entity_id: str) -> None: +def warn_dip(hass: HomeAssistant, entity_id: str, state: State) -> None: """Log a warning once if a sensor with state_class_total has a decreasing value. The log will be suppressed until two dips have been seen to prevent warning due to @@ -308,14 +308,17 @@ def warn_dip(hass: HomeAssistant, entity_id: str) -> None: return _LOGGER.warning( "Entity %s %shas state class total_increasing, but its state is " - "not strictly increasing. Please %s", + "not strictly increasing. Triggered by state %s with last_updated set to %s. " + "Please %s", entity_id, f"from integration {domain} " if domain else "", + state.state, + state.last_updated.isoformat(), _suggest_report_issue(hass, entity_id), ) -def warn_negative(hass: HomeAssistant, entity_id: str) -> None: +def warn_negative(hass: HomeAssistant, entity_id: str, state: State) -> None: """Log a warning once if a sensor with state_class_total has a negative value.""" if WARN_NEGATIVE not in hass.data: hass.data[WARN_NEGATIVE] = set() @@ -324,28 +327,34 @@ def warn_negative(hass: HomeAssistant, entity_id: str) -> None: domain = entity_sources(hass).get(entity_id, {}).get("domain") _LOGGER.warning( "Entity %s %shas state class total_increasing, but its state is " - "negative. Please %s", + "negative. Triggered by state %s with last_updated set to %s. Please %s", entity_id, f"from integration {domain} " if domain else "", + state.state, + state.last_updated.isoformat(), _suggest_report_issue(hass, entity_id), ) def reset_detected( - hass: HomeAssistant, entity_id: str, state: float, previous_state: float | None + hass: HomeAssistant, + entity_id: str, + fstate: float, + previous_fstate: float | None, + state: State, ) -> bool: """Test if a total_increasing sensor has been reset.""" - if previous_state is None: + if previous_fstate is None: return False - if 0.9 * previous_state <= state < previous_state: - warn_dip(hass, entity_id) + if 0.9 * previous_fstate <= fstate < previous_fstate: + warn_dip(hass, entity_id, state) - if state < 0: - warn_negative(hass, entity_id) + if fstate < 0: + warn_negative(hass, entity_id, state) raise HomeAssistantError - return state < 0.9 * previous_state + return fstate < 0.9 * previous_fstate def _wanted_statistics(sensor_states: list[State]) -> dict[str, set[str]]: @@ -547,13 +556,15 @@ def _compile_statistics( # noqa: C901 elif state_class == STATE_CLASS_TOTAL_INCREASING: try: if old_state is None or reset_detected( - hass, entity_id, fstate, new_state + hass, entity_id, fstate, new_state, state ): reset = True _LOGGER.info( - "Detected new cycle for %s, value dropped from %s to %s", + "Detected new cycle for %s, value dropped from %s to %s, " + "triggered by state with last_updated set to %s", entity_id, new_state, + state.last_updated.isoformat(), fstate, ) except HomeAssistantError: diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 96335d435da..032ff85561c 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -707,8 +707,10 @@ def test_compile_hourly_sum_statistics_negative_state( seq = [15, 16, 15, 16, 20, -20, 20, 10] states = {entity_id: []} + offending_state = 5 if state := hass.states.get(entity_id): states[entity_id].append(state) + offending_state = 6 one = zero for i in range(len(seq)): one = one + timedelta(seconds=5) @@ -745,8 +747,11 @@ def test_compile_hourly_sum_statistics_negative_state( }, ] assert "Error while processing event StatisticsTask" not in caplog.text + state = states[entity_id][offending_state].state + last_updated = states[entity_id][offending_state].last_updated.isoformat() assert ( - f"Entity {entity_id} {warning_1}has state class total_increasing, but its state is negative" + f"Entity {entity_id} {warning_1}has state class total_increasing, but its state " + f"is negative. Triggered by state {state} with last_updated set to {last_updated}." in caplog.text ) assert warning_2 in caplog.text @@ -965,15 +970,17 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip( wait_recording_done(hass) assert ( "Entity sensor.test1 has state class total_increasing, but its state is not " - "strictly increasing. Please create a bug report at https://github.com/" - "home-assistant/core/issues?q=is%3Aopen+is%3Aissue" + "strictly increasing." ) not in caplog.text recorder.do_adhoc_statistics(start=period2) wait_recording_done(hass) + state = states["sensor.test1"][6].state + last_updated = states["sensor.test1"][6].last_updated.isoformat() assert ( "Entity sensor.test1 has state class total_increasing, but its state is not " - "strictly increasing. Please create a bug report at https://github.com/" - "home-assistant/core/issues?q=is%3Aopen+is%3Aissue" + f"strictly increasing. Triggered by state {state} with last_updated set to " + f"{last_updated}. Please create a bug report at https://github.com/home-assistant" + "/core/issues?q=is%3Aopen+is%3Aissue" ) in caplog.text statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ From e130c8671b26560ccaa57e77e93ed05d3588be9d Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Wed, 13 Oct 2021 17:21:32 +0200 Subject: [PATCH 0301/1038] Bump version (#57607) --- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 27ffbfd10de..865663eff6d 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -2,7 +2,7 @@ "domain": "velbus", "name": "Velbus", "documentation": "https://www.home-assistant.io/integrations/velbus", - "requirements": ["velbus-aio==2021.9.4"], + "requirements": ["velbus-aio==2021.10.1"], "config_flow": true, "codeowners": ["@Cereal2nd", "@brefra"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 874157d517b..20e6c7cb8cd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2366,7 +2366,7 @@ uvcclient==0.11.0 vallox-websocket-api==2.8.1 # homeassistant.components.velbus -velbus-aio==2021.9.4 +velbus-aio==2021.10.1 # homeassistant.components.venstar venstarcolortouch==0.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9a55702ad97..40089dee5aa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1352,7 +1352,7 @@ url-normalize==1.4.1 uvcclient==0.11.0 # homeassistant.components.velbus -velbus-aio==2021.9.4 +velbus-aio==2021.10.1 # homeassistant.components.venstar venstarcolortouch==0.14 From 0ae1186554fe28cce16282b75a4fefc3c5434cf0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 13 Oct 2021 08:34:57 -0700 Subject: [PATCH 0302/1038] Use gather ipv wait to remove credentials to catch exceptions (#57596) --- homeassistant/auth/__init__.py | 2 +- tests/auth/test_init.py | 29 ++++++++++++++++++++--------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index f47228ee506..846e37a5d67 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -286,7 +286,7 @@ class AuthManager: ] if tasks: - await asyncio.wait(tasks) + await asyncio.gather(*tasks) await self._store.async_remove_user(user) diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index fa8c86536ca..ae2b1b53c12 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -17,7 +17,13 @@ from homeassistant.auth.const import MFA_SESSION_EXPIRATION from homeassistant.core import callback from homeassistant.util import dt as dt_util -from tests.common import CLIENT_ID, MockUser, ensure_auth_manager_loaded, flush_store +from tests.common import ( + CLIENT_ID, + MockUser, + async_capture_events, + ensure_auth_manager_loaded, + flush_store, +) @pytest.fixture @@ -931,14 +937,7 @@ async def test_enable_mfa_for_user(hass, hass_storage): async def test_async_remove_user(hass): """Test removing a user.""" - events = [] - - @callback - def user_removed(event): - events.append(event) - - hass.bus.async_listen("user_removed", user_removed) - + events = async_capture_events(hass, "user_removed") manager = await auth.auth_manager_from_config( hass, [ @@ -983,6 +982,18 @@ async def test_async_remove_user(hass): assert events[0].data["user_id"] == user.id +async def test_async_remove_user_fail_if_remove_credential_fails( + hass, hass_admin_user, hass_admin_credential +): + """Test removing a user.""" + await hass.auth.async_link_user(hass_admin_user, hass_admin_credential) + + with patch.object( + hass.auth, "async_remove_credentials", side_effect=ValueError + ), pytest.raises(ValueError): + await hass.auth.async_remove_user(hass_admin_user) + + async def test_new_users(mock_hass): """Test newly created users.""" manager = await auth.auth_manager_from_config( From ffbe4cffae56b394c05ee16911aaec862f7823fd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 13 Oct 2021 08:36:31 -0700 Subject: [PATCH 0303/1038] Guard linking credential that is already linked (#57595) * Guard linking credential that is already linked * Update test descriptions --- homeassistant/auth/__init__.py | 6 +++ homeassistant/components/auth/__init__.py | 10 ++++- tests/auth/test_init.py | 10 +++++ tests/components/auth/test_init_link_user.py | 47 ++++++++++++++++++++ 4 files changed, 72 insertions(+), 1 deletion(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 846e37a5d67..abd5ddc71d5 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -276,6 +276,12 @@ class AuthManager: self, user: models.User, credentials: models.Credentials ) -> None: """Link credentials to an existing user.""" + linked_user = await self.async_get_user_by_credentials(credentials) + if linked_user == user: + return + if linked_user is not None: + raise ValueError("Credential is already linked to a user") + await self._store.async_link_user(user, credentials) async def async_remove_user(self, user: models.User) -> None: diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index 4076cace2a2..5af5aea13c4 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -412,7 +412,15 @@ class LinkUserView(HomeAssistantView): if credentials is None: return self.json_message("Invalid code", status_code=HTTPStatus.BAD_REQUEST) - await hass.auth.async_link_user(user, credentials) + linked_user = await hass.auth.async_get_user_by_credentials(credentials) + if linked_user != user and linked_user is not None: + return self.json_message( + "Credential already linked", status_code=HTTPStatus.BAD_REQUEST + ) + + # No-op if credential is already linked to the user it will be linked to + if linked_user != user: + await hass.auth.async_link_user(user, credentials) return self.json_message("User linked") diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index ae2b1b53c12..ef1430f99a6 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -288,6 +288,16 @@ async def test_linking_user_to_two_auth_providers(hass, hass_storage): await manager.async_link_user(user, new_credential) assert len(user.credentials) == 2 + # Linking it again to same user is a no-op + await manager.async_link_user(user, new_credential) + assert len(user.credentials) == 2 + + # Linking a credential to a user while the credential is already linked to another user should raise + user_2 = await manager.async_create_user("User 2") + with pytest.raises(ValueError): + await manager.async_link_user(user_2, new_credential) + assert len(user_2.credentials) == 0 + async def test_saving_loading(hass, hass_storage): """Test storing and saving data. diff --git a/tests/components/auth/test_init_link_user.py b/tests/components/auth/test_init_link_user.py index 3f0e9bce063..711f8ad9c26 100644 --- a/tests/components/auth/test_init_link_user.py +++ b/tests/components/auth/test_init_link_user.py @@ -1,4 +1,6 @@ """Tests for the link user flow.""" +from unittest.mock import patch + from . import async_setup_auth from tests.common import CLIENT_ID, CLIENT_REDIRECT_URI @@ -122,3 +124,48 @@ async def test_link_user_invalid_auth(hass, aiohttp_client): assert resp.status == 401 assert len(info["user"].credentials) == 0 + + +async def test_link_user_already_linked_same_user(hass, aiohttp_client): + """Test linking a user to a credential it's already linked to.""" + info = await async_get_code(hass, aiohttp_client) + client = info["client"] + code = info["code"] + + # Link user + with patch.object( + hass.auth, "async_get_user_by_credentials", return_value=info["user"] + ): + resp = await client.post( + "/auth/link_user", + json={"client_id": CLIENT_ID, "code": code}, + headers={"authorization": f"Bearer {info['access_token']}"}, + ) + + assert resp.status == 200 + # The credential was not added because it saw that it was already linked + assert len(info["user"].credentials) == 0 + + +async def test_link_user_already_linked_other_user(hass, aiohttp_client): + """Test linking a user to a credential already linked to other user.""" + info = await async_get_code(hass, aiohttp_client) + client = info["client"] + code = info["code"] + + another_user = await hass.auth.async_create_user(name="Another") + + # Link user + with patch.object( + hass.auth, "async_get_user_by_credentials", return_value=another_user + ): + resp = await client.post( + "/auth/link_user", + json={"client_id": CLIENT_ID, "code": code}, + headers={"authorization": f"Bearer {info['access_token']}"}, + ) + + assert resp.status == 400 + # The credential was not added because it saw that it was already linked + assert len(info["user"].credentials) == 0 + assert len(another_user.credentials) == 0 From b86e19143dc47326d8ed61fcfe086c7828d20c5d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 13 Oct 2021 05:37:14 -1000 Subject: [PATCH 0304/1038] Prevent event loop delay / instability from discovery (#57463) --- homeassistant/components/dhcp/__init__.py | 67 +++++------- homeassistant/components/ssdp/__init__.py | 27 ++--- homeassistant/components/ssdp/flow.py | 50 --------- homeassistant/components/usb/__init__.py | 20 ++-- homeassistant/components/usb/flow.py | 48 --------- homeassistant/components/zeroconf/__init__.py | 100 ++++-------------- homeassistant/data_entry_flow.py | 17 +++ homeassistant/helpers/discovery_flow.py | 82 ++++++++++++++ tests/components/dhcp/test_init.py | 99 +++++++++++------ tests/components/ssdp/test_init.py | 4 +- tests/helpers/test_discovery_flow.py | 71 +++++++++++++ tests/test_data_entry_flow.py | 52 +++++++++ 12 files changed, 353 insertions(+), 284 deletions(-) delete mode 100644 homeassistant/components/ssdp/flow.py delete mode 100644 homeassistant/components/usb/flow.py create mode 100644 homeassistant/helpers/discovery_flow.py create mode 100644 tests/helpers/test_discovery_flow.py diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index 3e4fd8fec01..d52b30ccfb2 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -1,6 +1,5 @@ """The dhcp integration.""" -from abc import abstractmethod from datetime import timedelta import fnmatch from ipaddress import ip_address as make_ip_address @@ -17,6 +16,7 @@ from aiodiscover.discovery import ( from scapy.config import conf from scapy.error import Scapy_Exception +from homeassistant import config_entries from homeassistant.components.device_tracker.const import ( ATTR_HOST_NAME, ATTR_IP, @@ -31,6 +31,7 @@ from homeassistant.const import ( STATE_HOME, ) from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.helpers import discovery_flow from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.event import ( async_track_state_added_domain, @@ -38,10 +39,9 @@ from homeassistant.helpers.event import ( ) from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_dhcp +from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.network import is_invalid, is_link_local, is_loopback -from .const import DOMAIN - FILTER = "udp and (port 67 or 68)" REQUESTED_ADDR = "requested_addr" MESSAGE_TYPE = "message-type" @@ -89,6 +89,17 @@ class WatcherBase: self._address_data = address_data def process_client(self, ip_address, hostname, mac_address): + """Process a client.""" + return run_callback_threadsafe( + self.hass.loop, + self.async_process_client, + ip_address, + hostname, + mac_address, + ).result() + + @callback + def async_process_client(self, ip_address, hostname, mac_address): """Process a client.""" made_ip_address = make_ip_address(ip_address) @@ -101,7 +112,6 @@ class WatcherBase: return data = self._address_data.get(ip_address) - if ( data and data[MAC_ADDRESS] == mac_address @@ -111,12 +121,9 @@ class WatcherBase: # to process it return - self._address_data[ip_address] = {MAC_ADDRESS: mac_address, HOSTNAME: hostname} + data = {MAC_ADDRESS: mac_address, HOSTNAME: hostname} + self._address_data[ip_address] = data - self.process_updated_address_data(ip_address, self._address_data[ip_address]) - - def process_updated_address_data(self, ip_address, data): - """Process the address data update.""" lowercase_hostname = data[HOSTNAME].lower() uppercase_mac = data[MAC_ADDRESS].upper() @@ -139,23 +146,17 @@ class WatcherBase: continue _LOGGER.debug("Matched %s against %s", data, entry) - - self.create_task( - self.hass.config_entries.flow.async_init( - entry["domain"], - context={"source": DOMAIN}, - data={ - IP_ADDRESS: ip_address, - HOSTNAME: lowercase_hostname, - MAC_ADDRESS: data[MAC_ADDRESS], - }, - ) + discovery_flow.async_create_flow( + self.hass, + entry["domain"], + {"source": config_entries.SOURCE_DHCP}, + { + IP_ADDRESS: ip_address, + HOSTNAME: lowercase_hostname, + MAC_ADDRESS: data[MAC_ADDRESS], + }, ) - @abstractmethod - def create_task(self, task): - """Pass a task to async_add_task based on which context we are in.""" - class NetworkWatcher(WatcherBase): """Class to query ptr records routers.""" @@ -189,21 +190,17 @@ class NetworkWatcher(WatcherBase): """Start a new discovery task if one is not running.""" if self._discover_task and not self._discover_task.done(): return - self._discover_task = self.create_task(self.async_discover()) + self._discover_task = self.hass.async_create_task(self.async_discover()) async def async_discover(self): """Process discovery.""" for host in await self._discover_hosts.async_discover(): - self.process_client( + self.async_process_client( host[DISCOVERY_IP_ADDRESS], host[DISCOVERY_HOSTNAME], _format_mac(host[DISCOVERY_MAC_ADDRESS]), ) - def create_task(self, task): - """Pass a task to async_create_task since we are in async context.""" - return self.hass.async_create_task(task) - class DeviceTrackerWatcher(WatcherBase): """Class to watch dhcp data from routers.""" @@ -250,11 +247,7 @@ class DeviceTrackerWatcher(WatcherBase): if ip_address is None or mac_address is None: return - self.process_client(ip_address, hostname, _format_mac(mac_address)) - - def create_task(self, task): - """Pass a task to async_create_task since we are in async context.""" - return self.hass.async_create_task(task) + self.async_process_client(ip_address, hostname, _format_mac(mac_address)) class DHCPWatcher(WatcherBase): @@ -353,10 +346,6 @@ class DHCPWatcher(WatcherBase): if self._sniffer.thread: self._sniffer.thread.name = self.__class__.__name__ - def create_task(self, task): - """Pass a task to hass.add_job since we are in a thread.""" - return self.hass.add_job(task) - def _decode_dhcp_option(dhcp_options, key): """Extract and decode data from a packet option.""" diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index b06f1b34493..da46fc565d2 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -18,19 +18,14 @@ from async_upnp_client.utils import CaseInsensitiveDict from homeassistant import config_entries from homeassistant.components import network -from homeassistant.const import ( - EVENT_HOMEASSISTANT_STARTED, - EVENT_HOMEASSISTANT_STOP, - MATCH_ALL, -) +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, MATCH_ALL from homeassistant.core import HomeAssistant, callback as core_callback +from homeassistant.helpers import discovery_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_ssdp, bind_hass -from .flow import FlowDispatcher, SSDPFlow - DOMAIN = "ssdp" SCAN_INTERVAL = timedelta(seconds=60) @@ -222,7 +217,6 @@ class Scanner: self._cancel_scan: Callable[[], None] | None = None self._ssdp_listeners: list[SsdpListener] = [] self._callbacks: list[tuple[SsdpCallback, dict[str, str]]] = [] - self._flow_dispatcher: FlowDispatcher | None = None self._description_cache: DescriptionCache | None = None self.integration_matchers = integration_matchers @@ -327,14 +321,10 @@ class Scanner: session = async_get_clientsession(self.hass) requester = AiohttpSessionRequester(session, True, 10) self._description_cache = DescriptionCache(requester) - self._flow_dispatcher = FlowDispatcher(self.hass) await self._async_start_ssdp_listeners() self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STARTED, self._flow_dispatcher.async_start - ) self._cancel_scan = async_track_time_interval( self.hass, self.async_scan, SCAN_INTERVAL ) @@ -417,13 +407,12 @@ class Scanner: for domain in matching_domains: _LOGGER.debug("Discovered %s at %s", domain, location) - flow: SSDPFlow = { - "domain": domain, - "context": {"source": config_entries.SOURCE_SSDP}, - "data": discovery_info, - } - assert self._flow_dispatcher is not None - self._flow_dispatcher.create(flow) + discovery_flow.async_create_flow( + self.hass, + domain, + {"source": config_entries.SOURCE_SSDP}, + discovery_info, + ) async def _async_get_description_dict( self, location: str | None diff --git a/homeassistant/components/ssdp/flow.py b/homeassistant/components/ssdp/flow.py deleted file mode 100644 index 77f4cb107b8..00000000000 --- a/homeassistant/components/ssdp/flow.py +++ /dev/null @@ -1,50 +0,0 @@ -"""The SSDP integration.""" -from __future__ import annotations - -from collections.abc import Coroutine -from typing import Any, TypedDict - -from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult - - -class SSDPFlow(TypedDict): - """A queued ssdp discovery flow.""" - - domain: str - context: dict[str, Any] - data: dict - - -class FlowDispatcher: - """Dispatch discovery flows.""" - - def __init__(self, hass: HomeAssistant) -> None: - """Init the discovery dispatcher.""" - self.hass = hass - self.pending_flows: list[SSDPFlow] = [] - self.started = False - - @callback - def async_start(self, *_: Any) -> None: - """Start processing pending flows.""" - self.started = True - self.hass.loop.call_soon(self._async_process_pending_flows) - - def _async_process_pending_flows(self) -> None: - for flow in self.pending_flows: - self.hass.async_create_task(self._init_flow(flow)) - self.pending_flows = [] - - def create(self, flow: SSDPFlow) -> None: - """Create and add or queue a flow.""" - if self.started: - self.hass.async_create_task(self._init_flow(flow)) - else: - self.pending_flows.append(flow) - - def _init_flow(self, flow: SSDPFlow) -> Coroutine[None, None, FlowResult]: - """Create a flow.""" - return self.hass.config_entries.flow.async_init( - flow["domain"], context=flow["context"], data=flow["data"] - ) diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py index 095d72f3ed4..80d01417ea7 100644 --- a/homeassistant/components/usb/__init__.py +++ b/homeassistant/components/usb/__init__.py @@ -16,13 +16,12 @@ 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.helpers import system_info +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 .const import DOMAIN -from .flow import FlowDispatcher, USBFlow from .models import USBDevice from .utils import usb_device_from_port @@ -65,7 +64,7 @@ def get_serial_by_id(dev_path: str) -> str: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the USB Discovery integration.""" usb = await async_get_usb(hass) - usb_discovery = USBDiscovery(hass, FlowDispatcher(hass), usb) + usb_discovery = USBDiscovery(hass, usb) await usb_discovery.async_setup() hass.data[DOMAIN] = usb_discovery websocket_api.async_register_command(hass, websocket_usb_scan) @@ -86,12 +85,10 @@ class USBDiscovery: def __init__( self, hass: HomeAssistant, - flow_dispatcher: FlowDispatcher, usb: list[dict[str, str]], ) -> None: """Init USB Discovery.""" self.hass = hass - self.flow_dispatcher = flow_dispatcher self.usb = usb self.seen: set[tuple[str, ...]] = set() self.observer_active = False @@ -104,7 +101,6 @@ class USBDiscovery: async def async_start(self, event: Event) -> None: """Start USB Discovery and run a manual scan.""" - self.flow_dispatcher.async_start() await self._async_scan_serial() async def _async_start_monitor(self) -> None: @@ -193,12 +189,12 @@ class USBDiscovery: if len(matcher) < most_matched_fields: break - flow: USBFlow = { - "domain": matcher["domain"], - "context": {"source": config_entries.SOURCE_USB}, - "data": dataclasses.asdict(device), - } - self.flow_dispatcher.async_create(flow) + discovery_flow.async_create_flow( + self.hass, + matcher["domain"], + {"source": config_entries.SOURCE_USB}, + dataclasses.asdict(device), + ) @callback def _async_process_ports(self, ports: list[ListPortInfo]) -> None: diff --git a/homeassistant/components/usb/flow.py b/homeassistant/components/usb/flow.py deleted file mode 100644 index 00c40add92a..00000000000 --- a/homeassistant/components/usb/flow.py +++ /dev/null @@ -1,48 +0,0 @@ -"""The USB Discovery integration.""" -from __future__ import annotations - -from collections.abc import Coroutine -from typing import Any, TypedDict - -from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult - - -class USBFlow(TypedDict): - """A queued usb discovery flow.""" - - domain: str - context: dict[str, Any] - data: dict - - -class FlowDispatcher: - """Dispatch discovery flows.""" - - def __init__(self, hass: HomeAssistant) -> None: - """Init the discovery dispatcher.""" - self.hass = hass - self.pending_flows: list[USBFlow] = [] - self.started = False - - @callback - def async_start(self, *_: Any) -> None: - """Start processing pending flows.""" - self.started = True - for flow in self.pending_flows: - self.hass.async_create_task(self._init_flow(flow)) - self.pending_flows = [] - - @callback - def async_create(self, flow: USBFlow) -> None: - """Create and add or queue a flow.""" - if self.started: - self.hass.async_create_task(self._init_flow(flow)) - else: - self.pending_flows.append(flow) - - def _init_flow(self, flow: USBFlow) -> Coroutine[None, None, FlowResult]: - """Create a flow.""" - return self.hass.config_entries.flow.async_init( - flow["domain"], context=flow["context"], data=flow["data"] - ) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 4afb0a3c24d..1d72c7d20e9 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -from collections.abc import Coroutine from contextlib import suppress import fnmatch from ipaddress import IPv4Address, IPv6Address, ip_address @@ -21,12 +20,11 @@ from homeassistant.components.network import async_get_source_ip from homeassistant.components.network.models import Adapter from homeassistant.const import ( EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, __version__, ) from homeassistant.core import Event, HomeAssistant, callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import discovery_flow import homeassistant.helpers.config_validation as cv from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.helpers.typing import ConfigType @@ -91,14 +89,6 @@ class HaServiceInfo(TypedDict): properties: dict[str, Any] -class ZeroconfFlow(TypedDict): - """A queued zeroconf discovery flow.""" - - domain: str - context: dict[str, Any] - data: HaServiceInfo - - @bind_hass async def async_get_instance(hass: HomeAssistant) -> HaZeroconf: """Zeroconf instance to be shared with other integrations that use it.""" @@ -192,17 +182,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: uuid = await hass.helpers.instance_id.async_get() await _async_register_hass_zc_service(hass, aio_zc, uuid) - @callback - def _async_start_discovery(_event: Event) -> None: - """Start processing flows.""" - discovery.async_start() - async def _async_zeroconf_hass_stop(_event: Event) -> None: await discovery.async_stop() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_zeroconf_hass_stop) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_zeroconf_hass_start) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _async_start_discovery) return True @@ -288,40 +272,6 @@ async def _async_register_hass_zc_service( await aio_zc.async_register_service(info, allow_name_change=True) -class FlowDispatcher: - """Dispatch discovery flows.""" - - def __init__(self, hass: HomeAssistant) -> None: - """Init the discovery dispatcher.""" - self.hass = hass - self.pending_flows: list[ZeroconfFlow] = [] - self.started = False - - @callback - def async_start(self) -> None: - """Start processing pending flows.""" - self.started = True - self.hass.loop.call_soon(self._async_process_pending_flows) - - def _async_process_pending_flows(self) -> None: - for flow in self.pending_flows: - self.hass.async_create_task(self._init_flow(flow)) - self.pending_flows = [] - - def async_create(self, flow: ZeroconfFlow) -> None: - """Create and add or queue a flow.""" - if self.started: - self.hass.async_create_task(self._init_flow(flow)) - else: - self.pending_flows.append(flow) - - def _init_flow(self, flow: ZeroconfFlow) -> Coroutine[None, None, FlowResult]: - """Create a flow.""" - return self.hass.config_entries.flow.async_init( - flow["domain"], context=flow["context"], data=flow["data"] - ) - - class ZeroconfDiscovery: """Discovery via zeroconf.""" @@ -340,12 +290,10 @@ class ZeroconfDiscovery: self.homekit_models = homekit_models self.ipv6 = ipv6 - self.flow_dispatcher: FlowDispatcher | None = None self.async_service_browser: HaAsyncServiceBrowser | None = None async def async_setup(self) -> None: """Start discovery.""" - self.flow_dispatcher = FlowDispatcher(self.hass) types = list(self.zeroconf_types) # We want to make sure we know about other HomeAssistant # instances as soon as possible to avoid name conflicts @@ -363,12 +311,6 @@ class ZeroconfDiscovery: if self.async_service_browser: await self.async_service_browser.async_cancel() - @callback - def async_start(self) -> None: - """Start processing discovery flows.""" - assert self.flow_dispatcher is not None - self.flow_dispatcher.async_start() - @callback def async_service_update( self, @@ -404,12 +346,14 @@ class ZeroconfDiscovery: return _LOGGER.debug("Discovered new device %s %s", name, info) - assert self.flow_dispatcher is not None # If we can handle it as a HomeKit discovery, we do that here. if service_type in HOMEKIT_TYPES: - if pending_flow := handle_homekit(self.hass, self.homekit_models, info): - self.flow_dispatcher.async_create(pending_flow) + props = info["properties"] + if domain := async_get_homekit_discovery_domain(self.homekit_models, props): + discovery_flow.async_create_flow( + self.hass, domain, {"source": config_entries.SOURCE_HOMEKIT}, info + ) # Continue on here as homekit_controller # still needs to get updates on devices # so it can see when the 'c#' field is updated. @@ -417,10 +361,10 @@ class ZeroconfDiscovery: # We only send updates to homekit_controller # if the device is already paired in order to avoid # offering a second discovery for the same device - if pending_flow and HOMEKIT_PAIRED_STATUS_FLAG in info["properties"]: + if domain and HOMEKIT_PAIRED_STATUS_FLAG in props: try: # 0 means paired and not discoverable by iOS clients) - if int(info["properties"][HOMEKIT_PAIRED_STATUS_FLAG]): + if int(props[HOMEKIT_PAIRED_STATUS_FLAG]): return except ValueError: # HomeKit pairing status unknown @@ -466,24 +410,22 @@ class ZeroconfDiscovery: ): continue - flow: ZeroconfFlow = { - "domain": matcher["domain"], - "context": {"source": config_entries.SOURCE_ZEROCONF}, - "data": info, - } - self.flow_dispatcher.async_create(flow) + discovery_flow.async_create_flow( + self.hass, + matcher["domain"], + {"source": config_entries.SOURCE_ZEROCONF}, + info, + ) -def handle_homekit( - hass: HomeAssistant, homekit_models: dict[str, str], info: HaServiceInfo -) -> ZeroconfFlow | None: +def async_get_homekit_discovery_domain( + homekit_models: dict[str, str], props: dict[str, Any] +) -> str | None: """Handle a HomeKit discovery. - Return if discovery was forwarded. + Return the domain to forward the discovery data to """ model = None - props = info["properties"] - for key in props: if key.lower() == HOMEKIT_MODEL: model = props[key] @@ -500,11 +442,7 @@ def handle_homekit( ): continue - return { - "domain": homekit_models[test_model], - "context": {"source": config_entries.SOURCE_HOMEKIT}, - "data": info, - } + return homekit_models[test_model] return None diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 63d5566db40..791fd9d21c5 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -120,6 +120,19 @@ class FlowManager(abc.ABC): async def async_post_init(self, flow: FlowHandler, result: FlowResult) -> None: """Entry has finished executing its first step asynchronously.""" + @callback + def async_has_matching_flow( + self, handler: str, context: dict[str, Any], data: Any + ) -> bool: + """Check if an existing matching flow is in progress with the same handler, context, and data.""" + return any( + flow + for flow in self._progress.values() + if flow.handler == handler + and flow.context["source"] == context["source"] + and flow.init_data == data + ) + @callback def async_progress(self, include_uninitialized: bool = False) -> list[FlowResult]: """Return the flows in progress.""" @@ -173,6 +186,7 @@ class FlowManager(abc.ABC): flow.handler = handler flow.flow_id = uuid.uuid4().hex flow.context = context + flow.init_data = data self._progress[flow.flow_id] = flow result = await self._async_handle_step(flow, flow.init_step, data, init_done) return flow, result @@ -318,6 +332,9 @@ class FlowHandler: # Set by _async_create_flow callback init_step = "init" + # The initial data that was used to start the flow + init_data: Any = None + # Set by developer VERSION = 1 diff --git a/homeassistant/helpers/discovery_flow.py b/homeassistant/helpers/discovery_flow.py new file mode 100644 index 00000000000..5bb0da2dc05 --- /dev/null +++ b/homeassistant/helpers/discovery_flow.py @@ -0,0 +1,82 @@ +"""The discovery flow helper.""" +from __future__ import annotations + +from collections.abc import Coroutine +from typing import Any + +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import CoreState, Event, HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.loader import bind_hass +from homeassistant.util.async_ import gather_with_concurrency + +FLOW_INIT_LIMIT = 2 +DISCOVERY_FLOW_DISPATCHER = "discovery_flow_disptacher" + + +@bind_hass +@callback +def async_create_flow( + hass: HomeAssistant, domain: str, context: dict[str, Any], data: Any +) -> None: + """Create a discovery flow.""" + if hass.state == CoreState.running: + if init_coro := _async_init_flow(hass, domain, context, data): + hass.async_create_task(init_coro) + return + + if DISCOVERY_FLOW_DISPATCHER not in hass.data: + dispatcher = hass.data[DISCOVERY_FLOW_DISPATCHER] = FlowDispatcher(hass) + dispatcher.async_setup() + else: + dispatcher = hass.data[DISCOVERY_FLOW_DISPATCHER] + + return dispatcher.async_create(domain, context, data) + + +@callback +def _async_init_flow( + hass: HomeAssistant, domain: str, context: dict[str, Any], data: Any +) -> Coroutine[None, None, FlowResult] | None: + """Create a discovery flow.""" + # Avoid spawning flows that have the same initial discovery data + # as ones in progress as it may cause additional device probing + # which can overload devices since zeroconf/ssdp updates can happen + # multiple times in the same minute + if hass.config_entries.flow.async_has_matching_flow(domain, context, data): + return None + + return hass.config_entries.flow.async_init(domain, context=context, data=data) + + +class FlowDispatcher: + """Dispatch discovery flows.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Init the discovery dispatcher.""" + self.hass = hass + self.pending_flows: list[tuple[str, dict[str, Any], Any]] = [] + + @callback + def async_setup(self) -> None: + """Set up the flow disptcher.""" + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, self.async_start) + + @callback + 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], + ) + + @callback + def async_create(self, domain: str, context: dict[str, Any], data: Any) -> None: + """Create and add or queue a flow.""" + self.pending_flows.append((domain, context, data)) diff --git a/tests/components/dhcp/test_init.py b/tests/components/dhcp/test_init.py index f00a0135e8d..dc50edbeb10 100644 --- a/tests/components/dhcp/test_init.py +++ b/tests/components/dhcp/test_init.py @@ -3,6 +3,7 @@ import datetime import threading from unittest.mock import MagicMock, patch +from scapy import arch # pylint: unused-import # noqa: F401 from scapy.error import Scapy_Exception from scapy.layers.dhcp import DHCP from scapy.layers.l2 import Ether @@ -16,6 +17,7 @@ from homeassistant.components.device_tracker.const import ( ATTR_SOURCE_TYPE, SOURCE_TYPE_ROUTER, ) +from homeassistant.components.dhcp.const import DOMAIN from homeassistant.const import ( EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, @@ -129,11 +131,16 @@ async def _async_get_handle_dhcp_packet(hass, integration_matchers): {}, integration_matchers, ) - handle_dhcp_packet = None + async_handle_dhcp_packet = None def _mock_sniffer(*args, **kwargs): - nonlocal handle_dhcp_packet - handle_dhcp_packet = kwargs["prn"] + nonlocal async_handle_dhcp_packet + callback = kwargs["prn"] + + async def _async_handle_dhcp_packet(packet): + await hass.async_add_executor_job(callback, packet) + + async_handle_dhcp_packet = _async_handle_dhcp_packet return MagicMock() with patch("homeassistant.components.dhcp._verify_l2socket_setup",), patch( @@ -141,7 +148,7 @@ async def _async_get_handle_dhcp_packet(hass, integration_matchers): ), patch("scapy.sendrecv.AsyncSniffer", _mock_sniffer): await dhcp_watcher.async_start() - return handle_dhcp_packet + return async_handle_dhcp_packet async def test_dhcp_match_hostname_and_macaddress(hass): @@ -151,11 +158,13 @@ async def test_dhcp_match_hostname_and_macaddress(hass): ] packet = Ether(RAW_DHCP_REQUEST) - handle_dhcp_packet = await _async_get_handle_dhcp_packet(hass, integration_matchers) + async_handle_dhcp_packet = await _async_get_handle_dhcp_packet( + hass, integration_matchers + ) with patch.object(hass.config_entries.flow, "async_init") as mock_init: - handle_dhcp_packet(packet) + await async_handle_dhcp_packet(packet) # Ensure no change is ignored - handle_dhcp_packet(packet) + await async_handle_dhcp_packet(packet) assert len(mock_init.mock_calls) == 1 assert mock_init.mock_calls[0][1][0] == "mock-domain" @@ -177,11 +186,13 @@ async def test_dhcp_renewal_match_hostname_and_macaddress(hass): packet = Ether(RAW_DHCP_RENEWAL) - handle_dhcp_packet = await _async_get_handle_dhcp_packet(hass, integration_matchers) + async_handle_dhcp_packet = await _async_get_handle_dhcp_packet( + hass, integration_matchers + ) with patch.object(hass.config_entries.flow, "async_init") as mock_init: - handle_dhcp_packet(packet) + await async_handle_dhcp_packet(packet) # Ensure no change is ignored - handle_dhcp_packet(packet) + await async_handle_dhcp_packet(packet) assert len(mock_init.mock_calls) == 1 assert mock_init.mock_calls[0][1][0] == "mock-domain" @@ -201,9 +212,11 @@ async def test_dhcp_match_hostname(hass): packet = Ether(RAW_DHCP_REQUEST) - handle_dhcp_packet = await _async_get_handle_dhcp_packet(hass, integration_matchers) + async_handle_dhcp_packet = await _async_get_handle_dhcp_packet( + hass, integration_matchers + ) with patch.object(hass.config_entries.flow, "async_init") as mock_init: - handle_dhcp_packet(packet) + await async_handle_dhcp_packet(packet) assert len(mock_init.mock_calls) == 1 assert mock_init.mock_calls[0][1][0] == "mock-domain" @@ -223,9 +236,11 @@ async def test_dhcp_match_macaddress(hass): packet = Ether(RAW_DHCP_REQUEST) - handle_dhcp_packet = await _async_get_handle_dhcp_packet(hass, integration_matchers) + async_handle_dhcp_packet = await _async_get_handle_dhcp_packet( + hass, integration_matchers + ) with patch.object(hass.config_entries.flow, "async_init") as mock_init: - handle_dhcp_packet(packet) + await async_handle_dhcp_packet(packet) assert len(mock_init.mock_calls) == 1 assert mock_init.mock_calls[0][1][0] == "mock-domain" @@ -245,9 +260,11 @@ async def test_dhcp_match_macaddress_without_hostname(hass): packet = Ether(RAW_DHCP_REQUEST_WITHOUT_HOSTNAME) - handle_dhcp_packet = await _async_get_handle_dhcp_packet(hass, integration_matchers) + async_handle_dhcp_packet = await _async_get_handle_dhcp_packet( + hass, integration_matchers + ) with patch.object(hass.config_entries.flow, "async_init") as mock_init: - handle_dhcp_packet(packet) + await async_handle_dhcp_packet(packet) assert len(mock_init.mock_calls) == 1 assert mock_init.mock_calls[0][1][0] == "mock-domain" @@ -267,9 +284,11 @@ async def test_dhcp_nomatch(hass): packet = Ether(RAW_DHCP_REQUEST) - handle_dhcp_packet = await _async_get_handle_dhcp_packet(hass, integration_matchers) + async_handle_dhcp_packet = await _async_get_handle_dhcp_packet( + hass, integration_matchers + ) with patch.object(hass.config_entries.flow, "async_init") as mock_init: - handle_dhcp_packet(packet) + await async_handle_dhcp_packet(packet) assert len(mock_init.mock_calls) == 0 @@ -280,9 +299,11 @@ async def test_dhcp_nomatch_hostname(hass): packet = Ether(RAW_DHCP_REQUEST) - handle_dhcp_packet = await _async_get_handle_dhcp_packet(hass, integration_matchers) + async_handle_dhcp_packet = await _async_get_handle_dhcp_packet( + hass, integration_matchers + ) with patch.object(hass.config_entries.flow, "async_init") as mock_init: - handle_dhcp_packet(packet) + await async_handle_dhcp_packet(packet) assert len(mock_init.mock_calls) == 0 @@ -293,9 +314,11 @@ async def test_dhcp_nomatch_non_dhcp_packet(hass): packet = Ether(b"") - handle_dhcp_packet = await _async_get_handle_dhcp_packet(hass, integration_matchers) + async_handle_dhcp_packet = await _async_get_handle_dhcp_packet( + hass, integration_matchers + ) with patch.object(hass.config_entries.flow, "async_init") as mock_init: - handle_dhcp_packet(packet) + await async_handle_dhcp_packet(packet) assert len(mock_init.mock_calls) == 0 @@ -315,9 +338,11 @@ async def test_dhcp_nomatch_non_dhcp_request_packet(hass): ("hostname", b"connect"), ] - handle_dhcp_packet = await _async_get_handle_dhcp_packet(hass, integration_matchers) + async_handle_dhcp_packet = await _async_get_handle_dhcp_packet( + hass, integration_matchers + ) with patch.object(hass.config_entries.flow, "async_init") as mock_init: - handle_dhcp_packet(packet) + await async_handle_dhcp_packet(packet) assert len(mock_init.mock_calls) == 0 @@ -337,9 +362,11 @@ async def test_dhcp_invalid_hostname(hass): ("hostname", "connect"), ] - handle_dhcp_packet = await _async_get_handle_dhcp_packet(hass, integration_matchers) + async_handle_dhcp_packet = await _async_get_handle_dhcp_packet( + hass, integration_matchers + ) with patch.object(hass.config_entries.flow, "async_init") as mock_init: - handle_dhcp_packet(packet) + await async_handle_dhcp_packet(packet) assert len(mock_init.mock_calls) == 0 @@ -359,9 +386,11 @@ async def test_dhcp_missing_hostname(hass): ("hostname", None), ] - handle_dhcp_packet = await _async_get_handle_dhcp_packet(hass, integration_matchers) + async_handle_dhcp_packet = await _async_get_handle_dhcp_packet( + hass, integration_matchers + ) with patch.object(hass.config_entries.flow, "async_init") as mock_init: - handle_dhcp_packet(packet) + await async_handle_dhcp_packet(packet) assert len(mock_init.mock_calls) == 0 @@ -381,9 +410,11 @@ async def test_dhcp_invalid_option(hass): ("hostname"), ] - handle_dhcp_packet = await _async_get_handle_dhcp_packet(hass, integration_matchers) + async_handle_dhcp_packet = await _async_get_handle_dhcp_packet( + hass, integration_matchers + ) with patch.object(hass.config_entries.flow, "async_init") as mock_init: - handle_dhcp_packet(packet) + await async_handle_dhcp_packet(packet) assert len(mock_init.mock_calls) == 0 @@ -393,7 +424,7 @@ async def test_setup_and_stop(hass): assert await async_setup_component( hass, - dhcp.DOMAIN, + DOMAIN, {}, ) await hass.async_block_till_done() @@ -417,7 +448,7 @@ async def test_setup_fails_as_root(hass, caplog): assert await async_setup_component( hass, - dhcp.DOMAIN, + DOMAIN, {}, ) await hass.async_block_till_done() @@ -442,7 +473,7 @@ async def test_setup_fails_non_root(hass, caplog): assert await async_setup_component( hass, - dhcp.DOMAIN, + DOMAIN, {}, ) await hass.async_block_till_done() @@ -464,7 +495,7 @@ async def test_setup_fails_with_broken_libpcap(hass, caplog): assert await async_setup_component( hass, - dhcp.DOMAIN, + DOMAIN, {}, ) await hass.async_block_till_done() diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 64edd9e8341..0304f8f067b 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -431,7 +431,9 @@ async def test_scan_with_registered_callback( "homeassistant.components.ssdp.async_get_ssdp", return_value={"mock-domain": [{"st": "mock-st"}]}, ) -async def test_getting_existing_headers(mock_get_ssdp, hass, aioclient_mock): +async def test_getting_existing_headers( + mock_get_ssdp, hass, aioclient_mock, mock_flow_init +): """Test getting existing/previously scanned headers.""" aioclient_mock.get( "http://1.1.1.1", diff --git a/tests/helpers/test_discovery_flow.py b/tests/helpers/test_discovery_flow.py new file mode 100644 index 00000000000..549848e5c7b --- /dev/null +++ b/tests/helpers/test_discovery_flow.py @@ -0,0 +1,71 @@ +"""Test the discovery flow helper.""" + +from unittest.mock import AsyncMock, call, patch + +import pytest + +from homeassistant import config_entries +from homeassistant.core import EVENT_HOMEASSISTANT_STARTED, CoreState +from homeassistant.helpers import discovery_flow + + +@pytest.fixture +def mock_flow_init(hass): + """Mock hass.config_entries.flow.async_init.""" + with patch.object( + hass.config_entries.flow, "async_init", return_value=AsyncMock() + ) as mock_init: + yield mock_init + + +async def test_async_create_flow(hass, mock_flow_init): + """Test we can create a flow.""" + discovery_flow.async_create_flow( + hass, + "hue", + {"source": config_entries.SOURCE_HOMEKIT}, + {"properties": {"id": "aa:bb:cc:dd:ee:ff"}}, + ) + assert mock_flow_init.mock_calls == [ + call( + "hue", + context={"source": "homekit"}, + data={"properties": {"id": "aa:bb:cc:dd:ee:ff"}}, + ) + ] + + +async def test_async_create_flow_deferred_until_started(hass, mock_flow_init): + """Test flows are deferred until started.""" + hass.state = CoreState.stopped + discovery_flow.async_create_flow( + hass, + "hue", + {"source": config_entries.SOURCE_HOMEKIT}, + {"properties": {"id": "aa:bb:cc:dd:ee:ff"}}, + ) + assert not mock_flow_init.mock_calls + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert mock_flow_init.mock_calls == [ + call( + "hue", + context={"source": "homekit"}, + data={"properties": {"id": "aa:bb:cc:dd:ee:ff"}}, + ) + ] + + +async def test_async_create_flow_checks_existing_flows(hass, mock_flow_init): + """Test existing flows prevent an identical one from being creates.""" + with patch( + "homeassistant.data_entry_flow.FlowManager.async_has_matching_flow", + return_value=True, + ): + discovery_flow.async_create_flow( + hass, + "hue", + {"source": config_entries.SOURCE_HOMEKIT}, + {"properties": {"id": "aa:bb:cc:dd:ee:ff"}}, + ) + assert not mock_flow_init.mock_calls diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 4b5777d86f8..0aa3c01d50f 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -6,6 +6,7 @@ import pytest import voluptuous as vol from homeassistant import config_entries, data_entry_flow +from homeassistant.core import HomeAssistant from homeassistant.util.decorator import Registry from tests.common import async_capture_events @@ -397,3 +398,54 @@ async def test_init_unknown_flow(manager): manager, "async_create_flow", return_value=None ): await manager.async_init("test") + + +async def test_async_has_matching_flow( + hass: HomeAssistant, manager: data_entry_flow.FlowManager +): + """Test we can check for matching flows.""" + manager.hass = hass + + @manager.mock_reg_handler("test") + class TestFlow(data_entry_flow.FlowHandler): + VERSION = 5 + + async def async_step_init(self, user_input=None): + return self.async_show_progress( + step_id="init", + progress_action="task_one", + ) + + result = await manager.async_init( + "test", + context={"source": config_entries.SOURCE_HOMEKIT}, + data={"properties": {"id": "aa:bb:cc:dd:ee:ff"}}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS + assert result["progress_action"] == "task_one" + assert len(manager.async_progress()) == 1 + + assert ( + manager.async_has_matching_flow( + "test", + {"source": config_entries.SOURCE_HOMEKIT}, + {"properties": {"id": "aa:bb:cc:dd:ee:ff"}}, + ) + is True + ) + assert ( + manager.async_has_matching_flow( + "test", + {"source": config_entries.SOURCE_SSDP}, + {"properties": {"id": "aa:bb:cc:dd:ee:ff"}}, + ) + is False + ) + assert ( + manager.async_has_matching_flow( + "other", + {"source": config_entries.SOURCE_HOMEKIT}, + {"properties": {"id": "aa:bb:cc:dd:ee:ff"}}, + ) + is False + ) From 628e59ff11506370583c7325942bed02ff5fb42d Mon Sep 17 00:00:00 2001 From: Jason Nader Date: Thu, 14 Oct 2021 00:45:59 +0900 Subject: [PATCH 0305/1038] Clarify that only HTTPS can be used in fitbit (#57116) --- homeassistant/components/fitbit/sensor.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 34c4f61f554..96de36a1f29 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -29,7 +29,7 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.icon import icon_for_battery_level -from homeassistant.helpers.network import get_url +from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.json import load_json, save_json @@ -101,16 +101,21 @@ def request_app_setup( else: setup_platform(hass, config, add_entities, discovery_info) - start_url = f"{get_url(hass)}{FITBIT_AUTH_CALLBACK_PATH}" - - description = f"""Please create a Fitbit developer app at + try: + description = f"""Please create a Fitbit developer app at https://dev.fitbit.com/apps/new. For the OAuth 2.0 Application Type choose Personal. - Set the Callback URL to {start_url}. + Set the Callback URL to {get_url(hass, require_ssl=True)}{FITBIT_AUTH_CALLBACK_PATH}. + (Note: Your Home Assistant instance must be accessible via HTTPS.) They will provide you a Client ID and secret. These need to be saved into the file located at: {config_path}. Then come back here and hit the below button. """ + except NoURLAvailableError: + error_msg = """Could not find a SSL enabled URL for your Home Assistant instance. + Fitbit requires that your Home Assistant instance is accessible via HTTPS. + """ + configurator.notify_errors(_CONFIGURING["fitbit"], error_msg) submit = "I have saved my Client ID and Client Secret into fitbit.conf." @@ -136,7 +141,7 @@ def request_oauth_completion(hass: HomeAssistant) -> None: def fitbit_configuration_callback(fields: list[dict[str, str]]) -> None: """Handle configuration updates.""" - start_url = f"{get_url(hass)}{FITBIT_AUTH_START}" + start_url = f"{get_url(hass, require_ssl=True)}{FITBIT_AUTH_START}" description = f"Please authorize Fitbit by visiting {start_url}" @@ -236,7 +241,7 @@ def setup_platform( config_file.get(CONF_CLIENT_ID), config_file.get(CONF_CLIENT_SECRET) ) - redirect_uri = f"{get_url(hass)}{FITBIT_AUTH_CALLBACK_PATH}" + redirect_uri = f"{get_url(hass, require_ssl=True)}{FITBIT_AUTH_CALLBACK_PATH}" fitbit_auth_start_url, _ = oauth.authorize_token_url( redirect_uri=redirect_uri, From df4e8721e9d8a314f890662558b85f5b32e7661c Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 13 Oct 2021 19:04:23 +0200 Subject: [PATCH 0306/1038] ESPHome move ReconnectLogic to aioesphomeapi (#57601) --- homeassistant/components/esphome/__init__.py | 279 ++---------------- .../components/esphome/config_flow.py | 2 - .../components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/esphome/test_config_flow.py | 2 +- 6 files changed, 25 insertions(+), 264 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index de301e0c1bb..cffb8dc8ad9 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -1,8 +1,6 @@ """Support for esphome devices.""" from __future__ import annotations -import asyncio -from collections.abc import Awaitable from dataclasses import dataclass, field import functools import logging @@ -19,12 +17,12 @@ from aioesphomeapi import ( EntityState, HomeassistantServiceCall, InvalidEncryptionKeyAPIError, + ReconnectLogic, RequiresEncryptionAPIError, UserService, UserServiceArgType, ) import voluptuous as vol -from zeroconf import DNSPointer, RecordUpdate, RecordUpdateListener, Zeroconf from homeassistant import const from homeassistant.components import zeroconf @@ -119,7 +117,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: zeroconf_instance = await zeroconf.async_get_instance(hass) cli = APIClient( - hass.loop, host, port, password, @@ -259,7 +256,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _send_home_assistant_state(entity_id, attribute, hass.states.get(entity_id)) ) - async def on_login() -> None: + async def on_connect() -> None: """Subscribe to states and list entities on successful API login.""" nonlocal device_id try: @@ -285,8 +282,26 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Re-connection logic will trigger after this await cli.disconnect() + async def on_disconnect() -> None: + """Run disconnect callbacks on API disconnect.""" + for disconnect_cb in entry_data.disconnect_callbacks: + disconnect_cb() + entry_data.disconnect_callbacks = [] + entry_data.available = False + entry_data.async_update_device_state(hass) + + async def on_connect_error(err: Exception) -> None: + """Start reauth flow if appropriate connect error type.""" + if isinstance(err, (RequiresEncryptionAPIError, InvalidEncryptionKeyAPIError)): + entry.async_start_reauth(hass) + reconnect_logic = ReconnectLogic( - hass, cli, entry, host, on_login, zeroconf_instance + client=cli, + on_connect=on_connect, + on_disconnect=on_disconnect, + zeroconf_instance=zeroconf_instance, + name=host, + on_connect_error=on_connect_error, ) async def complete_setup() -> None: @@ -302,258 +317,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -class ReconnectLogic(RecordUpdateListener): - """Reconnectiong logic handler for ESPHome config entries. - - Contains two reconnect strategies: - - Connect with increasing time between connection attempts. - - Listen to zeroconf mDNS records, if any records are found for this device, try reconnecting immediately. - """ - - def __init__( - self, - hass: HomeAssistant, - cli: APIClient, - entry: ConfigEntry, - host: str, - on_login: Callable[[], Awaitable[None]], - zc: Zeroconf, - ) -> None: - """Initialize ReconnectingLogic.""" - self._hass = hass - self._cli = cli - self._entry = entry - self._host = host - self._on_login = on_login - self._zc = zc - # Flag to check if the device is connected - self._connected = True - self._connected_lock = asyncio.Lock() - self._zc_lock = asyncio.Lock() - self._zc_listening = False - # Event the different strategies use for issuing a reconnect attempt. - self._reconnect_event = asyncio.Event() - # The task containing the infinite reconnect loop while running - self._loop_task: asyncio.Task[None] | None = None - # How many reconnect attempts have there been already, used for exponential wait time - self._tries = 0 - self._tries_lock = asyncio.Lock() - # Track the wait task to cancel it on HA shutdown - self._wait_task: asyncio.Task[None] | None = None - self._wait_task_lock = asyncio.Lock() - - @property - def _entry_data(self) -> RuntimeEntryData | None: - domain_data = DomainData.get(self._hass) - try: - return domain_data.get_entry_data(self._entry) - except KeyError: - return None - - async def _on_disconnect(self) -> None: - """Log and issue callbacks when disconnecting.""" - if self._entry_data is None: - return - # This can happen often depending on WiFi signal strength. - # So therefore all these connection warnings are logged - # as infos. The "unavailable" logic will still trigger so the - # user knows if the device is not connected. - _LOGGER.info("Disconnected from ESPHome API for %s", self._host) - - # Run disconnect hooks - for disconnect_cb in self._entry_data.disconnect_callbacks: - disconnect_cb() - self._entry_data.disconnect_callbacks = [] - self._entry_data.available = False - self._entry_data.async_update_device_state(self._hass) - await self._start_zc_listen() - - # Reset tries - async with self._tries_lock: - self._tries = 0 - # Connected needs to be reset before the reconnect event (opposite order of check) - async with self._connected_lock: - self._connected = False - self._reconnect_event.set() - - async def _wait_and_start_reconnect(self) -> None: - """Wait for exponentially increasing time to issue next reconnect event.""" - async with self._tries_lock: - tries = self._tries - # If not first re-try, wait and print message - # Cap wait time at 1 minute. This is because while working on the - # device (e.g. soldering stuff), users don't want to have to wait - # a long time for their device to show up in HA again (this was - # mentioned a lot in early feedback) - tries = min(tries, 10) # prevent OverflowError - wait_time = int(round(min(1.8 ** tries, 60.0))) - if tries == 1: - _LOGGER.info("Trying to reconnect to %s in the background", self._host) - _LOGGER.debug("Retrying %s in %d seconds", self._host, wait_time) - await asyncio.sleep(wait_time) - async with self._wait_task_lock: - self._wait_task = None - self._reconnect_event.set() - - async def _try_connect(self) -> None: - """Try connecting to the API client.""" - async with self._tries_lock: - tries = self._tries - self._tries += 1 - - try: - await self._cli.connect(on_stop=self._on_disconnect, login=True) - except APIConnectionError as error: - if isinstance( - error, (RequiresEncryptionAPIError, InvalidEncryptionKeyAPIError) - ): - self._entry.async_start_reauth(self._hass) - - level = logging.WARNING if tries == 0 else logging.DEBUG - _LOGGER.log( - level, - "Can't connect to ESPHome API for %s (%s): %s", - self._entry.unique_id, - self._host, - error, - ) - await self._start_zc_listen() - # Schedule re-connect in event loop in order not to delay HA - # startup. First connect is scheduled in tracked tasks. - async with self._wait_task_lock: - # Allow only one wait task at a time - # can happen if mDNS record received while waiting, then use existing wait task - if self._wait_task is not None: - return - - self._wait_task = self._hass.loop.create_task( - self._wait_and_start_reconnect() - ) - else: - _LOGGER.info("Successfully connected to %s", self._host) - async with self._tries_lock: - self._tries = 0 - async with self._connected_lock: - self._connected = True - await self._stop_zc_listen() - self._hass.async_create_task(self._on_login()) - - async def _reconnect_once(self) -> None: - # Wait and clear reconnection event - await self._reconnect_event.wait() - self._reconnect_event.clear() - - # If in connected state, do not try to connect again. - async with self._connected_lock: - if self._connected: - return - - # Check if the entry got removed or disabled, in which case we shouldn't reconnect - if not DomainData.get(self._hass).is_entry_loaded(self._entry): - # When removing/disconnecting manually - return - - device_registry = self._hass.helpers.device_registry.async_get(self._hass) - devices = dr.async_entries_for_config_entry( - device_registry, self._entry.entry_id - ) - for device in devices: - # There is only one device in ESPHome - if device.disabled: - # Don't attempt to connect if it's disabled - return - - await self._try_connect() - - async def _reconnect_loop(self) -> None: - while True: - try: - await self._reconnect_once() - except asyncio.CancelledError: # pylint: disable=try-except-raise - raise - except Exception: # pylint: disable=broad-except - _LOGGER.error("Caught exception while reconnecting", exc_info=True) - - async def start(self) -> None: - """Start the reconnecting logic background task.""" - # Create reconnection loop outside of HA's tracked tasks in order - # not to delay startup. - self._loop_task = self._hass.loop.create_task(self._reconnect_loop()) - - async with self._connected_lock: - self._connected = False - self._reconnect_event.set() - - async def stop(self) -> None: - """Stop the reconnecting logic background task. Does not disconnect the client.""" - if self._loop_task is not None: - self._loop_task.cancel() - self._loop_task = None - async with self._wait_task_lock: - if self._wait_task is not None: - self._wait_task.cancel() - self._wait_task = None - await self._stop_zc_listen() - - async def _start_zc_listen(self) -> None: - """Listen for mDNS records. - - This listener allows us to schedule a reconnect as soon as a - received mDNS record indicates the node is up again. - """ - async with self._zc_lock: - if not self._zc_listening: - self._zc.async_add_listener(self, None) - self._zc_listening = True - - async def _stop_zc_listen(self) -> None: - """Stop listening for zeroconf updates.""" - async with self._zc_lock: - if self._zc_listening: - self._zc.async_remove_listener(self) - self._zc_listening = False - - @callback - def stop_callback(self) -> None: - """Stop as an async callback function.""" - self._hass.async_create_task(self.stop()) - - def async_update_records( - self, zc: Zeroconf, now: float, records: list[RecordUpdate] - ) -> None: - """Listen to zeroconf updated mDNS records. - - This is a mDNS record from the device and could mean it just woke up. - """ - # Check if already connected, no lock needed for this access and - # bail if either the entry was already teared down or we haven't received device info yet - if ( - self._connected - or self._reconnect_event.is_set() - or self._entry_data is None - or self._entry_data.device_info is None - ): - return - filter_alias = f"{self._entry_data.device_info.name}._esphomelib._tcp.local." - - for record_update in records: - # We only consider PTR records and match using the alias name - if ( - not isinstance(record_update.new, DNSPointer) - or record_update.new.alias != filter_alias - ): - continue - - # Tell reconnection logic to retry connection attempt now (even before reconnect timer finishes) - _LOGGER.debug( - "%s: Triggering reconnect because of received mDNS record %s", - self._host, - record_update.new, - ) - self._reconnect_event.set() - return - - async def _async_setup_device_registry( hass: HomeAssistant, entry: ConfigEntry, device_info: EsphomeDeviceInfo ) -> str: diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 7a7e45c440b..a794404b685 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -260,7 +260,6 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): assert self._host is not None assert self._port is not None cli = APIClient( - self.hass.loop, self._host, self._port, "", @@ -292,7 +291,6 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): assert self._host is not None assert self._port is not None cli = APIClient( - self.hass.loop, self._host, self._port, self._password, diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 307227be944..0bbdc454167 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==9.1.5"], + "requirements": ["aioesphomeapi==10.0.0"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], diff --git a/requirements_all.txt b/requirements_all.txt index 20e6c7cb8cd..dcc9efa042f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -161,7 +161,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==9.1.5 +aioesphomeapi==10.0.0 # homeassistant.components.flo aioflo==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 40089dee5aa..194544f985c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -109,7 +109,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==9.1.5 +aioesphomeapi==10.0.0 # homeassistant.components.flo aioflo==0.4.1 diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index b7916a3af8d..6a96e88cab0 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -33,7 +33,7 @@ def mock_client(): with patch("homeassistant.components.esphome.config_flow.APIClient") as mock_client: def mock_constructor( - loop, host, port, password, zeroconf_instance=None, noise_psk=None + host, port, password, zeroconf_instance=None, noise_psk=None ): """Fake the client constructor.""" mock_client.host = host From 16b7375e603154936f6542e451207153a5b0a998 Mon Sep 17 00:00:00 2001 From: Christian Manivong Date: Wed, 13 Oct 2021 20:20:38 +0200 Subject: [PATCH 0307/1038] Provide device_id in hue_event (#56982) Co-authored-by: Paulus Schoutsen --- homeassistant/components/hue/hue_event.py | 3 ++- tests/components/hue/test_sensor_base.py | 32 +++++++++++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/hue_event.py b/homeassistant/components/hue/hue_event.py index 069bb1e58b5..00b6c3e44f2 100644 --- a/homeassistant/components/hue/hue_event.py +++ b/homeassistant/components/hue/hue_event.py @@ -8,7 +8,7 @@ from aiohue.sensors import ( TYPE_ZLL_SWITCH, ) -from homeassistant.const import CONF_EVENT, CONF_ID, CONF_UNIQUE_ID +from homeassistant.const import CONF_DEVICE_ID, CONF_EVENT, CONF_ID, CONF_UNIQUE_ID from homeassistant.core import callback from homeassistant.util import dt as dt_util, slugify @@ -85,6 +85,7 @@ class HueEvent(GenericHueDevice): # Fire event data = { CONF_ID: self.event_id, + CONF_DEVICE_ID: self.device_registry_id, CONF_UNIQUE_ID: self.unique_id, CONF_EVENT: state, CONF_LAST_UPDATED: self.sensor.lastupdated, diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py index 5b3b6619efe..a639ca31113 100644 --- a/tests/components/hue/test_sensor_base.py +++ b/tests/components/hue/test_sensor_base.py @@ -3,14 +3,27 @@ import asyncio from unittest.mock import Mock import aiohue +import pytest +from homeassistant.components import hue from homeassistant.components.hue import sensor_base from homeassistant.components.hue.hue_event import CONF_HUE_EVENT from homeassistant.util import dt as dt_util from .conftest import create_mock_bridge, setup_bridge_for_sensors as setup_bridge -from tests.common import async_capture_events, async_fire_time_changed +from tests.common import ( + async_capture_events, + async_fire_time_changed, + mock_device_registry, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + PRESENCE_SENSOR_1_PRESENT = { "state": {"presence": True, "lastupdated": "2019-01-01T01:00:00"}, @@ -435,7 +448,7 @@ async def test_update_unauthorized(hass, mock_bridge): assert len(mock_bridge.handle_unauthorized_error.mock_calls) == 1 -async def test_hue_events(hass, mock_bridge): +async def test_hue_events(hass, mock_bridge, device_reg): """Test that hue remotes fire events when pressed.""" mock_bridge.mock_sensor_responses.append(SENSOR_RESPONSE) @@ -446,6 +459,10 @@ async def test_hue_events(hass, mock_bridge): assert len(hass.states.async_all()) == 7 assert len(events) == 0 + hue_tap_device = device_reg.async_get_device( + {(hue.DOMAIN, "00:00:00:00:00:44:23:08")} + ) + mock_bridge.api.sensors["7"].last_event = {"type": "button"} mock_bridge.api.sensors["8"].last_event = {"type": "button"} @@ -467,12 +484,17 @@ async def test_hue_events(hass, mock_bridge): assert len(hass.states.async_all()) == 7 assert len(events) == 1 assert events[-1].data == { + "device_id": hue_tap_device.id, "id": "hue_tap", "unique_id": "00:00:00:00:00:44:23:08-f2", "event": 18, "last_updated": "2019-12-28T22:58:03", } + hue_dimmer_device = device_reg.async_get_device( + {(hue.DOMAIN, "00:17:88:01:10:3e:3a:dc")} + ) + new_sensor_response = dict(new_sensor_response) new_sensor_response["8"] = dict(new_sensor_response["8"]) new_sensor_response["8"]["state"] = { @@ -491,6 +513,7 @@ async def test_hue_events(hass, mock_bridge): assert len(hass.states.async_all()) == 7 assert len(events) == 2 assert events[-1].data == { + "device_id": hue_dimmer_device.id, "id": "hue_dimmer_switch_1", "unique_id": "00:17:88:01:10:3e:3a:dc-02-fc00", "event": 3002, @@ -571,10 +594,15 @@ async def test_hue_events(hass, mock_bridge): ) await hass.async_block_till_done() + hue_aurora_device = device_reg.async_get_device( + {(hue.DOMAIN, "ff:ff:00:0f:e7:fd:bc:b7")} + ) + assert len(mock_bridge.mock_requests) == 6 assert len(hass.states.async_all()) == 8 assert len(events) == 3 assert events[-1].data == { + "device_id": hue_aurora_device.id, "id": "lutron_aurora_1", "unique_id": "ff:ff:00:0f:e7:fd:bc:b7-01-fc00-0014", "event": 2, From 6a72af63c28e27845d456aadad105677e9aaf07e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 13 Oct 2021 20:29:11 +0200 Subject: [PATCH 0308/1038] Refactor Tuya climate platform (#57609) --- homeassistant/components/climate/const.py | 1 + homeassistant/components/tuya/__init__.py | 4 +- homeassistant/components/tuya/base.py | 41 ++ homeassistant/components/tuya/climate.py | 652 ++++++++++++---------- homeassistant/components/tuya/const.py | 2 + 5 files changed, 402 insertions(+), 298 deletions(-) diff --git a/homeassistant/components/climate/const.py b/homeassistant/components/climate/const.py index 55387d71438..773ee5920da 100644 --- a/homeassistant/components/climate/const.py +++ b/homeassistant/components/climate/const.py @@ -70,6 +70,7 @@ FAN_DIFFUSE = "diffuse" # Possible swing state +SWING_ON = "on" SWING_OFF = "off" SWING_BOTH = "both" SWING_VERTICAL = "vertical" diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 1c7908ab986..b5ff3ec19ad 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -170,9 +170,9 @@ class DeviceListener(TuyaDeviceListener): """Update device status.""" if device.id in self.device_ids: _LOGGER.debug( - "_update-->%s;->>%s", - self, + "Received update for device %s: %s", device.id, + self.device_manager.device_map[device.id].status, ) dispatcher_send(self.hass, f"{TUYA_HA_SIGNAL_UPDATE_ENTITY}_{device.id}") diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index 0b2ca643e12..842b4728a19 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -1,6 +1,9 @@ """Tuya Home Assistant Base Device Model.""" from __future__ import annotations +from dataclasses import dataclass +import json +import logging from typing import Any from tuya_iot import TuyaDevice, TuyaDeviceManager @@ -10,6 +13,36 @@ from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN, TUYA_HA_SIGNAL_UPDATE_ENTITY +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class IntegerTypeData: + """Integer Type Data.""" + + min: int + max: int + unit: str + scale: float + step: float + + @staticmethod + def from_json(data: str) -> IntegerTypeData: + """Load JSON string and return a IntegerTypeData object.""" + return IntegerTypeData(**json.loads(data)) + + +@dataclass +class EnumTypeData: + """Enum Type Data.""" + + range: list[str] + + @staticmethod + def from_json(data: str) -> EnumTypeData: + """Load JSON string and return a EnumTypeData object.""" + return EnumTypeData(**json.loads(data)) + class TuyaHaEntity(Entity): """Tuya base device.""" @@ -54,4 +87,12 @@ class TuyaHaEntity(Entity): def _send_command(self, commands: list[dict[str, Any]]) -> None: """Send command to the device.""" + _LOGGER.debug( + "Sending commands for device %s: %s", self.tuya_device.id, commands + ) self.tuya_device_manager.send_commands(self.tuya_device.id, commands) + + @staticmethod + def scale(value: float | int, scale: float | int) -> float: + """Scale a value.""" + return value * 1.0 / (10 ** scale) diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index fd1b48f865b..64c0e00ccb6 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -1,25 +1,28 @@ """Support for Tuya Climate.""" - from __future__ import annotations -import json -import logging +from dataclasses import dataclass from typing import Any from tuya_iot import TuyaDevice, TuyaDeviceManager -from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate import ClimateEntity, ClimateEntityDescription from homeassistant.components.climate.const import ( - HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, + SWING_BOTH, + SWING_HORIZONTAL, + SWING_OFF, + SWING_ON, + SWING_VERTICAL, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT @@ -28,32 +31,53 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantTuyaData -from .base import TuyaHaEntity +from .base import EnumTypeData, IntegerTypeData, TuyaHaEntity from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode -_LOGGER = logging.getLogger(__name__) - -SWING_OFF = "swing_off" -SWING_VERTICAL = "swing_vertical" -SWING_HORIZONTAL = "swing_horizontal" -SWING_BOTH = "swing_both" - -DEFAULT_MIN_TEMP = 7 -DEFAULT_MAX_TEMP = 35 - TUYA_HVAC_TO_HA = { - "hot": HVAC_MODE_HEAT, + "auto": HVAC_MODE_HEAT_COOL, "cold": HVAC_MODE_COOL, + "heat": HVAC_MODE_HEAT, + "hot": HVAC_MODE_HEAT, + "manual": HVAC_MODE_HEAT_COOL, "wet": HVAC_MODE_DRY, "wind": HVAC_MODE_FAN_ONLY, - "auto": HVAC_MODE_AUTO, } -# https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq -TUYA_SUPPORT_TYPE = { - "kt", # Air conditioner - "qn", # Heater - "wk", # Thermostat + +@dataclass +class TuyaClimateSensorDescriptionMixin: + """Define an entity description mixin for climate entities.""" + + switch_only_hvac_mode: str + + +@dataclass +class TuyaClimateEntityDescription( + ClimateEntityDescription, TuyaClimateSensorDescriptionMixin +): + """Describe an Tuya climate entity.""" + + +CLIMATE_DESCRIPTIONS: dict[str, TuyaClimateEntityDescription] = { + # Air conditioner + # https://developer.tuya.com/en/docs/iot/categorykt?id=Kaiuz0z71ov2n + "kt": TuyaClimateEntityDescription( + key="kt", + switch_only_hvac_mode=HVAC_MODE_COOL, + ), + # Heater + # https://developer.tuya.com/en/docs/iot/f?id=K9gf46epy4j82 + "qn": TuyaClimateEntityDescription( + key="qn", + switch_only_hvac_mode=HVAC_MODE_HEAT, + ), + # Thermostat + # https://developer.tuya.com/en/docs/iot/f?id=K9gf45ld5l0t9 + "wk": TuyaClimateEntityDescription( + key="wk", + switch_only_hvac_mode=HVAC_MODE_HEAT_COOL, + ), } @@ -66,11 +90,17 @@ async def async_setup_entry( @callback def async_discover_device(device_ids: list[str]) -> None: """Discover and add a discovered Tuya climate.""" - entities: list[TuyaHaClimate] = [] + entities: list[TuyaClimateEntity] = [] for device_id in device_ids: device = hass_data.device_manager.device_map[device_id] - if device and device.category in TUYA_SUPPORT_TYPE: - entities.append(TuyaHaClimate(device, hass_data.device_manager)) + if device and device.category in CLIMATE_DESCRIPTIONS: + entities.append( + TuyaClimateEntity( + device, + hass_data.device_manager, + CLIMATE_DESCRIPTIONS[device.category], + ) + ) async_add_entities(entities) async_discover_device([*hass_data.device_manager.device_map]) @@ -80,56 +110,203 @@ async def async_setup_entry( ) -class TuyaHaClimate(TuyaHaEntity, ClimateEntity): - """Tuya Switch Device.""" +class TuyaClimateEntity(TuyaHaEntity, ClimateEntity): + """Tuya Climate Device.""" + + _current_humidity_dpcode: DPCode | None = None + _current_humidity_type: IntegerTypeData | None = None + _current_temperature_dpcode: DPCode | None = None + _current_temperature_type: IntegerTypeData | None = None + _hvac_to_tuya: dict[str, str] + _set_humidity_dpcode: DPCode | None = None + _set_humidity_type: IntegerTypeData | None = None + _set_temperature_dpcode: DPCode | None = None + _set_temperature_type: IntegerTypeData | None = None + entity_description: TuyaClimateEntityDescription + + def __init__( # noqa: C901 + self, + device: TuyaDevice, + device_manager: TuyaDeviceManager, + description: TuyaClimateEntityDescription, + ) -> None: + """Determine which values to use.""" + self._attr_target_temperature_step = 1.0 + self._attr_supported_features = 0 + self.entity_description = description - def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> None: - """Init Tuya Ha Climate.""" super().__init__(device, device_manager) - if DPCode.C_F in self.tuya_device.status: - self.dp_temp_unit = DPCode.C_F - else: - self.dp_temp_unit = DPCode.TEMP_UNIT_CONVERT - def get_temp_set_scale(self) -> int | None: - """Get temperature set scale.""" - dp_temp_set = DPCode.TEMP_SET if self.is_celsius() else DPCode.TEMP_SET_F - temp_set_value_range_item = self.tuya_device.status_range.get(dp_temp_set) - if not temp_set_value_range_item: - return None + # If both temperature values for celsius and fahrenheit are present, + # use whatever the device is set to, with a fallback to celsius. + if all( + dpcode in device.status + for dpcode in (DPCode.TEMP_CURRENT, DPCode.TEMP_CURRENT_F) + ) or all( + dpcode in device.status for dpcode in (DPCode.TEMP_SET, DPCode.TEMP_SET_F) + ): + self._attr_temperature_unit = TEMP_CELSIUS + if any( + "f" in device.status.get(dpcode, "").lower() + for dpcode in (DPCode.C_F, DPCode.TEMP_UNIT_CONVERT) + ): + self._attr_temperature_unit = TEMP_FAHRENHEIT - temp_set_value_range = json.loads(temp_set_value_range_item.values) - return temp_set_value_range.get("scale") + # If any DPCode handling celsius is present, use celsius. + elif any( + dpcode in device.status for dpcode in (DPCode.TEMP_CURRENT, DPCode.TEMP_SET) + ): + self._attr_temperature_unit = TEMP_CELSIUS - def get_temp_current_scale(self) -> int | None: - """Get temperature current scale.""" - dp_temp_current = ( - DPCode.TEMP_CURRENT if self.is_celsius() else DPCode.TEMP_CURRENT_F - ) - temp_current_value_range_item = self.tuya_device.status_range.get( - dp_temp_current - ) - if not temp_current_value_range_item: - return None + # If any DPCode handling fahrenheit is present, use celsius. + elif any( + dpcode in device.status + for dpcode in (DPCode.TEMP_CURRENT_F, DPCode.TEMP_SET_F) + ): + self._attr_temperature_unit = TEMP_FAHRENHEIT - temp_current_value_range = json.loads(temp_current_value_range_item.values) - return temp_current_value_range.get("scale") + # Determine dpcode to use for setting temperature + if all( + dpcode in device.status for dpcode in (DPCode.TEMP_SET, DPCode.TEMP_SET_F) + ): + self._set_temperature_dpcode = DPCode.TEMP_SET + if self._attr_temperature_unit == TEMP_FAHRENHEIT: + self._set_temperature_dpcode = DPCode.TEMP_SET_F + elif DPCode.TEMP_SET in device.status: + self._set_temperature_dpcode = DPCode.TEMP_SET + elif DPCode.TEMP_SET_F in device.status: + self._set_temperature_dpcode = DPCode.TEMP_SET_F - # Functions + # Get integer type data for the dpcode to set temperature, use + # it to define min, max & step temperatures + if ( + self._set_temperature_dpcode + and self._set_temperature_dpcode in device.status_range + ): + type_data = IntegerTypeData.from_json( + device.status_range[self._set_temperature_dpcode].values + ) + self._attr_supported_features |= SUPPORT_TARGET_TEMPERATURE + self._set_temperature_type = type_data + self._attr_max_temp = self.scale(type_data.max, type_data.scale) + self._attr_min_temp = self.scale(type_data.min, type_data.scale) + self._attr_target_temperature_step = self.scale( + type_data.step, type_data.scale + ) + + # Determine dpcode to use for getting the current temperature + if all( + dpcode in device.status + for dpcode in (DPCode.TEMP_CURRENT, DPCode.TEMP_CURRENT_F) + ): + self._current_temperature_dpcode = DPCode.TEMP_CURRENT + if self._attr_temperature_unit == TEMP_FAHRENHEIT: + self._current_temperature_dpcode = DPCode.TEMP_CURRENT_F + elif DPCode.TEMP_CURRENT in device.status: + self._current_temperature_dpcode = DPCode.TEMP_CURRENT + elif DPCode.TEMP_CURRENT_F in device.status: + self._current_temperature_dpcode = DPCode.TEMP_CURRENT_F + + # If we have a current temperature dpcode, get the integer type data + if ( + self._current_temperature_dpcode + and self._current_temperature_dpcode in device.status_range + ): + self._current_temperature_type = IntegerTypeData.from_json( + device.status_range[self._current_temperature_dpcode].values + ) + + # Determine HVAC modes + self._attr_hvac_modes = [] + self._hvac_to_tuya = {} + if DPCode.MODE in device.function: + data_type = EnumTypeData.from_json(device.function[DPCode.MODE].values) + self._attr_hvac_modes = [HVAC_MODE_OFF] + for tuya_mode, ha_mode in TUYA_HVAC_TO_HA.items(): + if tuya_mode in data_type.range: + self._hvac_to_tuya[ha_mode] = tuya_mode + self._attr_hvac_modes.append(ha_mode) + elif DPCode.SWITCH in device.function: + self._attr_hvac_modes = [ + HVAC_MODE_OFF, + description.switch_only_hvac_mode, + ] + + # Determine dpcode to use for setting the humidity + if ( + DPCode.HUMIDITY_SET in device.status + and DPCode.HUMIDITY_SET in device.status_range + ): + self._attr_supported_features |= SUPPORT_TARGET_HUMIDITY + self._set_humidity_dpcode = DPCode.HUMIDITY_SET + type_data = IntegerTypeData.from_json( + device.status_range[DPCode.HUMIDITY_SET].values + ) + self._set_humidity_type = type_data + self._attr_min_humidity = int(self.scale(type_data.max, type_data.scale)) + self._attr_max_humidity = int(self.scale(type_data.min, type_data.scale)) + + # Determine dpcode to use for getting the current humidity + if ( + DPCode.HUMIDITY_CURRENT in device.status + and DPCode.HUMIDITY_CURRENT in device.status_range + ): + self._current_humidity_dpcode = DPCode.HUMIDITY_CURRENT + self._current_humidity_type = IntegerTypeData.from_json( + self.tuya_device.status_range[DPCode.HUMIDITY_CURRENT].values + ) + + # Determine dpcode to use for getting the current humidity + if ( + DPCode.HUMIDITY_CURRENT in device.status + and DPCode.HUMIDITY_CURRENT in device.status_range + ): + self._current_humidity_dpcode = DPCode.HUMIDITY_CURRENT + self._current_humidity_type = IntegerTypeData.from_json( + self.tuya_device.status_range[DPCode.HUMIDITY_CURRENT].values + ) + + # Determine fan modes + if ( + DPCode.FAN_SPEED_ENUM in device.status + and DPCode.FAN_SPEED_ENUM in device.function + ): + self._attr_supported_features |= SUPPORT_FAN_MODE + self._attr_fan_modes = EnumTypeData.from_json( + self.tuya_device.status_range[DPCode.FAN_SPEED_ENUM].values + ).range + + # Determine swing modes + if any( + dpcode in self.tuya_device.function + for dpcode in ( + DPCode.SHAKE, + DPCode.SWING, + DPCode.SWITCH_HORIZONTAL, + DPCode.SWITCH_VERTICAL, + ) + ): + self._attr_supported_features |= SUPPORT_SWING_MODE + self._attr_swing_modes = [SWING_OFF] + if any( + dpcode in self.tuya_device.function + for dpcode in (DPCode.SHAKE, DPCode.SWING) + ): + self._attr_swing_modes.append(SWING_ON) + + if DPCode.SWITCH_HORIZONTAL in self.tuya_device.function: + self._attr_swing_modes.append(SWING_HORIZONTAL) + + if DPCode.SWITCH_VERTICAL in self.tuya_device.function: + self._attr_swing_modes.append(SWING_VERTICAL) def set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" - commands = [] - if hvac_mode == HVAC_MODE_OFF: - commands.append({"code": DPCode.SWITCH, "value": False}) - else: - commands.append({"code": DPCode.SWITCH, "value": True}) - - for tuya_mode, ha_mode in TUYA_HVAC_TO_HA.items(): - if ha_mode == hvac_mode: - commands.append({"code": DPCode.MODE, "value": tuya_mode}) - break - + commands = [{"code": DPCode.SWITCH, "value": hvac_mode != HVAC_MODE_OFF}] + if hvac_mode in self._hvac_to_tuya: + commands.append( + {"code": DPCode.MODE, "value": self._hvac_to_tuya[hvac_mode]} + ) self._send_command(commands) def set_fan_mode(self, fan_mode: str) -> None: @@ -138,294 +315,177 @@ class TuyaHaClimate(TuyaHaEntity, ClimateEntity): def set_humidity(self, humidity: float) -> None: """Set new target humidity.""" - self._send_command([{"code": DPCode.HUMIDITY_SET, "value": int(humidity)}]) - - def set_swing_mode(self, swing_mode: str) -> None: - """Set new target swing operation.""" - if swing_mode == SWING_BOTH: - commands = [ - {"code": DPCode.SWITCH_VERTICAL, "value": True}, - {"code": DPCode.SWITCH_HORIZONTAL, "value": True}, - ] - elif swing_mode == SWING_HORIZONTAL: - commands = [ - {"code": DPCode.SWITCH_VERTICAL, "value": False}, - {"code": DPCode.SWITCH_HORIZONTAL, "value": True}, - ] - elif swing_mode == SWING_VERTICAL: - commands = [ - {"code": DPCode.SWITCH_VERTICAL, "value": True}, - {"code": DPCode.SWITCH_HORIZONTAL, "value": False}, - ] - else: - commands = [ - {"code": DPCode.SWITCH_VERTICAL, "value": False}, - {"code": DPCode.SWITCH_HORIZONTAL, "value": False}, - ] - - self._send_command(commands) - - def set_temperature(self, **kwargs: Any) -> None: - """Set new target temperature.""" - _LOGGER.debug("climate temp-> %s", kwargs) - code = DPCode.TEMP_SET if self.is_celsius() else DPCode.TEMP_SET_F - temp_set_scale = self.get_temp_set_scale() - if not temp_set_scale: - return + if self._set_humidity_dpcode is None or self._set_humidity_type is None: + raise RuntimeError( + "Cannot set humidity, device doesn't provide methods to set it" + ) self._send_command( [ { - "code": code, - "value": int(kwargs["temperature"] * (10 ** temp_set_scale)), + "code": self._set_humidity_dpcode, + "value": self.scale(humidity, self._set_humidity_type.scale), } ] ) - def is_celsius(self) -> bool: - """Return True if device reports in Celsius.""" - if ( - self.dp_temp_unit in self.tuya_device.status - and self.tuya_device.status.get(self.dp_temp_unit).lower() == "c" - ): - return True - if ( - DPCode.TEMP_SET in self.tuya_device.status - or DPCode.TEMP_CURRENT in self.tuya_device.status - ): - return True - return False + def set_swing_mode(self, swing_mode: str) -> None: + """Set new target swing operation.""" + # The API accepts these all at once and will ignore the codes + # that don't apply to the device being controlled. + self._send_command( + [ + { + "code": DPCode.SHAKE, + "value": swing_mode == SWING_ON, + }, + { + "code": DPCode.SWING, + "value": swing_mode == SWING_ON, + }, + { + "code": DPCode.SWITCH_VERTICAL, + "value": swing_mode in (SWING_BOTH, SWING_VERTICAL), + }, + { + "code": DPCode.SWITCH_HORIZONTAL, + "value": swing_mode in (SWING_BOTH, SWING_HORIZONTAL), + }, + ] + ) - @property - def temperature_unit(self) -> str: - """Return true if fan is on.""" - if self.is_celsius(): - return TEMP_CELSIUS - return TEMP_FAHRENHEIT + def set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + if self._set_temperature_dpcode is None or self._set_temperature_type is None: + raise RuntimeError( + "Cannot set target temperature, device doesn't provide methods to set it" + ) + + self._send_command( + [ + { + "code": self._set_temperature_dpcode, + "value": round( + self.scale( + kwargs["temperature"], self._set_temperature_type.scale + ) + ), + } + ] + ) @property def current_temperature(self) -> float | None: """Return the current temperature.""" if ( - DPCode.TEMP_CURRENT not in self.tuya_device.status - and DPCode.TEMP_CURRENT_F not in self.tuya_device.status + self._current_temperature_dpcode is None + or self._current_temperature_type is None ): return None - temp_current_scale = self.get_temp_current_scale() - if not temp_current_scale: + temperature = self.tuya_device.status.get(self._current_temperature_dpcode) + if temperature is None: return None - if self.is_celsius(): - temperature = self.tuya_device.status.get(DPCode.TEMP_CURRENT) - if not temperature: - return None - return temperature * 1.0 / (10 ** temp_current_scale) - - temperature = self.tuya_device.status.get(DPCode.TEMP_CURRENT_F) - if not temperature: - return None - return temperature * 1.0 / (10 ** temp_current_scale) + return self.scale(temperature, self._current_temperature_type.scale) @property - def current_humidity(self) -> int: + def current_humidity(self) -> int | None: """Return the current humidity.""" - return int(self.tuya_device.status.get(DPCode.HUMIDITY_CURRENT, 0)) + if self._current_humidity_dpcode is None or self._current_humidity_type is None: + return None + + humidity = self.tuya_device.status.get(self._current_humidity_dpcode) + if humidity is None: + return None + + return round(self.scale(humidity, self._current_humidity_type.scale)) @property def target_temperature(self) -> float | None: """Return the temperature currently set to be reached.""" - temp_set_scale = self.get_temp_set_scale() - if temp_set_scale is None: + if self._set_temperature_dpcode is None or self._set_temperature_type is None: return None - dpcode_temp_set = self.tuya_device.status.get(DPCode.TEMP_SET) - if dpcode_temp_set is None: + temperature = self.tuya_device.status.get(self._set_temperature_dpcode) + if temperature is None: return None - return dpcode_temp_set * 1.0 / (10 ** temp_set_scale) + return self.scale(temperature, self._set_temperature_type.scale) @property - def max_temp(self) -> float: - """Return the maximum temperature.""" - scale = self.get_temp_set_scale() - if scale is None: - return DEFAULT_MAX_TEMP - - if self.is_celsius(): - if DPCode.TEMP_SET not in self.tuya_device.function: - return DEFAULT_MAX_TEMP - - function_item = self.tuya_device.function.get(DPCode.TEMP_SET) - if function_item is None: - return DEFAULT_MAX_TEMP - - temp_value = json.loads(function_item.values) - - temp_max = temp_value.get("max") - if temp_max is None: - return DEFAULT_MAX_TEMP - return temp_max * 1.0 / (10 ** scale) - if DPCode.TEMP_SET_F not in self.tuya_device.function: - return DEFAULT_MAX_TEMP - - function_item_f = self.tuya_device.function.get(DPCode.TEMP_SET_F) - if function_item_f is None: - return DEFAULT_MAX_TEMP - - temp_value_f = json.loads(function_item_f.values) - - temp_max_f = temp_value_f.get("max") - if temp_max_f is None: - return DEFAULT_MAX_TEMP - return temp_max_f * 1.0 / (10 ** scale) - - @property - def min_temp(self) -> float: - """Return the minimum temperature.""" - temp_set_scal = self.get_temp_set_scale() - if temp_set_scal is None: - return DEFAULT_MIN_TEMP - - if self.is_celsius(): - if DPCode.TEMP_SET not in self.tuya_device.function: - return DEFAULT_MIN_TEMP - - function_temp_item = self.tuya_device.function.get(DPCode.TEMP_SET) - if function_temp_item is None: - return DEFAULT_MIN_TEMP - temp_value = json.loads(function_temp_item.values) - temp_min = temp_value.get("min") - if temp_min is None: - return DEFAULT_MIN_TEMP - return temp_min * 1.0 / (10 ** temp_set_scal) - - if DPCode.TEMP_SET_F not in self.tuya_device.function: - return DEFAULT_MIN_TEMP - - temp_value_temp_f = self.tuya_device.function.get(DPCode.TEMP_SET_F) - if temp_value_temp_f is None: - return DEFAULT_MIN_TEMP - temp_value_f = json.loads(temp_value_temp_f.values) - - temp_min_f = temp_value_f.get("min") - if temp_min_f is None: - return DEFAULT_MIN_TEMP - - return temp_min_f * 1.0 / (10 ** temp_set_scal) - - @property - def target_temperature_step(self) -> float | None: - """Return target temperature setp.""" - if ( - DPCode.TEMP_SET not in self.tuya_device.status_range - and DPCode.TEMP_SET_F not in self.tuya_device.status_range - ): - return 1.0 - temp_set_value_range = json.loads( - self.tuya_device.status_range.get( - DPCode.TEMP_SET if self.is_celsius() else DPCode.TEMP_SET_F - ).values - ) - step = temp_set_value_range.get("step") - if step is None: + def target_humidity(self) -> int | None: + """Return the humidity currently set to be reached.""" + if self._set_humidity_dpcode is None or self._set_humidity_type is None: return None - temp_set_scale = self.get_temp_set_scale() - if temp_set_scale is None: + humidity = self.tuya_device.status.get(self._set_humidity_dpcode) + if humidity is None: return None - return step * 1.0 / (10 ** temp_set_scale) - - @property - def target_humidity(self) -> int: - """Return target humidity.""" - return int(self.tuya_device.status.get(DPCode.HUMIDITY_SET, 0)) + return round(self.scale(humidity, self._set_humidity_type.scale)) @property def hvac_mode(self) -> str: """Return hvac mode.""" - if not self.tuya_device.status.get(DPCode.SWITCH, False): + # If the switch off, hvac mode is off as well. Unless the switch + # the switch is on or doesn't exists of course... + if not self.tuya_device.status.get(DPCode.SWITCH, True): return HVAC_MODE_OFF - if DPCode.MODE not in self.tuya_device.status: + + if DPCode.MODE not in self.tuya_device.function: + if self.tuya_device.status.get(DPCode.SWITCH, False): + return self.entity_description.switch_only_hvac_mode return HVAC_MODE_OFF + if self.tuya_device.status.get(DPCode.MODE) is not None: return TUYA_HVAC_TO_HA[self.tuya_device.status[DPCode.MODE]] return HVAC_MODE_OFF - @property - def hvac_modes(self) -> list[str]: - """Return hvac modes for select.""" - if DPCode.MODE not in self.tuya_device.function: - return [] - modes = json.loads(self.tuya_device.function.get(DPCode.MODE, {}).values).get( - "range" - ) - - hvac_modes = [HVAC_MODE_OFF] - for tuya_mode, ha_mode in TUYA_HVAC_TO_HA.items(): - if tuya_mode in modes: - hvac_modes.append(ha_mode) - - return hvac_modes - @property def fan_mode(self) -> str | None: """Return fan mode.""" return self.tuya_device.status.get(DPCode.FAN_SPEED_ENUM) - @property - def fan_modes(self) -> list[str]: - """Return fan modes for select.""" - fan_speed_device_function = self.tuya_device.function.get(DPCode.FAN_SPEED_ENUM) - if not fan_speed_device_function: - return [] - return json.loads(fan_speed_device_function.values).get("range", []) - @property def swing_mode(self) -> str: """Return swing mode.""" - mode = 0 - if ( - DPCode.SWITCH_HORIZONTAL in self.tuya_device.status - and self.tuya_device.status.get(DPCode.SWITCH_HORIZONTAL) + if any( + self.tuya_device.status.get(dpcode) + for dpcode in (DPCode.SHAKE, DPCode.SWING) ): - mode += 1 - if ( - DPCode.SWITCH_VERTICAL in self.tuya_device.status - and self.tuya_device.status.get(DPCode.SWITCH_VERTICAL) - ): - mode += 2 + return SWING_ON - if mode == 3: + horizontal = self.tuya_device.status.get(DPCode.SWITCH_HORIZONTAL) + vertical = self.tuya_device.status.get(DPCode.SWITCH_VERTICAL) + if horizontal and vertical: return SWING_BOTH - if mode == 2: - return SWING_VERTICAL - if mode == 1: + if horizontal: return SWING_HORIZONTAL + if vertical: + return SWING_VERTICAL + return SWING_OFF - @property - def swing_modes(self) -> list[str]: - """Return swing mode for select.""" - return [SWING_OFF, SWING_HORIZONTAL, SWING_VERTICAL, SWING_BOTH] + def turn_on(self) -> None: + """Turn the device on, retaining current HVAC (if supported).""" + if DPCode.SWITCH in self.tuya_device.function: + self._send_command([{"code": DPCode.SWITCH, "value": True}]) + return - @property - def supported_features(self) -> int: - """Flag supported features.""" - supports = 0 - if ( - DPCode.TEMP_SET in self.tuya_device.status - or DPCode.TEMP_SET_F in self.tuya_device.status - ): - supports |= SUPPORT_TARGET_TEMPERATURE - if DPCode.FAN_SPEED_ENUM in self.tuya_device.status: - supports |= SUPPORT_FAN_MODE - if DPCode.HUMIDITY_SET in self.tuya_device.status: - supports |= SUPPORT_TARGET_HUMIDITY - if ( - DPCode.SWITCH_HORIZONTAL in self.tuya_device.status - or DPCode.SWITCH_VERTICAL in self.tuya_device.status - ): - supports |= SUPPORT_SWING_MODE - return supports + # Fake turn on + for mode in (HVAC_MODE_HEAT_COOL, HVAC_MODE_HEAT, HVAC_MODE_COOL): + if mode not in self.hvac_modes: + continue + self.set_hvac_mode(mode) + break + + def turn_off(self) -> None: + """Turn the device on, retaining current HVAC (if supported).""" + if DPCode.SWITCH in self.tuya_device.function: + self._send_command([{"code": DPCode.SWITCH, "value": False}]) + return + + # Fake turn off + if HVAC_MODE_OFF in self.hvac_modes: + self.set_hvac_mode(HVAC_MODE_OFF) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 359c1740c75..0c17db0fb50 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -76,8 +76,10 @@ class DPCode(str, Enum): LOCK = "lock" # Lock / Child lock MODE = "mode" # Working mode / Mode PUMP_RESET = "pump_reset" # Water pump reset + SHAKE = "shake" # Oscillating SPEED = "speed" # Speed level START = "start" # Start + SWING = "swing" # Swing mode SWITCH = "switch" # Switch SWITCH_HORIZONTAL = "switch_horizontal" # Horizontal swing flap switch SWITCH_LED = "switch_led" # Switch From 2734ae17f31a11b6ca3477e368f9f8c5b73a9dfc Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 13 Oct 2021 20:51:51 +0200 Subject: [PATCH 0309/1038] Modbus baseplatform.py and Validators.py 100% coverage (activate coverage) (#57546) --- .coveragerc | 2 -- tests/components/modbus/test_init.py | 42 +++++++++++++++++++++++ tests/components/modbus/test_switch.py | 47 +++++++++++++++++++++++++- 3 files changed, 88 insertions(+), 3 deletions(-) diff --git a/.coveragerc b/.coveragerc index a4288b278ca..5db28077014 100644 --- a/.coveragerc +++ b/.coveragerc @@ -651,10 +651,8 @@ omit = homeassistant/components/mitemp_bt/sensor.py homeassistant/components/mjpeg/camera.py homeassistant/components/mochad/* - homeassistant/components/modbus/base_platform.py homeassistant/components/modbus/climate.py homeassistant/components/modbus/modbus.py - homeassistant/components/modbus/validators.py homeassistant/components/modem_callerid/sensor.py homeassistant/components/motion_blinds/__init__.py homeassistant/components/motion_blinds/const.py diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index ba99df19b4d..b4657d7ee73 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -155,6 +155,13 @@ async def test_number_validator(): CONF_DATA_TYPE: DATA_TYPE_INT, CONF_SWAP: CONF_SWAP_BYTE, }, + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_COUNT: 2, + CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_STRUCTURE: ">i", + CONF_SWAP: CONF_SWAP_BYTE, + }, ], ) async def test_ok_struct_validator(do_config): @@ -197,6 +204,13 @@ async def test_ok_struct_validator(do_config): CONF_STRUCTURE: ">f", CONF_SWAP: CONF_SWAP_WORD, }, + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_COUNT: 1, + CONF_DATA_TYPE: DATA_TYPE_STRING, + CONF_STRUCTURE: ">f", + CONF_SWAP: CONF_SWAP_WORD, + }, ], ) async def test_exception_struct_validator(do_config): @@ -636,6 +650,34 @@ async def test_pymodbus_close_fail(hass, caplog, mock_pymodbus): # Close() is called as part of teardown +async def test_pymodbus_connect_fail(hass, caplog): + """Run test for failing pymodbus constructor.""" + config = { + DOMAIN: [ + { + CONF_NAME: TEST_MODBUS_NAME, + CONF_TYPE: TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, + } + ] + } + with mock.patch( + "homeassistant.components.modbus.modbus.ModbusTcpClient", autospec=True + ) as mock_pb: + caplog.set_level(logging.ERROR) + ExceptionMessage = "test connect exception" + mock_pb.connect.side_effect = ModbusException(ExceptionMessage) + + assert await async_setup_component(hass, DOMAIN, config) is True + + +# await hass.async_block_till_done() +# await hass.async_block_till_done() +# assert mock_pb.connect.called +# assert ExceptionMessage in caplog.text + + async def test_delay(hass, mock_pymodbus): """Run test for startup delay.""" diff --git a/tests/components/modbus/test_switch.py b/tests/components/modbus/test_switch.py index c14a7169ae0..7e3c2d5f6c2 100644 --- a/tests/components/modbus/test_switch.py +++ b/tests/components/modbus/test_switch.py @@ -41,7 +41,13 @@ from homeassistant.core import State from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from .conftest import TEST_ENTITY_NAME, TEST_MODBUS_HOST, TEST_PORT_TCP, ReadResult +from .conftest import ( + TEST_ENTITY_NAME, + TEST_MODBUS_HOST, + TEST_PORT_TCP, + ReadResult, + do_next_cycle, +) from tests.common import async_fire_time_changed @@ -210,6 +216,45 @@ async def test_all_switch(hass, mock_do_cycle, expected): assert hass.states.get(ENTITY_ID).state == expected +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_SWITCHES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_SCAN_INTERVAL: 10, + CONF_LAZY_ERROR: 2, + CONF_VERIFY: {}, + }, + ], + }, + ], +) +@pytest.mark.parametrize( + "register_words,do_exception,start_expect,end_expect", + [ + ( + [0x00], + True, + STATE_OFF, + STATE_UNAVAILABLE, + ), + ], +) +async def test_lazy_error_switch(hass, start_expect, end_expect, mock_do_cycle): + """Run test for given config.""" + now = mock_do_cycle + assert hass.states.get(ENTITY_ID).state == start_expect + now = await do_next_cycle(hass, now, 11) + assert hass.states.get(ENTITY_ID).state == start_expect + now = await do_next_cycle(hass, now, 11) + assert hass.states.get(ENTITY_ID).state == end_expect + + @pytest.mark.parametrize( "mock_test_state", [(State(ENTITY_ID, STATE_ON),)], From 14c380fb578066e13c1d5ae145ae9dc24c51e443 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 13 Oct 2021 21:15:34 +0200 Subject: [PATCH 0310/1038] Use EntityDescription in Tuya Switch platform (#57581) --- homeassistant/components/tuya/const.py | 17 ++ homeassistant/components/tuya/switch.py | 343 ++++++++++++++++++------ 2 files changed, 285 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 0c17db0fb50..73c7498cc40 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -64,6 +64,7 @@ class DPCode(str, Enum): ANION = "anion" # Ionizer unit BRIGHT_VALUE = "bright_value" # Brightness C_F = "c_f" # Temperature unit switching + CHILD_LOCK = "child_lock" # Child lock COLOUR_DATA = "colour_data" # Colored light mode COLOUR_DATA_V2 = "colour_data_v2" # Colored light mode FAN_DIRECTION = "fan_direction" # Fan direction @@ -81,9 +82,24 @@ class DPCode(str, Enum): START = "start" # Start SWING = "swing" # Swing mode SWITCH = "switch" # Switch + SWITCH_1 = "switch_1" # Switch 1 + SWITCH_2 = "switch_2" # Switch 2 + SWITCH_3 = "switch_3" # Switch 3 + SWITCH_4 = "switch_4" # Switch 4 + SWITCH_5 = "switch_5" # Switch 5 + SWITCH_6 = "switch_6" # Switch 6 + SWITCH_BACKLIGHT = "switch_backlight" # Backlight switch SWITCH_HORIZONTAL = "switch_horizontal" # Horizontal swing flap switch SWITCH_LED = "switch_led" # Switch + SWITCH_SPRAY = "switch_spray" # Spraying switch + SWITCH_USB1 = "switch_usb1" # USB 1 + SWITCH_USB2 = "switch_usb2" # USB 2 + SWITCH_USB3 = "switch_usb3" # USB 3 + SWITCH_USB4 = "switch_usb4" # USB 4 + SWITCH_USB5 = "switch_usb5" # USB 5 + SWITCH_USB6 = "switch_usb6" # USB 6 SWITCH_VERTICAL = "switch_vertical" # Vertical swing flap switch + SWITCH_VOICE = "switch_voice" # Voice switch TEMP_CURRENT = "temp_current" # Current temperature in °C TEMP_CURRENT_F = "temp_current_f" # Current temperature in °F TEMP_SET = "temp_set" # Set the temperature in °C @@ -91,6 +107,7 @@ class DPCode(str, Enum): TEMP_UNIT_CONVERT = "temp_unit_convert" # Temperature unit switching TEMP_VALUE = "temp_value" # Color temperature UV = "uv" # UV sterilization + WARM = "warm" # Heat preservation WATER_RESET = "water_reset" # Resetting of water usage days WET = "wet" # Humidification WORK_MODE = "work_mode" # Working mode diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 663603d2727..66a2e1551ef 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -5,7 +5,11 @@ from typing import Any from tuya_iot import TuyaDevice, TuyaDeviceManager -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import ( + DEVICE_CLASS_OUTLET, + SwitchEntity, + SwitchEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -15,18 +19,248 @@ from . import HomeAssistantTuyaData from .base import TuyaHaEntity from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode +# All descriptions can be found here. Mostly the Boolean data types in the +# default instruction set of each category end up being a Switch. # https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq -TUYA_SUPPORT_TYPE = { - "kg", # Switch - "cz", # Socket - "pc", # Power Strip - "bh", # Smart Kettle - "dlq", # Breaker - "cwysj", # Pet Water Feeder - "kj", # Air Purifier - "xxj", # Diffuser +SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { + # Smart Kettle + # https://developer.tuya.com/en/docs/iot/fbh?id=K9gf484m21yq7 + "bh": ( + SwitchEntityDescription( + key=DPCode.START, + name="Start", + icon="mdi:kettle-steam", + ), + SwitchEntityDescription( + key=DPCode.WARM, + name="Heat preservation", + ), + ), + # Pet Water Feeder + # https://developer.tuya.com/en/docs/iot/f?id=K9gf46aewxem5 + "cwysj": ( + SwitchEntityDescription( + key=DPCode.FILTER_RESET, + name="Filter reset", + icon="mdi:filter", + ), + SwitchEntityDescription( + key=DPCode.PUMP_RESET, + name="Water pump reset", + icon="mdi:pump", + ), + SwitchEntityDescription( + key=DPCode.SWITCH, + name="Power", + ), + SwitchEntityDescription( + key=DPCode.WATER_RESET, + name="Reset of water usage days", + icon="mdi:water-sync", + entity_registry_enabled_default=False, + ), + ), + # Cirquit Breaker + "dlq": ( + SwitchEntityDescription( + key=DPCode.CHILD_LOCK, + name="Child Lock", + icon="mdi:account-lock", + ), + SwitchEntityDescription( + key=DPCode.SWITCH_1, + name="Switch", + ), + ), + # Switch + # https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s + "kg": ( + SwitchEntityDescription( + key=DPCode.CHILD_LOCK, + name="Child Lock", + icon="mdi:account-lock", + ), + SwitchEntityDescription( + key=DPCode.SWITCH_1, + name="Switch 1", + device_class=DEVICE_CLASS_OUTLET, + ), + SwitchEntityDescription( + key=DPCode.SWITCH_2, + name="Switch 2", + device_class=DEVICE_CLASS_OUTLET, + ), + SwitchEntityDescription( + key=DPCode.SWITCH_3, + name="Switch 3", + device_class=DEVICE_CLASS_OUTLET, + ), + SwitchEntityDescription( + key=DPCode.SWITCH_4, + name="Switch 4", + device_class=DEVICE_CLASS_OUTLET, + ), + SwitchEntityDescription( + key=DPCode.SWITCH_5, + name="Switch 5", + device_class=DEVICE_CLASS_OUTLET, + ), + SwitchEntityDescription( + key=DPCode.SWITCH_6, + name="Switch 6", + device_class=DEVICE_CLASS_OUTLET, + ), + SwitchEntityDescription( + key=DPCode.SWITCH_USB1, + name="USB 1", + ), + SwitchEntityDescription( + key=DPCode.SWITCH_USB2, + name="USB 2", + ), + SwitchEntityDescription( + key=DPCode.SWITCH_USB3, + name="USB 3", + ), + SwitchEntityDescription( + key=DPCode.SWITCH_USB4, + name="USB 4", + ), + SwitchEntityDescription( + key=DPCode.SWITCH_USB5, + name="USB 5", + ), + SwitchEntityDescription( + key=DPCode.SWITCH_USB6, + name="USB 6", + ), + SwitchEntityDescription( + key=DPCode.SWITCH, + name="Switch", + device_class=DEVICE_CLASS_OUTLET, + ), + ), + # Air Purifier + # https://developer.tuya.com/en/docs/iot/f?id=K9gf46h2s6dzm + "kj": ( + SwitchEntityDescription( + key=DPCode.ANION, + name="Ionizer", + icon="mdi:minus-circle-outline", + ), + SwitchEntityDescription( + key=DPCode.FILTER_RESET, + name="Filter cartridge reset", + icon="mdi:filter", + entity_registry_enabled_default=False, + ), + SwitchEntityDescription( + key=DPCode.LOCK, + name="Child lock", + icon="mdi:account-lock", + ), + SwitchEntityDescription( + key=DPCode.SWITCH, + name="Power", + ), + SwitchEntityDescription( + key=DPCode.WET, + name="Humidification", + icon="mdi:water-percent", + ), + ), + # Power Socket + # https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s + "pc": ( + SwitchEntityDescription( + key=DPCode.CHILD_LOCK, + name="Child Lock", + icon="mdi:account-lock", + ), + SwitchEntityDescription( + key=DPCode.SWITCH_1, + name="Socket 1", + device_class=DEVICE_CLASS_OUTLET, + ), + SwitchEntityDescription( + key=DPCode.SWITCH_2, + name="Socket 2", + device_class=DEVICE_CLASS_OUTLET, + ), + SwitchEntityDescription( + key=DPCode.SWITCH_3, + name="Socket 3", + device_class=DEVICE_CLASS_OUTLET, + ), + SwitchEntityDescription( + key=DPCode.SWITCH_4, + name="Socket 4", + device_class=DEVICE_CLASS_OUTLET, + ), + SwitchEntityDescription( + key=DPCode.SWITCH_5, + name="Socket 5", + device_class=DEVICE_CLASS_OUTLET, + ), + SwitchEntityDescription( + key=DPCode.SWITCH_6, + name="Socket 6", + device_class=DEVICE_CLASS_OUTLET, + ), + SwitchEntityDescription( + key=DPCode.SWITCH_USB1, + name="USB 1", + ), + SwitchEntityDescription( + key=DPCode.SWITCH_USB2, + name="USB 2", + ), + SwitchEntityDescription( + key=DPCode.SWITCH_USB3, + name="USB 3", + ), + SwitchEntityDescription( + key=DPCode.SWITCH_USB4, + name="USB 4", + ), + SwitchEntityDescription( + key=DPCode.SWITCH_USB5, + name="USB 5", + ), + SwitchEntityDescription( + key=DPCode.SWITCH_USB6, + name="USB 6", + ), + SwitchEntityDescription( + key=DPCode.SWITCH, + name="Socket", + device_class=DEVICE_CLASS_OUTLET, + ), + ), + # Diffuser + "xxj": ( + SwitchEntityDescription( + key=DPCode.SWITCH, + name="Power", + ), + SwitchEntityDescription( + key=DPCode.SWITCH_SPRAY, + name="Spray", + icon="mdi:spray", + ), + SwitchEntityDescription( + key=DPCode.SWITCH_VOICE, + name="Voice", + icon="mdi:account-voice", + entity_registry_enabled_default=False, + ), + ), } +# Socket (duplicate of `pc`) +# https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s +SWITCHES["cz"] = SWITCHES["pc"] + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -37,9 +271,20 @@ async def async_setup_entry( @callback def async_discover_device(device_ids: list[str]) -> None: """Discover and add a discovered tuya sensor.""" - async_add_entities( - _setup_entities(hass, entry, hass_data.device_manager, device_ids) - ) + entities: list[TuyaHaSwitch] = [] + for device_id in device_ids: + device = hass_data.device_manager.device_map[device_id] + if descriptions := SWITCHES.get(device.category): + for description in descriptions: + if ( + description.key in device.function + or description.key in device.status + ): + entities.append( + TuyaHaSwitch(device, hass_data.device_manager, description) + ) + + async_add_entities(entities) async_discover_device([*hass_data.device_manager.device_map]) @@ -48,86 +293,34 @@ async def async_setup_entry( ) -def _setup_entities( - hass: HomeAssistant, - entry: ConfigEntry, - device_manager: TuyaDeviceManager, - device_ids: list[str], -) -> list[TuyaHaSwitch]: - """Set up Tuya Switch device.""" - entities: list[TuyaHaSwitch] = [] - for device_id in device_ids: - device = device_manager.device_map[device_id] - if device is None or device.category not in TUYA_SUPPORT_TYPE: - continue - - for function in device.function: - if device.category == "kj": - if function in [ - DPCode.ANION, - DPCode.FILTER_RESET, - DPCode.LIGHT, - DPCode.LOCK, - DPCode.UV, - DPCode.WET, - ]: - entities.append(TuyaHaSwitch(device, device_manager, function)) - - elif device.category == "cwysj": - if ( - function - in [ - DPCode.FILTER_RESET, - DPCode.UV, - DPCode.PUMP_RESET, - DPCode.WATER_RESET, - ] - or function.startswith(DPCode.SWITCH) - ): - entities.append(TuyaHaSwitch(device, device_manager, function)) - - elif function.startswith(DPCode.START) or function.startswith( - DPCode.SWITCH - ): - entities.append(TuyaHaSwitch(device, device_manager, function)) - - return entities - - class TuyaHaSwitch(TuyaHaEntity, SwitchEntity): """Tuya Switch Device.""" - dp_code_switch = DPCode.SWITCH - dp_code_start = DPCode.START - def __init__( - self, device: TuyaDevice, device_manager: TuyaDeviceManager, dp_code: str = "" + self, + device: TuyaDevice, + device_manager: TuyaDeviceManager, + description: SwitchEntityDescription, ) -> None: """Init TuyaHaSwitch.""" super().__init__(device, device_manager) - - self.dp_code = dp_code - self.channel = ( - dp_code.replace(DPCode.SWITCH, "") - if dp_code.startswith(DPCode.SWITCH) - else dp_code - ) - self._attr_unique_id = f"{super().unique_id}{self.channel}" + self.entity_description = description + self._attr_unique_id = f"{super().unique_id}{description.key}" @property def name(self) -> str | None: """Return Tuya device name.""" - return f"{self.tuya_device.name}{self.channel}" + return f"{self.tuya_device.name} {self.entity_description.name}" @property def is_on(self) -> bool: """Return true if switch is on.""" - return self.tuya_device.status.get(self.dp_code, False) + return self.tuya_device.status.get(self.entity_description.key, False) def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" - self._send_command([{"code": self.dp_code, "value": True}]) + self._send_command([{"code": self.entity_description.key, "value": True}]) def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" - self._send_command([{"code": self.dp_code, "value": False}]) + self._send_command([{"code": self.entity_description.key, "value": False}]) From 835e07f63ef8f761bffbb6b3782669dbd55b57f9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 13 Oct 2021 13:03:52 -0700 Subject: [PATCH 0311/1038] Remove debug log (#57619) --- homeassistant/helpers/script.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 289b649e90c..b6651d1dd47 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1156,7 +1156,6 @@ class Script: elif action == cv.SCRIPT_ACTION_CHOOSE: for choice in step[CONF_CHOOSE]: for cond in choice[CONF_CONDITIONS]: - _LOGGER.error("Extracting entities from: %s", cond) referenced |= condition.async_extract_entities(cond) Script._find_referenced_entities(referenced, choice[CONF_SEQUENCE]) if CONF_DEFAULT in step: From b2cef78d90ea06f3327181ec27cf178b794f7641 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 13 Oct 2021 22:12:07 +0200 Subject: [PATCH 0312/1038] Add binary sensor platform to Tuya (#57623) --- .coveragerc | 1 + .../components/tuya/binary_sensor.py | 98 +++++++++++++++++++ homeassistant/components/tuya/const.py | 5 +- 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/tuya/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index 5db28077014..0f453518adc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1113,6 +1113,7 @@ omit = homeassistant/components/travisci/sensor.py homeassistant/components/tuya/__init__.py homeassistant/components/tuya/base.py + homeassistant/components/tuya/binary_sensor.py homeassistant/components/tuya/climate.py homeassistant/components/tuya/const.py homeassistant/components/tuya/fan.py diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py new file mode 100644 index 00000000000..90b3d36c564 --- /dev/null +++ b/homeassistant/components/tuya/binary_sensor.py @@ -0,0 +1,98 @@ +"""Support for Tuya binary sensors.""" +from __future__ import annotations + +from tuya_iot import TuyaDevice, TuyaDeviceManager + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_DOOR, + BinarySensorEntity, + BinarySensorEntityDescription, +) +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 . import HomeAssistantTuyaData +from .base import TuyaHaEntity +from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode + +# All descriptions can be found here. Mostly the Boolean data types in the +# default status set of each category (that don't have a set instruction) +# end up being a binary sensor. +# https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq +BINARY_SENSORS: dict[str, tuple[BinarySensorEntityDescription, ...]] = { + # Door Window Sensor + # https://developer.tuya.com/en/docs/iot/s?id=K9gf48hm02l8m + "mcs": ( + BinarySensorEntityDescription( + key=DPCode.DOORCONTACT_STATE, + device_class=DEVICE_CLASS_DOOR, + ), + BinarySensorEntityDescription( + key=DPCode.TEMPER_ALARM, + name="Tamper", + entity_registry_enabled_default=False, + ), + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Tuya binary sensor dynamically through Tuya discovery.""" + hass_data: HomeAssistantTuyaData = hass.data[DOMAIN][entry.entry_id] + + @callback + def async_discover_device(device_ids: list[str]) -> None: + """Discover and add a discovered Tuya binary sensor.""" + entities: list[TuyaBinarySensorEntity] = [] + for device_id in device_ids: + device = hass_data.device_manager.device_map[device_id] + if descriptions := BINARY_SENSORS.get(device.category): + for description in descriptions: + if ( + description.key in device.function + or description.key in device.status + ): + entities.append( + TuyaBinarySensorEntity( + device, hass_data.device_manager, description + ) + ) + + async_add_entities(entities) + + async_discover_device([*hass_data.device_manager.device_map]) + + entry.async_on_unload( + async_dispatcher_connect(hass, TUYA_DISCOVERY_NEW, async_discover_device) + ) + + +class TuyaBinarySensorEntity(TuyaHaEntity, BinarySensorEntity): + """Tuya Binary Sensor Entity.""" + + def __init__( + self, + device: TuyaDevice, + device_manager: TuyaDeviceManager, + description: BinarySensorEntityDescription, + ) -> None: + """Init Tuya binary sensor.""" + super().__init__(device, device_manager) + self.entity_description = description + self._attr_unique_id = f"{super().unique_id}{description.key}" + + @property + def name(self) -> str | None: + """Return Tuya device name.""" + if self.entity_description.name is not None: + return f"{self.tuya_device.name} {self.entity_description.name}" + return self.tuya_device.name + + @property + def is_on(self) -> bool: + """Return true if sensor is on.""" + return self.tuya_device.status.get(self.entity_description.key, False) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 73c7498cc40..0efdb10b6a2 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -41,6 +41,7 @@ TUYA_SUPPORTED_PRODUCT_CATEGORIES = ( "kj", # Air Purifier "kj", # Air Purifier "kt", # Air conditioner + "mcs", # Door Window Sensor "pc", # Power Strip "qn", # Heater "wk", # Thermostat @@ -52,7 +53,7 @@ TUYA_SUPPORTED_PRODUCT_CATEGORIES = ( TUYA_SMART_APP = "tuyaSmart" SMARTLIFE_APP = "smartlife" -PLATFORMS = ["climate", "fan", "light", "scene", "switch"] +PLATFORMS = ["binary_sensor", "climate", "fan", "light", "scene", "switch"] class DPCode(str, Enum): @@ -67,6 +68,7 @@ class DPCode(str, Enum): CHILD_LOCK = "child_lock" # Child lock COLOUR_DATA = "colour_data" # Colored light mode COLOUR_DATA_V2 = "colour_data_v2" # Colored light mode + DOORCONTACT_STATE = "doorcontact_state" # Status of door window sensor FAN_DIRECTION = "fan_direction" # Fan direction FAN_SPEED_ENUM = "fan_speed_enum" # Speed mode FAN_SPEED_PERCENT = "fan_speed_percent" # Stepless speed @@ -106,6 +108,7 @@ class DPCode(str, Enum): TEMP_SET_F = "temp_set_f" # Set the temperature in °F TEMP_UNIT_CONVERT = "temp_unit_convert" # Temperature unit switching TEMP_VALUE = "temp_value" # Color temperature + TEMPER_ALARM = "temper_alarm" # Tamper alarm UV = "uv" # UV sterilization WARM = "warm" # Heat preservation WATER_RESET = "water_reset" # Resetting of water usage days From 838f79be3d98731806dfa6393be5e8dbd0148228 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 13 Oct 2021 22:27:56 +0200 Subject: [PATCH 0313/1038] Update tests searching for areas referenced in automations (#57558) --- tests/helpers/test_script.py | 77 ++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index f8d6c0c6e6b..fd98145cab2 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -2245,6 +2245,82 @@ async def test_propagate_error_service_exception(hass): assert_action_trace(expected_trace, expected_script_execution="error") +async def test_referenced_areas(hass): + """Test referenced areas.""" + script_obj = script.Script( + hass, + cv.SCRIPT_SCHEMA( + [ + { + "service": "test.script", + "data": {"area_id": "area_service_not_list"}, + }, + { + "service": "test.script", + "data": {"area_id": ["area_service_list"]}, + }, + { + "service": "test.script", + "data": {"area_id": "{{ 'area_service_template' }}"}, + }, + { + "service": "test.script", + "target": {"area_id": "area_in_target"}, + }, + { + "service": "test.script", + "data_template": {"area_id": "area_in_data_template"}, + }, + {"service": "test.script", "data": {"without": "area_id"}}, + { + "choose": [ + { + "conditions": "{{ true == false }}", + "sequence": [ + { + "service": "test.script", + "data": {"area_id": "area_choice_1_seq"}, + } + ], + }, + { + "conditions": "{{ true == false }}", + "sequence": [ + { + "service": "test.script", + "data": {"area_id": "area_choice_2_seq"}, + } + ], + }, + ], + "default": [ + { + "service": "test.script", + "data": {"area_id": "area_default_seq"}, + } + ], + }, + {"event": "test_event"}, + {"delay": "{{ delay_period }}"}, + ] + ), + "Test Name", + "test_domain", + ) + assert script_obj.referenced_areas == { + "area_choice_1_seq", + "area_choice_2_seq", + "area_default_seq", + "area_in_data_template", + "area_in_target", + "area_service_list", + "area_service_not_list", + # 'area_service_template', # no area extraction from template + } + # Test we cache results. + assert script_obj.referenced_areas is script_obj.referenced_areas + + async def test_referenced_entities(hass): """Test referenced entities.""" script_obj = script.Script( @@ -2332,6 +2408,7 @@ async def test_referenced_entities(hass): "light.entity_in_target", "light.service_list", "light.service_not_list", + # "light.service_template", # no entity extraction from template "scene.hello", "sensor.condition", } From 158dd1556c19a1857386fa0cc589d127825d2588 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 13 Oct 2021 22:36:49 +0200 Subject: [PATCH 0314/1038] Remove myself as code owner from Toon (#57625) --- CODEOWNERS | 1 - homeassistant/components/toon/manifest.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index e3a4cb00c83..ef702109461 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -534,7 +534,6 @@ homeassistant/components/tile/* @bachya homeassistant/components/time_date/* @fabaff homeassistant/components/tmb/* @alemuro homeassistant/components/todoist/* @boralyl -homeassistant/components/toon/* @frenck homeassistant/components/totalconnect/* @austinmroczek homeassistant/components/tplink/* @rytilahti @thegardenmonkey homeassistant/components/traccar/* @ludeeus diff --git a/homeassistant/components/toon/manifest.json b/homeassistant/components/toon/manifest.json index 6a5d52d393b..dc32b6bfac5 100644 --- a/homeassistant/components/toon/manifest.json +++ b/homeassistant/components/toon/manifest.json @@ -6,7 +6,7 @@ "requirements": ["toonapi==0.2.1"], "dependencies": ["http"], "after_dependencies": ["cloud"], - "codeowners": ["@frenck"], + "codeowners": [], "dhcp": [ { "hostname": "eneco-*", From b854a2537fff366820f3163cbfcfd9a9a2f6c937 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 13 Oct 2021 22:58:10 +0200 Subject: [PATCH 0315/1038] Use classmethod in Tuya TypeData classes (#57627) --- homeassistant/components/tuya/base.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index 842b4728a19..add4d210c4b 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -26,10 +26,10 @@ class IntegerTypeData: scale: float step: float - @staticmethod - def from_json(data: str) -> IntegerTypeData: + @classmethod + def from_json(cls, data: str) -> IntegerTypeData: """Load JSON string and return a IntegerTypeData object.""" - return IntegerTypeData(**json.loads(data)) + return cls(**json.loads(data)) @dataclass @@ -38,10 +38,10 @@ class EnumTypeData: range: list[str] - @staticmethod - def from_json(data: str) -> EnumTypeData: + @classmethod + def from_json(cls, data: str) -> EnumTypeData: """Load JSON string and return a EnumTypeData object.""" - return EnumTypeData(**json.loads(data)) + return cls(**json.loads(data)) class TuyaHaEntity(Entity): From b220ab6e9110a64401565a0dc926f217c4292371 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 13 Oct 2021 23:30:25 +0200 Subject: [PATCH 0316/1038] Prettify Tuya entity class names (#57629) --- homeassistant/components/tuya/base.py | 2 +- homeassistant/components/tuya/binary_sensor.py | 4 ++-- homeassistant/components/tuya/climate.py | 4 ++-- homeassistant/components/tuya/fan.py | 8 ++++---- homeassistant/components/tuya/light.py | 8 ++++---- homeassistant/components/tuya/scene.py | 6 ++++-- homeassistant/components/tuya/switch.py | 10 ++++++---- 7 files changed, 23 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index add4d210c4b..2be80f53776 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -44,7 +44,7 @@ class EnumTypeData: return cls(**json.loads(data)) -class TuyaHaEntity(Entity): +class TuyaEntity(Entity): """Tuya base device.""" _attr_should_poll = False diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index 90b3d36c564..4596b8638a4 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -14,7 +14,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantTuyaData -from .base import TuyaHaEntity +from .base import TuyaEntity from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode # All descriptions can be found here. Mostly the Boolean data types in the @@ -71,7 +71,7 @@ async def async_setup_entry( ) -class TuyaBinarySensorEntity(TuyaHaEntity, BinarySensorEntity): +class TuyaBinarySensorEntity(TuyaEntity, BinarySensorEntity): """Tuya Binary Sensor Entity.""" def __init__( diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index 64c0e00ccb6..f667eed4e93 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -31,7 +31,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantTuyaData -from .base import EnumTypeData, IntegerTypeData, TuyaHaEntity +from .base import EnumTypeData, IntegerTypeData, TuyaEntity from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode TUYA_HVAC_TO_HA = { @@ -110,7 +110,7 @@ async def async_setup_entry( ) -class TuyaClimateEntity(TuyaHaEntity, ClimateEntity): +class TuyaClimateEntity(TuyaEntity, ClimateEntity): """Tuya Climate Device.""" _current_humidity_dpcode: DPCode | None = None diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index eedc9020374..35b893f9103 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -25,7 +25,7 @@ from homeassistant.util.percentage import ( ) from . import HomeAssistantTuyaData -from .base import TuyaHaEntity +from .base import TuyaEntity from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode TUYA_SUPPORT_TYPE = { @@ -43,11 +43,11 @@ async def async_setup_entry( @callback def async_discover_device(device_ids: list[str]) -> None: """Discover and add a discovered tuya fan.""" - entities: list[TuyaHaFan] = [] + entities: list[TuyaFanEntity] = [] for device_id in device_ids: device = hass_data.device_manager.device_map[device_id] if device and device.category in TUYA_SUPPORT_TYPE: - entities.append(TuyaHaFan(device, hass_data.device_manager)) + entities.append(TuyaFanEntity(device, hass_data.device_manager)) async_add_entities(entities) async_discover_device([*hass_data.device_manager.device_map]) @@ -57,7 +57,7 @@ async def async_setup_entry( ) -class TuyaHaFan(TuyaHaEntity, FanEntity): +class TuyaFanEntity(TuyaEntity, FanEntity): """Tuya Fan Device.""" def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> None: diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 40f1628f2a7..0dc59faac8e 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -23,7 +23,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantTuyaData -from .base import TuyaHaEntity +from .base import TuyaEntity from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode _LOGGER = logging.getLogger(__name__) @@ -73,11 +73,11 @@ async def async_setup_entry( @callback def async_discover_device(device_ids: list[str]): """Discover and add a discovered tuya light.""" - entities: list[TuyaHaLight] = [] + entities: list[TuyaLightEntity] = [] for device_id in device_ids: device = hass_data.device_manager.device_map[device_id] if device and device.category in TUYA_SUPPORT_TYPE: - entities.append(TuyaHaLight(device, hass_data.device_manager)) + entities.append(TuyaLightEntity(device, hass_data.device_manager)) async_add_entities(entities) async_discover_device([*hass_data.device_manager.device_map]) @@ -87,7 +87,7 @@ async def async_setup_entry( ) -class TuyaHaLight(TuyaHaEntity, LightEntity): +class TuyaLightEntity(TuyaEntity, LightEntity): """Tuya light device.""" def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> None: diff --git a/homeassistant/components/tuya/scene.py b/homeassistant/components/tuya/scene.py index f008d5a5b7b..dadc64f9846 100644 --- a/homeassistant/components/tuya/scene.py +++ b/homeassistant/components/tuya/scene.py @@ -21,10 +21,12 @@ async def async_setup_entry( """Set up Tuya scenes.""" hass_data: HomeAssistantTuyaData = hass.data[DOMAIN][entry.entry_id] scenes = await hass.async_add_executor_job(hass_data.home_manager.query_scenes) - async_add_entities(TuyaHAScene(hass_data.home_manager, scene) for scene in scenes) + async_add_entities( + TuyaSceneEntity(hass_data.home_manager, scene) for scene in scenes + ) -class TuyaHAScene(Scene): +class TuyaSceneEntity(Scene): """Tuya Scene Remote.""" _should_poll = False diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 66a2e1551ef..c66674de715 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -16,7 +16,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantTuyaData -from .base import TuyaHaEntity +from .base import TuyaEntity from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode # All descriptions can be found here. Mostly the Boolean data types in the @@ -271,7 +271,7 @@ async def async_setup_entry( @callback def async_discover_device(device_ids: list[str]) -> None: """Discover and add a discovered tuya sensor.""" - entities: list[TuyaHaSwitch] = [] + entities: list[TuyaSwitchEntity] = [] for device_id in device_ids: device = hass_data.device_manager.device_map[device_id] if descriptions := SWITCHES.get(device.category): @@ -281,7 +281,9 @@ async def async_setup_entry( or description.key in device.status ): entities.append( - TuyaHaSwitch(device, hass_data.device_manager, description) + TuyaSwitchEntity( + device, hass_data.device_manager, description + ) ) async_add_entities(entities) @@ -293,7 +295,7 @@ async def async_setup_entry( ) -class TuyaHaSwitch(TuyaHaEntity, SwitchEntity): +class TuyaSwitchEntity(TuyaEntity, SwitchEntity): """Tuya Switch Device.""" def __init__( From b54fc0229d9f0d2b8be58032182e387516703ed6 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Wed, 13 Oct 2021 23:33:03 +0200 Subject: [PATCH 0317/1038] Use entity_registry_enabled_default for Nut sensors (#56854) --- homeassistant/components/nut/__init__.py | 12 ++-- homeassistant/components/nut/config_flow.py | 32 +++-------- homeassistant/components/nut/sensor.py | 56 ++++++++----------- homeassistant/components/nut/strings.json | 6 -- .../components/nut/translations/en.json | 8 +-- tests/components/nut/test_config_flow.py | 21 ++----- tests/components/nut/test_sensor.py | 13 +++++ tests/components/nut/util.py | 3 +- 8 files changed, 56 insertions(+), 95 deletions(-) diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index 33cbc9fb47c..ea57c5994c6 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -39,6 +39,11 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Network UPS Tools (NUT) from a config entry.""" + # strip out the stale options CONF_RESOURCES + if CONF_RESOURCES in entry.options: + new_options = {k: v for k, v in entry.options.items() if k != CONF_RESOURCES} + hass.config_entries.async_update_entry(entry, options=new_options) + config = entry.data host = config[CONF_HOST] port = config[CONF_PORT] @@ -156,13 +161,6 @@ def _unique_id_from_status(status): return "_".join(unique_id_group) -def find_resources_in_config_entry(config_entry): - """Find the configured resources in the config entry.""" - if CONF_RESOURCES in config_entry.options: - return config_entry.options[CONF_RESOURCES] - return config_entry.data[CONF_RESOURCES] - - 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) diff --git a/homeassistant/components/nut/config_flow.py b/homeassistant/components/nut/config_flow.py index 70c097bd6f1..9b45e270448 100644 --- a/homeassistant/components/nut/config_flow.py +++ b/homeassistant/components/nut/config_flow.py @@ -17,7 +17,7 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from . import PyNUTData, find_resources_in_config_entry +from . import PyNUTData from .const import ( DEFAULT_HOST, DEFAULT_PORT, @@ -229,35 +229,17 @@ class OptionsFlowHandler(config_entries.OptionsFlow): if user_input is not None: return self.async_create_entry(title="", data=user_input) - resources = find_resources_in_config_entry(self.config_entry) scan_interval = self.config_entry.options.get( CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL ) - errors = {} - try: - info = await validate_input(self.hass, self.config_entry.data) - except CannotConnect: - errors[CONF_BASE] = "cannot_connect" - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors[CONF_BASE] = "unknown" + base_schema = { + vol.Optional(CONF_SCAN_INTERVAL, default=scan_interval): vol.All( + vol.Coerce(int), vol.Clamp(min=10, max=300) + ) + } - if errors: - return self.async_show_form(step_id="abort", errors=errors) - - base_schema = _resource_schema_base(info["available_resources"], resources) - base_schema[ - vol.Optional(CONF_SCAN_INTERVAL, default=scan_interval) - ] = cv.positive_int - - return self.async_show_form( - step_id="init", data_schema=vol.Schema(base_schema), errors=errors - ) - - async def async_step_abort(self, user_input=None): - """Abort options flow.""" - return self.async_create_entry(title="", data=self.config_entry.options) + return self.async_show_form(step_id="init", data_schema=vol.Schema(base_schema)) class CannotConnect(exceptions.HomeAssistantError): diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index 5c965274eae..703e9ddd4ec 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -42,39 +42,29 @@ async def async_setup_entry(hass, config_entry, async_add_entities): data = pynut_data[PYNUT_DATA] status = data.status - entities = [] + enabled_resources = [ + resource.lower() for resource in config_entry.data[CONF_RESOURCES] + ] + resources = [sensor_id for sensor_id in SENSOR_TYPES if sensor_id in status] + # Display status is a special case that falls back to the status value + # of the UPS instead. + if KEY_STATUS in resources: + resources.append(KEY_STATUS_DISPLAY) - if CONF_RESOURCES in config_entry.options: - resources = config_entry.options[CONF_RESOURCES] - else: - resources = config_entry.data[CONF_RESOURCES] - - for resource in resources: - sensor_type = resource.lower() - - # Display status is a special case that falls back to the status value - # of the UPS instead. - if sensor_type in status or ( - sensor_type == KEY_STATUS_DISPLAY and KEY_STATUS in status - ): - entities.append( - NUTSensor( - coordinator, - data, - name.title(), - SENSOR_TYPES[sensor_type], - unique_id, - manufacturer, - model, - firmware, - ) - ) - else: - _LOGGER.info( - "Sensor type: %s does not appear in the NUT status " - "output, cannot add", - sensor_type, - ) + entities = [ + NUTSensor( + coordinator, + data, + name.title(), + SENSOR_TYPES[sensor_type], + unique_id, + manufacturer, + model, + firmware, + sensor_type in enabled_resources, + ) + for sensor_type in resources + ] async_add_entities(entities, True) @@ -92,6 +82,7 @@ class NUTSensor(CoordinatorEntity, SensorEntity): manufacturer: str | None, model: str | None, firmware: str | None, + enabled_default: bool, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) @@ -102,6 +93,7 @@ class NUTSensor(CoordinatorEntity, SensorEntity): self._device_name = name self._data = data self._unique_id = unique_id + self._attr_entity_registry_enabled_default = enabled_default self._attr_name = f"{name} {sensor_description.name}" if unique_id is not None: diff --git a/homeassistant/components/nut/strings.json b/homeassistant/components/nut/strings.json index 97e637fdcb3..179f974b870 100644 --- a/homeassistant/components/nut/strings.json +++ b/homeassistant/components/nut/strings.json @@ -35,16 +35,10 @@ "options": { "step": { "init": { - "description": "Choose Sensor Resources.", "data": { - "resources": "Resources", "scan_interval": "Scan Interval (seconds)" } } - }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "unknown": "[%key:common::config_flow::error::unknown%]" } } } diff --git a/homeassistant/components/nut/translations/en.json b/homeassistant/components/nut/translations/en.json index 3d57189f7a5..5975d734df4 100644 --- a/homeassistant/components/nut/translations/en.json +++ b/homeassistant/components/nut/translations/en.json @@ -33,17 +33,11 @@ } }, "options": { - "error": { - "cannot_connect": "Failed to connect", - "unknown": "Unexpected error" - }, "step": { "init": { "data": { - "resources": "Resources", "scan_interval": "Scan Interval (seconds)" - }, - "description": "Choose Sensor Resources." + } } } } diff --git a/tests/components/nut/test_config_flow.py b/tests/components/nut/test_config_flow.py index 135d0ef9efc..0799248384b 100644 --- a/tests/components/nut/test_config_flow.py +++ b/tests/components/nut/test_config_flow.py @@ -294,37 +294,25 @@ async def test_options_flow(hass): domain=DOMAIN, unique_id="abcde12345", data=VALID_CONFIG, - options={CONF_RESOURCES: ["battery.charge"]}, ) config_entry.add_to_hass(hass) - mock_pynut = _get_mock_pynutclient( - list_vars={"battery.voltage": "voltage"}, list_ups=["ups1"] - ) - - with patch( - "homeassistant.components.nut.PyNUTClient", - return_value=mock_pynut, - ), patch("homeassistant.components.nut.async_setup_entry", return_value=True): + with patch("homeassistant.components.nut.async_setup_entry", return_value=True): result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={CONF_RESOURCES: ["battery.voltage"]} + result["flow_id"], user_input={} ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - CONF_RESOURCES: ["battery.voltage"], CONF_SCAN_INTERVAL: 60, } - with patch( - "homeassistant.components.nut.PyNUTClient", - return_value=mock_pynut, - ), patch("homeassistant.components.nut.async_setup_entry", return_value=True): + with patch("homeassistant.components.nut.async_setup_entry", return_value=True): result2 = await hass.config_entries.options.async_init(config_entry.entry_id) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -332,11 +320,10 @@ async def test_options_flow(hass): result2 = await hass.config_entries.options.async_configure( result2["flow_id"], - user_input={CONF_RESOURCES: ["battery.voltage"], CONF_SCAN_INTERVAL: 12}, + user_input={CONF_SCAN_INTERVAL: 12}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - CONF_RESOURCES: ["battery.voltage"], CONF_SCAN_INTERVAL: 12, } diff --git a/tests/components/nut/test_sensor.py b/tests/components/nut/test_sensor.py index a8c0945c6c0..a5715ff9c8e 100644 --- a/tests/components/nut/test_sensor.py +++ b/tests/components/nut/test_sensor.py @@ -202,3 +202,16 @@ async def test_blazer_usb(hass): assert all( state.attributes[key] == expected_attributes[key] for key in expected_attributes ) + + +async def test_stale_options(hass): + """Test creation of sensors with stale options to remove.""" + + config_entry = await async_init_integration( + hass, "blazer_usb", ["battery.charge"], True + ) + registry = er.async_get(hass) + entry = registry.async_get("sensor.ups1_battery_charge") + assert entry + assert entry.unique_id == f"{config_entry.entry_id}_battery.charge" + assert config_entry.options == {} diff --git a/tests/components/nut/util.py b/tests/components/nut/util.py index 4e7506a9db1..8ac1d110512 100644 --- a/tests/components/nut/util.py +++ b/tests/components/nut/util.py @@ -18,7 +18,7 @@ def _get_mock_pynutclient(list_vars=None, list_ups=None): async def async_init_integration( - hass: HomeAssistant, ups_fixture: str, resources: list + hass: HomeAssistant, ups_fixture: str, resources: list, add_options: bool = False ) -> MockConfigEntry: """Set up the nexia integration in Home Assistant.""" @@ -34,6 +34,7 @@ async def async_init_integration( entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "mock", CONF_PORT: "mock", CONF_RESOURCES: resources}, + options={CONF_RESOURCES: resources} if add_options else {}, ) entry.add_to_hass(hass) From 45f3eb69910419f4708e5e5f57a1f37de26f8b90 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Oct 2021 00:20:13 +0200 Subject: [PATCH 0318/1038] Remove deprecated Wink integration (#57634) --- .coveragerc | 1 - .../components/discovery/__init__.py | 2 - homeassistant/components/wink/__init__.py | 971 ------------------ .../components/wink/alarm_control_panel.py | 75 -- .../components/wink/binary_sensor.py | 197 ---- homeassistant/components/wink/climate.py | 520 ---------- homeassistant/components/wink/cover.py | 57 - homeassistant/components/wink/fan.py | 112 -- homeassistant/components/wink/light.py | 114 -- homeassistant/components/wink/lock.py | 211 ---- homeassistant/components/wink/manifest.json | 9 - homeassistant/components/wink/scene.py | 34 - homeassistant/components/wink/sensor.py | 98 -- homeassistant/components/wink/services.yaml | 431 -------- homeassistant/components/wink/switch.py | 60 -- homeassistant/components/wink/water_heater.py | 143 --- requirements_all.txt | 6 - script/hassfest/coverage.py | 1 - 18 files changed, 3042 deletions(-) delete mode 100644 homeassistant/components/wink/__init__.py delete mode 100644 homeassistant/components/wink/alarm_control_panel.py delete mode 100644 homeassistant/components/wink/binary_sensor.py delete mode 100644 homeassistant/components/wink/climate.py delete mode 100644 homeassistant/components/wink/cover.py delete mode 100644 homeassistant/components/wink/fan.py delete mode 100644 homeassistant/components/wink/light.py delete mode 100644 homeassistant/components/wink/lock.py delete mode 100644 homeassistant/components/wink/manifest.json delete mode 100644 homeassistant/components/wink/scene.py delete mode 100644 homeassistant/components/wink/sensor.py delete mode 100644 homeassistant/components/wink/services.yaml delete mode 100644 homeassistant/components/wink/switch.py delete mode 100644 homeassistant/components/wink/water_heater.py diff --git a/.coveragerc b/.coveragerc index 0f453518adc..18139bddf08 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1191,7 +1191,6 @@ omit = homeassistant/components/webostv/* homeassistant/components/whois/sensor.py homeassistant/components/wiffi/* - homeassistant/components/wink/* homeassistant/components/wirelesstag/* homeassistant/components/wolflink/__init__.py homeassistant/components/wolflink/sensor.py diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index bade569bb46..595771cd673 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -38,7 +38,6 @@ SERVICE_SAMSUNG_PRINTER = "samsung_printer" SERVICE_TELLDUSLIVE = "tellstick" SERVICE_YEELIGHT = "yeelight" SERVICE_WEMO = "belkin_wemo" -SERVICE_WINK = "wink" SERVICE_XIAOMI_GW = "xiaomi_gw" # These have custom protocols @@ -94,7 +93,6 @@ MIGRATED_SERVICE_HANDLERS = [ "sonos", "songpal", SERVICE_WEMO, - SERVICE_WINK, SERVICE_XIAOMI_GW, "volumio", SERVICE_YEELIGHT, diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py deleted file mode 100644 index 702851a5e14..00000000000 --- a/homeassistant/components/wink/__init__.py +++ /dev/null @@ -1,971 +0,0 @@ -"""Support for Wink hubs.""" -from __future__ import annotations - -from datetime import timedelta -import json -import logging -import os -import time -from typing import Any - -from aiohttp.web import Response -from pubnubsubhandler import PubNubSubscriptionHandler -import pywink -import voluptuous as vol - -from homeassistant.components.http import HomeAssistantView -from homeassistant.const import ( - ATTR_BATTERY_LEVEL, - ATTR_NAME, - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, - CONF_EMAIL, - CONF_PASSWORD, - EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, - STATE_OFF, - STATE_ON, - __version__, -) -from homeassistant.core import callback -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import make_entity_service_schema -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.event import track_time_interval -from homeassistant.helpers.network import get_url -from homeassistant.util.json import load_json, save_json - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "wink" - -SUBSCRIPTION_HANDLER = None - -CONF_USER_AGENT = "user_agent" -CONF_OAUTH = "oauth" -CONF_LOCAL_CONTROL = "local_control" -CONF_MISSING_OAUTH_MSG = "Missing oauth2 credentials." - -ATTR_ACCESS_TOKEN = "access_token" -ATTR_REFRESH_TOKEN = "refresh_token" -ATTR_PAIRING_MODE = "pairing_mode" -ATTR_KIDDE_RADIO_CODE = "kidde_radio_code" -ATTR_HUB_NAME = "hub_name" - -WINK_AUTH_CALLBACK_PATH = "/auth/wink/callback" -WINK_AUTH_START = "/auth/wink" -WINK_CONFIG_FILE = ".wink.conf" -USER_AGENT = f"Manufacturer/Home-Assistant{__version__} python/3 Wink/3" - -DEFAULT_CONFIG = { - CONF_CLIENT_ID: "CLIENT_ID_HERE", - CONF_CLIENT_SECRET: "CLIENT_SECRET_HERE", -} - -SERVICE_ADD_NEW_DEVICES = "pull_newly_added_devices_from_wink" -SERVICE_REFRESH_STATES = "refresh_state_from_wink" -SERVICE_RENAME_DEVICE = "rename_wink_device" -SERVICE_DELETE_DEVICE = "delete_wink_device" -SERVICE_SET_PAIRING_MODE = "pair_new_device" -SERVICE_SET_CHIME_VOLUME = "set_chime_volume" -SERVICE_SET_SIREN_VOLUME = "set_siren_volume" -SERVICE_ENABLE_CHIME = "enable_chime" -SERVICE_SET_SIREN_TONE = "set_siren_tone" -SERVICE_SET_AUTO_SHUTOFF = "siren_set_auto_shutoff" -SERVICE_SIREN_STROBE_ENABLED = "set_siren_strobe_enabled" -SERVICE_CHIME_STROBE_ENABLED = "set_chime_strobe_enabled" -SERVICE_ENABLE_SIREN = "enable_siren" -SERVICE_SET_DIAL_CONFIG = "set_nimbus_dial_configuration" -SERVICE_SET_DIAL_STATE = "set_nimbus_dial_state" - -ATTR_VOLUME = "volume" -ATTR_TONE = "tone" -ATTR_ENABLED = "enabled" -ATTR_AUTO_SHUTOFF = "auto_shutoff" -ATTR_MIN_VALUE = "min_value" -ATTR_MAX_VALUE = "max_value" -ATTR_ROTATION = "rotation" -ATTR_SCALE = "scale" -ATTR_TICKS = "ticks" -ATTR_MIN_POSITION = "min_position" -ATTR_MAX_POSITION = "max_position" -ATTR_VALUE = "value" -ATTR_LABELS = "labels" - -SCALES = ["linear", "log"] -ROTATIONS = ["cw", "ccw"] - -VOLUMES = ["low", "medium", "high"] -TONES = [ - "doorbell", - "fur_elise", - "doorbell_extended", - "alert", - "william_tell", - "rondo_alla_turca", - "police_siren", - "evacuation", - "beep_beep", - "beep", -] -CHIME_TONES = TONES + ["inactive"] -AUTO_SHUTOFF_TIMES = [None, -1, 30, 60, 120] - -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Inclusive( - CONF_EMAIL, CONF_OAUTH, msg=CONF_MISSING_OAUTH_MSG - ): cv.string, - vol.Inclusive( - CONF_PASSWORD, CONF_OAUTH, msg=CONF_MISSING_OAUTH_MSG - ): cv.string, - vol.Inclusive( - CONF_CLIENT_ID, CONF_OAUTH, msg=CONF_MISSING_OAUTH_MSG - ): cv.string, - vol.Inclusive( - CONF_CLIENT_SECRET, CONF_OAUTH, msg=CONF_MISSING_OAUTH_MSG - ): cv.string, - vol.Optional(CONF_LOCAL_CONTROL, default=False): cv.boolean, - } - ), - }, - ), - extra=vol.ALLOW_EXTRA, -) - -RENAME_DEVICE_SCHEMA = make_entity_service_schema( - {vol.Required(ATTR_NAME): cv.string}, extra=vol.ALLOW_EXTRA -) - -DELETE_DEVICE_SCHEMA = make_entity_service_schema({}, extra=vol.ALLOW_EXTRA) - -SET_PAIRING_MODE_SCHEMA = vol.Schema( - { - vol.Required(ATTR_HUB_NAME): cv.string, - vol.Required(ATTR_PAIRING_MODE): cv.string, - vol.Optional(ATTR_KIDDE_RADIO_CODE): cv.string, - }, - extra=vol.ALLOW_EXTRA, -) - -SET_VOLUME_SCHEMA = make_entity_service_schema( - {vol.Required(ATTR_VOLUME): vol.In(VOLUMES)} -) - -SET_SIREN_TONE_SCHEMA = make_entity_service_schema( - {vol.Required(ATTR_TONE): vol.In(TONES)} -) - -SET_CHIME_MODE_SCHEMA = make_entity_service_schema( - {vol.Required(ATTR_TONE): vol.In(CHIME_TONES)} -) - -SET_AUTO_SHUTOFF_SCHEMA = make_entity_service_schema( - {vol.Required(ATTR_AUTO_SHUTOFF): vol.In(AUTO_SHUTOFF_TIMES)} -) - -SET_STROBE_ENABLED_SCHEMA = make_entity_service_schema( - {vol.Required(ATTR_ENABLED): cv.boolean} -) - -ENABLED_SIREN_SCHEMA = make_entity_service_schema( - {vol.Required(ATTR_ENABLED): cv.boolean} -) - -DIAL_CONFIG_SCHEMA = make_entity_service_schema( - { - vol.Optional(ATTR_MIN_VALUE): vol.Coerce(int), - vol.Optional(ATTR_MAX_VALUE): vol.Coerce(int), - vol.Optional(ATTR_MIN_POSITION): cv.positive_int, - vol.Optional(ATTR_MAX_POSITION): cv.positive_int, - vol.Optional(ATTR_ROTATION): vol.In(ROTATIONS), - vol.Optional(ATTR_SCALE): vol.In(SCALES), - vol.Optional(ATTR_TICKS): cv.positive_int, - } -) - -DIAL_STATE_SCHEMA = make_entity_service_schema( - { - vol.Required(ATTR_VALUE): vol.Coerce(int), - vol.Optional(ATTR_LABELS): cv.ensure_list(cv.string), - } -) - -WINK_COMPONENTS = [ - "binary_sensor", - "sensor", - "light", - "switch", - "lock", - "cover", - "climate", - "fan", - "alarm_control_panel", - "scene", - "water_heater", -] - -WINK_HUBS: list[Any] = [] - - -def _request_app_setup(hass, config): - """Assist user with configuring the Wink dev application.""" - hass.data[DOMAIN]["configurator"] = True - configurator = hass.components.configurator - - def wink_configuration_callback(callback_data): - """Handle configuration updates.""" - _config_path = hass.config.path(WINK_CONFIG_FILE) - if not os.path.isfile(_config_path): - setup(hass, config) - return - - client_id = callback_data.get(CONF_CLIENT_ID).strip() - client_secret = callback_data.get(CONF_CLIENT_SECRET).strip() - if None not in (client_id, client_secret): - save_json( - _config_path, - {CONF_CLIENT_ID: client_id, CONF_CLIENT_SECRET: client_secret}, - ) - setup(hass, config) - return - error_msg = "Your input was invalid. Please try again." - _configurator = hass.data[DOMAIN]["configuring"][DOMAIN] - configurator.notify_errors(_configurator, error_msg) - - start_url = f"{get_url(hass)}{WINK_AUTH_CALLBACK_PATH}" - - description = f"""Please create a Wink developer app at - https://developer.wink.com. - Add a Redirect URI of {start_url}. - They will provide you a Client ID and secret - after reviewing your request. - (This can take several days). - """ - - hass.data[DOMAIN]["configuring"][DOMAIN] = configurator.request_config( - DOMAIN, - wink_configuration_callback, - description=description, - submit_caption="submit", - description_image="/static/images/config_wink.png", - fields=[ - {"id": CONF_CLIENT_ID, "name": "Client ID", "type": "string"}, - {"id": CONF_CLIENT_SECRET, "name": "Client secret", "type": "string"}, - ], - ) - - -def _request_oauth_completion(hass, config): - """Request user complete Wink OAuth2 flow.""" - hass.data[DOMAIN]["configurator"] = True - configurator = hass.components.configurator - if DOMAIN in hass.data[DOMAIN]["configuring"]: - configurator.notify_errors( - hass.data[DOMAIN]["configuring"][DOMAIN], - "Failed to register, please try again.", - ) - return - - def wink_configuration_callback(callback_data): - """Call setup again.""" - setup(hass, config) - - start_url = f"{get_url(hass)}{WINK_AUTH_START}" - - description = f"Please authorize Wink by visiting {start_url}" - - hass.data[DOMAIN]["configuring"][DOMAIN] = configurator.request_config( - DOMAIN, wink_configuration_callback, description=description - ) - - -def setup(hass, config): # noqa: C901 - """Set up the Wink component.""" - _LOGGER.warning( - "The Wink integration has been deprecated and is pending removal in " - "Home Assistant Core 2021.11" - ) - - if hass.data.get(DOMAIN) is None: - hass.data[DOMAIN] = { - "unique_ids": [], - "entities": {}, - "oauth": {}, - "configuring": {}, - "pubnub": None, - "configurator": False, - } - - if config.get(DOMAIN) is not None: - client_id = config[DOMAIN].get(CONF_CLIENT_ID) - client_secret = config[DOMAIN].get(CONF_CLIENT_SECRET) - email = config[DOMAIN].get(CONF_EMAIL) - password = config[DOMAIN].get(CONF_PASSWORD) - local_control = config[DOMAIN].get(CONF_LOCAL_CONTROL) - else: - client_id = None - client_secret = None - email = None - password = None - local_control = None - hass.data[DOMAIN]["configurator"] = True - if None not in [client_id, client_secret]: - _LOGGER.info("Using legacy OAuth authentication") - if not local_control: - pywink.disable_local_control() - hass.data[DOMAIN]["oauth"][CONF_CLIENT_ID] = client_id - hass.data[DOMAIN]["oauth"][CONF_CLIENT_SECRET] = client_secret - hass.data[DOMAIN]["oauth"]["email"] = email - hass.data[DOMAIN]["oauth"]["password"] = password - pywink.legacy_set_wink_credentials(email, password, client_id, client_secret) - else: - _LOGGER.info("Using OAuth authentication") - if not local_control: - pywink.disable_local_control() - config_path = hass.config.path(WINK_CONFIG_FILE) - if os.path.isfile(config_path): - config_file = load_json(config_path) - if config_file == DEFAULT_CONFIG: - _request_app_setup(hass, config) - return True - # else move on because the user modified the file - else: - save_json(config_path, DEFAULT_CONFIG) - _request_app_setup(hass, config) - return True - - if DOMAIN in hass.data[DOMAIN]["configuring"]: - _configurator = hass.data[DOMAIN]["configuring"] - hass.components.configurator.request_done(_configurator.pop(DOMAIN)) - - # Using oauth - access_token = config_file.get(ATTR_ACCESS_TOKEN) - refresh_token = config_file.get(ATTR_REFRESH_TOKEN) - - # This will be called after authorizing Home-Assistant - if None not in (access_token, refresh_token): - pywink.set_wink_credentials( - config_file.get(CONF_CLIENT_ID), - config_file.get(CONF_CLIENT_SECRET), - access_token=access_token, - refresh_token=refresh_token, - ) - # This is called to create the redirect so the user can Authorize - # Home . - else: - - redirect_uri = f"{get_url(hass)}{WINK_AUTH_CALLBACK_PATH}" - - wink_auth_start_url = pywink.get_authorization_url( - config_file.get(CONF_CLIENT_ID), redirect_uri - ) - hass.http.register_redirect(WINK_AUTH_START, wink_auth_start_url) - hass.http.register_view( - WinkAuthCallbackView(config, config_file, pywink.request_token) - ) - _request_oauth_completion(hass, config) - return True - - pywink.set_user_agent(USER_AGENT) - sub_details = pywink.get_subscription_details() - hass.data[DOMAIN]["pubnub"] = PubNubSubscriptionHandler( - sub_details[0], origin=sub_details[1] - ) - - def _subscribe(): - hass.data[DOMAIN]["pubnub"].subscribe() - - # Call subscribe after the user sets up wink via the configurator - # All other methods will complete setup before - # EVENT_HOMEASSISTANT_START is called meaning they - # will call subscribe via the method below. (start_subscription) - if hass.data[DOMAIN]["configurator"]: - _subscribe() - - def keep_alive_call(event_time): - """Call the Wink API endpoints to keep PubNub working.""" - _LOGGER.info("Polling the Wink API to keep PubNub updates flowing") - pywink.set_user_agent(str(int(time.time()))) - _temp_response = pywink.get_user() - _LOGGER.debug(str(json.dumps(_temp_response))) - time.sleep(1) - pywink.set_user_agent(USER_AGENT) - _temp_response = pywink.wink_api_fetch() - _LOGGER.debug("%s", _temp_response) - _temp_response = pywink.post_session() - _LOGGER.debug("%s", _temp_response) - - # Call the Wink API every hour to keep PubNub updates flowing - track_time_interval(hass, keep_alive_call, timedelta(minutes=60)) - - def start_subscription(event): - """Start the PubNub subscription.""" - _subscribe() - - hass.bus.listen(EVENT_HOMEASSISTANT_START, start_subscription) - - def stop_subscription(event): - """Stop the PubNub subscription.""" - hass.data[DOMAIN]["pubnub"].unsubscribe() - hass.data[DOMAIN]["pubnub"] = None - - hass.bus.listen(EVENT_HOMEASSISTANT_STOP, stop_subscription) - - def save_credentials(event): - """Save currently set OAuth credentials.""" - if hass.data[DOMAIN]["oauth"].get("email") is None: - config_path = hass.config.path(WINK_CONFIG_FILE) - _config = pywink.get_current_oauth_credentials() - save_json(config_path, _config) - - hass.bus.listen(EVENT_HOMEASSISTANT_STOP, save_credentials) - - # Save the users potentially updated oauth credentials at a regular - # interval to prevent them from being expired after a HA reboot. - track_time_interval(hass, save_credentials, timedelta(minutes=60)) - - def force_update(call): - """Force all devices to poll the Wink API.""" - _LOGGER.info("Refreshing Wink states from API") - for entity_list in hass.data[DOMAIN]["entities"].values(): - # Throttle the calls to Wink API - for entity in entity_list: - time.sleep(1) - entity.schedule_update_ha_state(True) - - hass.services.register(DOMAIN, SERVICE_REFRESH_STATES, force_update) - - def pull_new_devices(call): - """Pull new devices added to users Wink account since startup.""" - _LOGGER.info("Getting new devices from Wink API") - for _component in WINK_COMPONENTS: - discovery.load_platform(hass, _component, DOMAIN, {}, config) - - hass.services.register(DOMAIN, SERVICE_ADD_NEW_DEVICES, pull_new_devices) - - def set_pairing_mode(call): - """Put the hub in provided pairing mode.""" - hub_name = call.data.get("hub_name") - pairing_mode = call.data.get("pairing_mode") - kidde_code = call.data.get("kidde_radio_code") - for hub in WINK_HUBS: - if hub.name() == hub_name: - hub.pair_new_device(pairing_mode, kidde_radio_code=kidde_code) - - def rename_device(call): - """Set specified device's name.""" - # This should only be called on one device at a time. - found_device = None - entity_id = call.data.get("entity_id")[0] - all_devices = [] - for list_of_devices in hass.data[DOMAIN]["entities"].values(): - all_devices += list_of_devices - for device in all_devices: - if device.entity_id == entity_id: - found_device = device - if found_device is not None: - name = call.data.get("name") - found_device.wink.set_name(name) - - hass.services.register( - DOMAIN, SERVICE_RENAME_DEVICE, rename_device, schema=RENAME_DEVICE_SCHEMA - ) - - def delete_device(call): - """Delete specified device.""" - # This should only be called on one device at a time. - found_device = None - entity_id = call.data.get("entity_id")[0] - all_devices = [] - for list_of_devices in hass.data[DOMAIN]["entities"].values(): - all_devices += list_of_devices - for device in all_devices: - if device.entity_id == entity_id: - found_device = device - if found_device is not None: - found_device.wink.remove_device() - - hass.services.register( - DOMAIN, SERVICE_DELETE_DEVICE, delete_device, schema=DELETE_DEVICE_SCHEMA - ) - - hubs = pywink.get_hubs() - for hub in hubs: - if hub.device_manufacturer() == "wink": - WINK_HUBS.append(hub) - - if WINK_HUBS: - hass.services.register( - DOMAIN, - SERVICE_SET_PAIRING_MODE, - set_pairing_mode, - schema=SET_PAIRING_MODE_SCHEMA, - ) - - def nimbus_service_handle(service): - """Handle nimbus services.""" - entity_id = service.data.get("entity_id")[0] - _all_dials = [] - for sensor in hass.data[DOMAIN]["entities"]["sensor"]: - if isinstance(sensor, WinkNimbusDialDevice): - _all_dials.append(sensor) - for _dial in _all_dials: - if _dial.entity_id == entity_id: - if service.service == SERVICE_SET_DIAL_CONFIG: - _dial.set_configuration(**service.data) - if service.service == SERVICE_SET_DIAL_STATE: - _dial.wink.set_state( - service.data.get("value"), service.data.get("labels") - ) - - def siren_service_handle(service): - """Handle siren services.""" - entity_ids = service.data.get("entity_id") - all_sirens = [] - for switch in hass.data[DOMAIN]["entities"]["switch"]: - if isinstance(switch, WinkSirenDevice): - all_sirens.append(switch) - sirens_to_set = [] - if entity_ids is None: - sirens_to_set = all_sirens - else: - for siren in all_sirens: - if siren.entity_id in entity_ids: - sirens_to_set.append(siren) - - for siren in sirens_to_set: - _man = siren.wink.device_manufacturer() - if ( - service.service != SERVICE_SET_AUTO_SHUTOFF - and service.service != SERVICE_ENABLE_SIREN - and _man not in ("dome", "wink") - ): - _LOGGER.error("Service only valid for Dome or Wink sirens") - return - - if service.service == SERVICE_ENABLE_SIREN: - siren.wink.set_state(service.data.get(ATTR_ENABLED)) - elif service.service == SERVICE_SET_AUTO_SHUTOFF: - siren.wink.set_auto_shutoff(service.data.get(ATTR_AUTO_SHUTOFF)) - elif service.service == SERVICE_SET_CHIME_VOLUME: - siren.wink.set_chime_volume(service.data.get(ATTR_VOLUME)) - elif service.service == SERVICE_SET_SIREN_VOLUME: - siren.wink.set_siren_volume(service.data.get(ATTR_VOLUME)) - elif service.service == SERVICE_SET_SIREN_TONE: - siren.wink.set_siren_sound(service.data.get(ATTR_TONE)) - elif service.service == SERVICE_ENABLE_CHIME: - siren.wink.set_chime(service.data.get(ATTR_TONE)) - elif service.service == SERVICE_SIREN_STROBE_ENABLED: - siren.wink.set_siren_strobe_enabled(service.data.get(ATTR_ENABLED)) - elif service.service == SERVICE_CHIME_STROBE_ENABLED: - siren.wink.set_chime_strobe_enabled(service.data.get(ATTR_ENABLED)) - - # Load components for the devices in Wink that we support - for wink_component in WINK_COMPONENTS: - hass.data[DOMAIN]["entities"][wink_component] = [] - discovery.load_platform(hass, wink_component, DOMAIN, {}, config) - - component = EntityComponent(_LOGGER, DOMAIN, hass) - - sirens = [] - has_dome_or_wink_siren = False - for siren in pywink.get_sirens(): - _man = siren.device_manufacturer() - if _man in ("dome", "wink"): - has_dome_or_wink_siren = True - _id = siren.object_id() + siren.name() - if _id not in hass.data[DOMAIN]["unique_ids"]: - sirens.append(WinkSirenDevice(siren, hass)) - - if sirens: - - hass.services.register( - DOMAIN, - SERVICE_SET_AUTO_SHUTOFF, - siren_service_handle, - schema=SET_AUTO_SHUTOFF_SCHEMA, - ) - - hass.services.register( - DOMAIN, - SERVICE_ENABLE_SIREN, - siren_service_handle, - schema=ENABLED_SIREN_SCHEMA, - ) - - if has_dome_or_wink_siren: - - hass.services.register( - DOMAIN, - SERVICE_SET_SIREN_TONE, - siren_service_handle, - schema=SET_SIREN_TONE_SCHEMA, - ) - - hass.services.register( - DOMAIN, - SERVICE_ENABLE_CHIME, - siren_service_handle, - schema=SET_CHIME_MODE_SCHEMA, - ) - - hass.services.register( - DOMAIN, - SERVICE_SET_SIREN_VOLUME, - siren_service_handle, - schema=SET_VOLUME_SCHEMA, - ) - - hass.services.register( - DOMAIN, - SERVICE_SET_CHIME_VOLUME, - siren_service_handle, - schema=SET_VOLUME_SCHEMA, - ) - - hass.services.register( - DOMAIN, - SERVICE_SIREN_STROBE_ENABLED, - siren_service_handle, - schema=SET_STROBE_ENABLED_SCHEMA, - ) - - hass.services.register( - DOMAIN, - SERVICE_CHIME_STROBE_ENABLED, - siren_service_handle, - schema=SET_STROBE_ENABLED_SCHEMA, - ) - - component.add_entities(sirens) - - nimbi = [] - dials = {} - all_nimbi = pywink.get_cloud_clocks() - all_dials = [] - for nimbus in all_nimbi: - if nimbus.object_type() == "cloud_clock": - nimbi.append(nimbus) - dials[nimbus.object_id()] = [] - for nimbus in all_nimbi: - if nimbus.object_type() == "dial": - dials[nimbus.parent_id()].append(nimbus) - - for nimbus in nimbi: - for dial in dials[nimbus.object_id()]: - all_dials.append(WinkNimbusDialDevice(nimbus, dial, hass)) - - if nimbi: - hass.services.register( - DOMAIN, - SERVICE_SET_DIAL_CONFIG, - nimbus_service_handle, - schema=DIAL_CONFIG_SCHEMA, - ) - - hass.services.register( - DOMAIN, - SERVICE_SET_DIAL_STATE, - nimbus_service_handle, - schema=DIAL_STATE_SCHEMA, - ) - - component.add_entities(all_dials) - - return True - - -class WinkAuthCallbackView(HomeAssistantView): - """Handle OAuth finish callback requests.""" - - url = "/auth/wink/callback" - name = "auth:wink:callback" - requires_auth = False - - def __init__(self, config, config_file, request_token): - """Initialize the OAuth callback view.""" - self.config = config - self.config_file = config_file - self.request_token = request_token - - @callback - def get(self, request): - """Finish OAuth callback request.""" - hass = request.app["hass"] - data = request.query - - response_message = """Wink has been successfully authorized! - You can close this window now! For the best results you should reboot - Home Assistant""" - html_response = """Wink Auth -

{}

""" - - if data.get("code") is not None: - response = self.request_token( - data.get("code"), self.config_file[CONF_CLIENT_SECRET] - ) - - config_contents = { - ATTR_ACCESS_TOKEN: response["access_token"], - ATTR_REFRESH_TOKEN: response["refresh_token"], - CONF_CLIENT_ID: self.config_file[CONF_CLIENT_ID], - CONF_CLIENT_SECRET: self.config_file[CONF_CLIENT_SECRET], - } - save_json(hass.config.path(WINK_CONFIG_FILE), config_contents) - - hass.async_add_job(setup, hass, self.config) - - return Response( - text=html_response.format(response_message), content_type="text/html" - ) - - error_msg = "No code returned from Wink API" - _LOGGER.error(error_msg) - return Response(text=html_response.format(error_msg), content_type="text/html") - - -class WinkDevice(Entity): - """Representation a base Wink device.""" - - def __init__(self, wink, hass): - """Initialize the Wink device.""" - self.hass = hass - self.wink = wink - hass.data[DOMAIN]["pubnub"].add_subscription( - self.wink.pubnub_channel, self._pubnub_update - ) - hass.data[DOMAIN]["unique_ids"].append(self.wink.object_id() + self.wink.name()) - - def _pubnub_update(self, message): - _LOGGER.debug(message) - try: - if message is None: - _LOGGER.error( - "Error on pubnub update for %s polling API for current state", - self.name, - ) - self.schedule_update_ha_state(True) - else: - self.wink.pubnub_update(message) - self.schedule_update_ha_state() - except (ValueError, KeyError, AttributeError): - _LOGGER.error( - "Error in pubnub JSON for %s polling API for current state", self.name - ) - self.schedule_update_ha_state(True) - - @property - def name(self): - """Return the name of the device.""" - return self.wink.name() - - @property - def unique_id(self): - """Return the unique id of the Wink device.""" - if hasattr(self.wink, "capability") and self.wink.capability() is not None: - return f"{self.wink.object_id()}_{self.wink.capability()}" - return self.wink.object_id() - - @property - def available(self): - """Return true if connection == True.""" - return self.wink.available() - - def update(self): - """Update state of the device.""" - self.wink.update_state() - - @property - def should_poll(self): - """Only poll if we are not subscribed to pubnub.""" - return self.wink.pubnub_channel is None - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - attributes = {} - battery = self._battery_level - if battery: - attributes[ATTR_BATTERY_LEVEL] = battery - man_dev_model = self._manufacturer_device_model - if man_dev_model: - attributes["manufacturer_device_model"] = man_dev_model - man_dev_id = self._manufacturer_device_id - if man_dev_id: - attributes["manufacturer_device_id"] = man_dev_id - dev_man = self._device_manufacturer - if dev_man: - attributes["device_manufacturer"] = dev_man - model_name = self._model_name - if model_name: - attributes["model_name"] = model_name - tamper = self._tamper - if tamper is not None: - attributes["tamper_detected"] = tamper - return attributes - - @property - def _battery_level(self): - """Return the battery level.""" - if self.wink.battery_level() is not None: - return self.wink.battery_level() * 100 - - @property - def _manufacturer_device_model(self): - """Return the manufacturer device model.""" - return self.wink.manufacturer_device_model() - - @property - def _manufacturer_device_id(self): - """Return the manufacturer device id.""" - return self.wink.manufacturer_device_id() - - @property - def _device_manufacturer(self): - """Return the device manufacturer.""" - return self.wink.device_manufacturer() - - @property - def _model_name(self): - """Return the model name.""" - return self.wink.model_name() - - @property - def _tamper(self): - """Return the devices tamper status.""" - if hasattr(self.wink, "tamper_detected"): - return self.wink.tamper_detected() - return None - - -class WinkSirenDevice(WinkDevice): - """Representation of a Wink siren device.""" - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self.hass.data[DOMAIN]["entities"]["switch"].append(self) - - @property - def state(self): - """Return sirens state.""" - if self.wink.state(): - return STATE_ON - return STATE_OFF - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return "mdi:bell-ring" - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - attributes = super().extra_state_attributes - - auto_shutoff = self.wink.auto_shutoff() - if auto_shutoff is not None: - attributes["auto_shutoff"] = auto_shutoff - - siren_volume = self.wink.siren_volume() - if siren_volume is not None: - attributes["siren_volume"] = siren_volume - - chime_volume = self.wink.chime_volume() - if chime_volume is not None: - attributes["chime_volume"] = chime_volume - - strobe_enabled = self.wink.strobe_enabled() - if strobe_enabled is not None: - attributes["siren_strobe_enabled"] = strobe_enabled - - chime_strobe_enabled = self.wink.chime_strobe_enabled() - if chime_strobe_enabled is not None: - attributes["chime_strobe_enabled"] = chime_strobe_enabled - - siren_sound = self.wink.siren_sound() - if siren_sound is not None: - attributes["siren_sound"] = siren_sound - - chime_mode = self.wink.chime_mode() - if chime_mode is not None: - attributes["chime_mode"] = chime_mode - - return attributes - - -class WinkNimbusDialDevice(WinkDevice): - """Representation of the Quirky Nimbus device.""" - - def __init__(self, nimbus, dial, hass): - """Initialize the Nimbus dial.""" - super().__init__(dial, hass) - self.parent = nimbus - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self.hass.data[DOMAIN]["entities"]["sensor"].append(self) - - @property - def state(self): - """Return dials current value.""" - return self.wink.state() - - @property - def name(self): - """Return the name of the device.""" - return f"{self.parent.name()} dial {self.wink.index() + 1}" - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - attributes = super().extra_state_attributes - dial_attributes = self.dial_attributes() - - return {**attributes, **dial_attributes} - - def dial_attributes(self): - """Return the dial only attributes.""" - return { - "labels": self.wink.labels(), - "position": self.wink.position(), - "rotation": self.wink.rotation(), - "max_value": self.wink.max_value(), - "min_value": self.wink.min_value(), - "num_ticks": self.wink.ticks(), - "scale_type": self.wink.scale(), - "max_position": self.wink.max_position(), - "min_position": self.wink.min_position(), - } - - def set_configuration(self, **kwargs): - """ - Set the dial config. - - Anything not sent will default to current setting. - """ - attributes = {**self.dial_attributes(), **kwargs} - - min_value = attributes["min_value"] - max_value = attributes["max_value"] - rotation = attributes["rotation"] - ticks = attributes["num_ticks"] - scale = attributes["scale_type"] - min_position = attributes["min_position"] - max_position = attributes["max_position"] - - self.wink.set_configuration( - min_value, - max_value, - rotation, - scale=scale, - ticks=ticks, - min_position=min_position, - max_position=max_position, - ) diff --git a/homeassistant/components/wink/alarm_control_panel.py b/homeassistant/components/wink/alarm_control_panel.py deleted file mode 100644 index 2f5ac83c6f5..00000000000 --- a/homeassistant/components/wink/alarm_control_panel.py +++ /dev/null @@ -1,75 +0,0 @@ -"""Support Wink alarm control panels.""" -import pywink - -import homeassistant.components.alarm_control_panel as alarm -from homeassistant.components.alarm_control_panel.const import ( - SUPPORT_ALARM_ARM_AWAY, - SUPPORT_ALARM_ARM_HOME, -) -from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_HOME, - STATE_ALARM_DISARMED, -) - -from . import DOMAIN, WinkDevice - -STATE_ALARM_PRIVACY = "Private" - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Wink platform.""" - - for camera in pywink.get_cameras(): - # get_cameras returns multiple device types. - # Only add those that aren't sensors. - try: - camera.capability() - except AttributeError: - _id = camera.object_id() + camera.name() - if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkCameraDevice(camera, hass)]) - - -class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanelEntity): - """Representation a Wink camera alarm.""" - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self.hass.data[DOMAIN]["entities"]["alarm_control_panel"].append(self) - - @property - def state(self): - """Return the state of the device.""" - wink_state = self.wink.state() - if wink_state == "away": - state = STATE_ALARM_ARMED_AWAY - elif wink_state == "home": - state = STATE_ALARM_DISARMED - elif wink_state == "night": - state = STATE_ALARM_ARMED_HOME - else: - state = None - return state - - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY - - def alarm_disarm(self, code=None): - """Send disarm command.""" - self.wink.set_mode("home") - - def alarm_arm_home(self, code=None): - """Send arm home command.""" - self.wink.set_mode("night") - - def alarm_arm_away(self, code=None): - """Send arm away command.""" - self.wink.set_mode("away") - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return {"private": self.wink.private()} diff --git a/homeassistant/components/wink/binary_sensor.py b/homeassistant/components/wink/binary_sensor.py deleted file mode 100644 index 6a5977c1dc2..00000000000 --- a/homeassistant/components/wink/binary_sensor.py +++ /dev/null @@ -1,197 +0,0 @@ -"""Support for Wink binary sensors.""" -import logging - -import pywink - -from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_OCCUPANCY, - DEVICE_CLASS_OPENING, - DEVICE_CLASS_SMOKE, - DEVICE_CLASS_SOUND, - DEVICE_CLASS_VIBRATION, - BinarySensorEntity, -) - -from . import DOMAIN, WinkDevice - -_LOGGER = logging.getLogger(__name__) - -# These are the available sensors mapped to binary_sensor class -SENSOR_TYPES = { - "brightness": "light", - "capturing_audio": DEVICE_CLASS_SOUND, - "capturing_video": None, - "co_detected": "gas", - "liquid_detected": DEVICE_CLASS_MOISTURE, - "loudness": DEVICE_CLASS_SOUND, - "motion": DEVICE_CLASS_MOTION, - "noise": DEVICE_CLASS_SOUND, - "opened": DEVICE_CLASS_OPENING, - "presence": DEVICE_CLASS_OCCUPANCY, - "smoke_detected": DEVICE_CLASS_SMOKE, - "vibration": DEVICE_CLASS_VIBRATION, -} - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Wink binary sensor platform.""" - - for sensor in pywink.get_sensors(): - _id = sensor.object_id() + sensor.name() - if ( - _id not in hass.data[DOMAIN]["unique_ids"] - and sensor.capability() in SENSOR_TYPES - ): - add_entities([WinkBinarySensorEntity(sensor, hass)]) - - for key in pywink.get_keys(): - _id = key.object_id() + key.name() - if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkBinarySensorEntity(key, hass)]) - - for sensor in pywink.get_smoke_and_co_detectors(): - _id = sensor.object_id() + sensor.name() - if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkSmokeDetector(sensor, hass)]) - - for hub in pywink.get_hubs(): - _id = hub.object_id() + hub.name() - if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkHub(hub, hass)]) - - for remote in pywink.get_remotes(): - _id = remote.object_id() + remote.name() - if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkRemote(remote, hass)]) - - for button in pywink.get_buttons(): - _id = button.object_id() + button.name() - if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkButton(button, hass)]) - - for gang in pywink.get_gangs(): - _id = gang.object_id() + gang.name() - if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkGang(gang, hass)]) - - for door_bell_sensor in pywink.get_door_bells(): - _id = door_bell_sensor.object_id() + door_bell_sensor.name() - if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkBinarySensorEntity(door_bell_sensor, hass)]) - - for camera_sensor in pywink.get_cameras(): - _id = camera_sensor.object_id() + camera_sensor.name() - if _id not in hass.data[DOMAIN]["unique_ids"]: - try: - if camera_sensor.capability() in SENSOR_TYPES: - add_entities([WinkBinarySensorEntity(camera_sensor, hass)]) - except AttributeError: - _LOGGER.info("Device isn't a sensor, skipping") - - -class WinkBinarySensorEntity(WinkDevice, BinarySensorEntity): - """Representation of a Wink binary sensor.""" - - def __init__(self, wink, hass): - """Initialize the Wink binary sensor.""" - super().__init__(wink, hass) - if hasattr(self.wink, "unit"): - self._unit_of_measurement = self.wink.unit() - else: - self._unit_of_measurement = None - if hasattr(self.wink, "capability"): - self.capability = self.wink.capability() - else: - self.capability = None - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self.hass.data[DOMAIN]["entities"]["binary_sensor"].append(self) - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self.wink.state() - - @property - def device_class(self): - """Return the class of this sensor, from DEVICE_CLASSES.""" - return SENSOR_TYPES.get(self.capability) - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - return super().extra_state_attributes - - -class WinkSmokeDetector(WinkBinarySensorEntity): - """Representation of a Wink Smoke detector.""" - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - _attributes = super().extra_state_attributes - _attributes["test_activated"] = self.wink.test_activated() - return _attributes - - -class WinkHub(WinkBinarySensorEntity): - """Representation of a Wink Hub.""" - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - _attributes = super().extra_state_attributes - _attributes["update_needed"] = self.wink.update_needed() - _attributes["firmware_version"] = self.wink.firmware_version() - _attributes["pairing_mode"] = self.wink.pairing_mode() - _kidde_code = self.wink.kidde_radio_code() - if _kidde_code is not None: - # The service call to set the Kidde code - # takes a string of 1s and 0s so it makes - # sense to display it to the user that way - _formatted_kidde_code = f"{_kidde_code:b}".zfill(8) - _attributes["kidde_radio_code"] = _formatted_kidde_code - return _attributes - - -class WinkRemote(WinkBinarySensorEntity): - """Representation of a Wink Lutron Connected bulb remote.""" - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - _attributes = super().extra_state_attributes - _attributes["button_on_pressed"] = self.wink.button_on_pressed() - _attributes["button_off_pressed"] = self.wink.button_off_pressed() - _attributes["button_up_pressed"] = self.wink.button_up_pressed() - _attributes["button_down_pressed"] = self.wink.button_down_pressed() - return _attributes - - @property - def device_class(self): - """Return the class of this sensor, from DEVICE_CLASSES.""" - return None - - -class WinkButton(WinkBinarySensorEntity): - """Representation of a Wink Relay button.""" - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - _attributes = super().extra_state_attributes - _attributes["pressed"] = self.wink.pressed() - _attributes["long_pressed"] = self.wink.long_pressed() - return _attributes - - -class WinkGang(WinkBinarySensorEntity): - """Representation of a Wink Relay gang.""" - - @property - def is_on(self): - """Return true if the gang is connected.""" - return self.wink.state() diff --git a/homeassistant/components/wink/climate.py b/homeassistant/components/wink/climate.py deleted file mode 100644 index 7836d71614f..00000000000 --- a/homeassistant/components/wink/climate.py +++ /dev/null @@ -1,520 +0,0 @@ -"""Support for Wink thermostats and Air Conditioners.""" -import logging - -import pywink - -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( - ATTR_TARGET_TEMP_HIGH, - ATTR_TARGET_TEMP_LOW, - CURRENT_HVAC_COOL, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_OFF, - FAN_AUTO, - FAN_HIGH, - FAN_LOW, - FAN_MEDIUM, - FAN_ON, - HVAC_MODE_AUTO, - HVAC_MODE_COOL, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_HEAT, - HVAC_MODE_OFF, - PRESET_AWAY, - PRESET_ECO, - PRESET_NONE, - SUPPORT_AUX_HEAT, - SUPPORT_FAN_MODE, - SUPPORT_PRESET_MODE, - SUPPORT_TARGET_TEMPERATURE, - SUPPORT_TARGET_TEMPERATURE_RANGE, -) -from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, TEMP_CELSIUS -from homeassistant.helpers.temperature import display_temp as show_temp - -from . import DOMAIN, WinkDevice - -_LOGGER = logging.getLogger(__name__) - -ATTR_ECO_TARGET = "eco_target" -ATTR_EXTERNAL_TEMPERATURE = "external_temperature" -ATTR_OCCUPIED = "occupied" -ATTR_SCHEDULE_ENABLED = "schedule_enabled" -ATTR_SMART_TEMPERATURE = "smart_temperature" -ATTR_TOTAL_CONSUMPTION = "total_consumption" - -HA_HVAC_TO_WINK = { - HVAC_MODE_AUTO: "auto", - HVAC_MODE_COOL: "cool_only", - HVAC_MODE_FAN_ONLY: "fan_only", - HVAC_MODE_HEAT: "heat_only", - HVAC_MODE_OFF: "off", -} - -WINK_HVAC_TO_HA = {value: key for key, value in HA_HVAC_TO_WINK.items()} - -SUPPORT_FLAGS_THERMOSTAT = ( - SUPPORT_TARGET_TEMPERATURE - | SUPPORT_TARGET_TEMPERATURE_RANGE - | SUPPORT_FAN_MODE - | SUPPORT_AUX_HEAT -) -SUPPORT_FAN_THERMOSTAT = [FAN_AUTO, FAN_ON] -SUPPORT_PRESET_THERMOSTAT = [PRESET_AWAY, PRESET_ECO] - -SUPPORT_FLAGS_AC = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | SUPPORT_PRESET_MODE -SUPPORT_FAN_AC = [FAN_HIGH, FAN_LOW, FAN_MEDIUM] -SUPPORT_PRESET_AC = [PRESET_NONE, PRESET_ECO] - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Wink climate devices.""" - for climate in pywink.get_thermostats(): - _id = climate.object_id() + climate.name() - if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkThermostat(climate, hass)]) - for climate in pywink.get_air_conditioners(): - _id = climate.object_id() + climate.name() - if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkAC(climate, hass)]) - - -class WinkThermostat(WinkDevice, ClimateEntity): - """Representation of a Wink thermostat.""" - - @property - def supported_features(self): - """Return the list of supported features.""" - return SUPPORT_FLAGS_THERMOSTAT - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self.hass.data[DOMAIN]["entities"]["climate"].append(self) - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - # The Wink API always returns temp in Celsius - return TEMP_CELSIUS - - @property - def extra_state_attributes(self): - """Return the optional device state attributes.""" - data = {} - if self.external_temperature is not None: - data[ATTR_EXTERNAL_TEMPERATURE] = show_temp( - self.hass, - self.external_temperature, - self.temperature_unit, - PRECISION_TENTHS, - ) - - if self.smart_temperature: - data[ATTR_SMART_TEMPERATURE] = self.smart_temperature - - if self.occupied is not None: - data[ATTR_OCCUPIED] = self.occupied - - if self.eco_target is not None: - data[ATTR_ECO_TARGET] = self.eco_target - - return data - - @property - def current_temperature(self): - """Return the current temperature.""" - return self.wink.current_temperature() - - @property - def current_humidity(self): - """Return the current humidity.""" - if self.wink.current_humidity() is not None: - # The API states humidity will be a float 0-1 - # the only example API response with humidity listed show an int - # This will address both possibilities - if self.wink.current_humidity() < 1: - return self.wink.current_humidity() * 100 - return self.wink.current_humidity() - return None - - @property - def external_temperature(self): - """Return the current external temperature.""" - return self.wink.current_external_temperature() - - @property - def smart_temperature(self): - """Return the current average temp of all remote sensor.""" - return self.wink.current_smart_temperature() - - @property - def eco_target(self): - """Return status of eco target (Is the thermostat in eco mode).""" - return self.wink.eco_target() - - @property - def occupied(self): - """Return status of if the thermostat has detected occupancy.""" - return self.wink.occupied() - - @property - def preset_mode(self): - """Return the current preset mode, e.g., home, away, temp.""" - mode = self.wink.current_hvac_mode() - if mode == "eco": - return PRESET_ECO - if self.wink.away(): - return PRESET_AWAY - return None - - @property - def preset_modes(self): - """Return a list of available preset modes.""" - return SUPPORT_PRESET_THERMOSTAT - - @property - def target_humidity(self): - """Return the humidity we try to reach.""" - target_hum = None - if self.wink.current_humidifier_mode() == "on": - if self.wink.current_humidifier_set_point() is not None: - target_hum = self.wink.current_humidifier_set_point() * 100 - elif self.wink.current_dehumidifier_mode() == "on": - if self.wink.current_dehumidifier_set_point() is not None: - target_hum = self.wink.current_dehumidifier_set_point() * 100 - else: - target_hum = None - return target_hum - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - if self.hvac_mode != HVAC_MODE_AUTO and not self.wink.away(): - if self.hvac_mode == HVAC_MODE_COOL: - return self.wink.current_max_set_point() - if self.hvac_mode == HVAC_MODE_HEAT: - return self.wink.current_min_set_point() - return None - - @property - def target_temperature_low(self): - """Return the lower bound temperature we try to reach.""" - if self.hvac_mode == HVAC_MODE_AUTO: - return self.wink.current_min_set_point() - return None - - @property - def target_temperature_high(self): - """Return the higher bound temperature we try to reach.""" - if self.hvac_mode == HVAC_MODE_AUTO: - return self.wink.current_max_set_point() - return None - - @property - def is_aux_heat(self): - """Return true if aux heater.""" - if "aux" not in self.wink.hvac_modes(): - return None - if self.wink.current_hvac_mode() == "aux": - return True - return False - - @property - def hvac_mode(self) -> str: - """Return hvac operation ie. heat, cool mode. - - Need to be one of HVAC_MODE_*. - """ - if not self.wink.is_on(): - return HVAC_MODE_OFF - - wink_mode = self.wink.current_hvac_mode() - if wink_mode == "aux": - return HVAC_MODE_HEAT - if wink_mode == "eco": - return HVAC_MODE_AUTO - return WINK_HVAC_TO_HA.get(wink_mode, "") - - @property - def hvac_modes(self): - """Return the list of available hvac operation modes. - - Need to be a subset of HVAC_MODES. - """ - hvac_list = [HVAC_MODE_OFF] - - modes = self.wink.hvac_modes() - for mode in modes: - if mode in ("eco", "aux"): - continue - try: - ha_mode = WINK_HVAC_TO_HA[mode] - hvac_list.append(ha_mode) - except KeyError: - _LOGGER.error( - "Invalid operation mode mapping. %s doesn't map. " - "Please report this", - mode, - ) - return hvac_list - - @property - def hvac_action(self): - """Return the current running hvac operation if supported. - - Need to be one of CURRENT_HVAC_*. - """ - if not self.wink.is_on(): - return CURRENT_HVAC_OFF - if self.wink.cool_on(): - return CURRENT_HVAC_COOL - if self.wink.heat_on(): - return CURRENT_HVAC_HEAT - return CURRENT_HVAC_IDLE - - def set_temperature(self, **kwargs): - """Set new target temperature.""" - target_temp = kwargs.get(ATTR_TEMPERATURE) - target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) - target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) - if target_temp is not None: - if self.hvac_mode == HVAC_MODE_COOL: - target_temp_high = target_temp - if self.hvac_mode == HVAC_MODE_HEAT: - target_temp_low = target_temp - self.wink.set_temperature(target_temp_low, target_temp_high) - - def set_hvac_mode(self, hvac_mode): - """Set new target hvac mode.""" - hvac_mode_to_set = HA_HVAC_TO_WINK.get(hvac_mode) - self.wink.set_operation_mode(hvac_mode_to_set) - - def set_preset_mode(self, preset_mode): - """Set new preset mode.""" - # Away - if preset_mode != PRESET_AWAY and self.wink.away(): - self.wink.set_away_mode(False) - elif preset_mode == PRESET_AWAY: - self.wink.set_away_mode() - - if preset_mode == PRESET_ECO: - self.wink.set_operation_mode("eco") - - @property - def fan_mode(self): - """Return whether the fan is on.""" - if self.wink.current_fan_mode() == "on": - return FAN_ON - if self.wink.current_fan_mode() == "auto": - return FAN_AUTO - # No Fan available so disable slider - return None - - @property - def fan_modes(self): - """List of available fan modes.""" - if self.wink.has_fan(): - return SUPPORT_FAN_THERMOSTAT - return None - - def set_fan_mode(self, fan_mode): - """Turn fan on/off.""" - self.wink.set_fan_mode(fan_mode.lower()) - - def turn_aux_heat_on(self): - """Turn auxiliary heater on.""" - self.wink.set_operation_mode("aux") - - def turn_aux_heat_off(self): - """Turn auxiliary heater off.""" - self.wink.set_operation_mode("heat_only") - - @property - def min_temp(self): - """Return the minimum temperature.""" - minimum = 7 # Default minimum - min_min = self.wink.min_min_set_point() - min_max = self.wink.min_max_set_point() - if self.hvac_mode == HVAC_MODE_HEAT: - if min_min: - return_value = min_min - else: - return_value = minimum - elif self.hvac_mode == HVAC_MODE_COOL: - if min_max: - return_value = min_max - else: - return_value = minimum - elif self.hvac_mode == HVAC_MODE_AUTO: - if min_min and min_max: - return_value = min(min_min, min_max) - else: - return_value = minimum - else: - return_value = minimum - return return_value - - @property - def max_temp(self): - """Return the maximum temperature.""" - maximum = 35 # Default maximum - max_min = self.wink.max_min_set_point() - max_max = self.wink.max_max_set_point() - if self.hvac_mode == HVAC_MODE_HEAT: - if max_min: - return_value = max_min - else: - return_value = maximum - elif self.hvac_mode == HVAC_MODE_COOL: - if max_max: - return_value = max_max - else: - return_value = maximum - elif self.hvac_mode == HVAC_MODE_AUTO: - if max_min and max_max: - return_value = min(max_min, max_max) - else: - return_value = maximum - else: - return_value = maximum - return return_value - - -class WinkAC(WinkDevice, ClimateEntity): - """Representation of a Wink air conditioner.""" - - @property - def supported_features(self): - """Return the list of supported features.""" - return SUPPORT_FLAGS_AC - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - # The Wink API always returns temp in Celsius - return TEMP_CELSIUS - - @property - def extra_state_attributes(self): - """Return the optional device state attributes.""" - data = {} - data[ATTR_TOTAL_CONSUMPTION] = self.wink.total_consumption() - data[ATTR_SCHEDULE_ENABLED] = self.wink.schedule_enabled() - - return data - - @property - def current_temperature(self): - """Return the current temperature.""" - return self.wink.current_temperature() - - @property - def preset_mode(self): - """Return the current preset mode, e.g., home, away, temp.""" - if not self.wink.is_on(): - return PRESET_NONE - - mode = self.wink.current_mode() - if mode == "auto_eco": - return PRESET_ECO - return PRESET_NONE - - @property - def preset_modes(self): - """Return a list of available preset modes.""" - return SUPPORT_PRESET_AC - - @property - def hvac_mode(self) -> str: - """Return hvac operation ie. heat, cool mode. - - Need to be one of HVAC_MODE_*. - """ - if not self.wink.is_on(): - return HVAC_MODE_OFF - - wink_mode = self.wink.current_mode() - if wink_mode == "auto_eco": - return HVAC_MODE_COOL - return WINK_HVAC_TO_HA.get(wink_mode, "") - - @property - def hvac_modes(self): - """Return the list of available hvac operation modes. - - Need to be a subset of HVAC_MODES. - """ - hvac_list = [HVAC_MODE_OFF] - - modes = self.wink.modes() - for mode in modes: - if mode == "auto_eco": - continue - try: - ha_mode = WINK_HVAC_TO_HA[mode] - hvac_list.append(ha_mode) - except KeyError: - _LOGGER.error( - "Invalid operation mode mapping. %s doesn't map. " - "Please report this", - mode, - ) - return hvac_list - - def set_temperature(self, **kwargs): - """Set new target temperature.""" - target_temp = kwargs.get(ATTR_TEMPERATURE) - self.wink.set_temperature(target_temp) - - def set_hvac_mode(self, hvac_mode): - """Set new target hvac mode.""" - hvac_mode_to_set = HA_HVAC_TO_WINK.get(hvac_mode) - self.wink.set_operation_mode(hvac_mode_to_set) - - def set_preset_mode(self, preset_mode): - """Set new preset mode.""" - if preset_mode == PRESET_ECO: - self.wink.set_operation_mode("auto_eco") - elif self.hvac_mode == HVAC_MODE_COOL and preset_mode == PRESET_NONE: - self.set_hvac_mode(HVAC_MODE_COOL) - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self.wink.current_max_set_point() - - @property - def fan_mode(self): - """ - Return the current fan mode. - - The official Wink app only supports 3 modes [low, medium, high] - which are equal to [0.33, 0.66, 1.0] respectively. - """ - speed = self.wink.current_fan_speed() - if speed <= 0.33: - return FAN_LOW - if speed <= 0.66: - return FAN_MEDIUM - return FAN_HIGH - - @property - def fan_modes(self): - """Return a list of available fan modes.""" - return SUPPORT_FAN_AC - - def set_fan_mode(self, fan_mode): - """ - Set fan speed. - - The official Wink app only supports 3 modes [low, medium, high] - which are equal to [0.33, 0.66, 1.0] respectively. - """ - if fan_mode == FAN_LOW: - speed = 0.33 - elif fan_mode == FAN_MEDIUM: - speed = 0.66 - elif fan_mode == FAN_HIGH: - speed = 1.0 - self.wink.set_ac_fan_speed(speed) diff --git a/homeassistant/components/wink/cover.py b/homeassistant/components/wink/cover.py deleted file mode 100644 index f2f4241c64d..00000000000 --- a/homeassistant/components/wink/cover.py +++ /dev/null @@ -1,57 +0,0 @@ -"""Support for Wink covers.""" -import pywink - -from homeassistant.components.cover import ATTR_POSITION, CoverEntity - -from . import DOMAIN, WinkDevice - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Wink cover platform.""" - - for shade in pywink.get_shades(): - _id = shade.object_id() + shade.name() - if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkCoverEntity(shade, hass)]) - for shade in pywink.get_shade_groups(): - _id = shade.object_id() + shade.name() - if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkCoverEntity(shade, hass)]) - for door in pywink.get_garage_doors(): - _id = door.object_id() + door.name() - if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkCoverEntity(door, hass)]) - - -class WinkCoverEntity(WinkDevice, CoverEntity): - """Representation of a Wink cover device.""" - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self.hass.data[DOMAIN]["entities"]["cover"].append(self) - - def close_cover(self, **kwargs): - """Close the cover.""" - self.wink.set_state(0) - - def open_cover(self, **kwargs): - """Open the cover.""" - self.wink.set_state(1) - - def set_cover_position(self, **kwargs): - """Move the cover shutter to a specific position.""" - position = kwargs.get(ATTR_POSITION) - self.wink.set_state(position / 100) - - @property - def current_cover_position(self): - """Return the current position of cover shutter.""" - if self.wink.state() is not None: - return int(self.wink.state() * 100) - return None - - @property - def is_closed(self): - """Return if the cover is closed.""" - state = self.wink.state() - return bool(state == 0) diff --git a/homeassistant/components/wink/fan.py b/homeassistant/components/wink/fan.py deleted file mode 100644 index b918d596ef4..00000000000 --- a/homeassistant/components/wink/fan.py +++ /dev/null @@ -1,112 +0,0 @@ -"""Support for Wink fans.""" -from __future__ import annotations - -import pywink - -from homeassistant.components.fan import ( - SPEED_HIGH, - SPEED_LOW, - SPEED_MEDIUM, - SUPPORT_DIRECTION, - SUPPORT_SET_SPEED, - FanEntity, -) - -from . import DOMAIN, WinkDevice - -SPEED_AUTO = "auto" -SPEED_LOWEST = "lowest" -SUPPORTED_FEATURES = SUPPORT_DIRECTION + SUPPORT_SET_SPEED - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Wink platform.""" - - for fan in pywink.get_fans(): - if fan.object_id() + fan.name() not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkFanDevice(fan, hass)]) - - -class WinkFanDevice(WinkDevice, FanEntity): - """Representation of a Wink fan.""" - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self.hass.data[DOMAIN]["entities"]["fan"].append(self) - - def set_direction(self, direction: str) -> None: - """Set the direction of the fan.""" - self.wink.set_fan_direction(direction) - - def set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" - self.wink.set_state(True, speed) - - # - # The fan entity model has changed to use percentages and preset_modes - # instead of speeds. - # - # Please review - # https://developers.home-assistant.io/docs/core/entity/fan/ - # - def turn_on( - self, - speed: str = None, - percentage: int = None, - preset_mode: str = None, - **kwargs, - ) -> None: - """Turn on the fan.""" - self.wink.set_state(True, speed) - - def turn_off(self, **kwargs) -> None: - """Turn off the fan.""" - self.wink.set_state(False) - - @property - def is_on(self): - """Return true if the entity is on.""" - return self.wink.state() - - @property - def speed(self) -> str | None: - """Return the current speed.""" - current_wink_speed = self.wink.current_fan_speed() - if SPEED_AUTO == current_wink_speed: - return SPEED_AUTO - if SPEED_LOWEST == current_wink_speed: - return SPEED_LOWEST - if SPEED_LOW == current_wink_speed: - return SPEED_LOW - if SPEED_MEDIUM == current_wink_speed: - return SPEED_MEDIUM - if SPEED_HIGH == current_wink_speed: - return SPEED_HIGH - return None - - @property - def current_direction(self): - """Return direction of the fan [forward, reverse].""" - return self.wink.current_fan_direction() - - @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - wink_supported_speeds = self.wink.fan_speeds() - supported_speeds = [] - if SPEED_AUTO in wink_supported_speeds: - supported_speeds.append(SPEED_AUTO) - if SPEED_LOWEST in wink_supported_speeds: - supported_speeds.append(SPEED_LOWEST) - if SPEED_LOW in wink_supported_speeds: - supported_speeds.append(SPEED_LOW) - if SPEED_MEDIUM in wink_supported_speeds: - supported_speeds.append(SPEED_MEDIUM) - if SPEED_HIGH in wink_supported_speeds: - supported_speeds.append(SPEED_HIGH) - return supported_speeds - - @property - def supported_features(self) -> int: - """Flag supported features.""" - return SUPPORTED_FEATURES diff --git a/homeassistant/components/wink/light.py b/homeassistant/components/wink/light.py deleted file mode 100644 index 4d20cf4dd5a..00000000000 --- a/homeassistant/components/wink/light.py +++ /dev/null @@ -1,114 +0,0 @@ -"""Support for Wink lights.""" -import pywink - -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, - ATTR_COLOR_TEMP, - ATTR_HS_COLOR, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, - LightEntity, -) -from homeassistant.util import color as color_util -from homeassistant.util.color import ( - color_temperature_mired_to_kelvin as mired_to_kelvin, -) - -from . import DOMAIN, WinkDevice - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Wink lights.""" - - for light in pywink.get_light_bulbs(): - _id = light.object_id() + light.name() - if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkLight(light, hass)]) - for light in pywink.get_light_groups(): - _id = light.object_id() + light.name() - if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkLight(light, hass)]) - - -class WinkLight(WinkDevice, LightEntity): - """Representation of a Wink light.""" - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self.hass.data[DOMAIN]["entities"]["light"].append(self) - - @property - def is_on(self): - """Return true if light is on.""" - return self.wink.state() - - @property - def brightness(self): - """Return the brightness of the light.""" - if self.wink.brightness() is not None: - return int(self.wink.brightness() * 255) - return None - - @property - def hs_color(self): - """Define current bulb color.""" - if self.wink.supports_xy_color(): - return color_util.color_xy_to_hs(*self.wink.color_xy()) - - if self.wink.supports_hue_saturation(): - hue = self.wink.color_hue() - saturation = self.wink.color_saturation() - if hue is not None and saturation is not None: - return hue * 360, saturation * 100 - - return None - - @property - def color_temp(self): - """Define current bulb color in degrees Kelvin.""" - if not self.wink.supports_temperature(): - return None - return color_util.color_temperature_kelvin_to_mired( - self.wink.color_temperature_kelvin() - ) - - @property - def supported_features(self): - """Flag supported features.""" - supports = SUPPORT_BRIGHTNESS - if self.wink.supports_temperature(): - supports = supports | SUPPORT_COLOR_TEMP - if self.wink.supports_xy_color(): - supports = supports | SUPPORT_COLOR - elif self.wink.supports_hue_saturation(): - supports = supports | SUPPORT_COLOR - return supports - - def turn_on(self, **kwargs): - """Turn the switch on.""" - brightness = kwargs.get(ATTR_BRIGHTNESS) - hs_color = kwargs.get(ATTR_HS_COLOR) - color_temp_mired = kwargs.get(ATTR_COLOR_TEMP) - - state_kwargs = {} - - if hs_color: - if self.wink.supports_xy_color(): - xy_color = color_util.color_hs_to_xy(*hs_color) - state_kwargs["color_xy"] = xy_color - if self.wink.supports_hue_saturation(): - hs_scaled = hs_color[0] / 360, hs_color[1] / 100 - state_kwargs["color_hue_saturation"] = hs_scaled - - if color_temp_mired: - state_kwargs["color_kelvin"] = mired_to_kelvin(color_temp_mired) - - if brightness: - state_kwargs["brightness"] = brightness / 255.0 - - self.wink.set_state(True, **state_kwargs) - - def turn_off(self, **kwargs): - """Turn the switch off.""" - self.wink.set_state(False) diff --git a/homeassistant/components/wink/lock.py b/homeassistant/components/wink/lock.py deleted file mode 100644 index 63a67d9f1ac..00000000000 --- a/homeassistant/components/wink/lock.py +++ /dev/null @@ -1,211 +0,0 @@ -"""Support for Wink locks.""" -import pywink -import voluptuous as vol - -from homeassistant.components.lock import LockEntity -from homeassistant.const import ( - ATTR_CODE, - ATTR_ENTITY_ID, - ATTR_MODE, - ATTR_NAME, - STATE_UNKNOWN, -) -import homeassistant.helpers.config_validation as cv - -from . import DOMAIN, WinkDevice - -SERVICE_SET_VACATION_MODE = "set_lock_vacation_mode" -SERVICE_SET_ALARM_MODE = "set_lock_alarm_mode" -SERVICE_SET_ALARM_SENSITIVITY = "set_lock_alarm_sensitivity" -SERVICE_SET_ALARM_STATE = "set_lock_alarm_state" -SERVICE_SET_BEEPER_STATE = "set_lock_beeper_state" -SERVICE_ADD_KEY = "add_new_lock_key_code" - -ATTR_ENABLED = "enabled" -ATTR_SENSITIVITY = "sensitivity" - -ALARM_SENSITIVITY_MAP = { - "low": 0.2, - "medium_low": 0.4, - "medium": 0.6, - "medium_high": 0.8, - "high": 1.0, -} - -ALARM_MODES_MAP = { - "activity": "alert", - "forced_entry": "forced_entry", - "tamper": "tamper", -} - -SET_ENABLED_SCHEMA = vol.Schema( - {vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_ENABLED): cv.string} -) - -SET_SENSITIVITY_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_SENSITIVITY): vol.In(ALARM_SENSITIVITY_MAP), - } -) - -SET_ALARM_MODES_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_MODE): vol.In(ALARM_MODES_MAP), - } -) - -ADD_KEY_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_NAME): cv.string, - vol.Required(ATTR_CODE): cv.positive_int, - } -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Wink platform.""" - - for lock in pywink.get_locks(): - _id = lock.object_id() + lock.name() - if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkLockDevice(lock, hass)]) - - def service_handle(service): - """Handle for services.""" - entity_ids = service.data.get("entity_id") - all_locks = hass.data[DOMAIN]["entities"]["lock"] - locks_to_set = [] - if entity_ids is None: - locks_to_set = all_locks - else: - for lock in all_locks: - if lock.entity_id in entity_ids: - locks_to_set.append(lock) - - for lock in locks_to_set: - if service.service == SERVICE_SET_VACATION_MODE: - lock.set_vacation_mode(service.data.get(ATTR_ENABLED)) - elif service.service == SERVICE_SET_ALARM_STATE: - lock.set_alarm_state(service.data.get(ATTR_ENABLED)) - elif service.service == SERVICE_SET_BEEPER_STATE: - lock.set_beeper_state(service.data.get(ATTR_ENABLED)) - elif service.service == SERVICE_SET_ALARM_MODE: - lock.set_alarm_mode(service.data.get(ATTR_MODE)) - elif service.service == SERVICE_SET_ALARM_SENSITIVITY: - lock.set_alarm_sensitivity(service.data.get(ATTR_SENSITIVITY)) - elif service.service == SERVICE_ADD_KEY: - name = service.data.get(ATTR_NAME) - code = service.data.get(ATTR_CODE) - lock.add_new_key(code, name) - - hass.services.register( - DOMAIN, SERVICE_SET_VACATION_MODE, service_handle, schema=SET_ENABLED_SCHEMA - ) - - hass.services.register( - DOMAIN, SERVICE_SET_ALARM_STATE, service_handle, schema=SET_ENABLED_SCHEMA - ) - - hass.services.register( - DOMAIN, SERVICE_SET_BEEPER_STATE, service_handle, schema=SET_ENABLED_SCHEMA - ) - - hass.services.register( - DOMAIN, SERVICE_SET_ALARM_MODE, service_handle, schema=SET_ALARM_MODES_SCHEMA - ) - - hass.services.register( - DOMAIN, - SERVICE_SET_ALARM_SENSITIVITY, - service_handle, - schema=SET_SENSITIVITY_SCHEMA, - ) - - hass.services.register( - DOMAIN, SERVICE_ADD_KEY, service_handle, schema=ADD_KEY_SCHEMA - ) - - -class WinkLockDevice(WinkDevice, LockEntity): - """Representation of a Wink lock.""" - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self.hass.data[DOMAIN]["entities"]["lock"].append(self) - - @property - def is_locked(self): - """Return true if device is locked.""" - return self.wink.state() - - def lock(self, **kwargs): - """Lock the device.""" - self.wink.set_state(True) - - def unlock(self, **kwargs): - """Unlock the device.""" - self.wink.set_state(False) - - def set_alarm_state(self, enabled): - """Set lock's alarm state.""" - self.wink.set_alarm_state(enabled) - - def set_vacation_mode(self, enabled): - """Set lock's vacation mode.""" - self.wink.set_vacation_mode(enabled) - - def set_beeper_state(self, enabled): - """Set lock's beeper mode.""" - self.wink.set_beeper_mode(enabled) - - def add_new_key(self, code, name): - """Add a new user key code.""" - self.wink.add_new_key(code, name) - - def set_alarm_sensitivity(self, sensitivity): - """ - Set lock's alarm sensitivity. - - Valid sensitivities: - 0.2, 0.4, 0.6, 0.8, 1.0 - """ - self.wink.set_alarm_sensitivity(sensitivity) - - def set_alarm_mode(self, mode): - """ - Set lock's alarm mode. - - Valid modes: - alert - Beep when lock is locked or unlocked - tamper - 15 sec alarm when lock is disturbed when locked - forced_entry - 3 min alarm when significant force applied - to door when locked. - """ - self.wink.set_alarm_mode(mode) - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - super_attrs = super().extra_state_attributes - sensitivity = dict_value_to_key( - ALARM_SENSITIVITY_MAP, self.wink.alarm_sensitivity() - ) - super_attrs["alarm_sensitivity"] = sensitivity - super_attrs["vacation_mode"] = self.wink.vacation_mode_enabled() - super_attrs["beeper_mode"] = self.wink.beeper_enabled() - super_attrs["auto_lock"] = self.wink.auto_lock_enabled() - alarm_mode = dict_value_to_key(ALARM_MODES_MAP, self.wink.alarm_mode()) - super_attrs["alarm_mode"] = alarm_mode - super_attrs["alarm_enabled"] = self.wink.alarm_enabled() - return super_attrs - - -def dict_value_to_key(dict_map, comp_value): - """Return the key that has the provided value.""" - for key, value in dict_map.items(): - if value == comp_value: - return key - return STATE_UNKNOWN diff --git a/homeassistant/components/wink/manifest.json b/homeassistant/components/wink/manifest.json deleted file mode 100644 index e4da7b9c03a..00000000000 --- a/homeassistant/components/wink/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "wink", - "name": "Wink", - "documentation": "https://www.home-assistant.io/integrations/wink", - "requirements": ["pubnubsub-handler==1.0.9", "python-wink==1.10.5"], - "dependencies": ["configurator", "http"], - "codeowners": [], - "iot_class": "cloud_polling" -} diff --git a/homeassistant/components/wink/scene.py b/homeassistant/components/wink/scene.py deleted file mode 100644 index 3f4724957f8..00000000000 --- a/homeassistant/components/wink/scene.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Support for Wink scenes.""" -from typing import Any - -import pywink - -from homeassistant.components.scene import Scene - -from . import DOMAIN, WinkDevice - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Wink platform.""" - - for scene in pywink.get_scenes(): - _id = scene.object_id() + scene.name() - if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkScene(scene, hass)]) - - -class WinkScene(WinkDevice, Scene): - """Representation of a Wink shortcut/scene.""" - - def __init__(self, wink, hass): - """Initialize the Wink device.""" - super().__init__(wink, hass) - hass.data[DOMAIN]["entities"]["scene"].append(self) - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self.hass.data[DOMAIN]["entities"]["scene"].append(self) - - def activate(self, **kwargs: Any) -> None: - """Activate the scene.""" - self.wink.activate() diff --git a/homeassistant/components/wink/sensor.py b/homeassistant/components/wink/sensor.py deleted file mode 100644 index 86199f44e91..00000000000 --- a/homeassistant/components/wink/sensor.py +++ /dev/null @@ -1,98 +0,0 @@ -"""Support for Wink sensors.""" -from contextlib import suppress -import logging - -import pywink - -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import DEGREE, TEMP_CELSIUS - -from . import DOMAIN, WinkDevice - -_LOGGER = logging.getLogger(__name__) - -SENSOR_TYPES = ["temperature", "humidity", "balance", "proximity"] - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Wink platform.""" - - for sensor in pywink.get_sensors(): - _id = sensor.object_id() + sensor.name() - if ( - _id not in hass.data[DOMAIN]["unique_ids"] - and sensor.capability() in SENSOR_TYPES - ): - add_entities([WinkSensorEntity(sensor, hass)]) - - for eggtray in pywink.get_eggtrays(): - _id = eggtray.object_id() + eggtray.name() - if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkSensorEntity(eggtray, hass)]) - - for tank in pywink.get_propane_tanks(): - _id = tank.object_id() + tank.name() - if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkSensorEntity(tank, hass)]) - - for piggy_bank in pywink.get_piggy_banks(): - _id = piggy_bank.object_id() + piggy_bank.name() - if _id not in hass.data[DOMAIN]["unique_ids"]: - try: - if piggy_bank.capability() in SENSOR_TYPES: - add_entities([WinkSensorEntity(piggy_bank, hass)]) - except AttributeError: - _LOGGER.info("Device is not a sensor") - - -class WinkSensorEntity(WinkDevice, SensorEntity): - """Representation of a Wink sensor.""" - - def __init__(self, wink, hass): - """Initialize the Wink device.""" - super().__init__(wink, hass) - self.capability = self.wink.capability() - if self.wink.unit() == DEGREE: - self._unit_of_measurement = TEMP_CELSIUS - else: - self._unit_of_measurement = self.wink.unit() - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self.hass.data[DOMAIN]["entities"]["sensor"].append(self) - - @property - def native_value(self): - """Return the state.""" - state = None - if self.capability == "humidity": - if self.wink.state() is not None: - state = round(self.wink.state()) - elif self.capability == "temperature": - if self.wink.state() is not None: - state = round(self.wink.state(), 1) - elif self.capability == "balance": - if self.wink.state() is not None: - state = round(self.wink.state() / 100, 2) - elif self.capability == "proximity": - if self.wink.state() is not None: - state = self.wink.state() - else: - state = self.wink.state() - return state - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - super_attrs = super().extra_state_attributes - - # Ignore error, this sensor isn't an eggminder - with suppress(AttributeError): - super_attrs["egg_times"] = self.wink.eggs() - - return super_attrs diff --git a/homeassistant/components/wink/services.yaml b/homeassistant/components/wink/services.yaml deleted file mode 100644 index 851f3bb9a43..00000000000 --- a/homeassistant/components/wink/services.yaml +++ /dev/null @@ -1,431 +0,0 @@ -# Describes the format for available Wink services -pair_new_device: - name: Pair new device - description: Pair a new device to a Wink Hub. - fields: - hub_name: - name: Hub name - description: The name of the hub to pair a new device to. - required: true - example: "My hub" - selector: - text: - pairing_mode: - name: Pairing mode - description: Mode. - required: true - selector: - select: - options: - - 'bluetooth' - - 'kidde' - - 'lutron' - - 'zigbee' - - 'zwave' - - 'zwave_exclusion' - - 'zwave_network_rediscovery' - kidde_radio_code: - name: Kidde radio code - description: "A string of 8 1s and 0s one for each dip switch on the kidde device left --> right = 1 --> 8. Down = 1 and Up = 0" - example: "10101010" - selector: - text: - -rename_wink_device: - name: Rename wink device - description: Rename the provided device. - target: - entity: - integration: wink - fields: - name: - name: Name - description: The name to change it to. - required: true - example: back_door - selector: - text: - -delete_wink_device: - name: Delete wink device - description: Remove/unpair device from Wink. - target: - entity: - integration: wink - -pull_newly_added_devices_from_wink: - name: Pull newly added devices from wink - description: Pull newly paired devices from Wink. - -refresh_state_from_wink: - name: Refresh state from wink - description: Pull the latest states for every device. - -set_siren_volume: - name: Set siren volume - description: Set the volume of the siren for a Dome siren/chime. - target: - entity: - integration: wink - domain: switch - fields: - volume: - name: Volume - description: Volume level. - required: true - selector: - select: - options: - - 'low' - - 'medium' - - 'high' - -enable_chime: - name: Enable chime - description: Enable the chime of a Dome siren with the provided sound. - target: - entity: - integration: wink - domain: switch - fields: - tone: - name: Tone - description: >- - The tone to use for the chime. - required: true - selector: - select: - options: - - 'alert' - - 'beep' - - 'beep_beep' - - 'doorbell' - - 'doorbell_extended' - - 'evacuation' - - 'fur_elise' - - 'inactive' - - 'police_siren' - - 'rondo_alla_turca' - - 'william_tell' - -set_siren_tone: - name: Set siren tone - description: Set the sound to use when the siren is enabled. (This doesn't enable the siren) - target: - entity: - integration: wink - domain: switch - fields: - tone: - name: Tone - description: >- - The tone to use for the chime. - required: true - selector: - select: - options: - - 'alert' - - 'beep' - - 'beep_beep' - - 'doorbell' - - 'doorbell_extended' - - 'evacuation' - - 'fur_elise' - - 'inactive' - - 'police_siren' - - 'rondo_alla_turca' - - 'william_tell' - -siren_set_auto_shutoff: - name: Siren set auto shutoff - description: How long to sound the siren before turning off. - target: - entity: - integration: wink - domain: switch - fields: - auto_shutoff: - name: Auto shutoff - description: >- - The time in seconds to sound the siren. (None and -1 are forever. Use None for gocontrol, and -1 for Dome) - required: true - selector: - select: - options: - - 'None' - - '-1' - - '30' - - '60' - - '120' - -set_siren_strobe_enabled: - name: Set siren strobe enabled - description: Enable or disable the strobe light when the siren is sounding. - target: - entity: - integration: wink - domain: switch - fields: - enabled: - name: Enabled - description: "True or False" - required: true - selector: - boolean: - -set_chime_strobe_enabled: - name: Set chime strobe enabled - description: Enable or disable the strobe light when the chime is sounding. - target: - entity: - integration: wink - domain: switch - fields: - enabled: - name: Enabled - description: "True or False" - required: true - selector: - boolean: - -enable_siren: - name: Enable siren - description: Enable/disable the siren. - target: - entity: - integration: wink - domain: switch - fields: - enabled: - name: Enabled - description: "true or false" - required: true - selector: - boolean: - -set_chime_volume: - name: Set chime volume - description: Set the volume of the chime for a Dome siren/chime. - target: - entity: - integration: wink - domain: switch - fields: - volume: - name: Volume - description: Volume level. - required: true - selector: - select: - options: - - 'low' - - 'medium' - - 'high' - -set_nimbus_dial_configuration: - name: Set nimbus dial configuration - description: Set the configuration of an individual nimbus dial - target: - entity: - integration: wink - domain: switch - fields: - rotation: - name: Rotation - description: Direction dial hand should spin. - selector: - select: - options: - - 'cw' - - 'ccw' - ticks: - name: Ticks - description: Number of times the hand should move - selector: - number: - min: 0 - max: 3600 - scale: - name: Scale - description: How the dial should move in response to higher values. - selector: - select: - options: - - 'linear' - - 'log' - min_value: - name: minimum value - description: The minimum value allowed to be set - example: 0 - selector: - text: - max_value: - name: Maximum value - description: The maximum value allowed to be set - example: 500 - selector: - text: - min_position: - name: Minimum position - description: The minimum position the dial hand can rotate to generally. - selector: - number: - min: 0 - max: 360 - max_position: - name: Maximum position - description: The maximum position the dial hand can rotate to generally. - selector: - number: - min: 0 - max: 360 - -set_nimbus_dial_state: - name: Set nimbus dial state - description: Set the value and labels of an individual nimbus dial - target: - entity: - integration: wink - fields: - value: - name: Value - description: The value that should be set (Should be between min_value and max_value) - required: true - example: 250 - selector: - text: - labels: - name: Labels - description: >- - The values shown on the dial labels ["Dial 1", "test"] the first value - is what is shown by default the second value is shown when the nimbus is - pressed. - example: ["example", "test"] - selector: - object: - -set_lock_vacation_mode: - name: Set lock vacation mode - description: Set vacation mode for all or specified locks. Disables all user codes. - fields: - entity_id: - name: Entity - description: Name of lock to unlock. - selector: - entity: - integration: wink - domain: lock - enabled: - name: Enabled - description: enable or disable. true or false. - required: true - selector: - boolean: - -set_lock_alarm_mode: - name: Set lock alarm mode - description: Set alarm mode for all or specified locks. - fields: - entity_id: - name: Entity - description: Name of lock to unlock. - selector: - entity: - integration: wink - domain: lock - mode: - name: Mode - description: Select mode. - required: true - selector: - select: - options: - - 'activity' - - 'forced_entry' - - 'tamper' - -set_lock_alarm_sensitivity: - name: Set lock alarm sensitivity - description: Set alarm sensitivity for all or specified locks. - fields: - entity_id: - name: Entity - description: Name of lock to unlock. - selector: - entity: - integration: wink - domain: lock - sensitivity: - name: Sensitivity - description: Choose the sensitivity. - required: true - selector: - select: - options: - - 'low' - - 'medium_low' - - 'medium' - - 'medium_high' - - 'high' - -set_lock_alarm_state: - name: Set lok alarm state - description: Set alarm state. - fields: - entity_id: - name: Entity - description: Name of lock to unlock. - selector: - entity: - integration: wink - domain: lock - enabled: - name: Enabled - description: enable or disable. - required: true - selector: - boolean: - -set_lock_beeper_state: - name: Set lock beeper state - description: Set beeper state. - fields: - entity_id: - name: Entity - description: Name of lock to unlock. - selector: - entity: - integration: wink - domain: lock - enabled: - name: Enabled - description: enable or disable. - required: true - selector: - boolean: - -add_new_lock_key_code: - name: Add new lock key code - description: Add a new user key code. - fields: - entity_id: - name: Entity - description: Name of lock to unlock. - selector: - entity: - integration: wink - domain: lock - name: - name: Name - description: name of the new key code. - required: true - example: Bob - selector: - text: - code: - name: Code - description: new key code, length must match length of other codes. Default length is 4. - required: true - example: 1234 - selector: - text: diff --git a/homeassistant/components/wink/switch.py b/homeassistant/components/wink/switch.py deleted file mode 100644 index d377ae0cddf..00000000000 --- a/homeassistant/components/wink/switch.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Support for Wink switches.""" -import pywink - -from homeassistant.helpers.entity import ToggleEntity - -from . import DOMAIN, WinkDevice - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Wink platform.""" - - for switch in pywink.get_switches(): - _id = switch.object_id() + switch.name() - if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkToggleDevice(switch, hass)]) - for switch in pywink.get_powerstrips(): - _id = switch.object_id() + switch.name() - if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkToggleDevice(switch, hass)]) - for sprinkler in pywink.get_sprinklers(): - _id = sprinkler.object_id() + sprinkler.name() - if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkToggleDevice(sprinkler, hass)]) - for switch in pywink.get_binary_switch_groups(): - _id = switch.object_id() + switch.name() - if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkToggleDevice(switch, hass)]) - - -class WinkToggleDevice(WinkDevice, ToggleEntity): - """Representation of a Wink toggle device.""" - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self.hass.data[DOMAIN]["entities"]["switch"].append(self) - - @property - def is_on(self): - """Return true if device is on.""" - return self.wink.state() - - def turn_on(self, **kwargs): - """Turn the device on.""" - self.wink.set_state(True) - - def turn_off(self, **kwargs): - """Turn the device off.""" - self.wink.set_state(False) - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - attributes = super().extra_state_attributes - try: - event = self.wink.last_event() - if event is not None: - attributes["last_event"] = event - except AttributeError: - pass - return attributes diff --git a/homeassistant/components/wink/water_heater.py b/homeassistant/components/wink/water_heater.py deleted file mode 100644 index bf5e8434746..00000000000 --- a/homeassistant/components/wink/water_heater.py +++ /dev/null @@ -1,143 +0,0 @@ -"""Support for Wink water heaters.""" -import logging - -import pywink - -from homeassistant.components.water_heater import ( - ATTR_TEMPERATURE, - STATE_ECO, - STATE_ELECTRIC, - STATE_GAS, - STATE_HEAT_PUMP, - STATE_HIGH_DEMAND, - STATE_PERFORMANCE, - SUPPORT_AWAY_MODE, - SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_TEMPERATURE, - WaterHeaterEntity, -) -from homeassistant.const import STATE_OFF, STATE_UNKNOWN, TEMP_CELSIUS - -from . import DOMAIN, WinkDevice - -_LOGGER = logging.getLogger(__name__) - -SUPPORT_FLAGS_HEATER = ( - SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | SUPPORT_AWAY_MODE -) - -ATTR_RHEEM_TYPE = "rheem_type" -ATTR_VACATION_MODE = "vacation_mode" - -HA_STATE_TO_WINK = { - STATE_ECO: "eco", - STATE_ELECTRIC: "electric_only", - STATE_GAS: "gas", - STATE_HEAT_PUMP: "heat_pump", - STATE_HIGH_DEMAND: "high_demand", - STATE_OFF: "off", - STATE_PERFORMANCE: "performance", -} - -WINK_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_WINK.items()} - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Wink water heater devices.""" - - for water_heater in pywink.get_water_heaters(): - _id = water_heater.object_id() + water_heater.name() - if _id not in hass.data[DOMAIN]["unique_ids"]: - add_entities([WinkWaterHeater(water_heater, hass)]) - - -class WinkWaterHeater(WinkDevice, WaterHeaterEntity): - """Representation of a Wink water heater.""" - - @property - def supported_features(self): - """Return the list of supported features.""" - return SUPPORT_FLAGS_HEATER - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - # The Wink API always returns temp in Celsius - return TEMP_CELSIUS - - @property - def extra_state_attributes(self): - """Return the optional device state attributes.""" - data = {} - data[ATTR_VACATION_MODE] = self.wink.vacation_mode_enabled() - data[ATTR_RHEEM_TYPE] = self.wink.rheem_type() - - return data - - @property - def current_operation(self): - """ - Return current operation one of the following. - - ["eco", "performance", "heat_pump", - "high_demand", "electric_only", "gas] - """ - if not self.wink.is_on(): - current_op = STATE_OFF - else: - current_op = WINK_STATE_TO_HA.get(self.wink.current_mode()) - if current_op is None: - current_op = STATE_UNKNOWN - return current_op - - @property - def operation_list(self): - """List of available operation modes.""" - op_list = ["off"] - modes = self.wink.modes() - for mode in modes: - if mode == "aux": - continue - ha_mode = WINK_STATE_TO_HA.get(mode) - if ha_mode is not None: - op_list.append(ha_mode) - else: - error = ( - "Invalid operation mode mapping. " - f"{mode} doesn't map. Please report this." - ) - _LOGGER.error(error) - return op_list - - def set_temperature(self, **kwargs): - """Set new target temperature.""" - target_temp = kwargs.get(ATTR_TEMPERATURE) - self.wink.set_temperature(target_temp) - - def set_operation_mode(self, operation_mode): - """Set operation mode.""" - op_mode_to_set = HA_STATE_TO_WINK.get(operation_mode) - self.wink.set_operation_mode(op_mode_to_set) - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self.wink.current_set_point() - - def turn_away_mode_on(self): - """Turn away on.""" - self.wink.set_vacation_mode(True) - - def turn_away_mode_off(self): - """Turn away off.""" - self.wink.set_vacation_mode(False) - - @property - def min_temp(self): - """Return the minimum temperature.""" - return self.wink.min_set_point() - - @property - def max_temp(self): - """Return the maximum temperature.""" - return self.wink.max_set_point() diff --git a/requirements_all.txt b/requirements_all.txt index dcc9efa042f..42577015a37 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1259,9 +1259,6 @@ proxmoxer==1.1.1 # homeassistant.components.systemmonitor psutil==5.8.0 -# homeassistant.components.wink -pubnubsub-handler==1.0.9 - # homeassistant.components.pulseaudio_loopback pulsectl==20.2.4 @@ -1948,9 +1945,6 @@ python-vlc==1.1.2 # homeassistant.components.whois python-whois==0.7.3 -# homeassistant.components.wink -python-wink==1.10.5 - # homeassistant.components.awair python_awair==0.2.1 diff --git a/script/hassfest/coverage.py b/script/hassfest/coverage.py index 1a8609bb4e8..1e91edee90e 100644 --- a/script/hassfest/coverage.py +++ b/script/hassfest/coverage.py @@ -62,7 +62,6 @@ ALLOWED_IGNORE_VIOLATIONS = { ("velux", "scene.py"), ("wemo", "config_flow.py"), ("wiffi", "config_flow.py"), - ("wink", "scene.py"), } From 8c326198cfc298546b00afb849820c8a555138b9 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 14 Oct 2021 00:11:41 +0000 Subject: [PATCH 0319/1038] [ci skip] Translation update --- .../components/dlna_dmr/translations/nl.json | 3 +++ .../components/efergy/translations/it.json | 21 +++++++++++++++++ .../components/efergy/translations/nl.json | 21 +++++++++++++++++ .../environment_canada/translations/ca.json | 8 +++++-- .../environment_canada/translations/it.json | 23 +++++++++++++++++++ .../environment_canada/translations/nl.json | 23 +++++++++++++++++++ .../components/nut/translations/en.json | 8 ++++++- .../stookalert/translations/nl.json | 14 +++++++++++ .../components/tradfri/translations/ca.json | 1 + .../components/tradfri/translations/de.json | 1 + .../components/tradfri/translations/en.json | 1 + .../components/tradfri/translations/et.json | 1 + .../components/tradfri/translations/it.json | 1 + .../components/tradfri/translations/nl.json | 1 + .../components/tradfri/translations/ru.json | 1 + .../tradfri/translations/zh-Hant.json | 1 + .../components/tuya/translations/zh-Hans.json | 23 +++++++++++++++++-- .../components/vacuum/translations/el.json | 6 ++--- .../components/watttime/translations/it.json | 10 +++++++- .../components/watttime/translations/nl.json | 10 +++++++- 20 files changed, 168 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/efergy/translations/it.json create mode 100644 homeassistant/components/efergy/translations/nl.json create mode 100644 homeassistant/components/environment_canada/translations/it.json create mode 100644 homeassistant/components/environment_canada/translations/nl.json create mode 100644 homeassistant/components/stookalert/translations/nl.json diff --git a/homeassistant/components/dlna_dmr/translations/nl.json b/homeassistant/components/dlna_dmr/translations/nl.json index 7387494b9b7..9a39aebb4ce 100644 --- a/homeassistant/components/dlna_dmr/translations/nl.json +++ b/homeassistant/components/dlna_dmr/translations/nl.json @@ -17,6 +17,9 @@ "confirm": { "description": "Wilt u beginnen met instellen?" }, + "import_turn_on": { + "description": "Zet het apparaat aan en klik op verzenden om door te gaan met de migratie" + }, "user": { "data": { "url": "URL" diff --git a/homeassistant/components/efergy/translations/it.json b/homeassistant/components/efergy/translations/it.json new file mode 100644 index 00000000000..d5677424d42 --- /dev/null +++ b/homeassistant/components/efergy/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API" + }, + "title": "Efergy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/nl.json b/homeassistant/components/efergy/translations/nl.json new file mode 100644 index 00000000000..4f97bad11a0 --- /dev/null +++ b/homeassistant/components/efergy/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "api_key": "API-sleutel" + }, + "title": "Efergy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/ca.json b/homeassistant/components/environment_canada/translations/ca.json index d1dd85556eb..f847b2dc5ac 100644 --- a/homeassistant/components/environment_canada/translations/ca.json +++ b/homeassistant/components/environment_canada/translations/ca.json @@ -3,6 +3,8 @@ "error": { "bad_station_id": "L'ID d'estaci\u00f3 no \u00e9s v\u00e0lid, no est\u00e0 present o no es troba a la base de dades d'IDs d'estacions", "cannot_connect": "Ha fallat la connexi\u00f3", + "error_response": "Resposta d'error d'Environment Canada", + "too_many_attempts": "Les connexions a Environment Canada estan limitades; torna-ho a provar d'aqu\u00ed a 60 segons", "unknown": "Error inesperat" }, "step": { @@ -11,8 +13,10 @@ "language": "Idioma de la informaci\u00f3 meteorol\u00f2gica", "latitude": "Latitud", "longitude": "Longitud", - "station": "ID de l'estaci\u00f3 meteorol\u00f2gica" - } + "station": "ID d'estaci\u00f3 meteorol\u00f2gica" + }, + "description": "Cal especificar un identificador d'estaci\u00f3 o una latitud/longitud. La latitud/longitud que s'utilitza de manera predeterminada s'obt\u00e9 dels valors configurats a la instal\u00b7laci\u00f3 de Home Assistant. Si s'especifiquen coordenades, s'utilitzar\u00e0 l'estaci\u00f3 meteorol\u00f2gica m\u00e9s propera a aquestes coordenades. Si s'utilitza un codi d'estaci\u00f3, ha de ser amb el format: PP/codi, on PP s\u00f3n les dues lletres de prov\u00edncia i el codi \u00e9s l'identificador d'estaci\u00f3. Pots trobar la llista d'IDs d'estaci\u00f3 aqu\u00ed: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. La informaci\u00f3 meteorol\u00f2gica es pot obtenir en angl\u00e8s o franc\u00e8s.", + "title": "Environment Canada: ubicaci\u00f3 meteorol\u00f2gica i idioma" } } } diff --git a/homeassistant/components/environment_canada/translations/it.json b/homeassistant/components/environment_canada/translations/it.json new file mode 100644 index 00000000000..f599eae7fe2 --- /dev/null +++ b/homeassistant/components/environment_canada/translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "bad_station_id": "L'ID stazione non \u00e8 valido, mancante o non \u00e8 presente nel database degli ID stazione", + "cannot_connect": "Impossibile connettersi", + "error_response": "Risposta di Environment Canada in errore", + "too_many_attempts": "I collegamenti con Environment Canada sono limitati; Riprova tra 60 secondi", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "language": "Lingua delle informazioni meteo", + "latitude": "Latitudine", + "longitude": "Logitudine", + "station": "ID stazione meteo" + }, + "description": "\u00c8 necessario specificare un ID stazione o latitudine/longitudine. La latitudine/longitudine predefinita utilizzata sono i valori configurati nell'installazione di Home Assistant. Se si specificano le coordinate, verr\u00e0 utilizzata la stazione meteorologica pi\u00f9 vicina alle coordinate. Se viene utilizzato un codice di stazione, deve seguire il formato: PP/codice, dove PP \u00e8 la provincia in due lettere e codice \u00e8 l'identificativo della stazione. L'elenco degli ID delle stazioni \u00e8 disponibile qui: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. Le informazioni meteorologiche possono essere recuperate in inglese o francese.", + "title": "Environment Canada: posizione meteo e lingua" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/nl.json b/homeassistant/components/environment_canada/translations/nl.json new file mode 100644 index 00000000000..65d9aa9c906 --- /dev/null +++ b/homeassistant/components/environment_canada/translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "bad_station_id": "Station-ID is ongeldig, ontbreekt of is niet gevonden in de database met stations-ID's", + "cannot_connect": "Kan geen verbinding maken", + "error_response": "Antwoord van Environment Canada is fout", + "too_many_attempts": "Verbindingen met Environment Canada zijn gelimiteerd; Probeer opnieuw binnen 60 seconden", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "language": "Taal voor weerinformatie", + "latitude": "Breedtegraad", + "longitude": "Lengtegraad", + "station": "Weerstation-ID" + }, + "description": "Er moet een station-ID of een lengte-/breedtegraad worden opgegeven. De standaard gebruikte breedtegraad/lengtegraad zijn de waarden die in uw Home Assistant installatie zijn geconfigureerd. Als u co\u00f6rdinaten opgeeft, wordt het weerstation gebruikt dat zich het dichtst bij de co\u00f6rdinaten bevindt. Als een stationcode wordt gebruikt, moet deze het volgende formaat hebben PP/code, waarbij PP de provincie is met twee letters en code de ID van het station. De lijst van station ID's kan hier worden gevonden: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. Weerinformatie kan worden opgevraagd in het Engels of Frans.", + "title": "Omgeving Canada: weerlocatie en taal" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/translations/en.json b/homeassistant/components/nut/translations/en.json index 5975d734df4..3d57189f7a5 100644 --- a/homeassistant/components/nut/translations/en.json +++ b/homeassistant/components/nut/translations/en.json @@ -33,11 +33,17 @@ } }, "options": { + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, "step": { "init": { "data": { + "resources": "Resources", "scan_interval": "Scan Interval (seconds)" - } + }, + "description": "Choose Sensor Resources." } } } diff --git a/homeassistant/components/stookalert/translations/nl.json b/homeassistant/components/stookalert/translations/nl.json new file mode 100644 index 00000000000..b6bd443c31b --- /dev/null +++ b/homeassistant/components/stookalert/translations/nl.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Service is al geconfigureerd" + }, + "step": { + "user": { + "data": { + "province": "Provincie" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/translations/ca.json b/homeassistant/components/tradfri/translations/ca.json index a1ab94a0920..a2d272949c2 100644 --- a/homeassistant/components/tradfri/translations/ca.json +++ b/homeassistant/components/tradfri/translations/ca.json @@ -5,6 +5,7 @@ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs" }, "error": { + "cannot_authenticate": "No es pot autenticar, el Gateway est\u00e0 vinculat amb un altre servidor com, per exemple, Homekit?", "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_key": "Ha fallat el registre amb la clau proporcionada. Si aix\u00f2 continua passant, intenta reiniciar la passarel\u00b7la d'enlla\u00e7.", "timeout": "S'ha acabat el temps d'espera durant la validaci\u00f3 del codi." diff --git a/homeassistant/components/tradfri/translations/de.json b/homeassistant/components/tradfri/translations/de.json index ee72be60028..2278f30cab6 100644 --- a/homeassistant/components/tradfri/translations/de.json +++ b/homeassistant/components/tradfri/translations/de.json @@ -5,6 +5,7 @@ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt" }, "error": { + "cannot_authenticate": "Authentifizierung nicht m\u00f6glich, ist das Gateway mit einem anderen Server wie z.B. Homekit gekoppelt?", "cannot_connect": "Verbindung fehlgeschlagen", "invalid_key": "Registrierung mit angegebenem Schl\u00fcssel fehlgeschlagen. Wenn dies weiterhin geschieht, starte den Gateway neu.", "timeout": "Timeout bei der \u00dcberpr\u00fcfung des Codes." diff --git a/homeassistant/components/tradfri/translations/en.json b/homeassistant/components/tradfri/translations/en.json index 9035496a0cc..b29989cba7a 100644 --- a/homeassistant/components/tradfri/translations/en.json +++ b/homeassistant/components/tradfri/translations/en.json @@ -5,6 +5,7 @@ "already_in_progress": "Configuration flow is already in progress" }, "error": { + "cannot_authenticate": "Cannot authenticate, is Gateway paired with another server like e.g. Homekit?", "cannot_connect": "Failed to connect", "invalid_key": "Failed to register with provided key. If this keeps happening, try restarting the gateway.", "timeout": "Timeout validating the code." diff --git a/homeassistant/components/tradfri/translations/et.json b/homeassistant/components/tradfri/translations/et.json index 00c804e6b05..6c3a4919944 100644 --- a/homeassistant/components/tradfri/translations/et.json +++ b/homeassistant/components/tradfri/translations/et.json @@ -5,6 +5,7 @@ "already_in_progress": "Seadistamine on juba k\u00e4imas" }, "error": { + "cannot_authenticate": "Tuvastamine nurjus, kas Gateway on seotud m\u00f5ne teise serveriga, n\u00e4iteks Homekitiga?", "cannot_connect": "\u00dchendamine nurjus", "invalid_key": "Sisestatud v\u00f5tmega registreerimine nurjus. Kui see juhtub, proovi l\u00fc\u00fcs taask\u00e4ivitada.", "timeout": "Koodi kinnitamise ajal\u00f5pp." diff --git a/homeassistant/components/tradfri/translations/it.json b/homeassistant/components/tradfri/translations/it.json index c05098d97d2..7af5aaa7dd4 100644 --- a/homeassistant/components/tradfri/translations/it.json +++ b/homeassistant/components/tradfri/translations/it.json @@ -5,6 +5,7 @@ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso" }, "error": { + "cannot_authenticate": "Impossibile eseguire l'autenticazione, il Gateway \u00e8 associato a un altro server come ad esempio Homekit?", "cannot_connect": "Impossibile connettersi", "invalid_key": "Impossibile registrarsi con la chiave fornita. Se questo continua a succedere, prova a riavviare il gateway.", "timeout": "Tempo scaduto per la validazione del codice." diff --git a/homeassistant/components/tradfri/translations/nl.json b/homeassistant/components/tradfri/translations/nl.json index e70e515c4d2..752270e78c6 100644 --- a/homeassistant/components/tradfri/translations/nl.json +++ b/homeassistant/components/tradfri/translations/nl.json @@ -5,6 +5,7 @@ "already_in_progress": "De configuratiestroom is al aan de gang" }, "error": { + "cannot_authenticate": "Kan niet authenticeren, is Gateway gekoppeld met een andere server zoals bijv. Homekit?", "cannot_connect": "Kan geen verbinding maken", "invalid_key": "Mislukt om te registreren met de meegeleverde sleutel. Als dit blijft gebeuren, probeer dan de gateway opnieuw op te starten.", "timeout": "Time-out bij validatie van code" diff --git a/homeassistant/components/tradfri/translations/ru.json b/homeassistant/components/tradfri/translations/ru.json index adb2b3aa18f..3851cc07428 100644 --- a/homeassistant/components/tradfri/translations/ru.json +++ b/homeassistant/components/tradfri/translations/ru.json @@ -5,6 +5,7 @@ "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." }, "error": { + "cannot_authenticate": "\u041d\u0435 \u0443\u0434\u0430\u0451\u0442\u0441\u044f \u043f\u0440\u043e\u0439\u0442\u0438 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e, \u0448\u043b\u044e\u0437 \u0443\u0436\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d \u043a \u0434\u0440\u0443\u0433\u043e\u043c\u0443 \u0441\u0435\u0440\u0432\u0435\u0440\u0443 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, HomeKit).", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_key": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0441 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u043c \u043a\u043b\u044e\u0447\u043e\u043c. \u0415\u0441\u043b\u0438 \u044d\u0442\u043e \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0448\u043b\u044e\u0437.", "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430." diff --git a/homeassistant/components/tradfri/translations/zh-Hant.json b/homeassistant/components/tradfri/translations/zh-Hant.json index 36c6e124f98..d0154478b24 100644 --- a/homeassistant/components/tradfri/translations/zh-Hant.json +++ b/homeassistant/components/tradfri/translations/zh-Hant.json @@ -5,6 +5,7 @@ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d" }, "error": { + "cannot_authenticate": "\u7121\u6cd5\u9a57\u8b49\uff0c\u7db2\u95dc\u662f\u5426\u8207\u5176\u4ed6\u4f3a\u670d\u5668\u3001\u4f8b\u5982 Homekit \u5df2\u9032\u884c\u914d\u5c0d\uff1f", "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_key": "\u63d0\u4f9b\u4e4b\u5b89\u5168\u78bc\u8a3b\u518a\u5931\u6557\u3002\u5047\u5982\u6b64\u60c5\u6cc1\u6301\u7e8c\u767c\u751f\uff0c\u8acb\u5617\u8a66\u91cd\u555f\u9598\u9053\u5668\u3002", "timeout": "\u8a8d\u8b49\u78bc\u903e\u6642\u3002" diff --git a/homeassistant/components/tuya/translations/zh-Hans.json b/homeassistant/components/tuya/translations/zh-Hans.json index ff3887c840d..5c4e0420f93 100644 --- a/homeassistant/components/tuya/translations/zh-Hans.json +++ b/homeassistant/components/tuya/translations/zh-Hans.json @@ -6,15 +6,32 @@ "single_instance_allowed": "\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86\uff0c\u4e14\u53ea\u80fd\u914d\u7f6e\u4e00\u6b21\u3002" }, "error": { - "invalid_auth": "\u8eab\u4efd\u8ba4\u8bc1\u65e0\u6548" + "invalid_auth": "\u8eab\u4efd\u8ba4\u8bc1\u65e0\u6548", + "login_error": "\u767b\u5f55\u5931\u8d25({code}: {msg}" }, "flow_title": "\u6d82\u9e26\u914d\u7f6e", "step": { + "login": { + "data": { + "access_id": "\u8bbf\u95ee ID", + "access_secret": "\u8bbf\u95ee\u5bc6\u94a5", + "country_code": "\u56fd\u5bb6\u4ee3\u7801", + "endpoint": "\u53ef\u7528\u533a\u57df", + "password": "\u5bc6\u7801", + "tuya_app_type": "\u79fb\u52a8\u5e94\u7528", + "username": "\u5e10\u6237" + }, + "description": "\u8bf7\u8f93\u5165\u60a8\u7684\u6d82\u9e26\u8d26\u6237\u4fe1\u606f", + "title": "\u6d82\u9e26" + }, "user": { "data": { + "access_id": "\u6d82\u9e26\u7269\u8054\u7f51\u8bbe\u5907\u63a5\u5165 ID", + "access_secret": "\u6d82\u9e26\u7269\u8054\u7f51\u8bbe\u5907\u63a5\u5165\u5bc6\u94a5", "country_code": "\u60a8\u7684\u5e10\u6237\u56fd\u5bb6(\u5730\u533a)\u4ee3\u7801\uff08\u4f8b\u5982\u4e2d\u56fd\u4e3a 86\uff0c\u7f8e\u56fd\u4e3a 1\uff09", "password": "\u5bc6\u7801", "platform": "\u60a8\u6ce8\u518c\u5e10\u6237\u7684\u5e94\u7528", + "region": "\u5730\u533a", "username": "\u7528\u6237\u540d" }, "description": "\u8bf7\u8f93\u5165\u6d82\u9e26\u8d26\u6237\u4fe1\u606f\u3002", @@ -39,7 +56,9 @@ "max_temp": "\u6700\u9ad8\u76ee\u6807\u6e29\u5ea6\uff08min \u548c max \u4e3a 0 \u65f6\u4f7f\u7528\u9ed8\u8ba4\uff09", "min_kelvin": "\u6700\u4f4e\u652f\u6301\u8272\u6e29\uff08\u5f00\u6c0f\uff09", "min_temp": "\u6700\u4f4e\u76ee\u6807\u6e29\u5ea6\uff08min \u548c max \u4e3a 0 \u65f6\u4f7f\u7528\u9ed8\u8ba4\uff09", + "set_temp_divided": "\u4f7f\u7528\u5212\u5206\u7684\u6e29\u5ea6\u503c\u6765\u8bbe\u7f6e\u6e29\u5ea6\u547d\u4ee4", "support_color": "\u5f3a\u5236\u652f\u6301\u8c03\u8272", + "temp_step_override": "\u76ee\u6807\u6e29\u5ea6\u6b65\u8fdb", "tuya_max_coltemp": "\u8bbe\u5907\u62a5\u544a\u7684\u6700\u9ad8\u8272\u6e29", "unit_of_measurement": "\u8bbe\u5907\u4f7f\u7528\u7684\u6e29\u5ea6\u5355\u4f4d" }, @@ -53,7 +72,7 @@ "query_interval": "\u67e5\u8be2\u8bbe\u5907\u8f6e\u8be2\u95f4\u9694\uff08\u4ee5\u79d2\u4e3a\u5355\u4f4d\uff09" }, "description": "\u8bf7\u4e0d\u8981\u5c06\u8f6e\u8be2\u95f4\u9694\u8bbe\u7f6e\u5f97\u592a\u4f4e\uff0c\u5426\u5219\u5c06\u8c03\u7528\u5931\u8d25\u5e76\u5728\u65e5\u5fd7\u751f\u6210\u9519\u8bef\u6d88\u606f", - "title": "\u914d\u7f6e\u6d82\u9e26\u9009\u9879" + "title": "\u6d82\u9e26\u914d\u7f6e\u9009\u9879" } } } diff --git a/homeassistant/components/vacuum/translations/el.json b/homeassistant/components/vacuum/translations/el.json index 3686450d9fb..22423a4d8b8 100644 --- a/homeassistant/components/vacuum/translations/el.json +++ b/homeassistant/components/vacuum/translations/el.json @@ -2,14 +2,14 @@ "state": { "_": { "cleaning": "\u039a\u03b1\u03b8\u03b1\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2", - "docked": "\u039a\u03b1\u03c1\u03c6\u03b9\u03c4\u03c3\u03c9\u03bc\u03ad\u03bd\u03bf", + "docked": "\u03a6\u03bf\u03c1\u03c4\u03af\u03b6\u03b5\u03b9", "error": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1", "idle": "\u03a3\u03b5 \u03b1\u03b4\u03c1\u03ac\u03bd\u03b5\u03b9\u03b1", "off": "\u039c\u03b7 \u0395\u03bd\u03b5\u03c1\u03b3\u03cc", "on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc", "paused": "\u03a0\u03b1\u03cd\u03c3\u03b7", - "returning": "\u0395\u03c0\u03b9\u03c3\u03c4\u03c1\u03bf\u03c6\u03ae \u03c3\u03c4\u03bf dock" + "returning": "\u03a0\u03c1\u03bf\u03c2 \u03c6\u03cc\u03c1\u03c4\u03b9\u03c3\u03b7" } }, - "title": "\u0395\u03ba\u03ba\u03ad\u03bd\u03c9\u03c3\u03b7" + "title": "\u03a1\u03bf\u03bc\u03c0\u03bf\u03c4\u03b9\u03ba\u03ae \u03c3\u03ba\u03bf\u03cd\u03c0\u03b1" } \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/it.json b/homeassistant/components/watttime/translations/it.json index 40f41c1d046..4be720042f1 100644 --- a/homeassistant/components/watttime/translations/it.json +++ b/homeassistant/components/watttime/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", @@ -22,6 +23,13 @@ }, "description": "Scegli una posizione da monitorare:" }, + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "Si prega di reinserire la password per {username}:", + "title": "Autenticare nuovamente l'integrazione" + }, "user": { "data": { "password": "Password", diff --git a/homeassistant/components/watttime/translations/nl.json b/homeassistant/components/watttime/translations/nl.json index f6776744cbb..045533d2336 100644 --- a/homeassistant/components/watttime/translations/nl.json +++ b/homeassistant/components/watttime/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { "invalid_auth": "Ongeldige authenticatie", @@ -22,6 +23,13 @@ }, "description": "Kies een locatie om te monitoren:" }, + "reauth_confirm": { + "data": { + "password": "Wachtwoord" + }, + "description": "Voer het wachtwoord voor {username} opnieuw in:", + "title": "Verifieer de integratie opnieuw" + }, "user": { "data": { "password": "Wachtwoord", From 827501659c926ace3741425760b1294d2e93b48e Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Thu, 14 Oct 2021 04:05:06 +0200 Subject: [PATCH 0320/1038] Nut: Use coordinator data, code cleanup and add test coverage (#57643) --- .coveragerc | 1 - CODEOWNERS | 2 +- homeassistant/components/nut/__init__.py | 10 +-- homeassistant/components/nut/const.py | 1 - homeassistant/components/nut/manifest.json | 2 +- homeassistant/components/nut/sensor.py | 27 +++----- tests/components/nut/test_sensor.py | 73 +++++++++++++++++++++- 7 files changed, 83 insertions(+), 33 deletions(-) diff --git a/.coveragerc b/.coveragerc index 18139bddf08..d09bab53760 100644 --- a/.coveragerc +++ b/.coveragerc @@ -731,7 +731,6 @@ omit = homeassistant/components/nuki/const.py homeassistant/components/nuki/binary_sensor.py homeassistant/components/nuki/lock.py - homeassistant/components/nut/sensor.py homeassistant/components/nx584/alarm_control_panel.py homeassistant/components/nzbget/coordinator.py homeassistant/components/obihai/* diff --git a/CODEOWNERS b/CODEOWNERS index ef702109461..bed454c62a7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -361,7 +361,7 @@ homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte homeassistant/components/nuki/* @pschmitt @pvizeli @pree homeassistant/components/numato/* @clssn homeassistant/components/number/* @home-assistant/core @Shulyaka -homeassistant/components/nut/* @bdraco +homeassistant/components/nut/* @bdraco @ollo69 homeassistant/components/nws/* @MatthewFlamm homeassistant/components/nzbget/* @chriscla homeassistant/components/obihai/* @dshokouhi diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index ea57c5994c6..6c8b5c69e80 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -16,7 +16,6 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( @@ -28,7 +27,6 @@ from .const import ( PYNUT_FIRMWARE, PYNUT_MANUFACTURER, PYNUT_MODEL, - PYNUT_NAME, PYNUT_UNIQUE_ID, UNDO_UPDATE_LISTENER, ) @@ -61,6 +59,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.async_add_executor_job(data.update) if not data.status: raise UpdateFailed("Error fetching UPS state") + return data.status coordinator = DataUpdateCoordinator( hass, @@ -72,11 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Fetch initial data so we have data when entities subscribe await coordinator.async_config_entry_first_refresh() - status = data.status - - if not status: - _LOGGER.error("NUT Sensor has no data, unable to set up") - raise ConfigEntryNotReady + status = coordinator.data _LOGGER.debug("NUT Sensors Available: %s", status) @@ -95,7 +90,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: PYNUT_MANUFACTURER: _manufacturer_from_status(status), PYNUT_MODEL: _model_from_status(status), PYNUT_FIRMWARE: _firmware_from_status(status), - PYNUT_NAME: data.name, UNDO_UPDATE_LISTENER: undo_listener, } diff --git a/homeassistant/components/nut/const.py b/homeassistant/components/nut/const.py index 3861c608631..250d999ce35 100644 --- a/homeassistant/components/nut/const.py +++ b/homeassistant/components/nut/const.py @@ -45,7 +45,6 @@ PYNUT_UNIQUE_ID = "unique_id" PYNUT_MANUFACTURER = "manufacturer" PYNUT_MODEL = "model" PYNUT_FIRMWARE = "firmware" -PYNUT_NAME = "name" SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.status.display": SensorEntityDescription( diff --git a/homeassistant/components/nut/manifest.json b/homeassistant/components/nut/manifest.json index 388858b93f0..2489078ebd6 100644 --- a/homeassistant/components/nut/manifest.json +++ b/homeassistant/components/nut/manifest.json @@ -3,7 +3,7 @@ "name": "Network UPS Tools (NUT)", "documentation": "https://www.home-assistant.io/integrations/nut", "requirements": ["pynut2==2.1.2"], - "codeowners": ["@bdraco"], + "codeowners": ["@bdraco", "@ollo69"], "config_flow": true, "zeroconf": ["_nut._tcp.local."], "iot_class": "local_polling" diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index 703e9ddd4ec..48253674be8 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -20,7 +20,6 @@ from .const import ( PYNUT_FIRMWARE, PYNUT_MANUFACTURER, PYNUT_MODEL, - PYNUT_NAME, PYNUT_UNIQUE_ID, SENSOR_TYPES, STATE_TYPES, @@ -37,10 +36,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): manufacturer = pynut_data[PYNUT_MANUFACTURER] model = pynut_data[PYNUT_MODEL] firmware = pynut_data[PYNUT_FIRMWARE] - name = pynut_data[PYNUT_NAME] coordinator = pynut_data[COORDINATOR] data = pynut_data[PYNUT_DATA] - status = data.status + status = coordinator.data enabled_resources = [ resource.lower() for resource in config_entry.data[CONF_RESOURCES] @@ -55,7 +53,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): NUTSensor( coordinator, data, - name.title(), SENSOR_TYPES[sensor_type], unique_id, manufacturer, @@ -76,7 +73,6 @@ class NUTSensor(CoordinatorEntity, SensorEntity): self, coordinator: DataUpdateCoordinator, data: PyNUTData, - name: str, sensor_description: SensorEntityDescription, unique_id: str, manufacturer: str | None, @@ -90,20 +86,16 @@ class NUTSensor(CoordinatorEntity, SensorEntity): self._manufacturer = manufacturer self._firmware = firmware self._model = model - self._device_name = name - self._data = data + self._device_name = data.name.title() self._unique_id = unique_id - self._attr_entity_registry_enabled_default = enabled_default - self._attr_name = f"{name} {sensor_description.name}" - if unique_id is not None: - self._attr_unique_id = f"{unique_id}_{sensor_description.key}" + self._attr_entity_registry_enabled_default = enabled_default + self._attr_name = f"{self._device_name} {sensor_description.name}" + self._attr_unique_id = f"{unique_id}_{sensor_description.key}" @property def device_info(self): """Device info for the ups.""" - if not self._unique_id: - return None device_info = { "identifiers": {(DOMAIN, self._unique_id)}, "name": self._device_name, @@ -119,17 +111,14 @@ class NUTSensor(CoordinatorEntity, SensorEntity): @property def native_value(self): """Return entity state from ups.""" - if not self._data.status: - return None + status = self.coordinator.data if self.entity_description.key == KEY_STATUS_DISPLAY: - return _format_display_state(self._data.status) - return self._data.status.get(self.entity_description.key) + return _format_display_state(status) + return status.get(self.entity_description.key) def _format_display_state(status): """Return UPS display state.""" - if status is None: - return STATE_TYPES["OFF"] try: return " ".join(STATE_TYPES[state] for state in status[KEY_STATUS].split()) except KeyError: diff --git a/tests/components/nut/test_sensor.py b/tests/components/nut/test_sensor.py index a5715ff9c8e..4b1e1cc8a9a 100644 --- a/tests/components/nut/test_sensor.py +++ b/tests/components/nut/test_sensor.py @@ -1,9 +1,20 @@ """The sensor tests for the nut platform.""" -from homeassistant.const import PERCENTAGE +from unittest.mock import patch + +from homeassistant.components.nut.const import DOMAIN +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + CONF_RESOURCES, + PERCENTAGE, + STATE_UNKNOWN, +) from homeassistant.helpers import entity_registry as er -from .util import async_init_integration +from .util import _get_mock_pynutclient, async_init_integration + +from tests.common import MockConfigEntry async def test_pr3000rt2u(hass): @@ -204,6 +215,64 @@ async def test_blazer_usb(hass): ) +async def test_state_sensors(hass): + """Test creation of status display sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "mock", + CONF_PORT: "mock", + CONF_RESOURCES: ["ups.status", "ups.status.display"], + }, + ) + entry.add_to_hass(hass) + + mock_pynut = _get_mock_pynutclient( + list_ups={"ups1": "UPS 1"}, list_vars={"ups.status": "OL"} + ) + + with patch( + "homeassistant.components.nut.PyNUTClient", + return_value=mock_pynut, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state1 = hass.states.get("sensor.ups1_status") + state2 = hass.states.get("sensor.ups1_status_data") + assert state1.state == "Online" + assert state2.state == "OL" + + +async def test_unknown_state_sensors(hass): + """Test creation of unknown status display sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "mock", + CONF_PORT: "mock", + CONF_RESOURCES: ["ups.status", "ups.status.display"], + }, + ) + entry.add_to_hass(hass) + + mock_pynut = _get_mock_pynutclient( + list_ups={"ups1": "UPS 1"}, list_vars={"ups.status": "OQ"} + ) + + with patch( + "homeassistant.components.nut.PyNUTClient", + return_value=mock_pynut, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state1 = hass.states.get("sensor.ups1_status") + state2 = hass.states.get("sensor.ups1_status_data") + assert state1.state == STATE_UNKNOWN + assert state2.state == "OQ" + + async def test_stale_options(hass): """Test creation of sensors with stale options to remove.""" From 0da1f9544e6696028b9b0e298b872cbc7dc524ce Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 14 Oct 2021 08:45:58 +0200 Subject: [PATCH 0321/1038] Correct state classes for systemmonitor sensors (#57615) --- .../components/systemmonitor/sensor.py | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 9360f2a3168..f630e4215f7 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -16,7 +16,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, - STATE_CLASS_TOTAL, + STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, SensorEntity, SensorEntityDescription, @@ -80,21 +80,21 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = { name="Disk free", native_unit_of_measurement=DATA_GIBIBYTES, icon="mdi:harddisk", - state_class=STATE_CLASS_TOTAL, + state_class=STATE_CLASS_MEASUREMENT, ), "disk_use": SysMonitorSensorEntityDescription( key="disk_use", name="Disk use", native_unit_of_measurement=DATA_GIBIBYTES, icon="mdi:harddisk", - state_class=STATE_CLASS_TOTAL, + state_class=STATE_CLASS_MEASUREMENT, ), "disk_use_percent": SysMonitorSensorEntityDescription( key="disk_use_percent", name="Disk use (percent)", native_unit_of_measurement=PERCENTAGE, icon="mdi:harddisk", - state_class=STATE_CLASS_TOTAL, + state_class=STATE_CLASS_MEASUREMENT, ), "ipv4_address": SysMonitorSensorEntityDescription( key="ipv4_address", @@ -117,40 +117,40 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = { key="load_15m", name="Load (15m)", icon=CPU_ICON, - state_class=STATE_CLASS_TOTAL, + state_class=STATE_CLASS_MEASUREMENT, ), "load_1m": SysMonitorSensorEntityDescription( key="load_1m", name="Load (1m)", icon=CPU_ICON, - state_class=STATE_CLASS_TOTAL, + state_class=STATE_CLASS_MEASUREMENT, ), "load_5m": SysMonitorSensorEntityDescription( key="load_5m", name="Load (5m)", icon=CPU_ICON, - state_class=STATE_CLASS_TOTAL, + state_class=STATE_CLASS_MEASUREMENT, ), "memory_free": SysMonitorSensorEntityDescription( key="memory_free", name="Memory free", native_unit_of_measurement=DATA_MEBIBYTES, icon="mdi:memory", - state_class=STATE_CLASS_TOTAL, + state_class=STATE_CLASS_MEASUREMENT, ), "memory_use": SysMonitorSensorEntityDescription( key="memory_use", name="Memory use", native_unit_of_measurement=DATA_MEBIBYTES, icon="mdi:memory", - state_class=STATE_CLASS_TOTAL, + state_class=STATE_CLASS_MEASUREMENT, ), "memory_use_percent": SysMonitorSensorEntityDescription( key="memory_use_percent", name="Memory use (percent)", native_unit_of_measurement=PERCENTAGE, icon="mdi:memory", - state_class=STATE_CLASS_TOTAL, + state_class=STATE_CLASS_MEASUREMENT, ), "network_in": SysMonitorSensorEntityDescription( key="network_in", @@ -187,7 +187,7 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = { name="Network throughput in", native_unit_of_measurement=DATA_RATE_MEGABYTES_PER_SECOND, icon="mdi:server-network", - state_class=STATE_CLASS_TOTAL, + state_class=STATE_CLASS_MEASUREMENT, mandatory_arg=True, ), "throughput_network_out": SysMonitorSensorEntityDescription( @@ -195,14 +195,14 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = { name="Network throughput out", native_unit_of_measurement=DATA_RATE_MEGABYTES_PER_SECOND, icon="mdi:server-network", - state_class=STATE_CLASS_TOTAL, + state_class=STATE_CLASS_MEASUREMENT, mandatory_arg=True, ), "process": SysMonitorSensorEntityDescription( key="process", name="Process", icon=CPU_ICON, - state_class=STATE_CLASS_TOTAL, + state_class=STATE_CLASS_MEASUREMENT, mandatory_arg=True, ), "processor_use": SysMonitorSensorEntityDescription( @@ -210,35 +210,35 @@ SENSOR_TYPES: dict[str, SysMonitorSensorEntityDescription] = { name="Processor use", native_unit_of_measurement=PERCENTAGE, icon=CPU_ICON, - state_class=STATE_CLASS_TOTAL, + state_class=STATE_CLASS_MEASUREMENT, ), "processor_temperature": SysMonitorSensorEntityDescription( key="processor_temperature", name="Processor temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_TOTAL, + state_class=STATE_CLASS_MEASUREMENT, ), "swap_free": SysMonitorSensorEntityDescription( key="swap_free", name="Swap free", native_unit_of_measurement=DATA_MEBIBYTES, icon="mdi:harddisk", - state_class=STATE_CLASS_TOTAL, + state_class=STATE_CLASS_MEASUREMENT, ), "swap_use": SysMonitorSensorEntityDescription( key="swap_use", name="Swap use", native_unit_of_measurement=DATA_MEBIBYTES, icon="mdi:harddisk", - state_class=STATE_CLASS_TOTAL, + state_class=STATE_CLASS_MEASUREMENT, ), "swap_use_percent": SysMonitorSensorEntityDescription( key="swap_use_percent", name="Swap use (percent)", native_unit_of_measurement=PERCENTAGE, icon="mdi:harddisk", - state_class=STATE_CLASS_TOTAL, + state_class=STATE_CLASS_MEASUREMENT, ), } From 26faac05678e77b455e19bedf2b41fc6b2713656 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Oct 2021 08:46:39 +0200 Subject: [PATCH 0322/1038] Remove YAML configuration from Rainforest Eagle (#57636) --- .../rainforest_eagle/config_flow.py | 6 -- .../components/rainforest_eagle/sensor.py | 50 +-------------- .../components/rainforest_eagle/test_init.py | 64 ------------------- 3 files changed, 2 insertions(+), 118 deletions(-) delete mode 100644 tests/components/rainforest_eagle/test_init.py diff --git a/homeassistant/components/rainforest_eagle/config_flow.py b/homeassistant/components/rainforest_eagle/config_flow.py index be921c31bf7..f1e01a9d77a 100644 --- a/homeassistant/components/rainforest_eagle/config_flow.py +++ b/homeassistant/components/rainforest_eagle/config_flow.py @@ -72,9 +72,3 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", data_schema=create_schema(user_input), errors=errors ) - - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: - """Handle the import step.""" - await self.async_set_unique_id(user_input[CONF_CLOUD_ID]) - self._abort_if_unique_id_configured() - return await self.async_step_user(user_input) diff --git a/homeassistant/components/rainforest_eagle/sensor.py b/homeassistant/components/rainforest_eagle/sensor.py index 6f6b496cfca..35864e55fa4 100644 --- a/homeassistant/components/rainforest_eagle/sensor.py +++ b/homeassistant/components/rainforest_eagle/sensor.py @@ -1,40 +1,28 @@ """Support for the Rainforest Eagle energy monitor.""" from __future__ import annotations -import logging -from typing import Any - -import voluptuous as vol - from homeassistant.components.sensor import ( DEVICE_CLASS_ENERGY, - PLATFORM_SCHEMA, STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, SensorEntity, SensorEntityDescription, StateType, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_HOST, - CONF_IP_ADDRESS, DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, POWER_KILO_WATT, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import CONF_CLOUD_ID, CONF_INSTALL_CODE, DOMAIN +from .const import DOMAIN from .data import EagleDataCoordinator -_LOGGER = logging.getLogger(__name__) - SENSORS = ( SensorEntityDescription( key="zigbee:InstantaneousDemand", @@ -60,40 +48,6 @@ SENSORS = ( ), ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_IP_ADDRESS): cv.string, - vol.Required(CONF_CLOUD_ID): cv.string, - vol.Required(CONF_INSTALL_CODE): cv.string, - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: dict[str, Any] | None = None, -): - """Import config as config entry.""" - _LOGGER.warning( - "Configuration of the rainforest_eagle platform in YAML is deprecated " - "and will be removed in Home Assistant 2021.11; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_HOST: config[CONF_IP_ADDRESS], - CONF_CLOUD_ID: config[CONF_CLOUD_ID], - CONF_INSTALL_CODE: config[CONF_INSTALL_CODE], - }, - ) - ) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback diff --git a/tests/components/rainforest_eagle/test_init.py b/tests/components/rainforest_eagle/test_init.py deleted file mode 100644 index f4ce029231a..00000000000 --- a/tests/components/rainforest_eagle/test_init.py +++ /dev/null @@ -1,64 +0,0 @@ -"""Tests for the Rainforest Eagle integration.""" -from unittest.mock import patch - -from homeassistant import config_entries -from homeassistant.components.rainforest_eagle.const import ( - CONF_CLOUD_ID, - CONF_HARDWARE_ADDRESS, - CONF_INSTALL_CODE, - DOMAIN, - TYPE_EAGLE_200, -) -from homeassistant.const import CONF_HOST, CONF_TYPE -from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT -from homeassistant.setup import async_setup_component - - -async def test_import(hass: HomeAssistant) -> None: - """Test we get the form.""" - - with patch( - "homeassistant.components.rainforest_eagle.data.async_get_type", - return_value=(TYPE_EAGLE_200, "mock-hw"), - ), patch( - "homeassistant.components.rainforest_eagle.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - await async_setup_component( - hass, - "sensor", - { - "sensor": { - "platform": DOMAIN, - "ip_address": "192.168.1.55", - CONF_CLOUD_ID: "abcdef", - CONF_INSTALL_CODE: "123456", - } - }, - ) - await hass.async_block_till_done() - - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 - entry = entries[0] - - assert entry.title == "abcdef" - assert entry.data == { - CONF_TYPE: TYPE_EAGLE_200, - CONF_HOST: "192.168.1.55", - CONF_CLOUD_ID: "abcdef", - CONF_INSTALL_CODE: "123456", - CONF_HARDWARE_ADDRESS: "mock-hw", - } - assert len(mock_setup_entry.mock_calls) == 1 - - # Second time we should get already_configured - result2 = await hass.config_entries.flow.async_init( - DOMAIN, - data={CONF_CLOUD_ID: "abcdef", CONF_INSTALL_CODE: "123456"}, - context={"source": config_entries.SOURCE_IMPORT}, - ) - - assert result2["type"] == RESULT_TYPE_ABORT - assert result2["reason"] == "already_configured" From 8e18ca3b6e22855568e86dd941cea86b1572e23c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 14 Oct 2021 09:47:13 +0300 Subject: [PATCH 0323/1038] Use HTTPStatus instead of HTTP_* int constants in mobile_app responses (#56418) --- .../components/mobile_app/helpers.py | 23 +++++++++++-------- .../components/mobile_app/webhook.py | 9 ++++---- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index 2325a75e630..6b6b9b51d13 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Callable +from http import HTTPStatus import json import logging @@ -9,12 +10,7 @@ from aiohttp.web import Response, json_response from nacl.encoding import Base64Encoder from nacl.secret import SecretBox -from homeassistant.const import ( - ATTR_DEVICE_ID, - CONTENT_TYPE_JSON, - HTTP_BAD_REQUEST, - HTTP_OK, -) +from homeassistant.const import ATTR_DEVICE_ID, CONTENT_TYPE_JSON from homeassistant.core import Context, HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.json import JSONEncoder @@ -95,7 +91,9 @@ def registration_context(registration: dict) -> Context: return Context(user_id=registration[CONF_USER_ID]) -def empty_okay_response(headers: dict = None, status: int = HTTP_OK) -> Response: +def empty_okay_response( + headers: dict = None, status: HTTPStatus = HTTPStatus.OK +) -> Response: """Return a Response with empty JSON object and a 200.""" return Response( text="{}", status=status, content_type=CONTENT_TYPE_JSON, headers=headers @@ -103,7 +101,10 @@ def empty_okay_response(headers: dict = None, status: int = HTTP_OK) -> Response def error_response( - code: str, message: str, status: int = HTTP_BAD_REQUEST, headers: dict = None + code: str, + message: str, + status: HTTPStatus = HTTPStatus.BAD_REQUEST, + headers: dict = None, ) -> Response: """Return an error Response.""" return json_response( @@ -147,7 +148,11 @@ def savable_state(hass: HomeAssistant) -> dict: def webhook_response( - data, *, registration: dict, status: int = HTTP_OK, headers: dict = None + data, + *, + registration: dict, + status: HTTPStatus = HTTPStatus.OK, + headers: dict = None, ) -> Response: """Return a encrypted response if registration supports it.""" data = json.dumps(data, cls=JSONEncoder) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 99bb153f3ee..eb2d64114b3 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -2,6 +2,7 @@ import asyncio from contextlib import suppress from functools import wraps +from http import HTTPStatus import logging import secrets @@ -30,8 +31,6 @@ from homeassistant.const import ( ATTR_SERVICE_DATA, ATTR_SUPPORTED_FEATURES, CONF_WEBHOOK_ID, - HTTP_BAD_REQUEST, - HTTP_CREATED, ) from homeassistant.core import EventOrigin, HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceNotFound @@ -158,7 +157,7 @@ async def handle_webhook( req_data = await request.json() except ValueError: _LOGGER.warning("Received invalid JSON from mobile_app device: %s", device_name) - return empty_okay_response(status=HTTP_BAD_REQUEST) + return empty_okay_response(status=HTTPStatus.BAD_REQUEST) if ( ATTR_WEBHOOK_ENCRYPTED not in req_data @@ -265,7 +264,7 @@ async def webhook_stream_camera(hass, config_entry, data): return webhook_response( {"success": False}, registration=config_entry.data, - status=HTTP_BAD_REQUEST, + status=HTTPStatus.BAD_REQUEST, ) resp = {"mjpeg_path": f"/api/camera_proxy_stream/{camera.entity_id}"} @@ -435,7 +434,7 @@ async def webhook_register_sensor(hass, config_entry, data): return webhook_response( {"success": True}, registration=config_entry.data, - status=HTTP_CREATED, + status=HTTPStatus.CREATED, ) From f62cadf32c31c807b490d3ec578264b7d52cc10d Mon Sep 17 00:00:00 2001 From: Jason Madigan Date: Thu, 14 Oct 2021 07:54:16 +0100 Subject: [PATCH 0324/1038] Use reference strings in soma (#57564) --- homeassistant/components/soma/strings.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/soma/strings.json b/homeassistant/components/soma/strings.json index a31b404dad7..7181698db40 100644 --- a/homeassistant/components/soma/strings.json +++ b/homeassistant/components/soma/strings.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "already_setup": "You can only configure one Soma account.", - "authorize_url_timeout": "Timeout generating authorize URL.", + "already_setup": "[%key:common::config_flow::abort::single_instance_allowed%]", + "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "missing_configuration": "The Soma component is not configured. Please follow the documentation.", "result_error": "SOMA Connect responded with error status.", - "connection_error": "Failed to connect to SOMA Connect." + "connection_error": "[%key:common::config_flow::error::cannot_connect%]" }, "create_entry": { - "default": "Successfully authenticated with Soma." + "default": "[%key:common::config_flow::create_entry::authenticated%]" }, "step": { "user": { From 5cd21679249b099f52091e6f6d17e97f18c499e7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Oct 2021 09:29:31 +0200 Subject: [PATCH 0325/1038] Upgrade pyyaml to 6.0 (#57648) --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7a9875f05aa..762d0fd47ba 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -25,7 +25,7 @@ pip>=8.0.3,<20.3 pyserial==3.5 python-slugify==4.0.1 pyudev==0.22.0 -pyyaml==5.4.1 +pyyaml==6.0 requests==2.26.0 scapy==2.4.5 sqlalchemy==1.4.23 diff --git a/requirements.txt b/requirements.txt index cd0a920111d..abe90479f01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ PyJWT==2.1.0 cryptography==3.4.8 pip>=8.0.3,<20.3 python-slugify==4.0.1 -pyyaml==5.4.1 +pyyaml==6.0 requests==2.26.0 voluptuous==0.12.2 voluptuous-serialize==2.4.0 diff --git a/setup.py b/setup.py index f2f17f67c75..38b05a6f0f3 100755 --- a/setup.py +++ b/setup.py @@ -48,7 +48,7 @@ REQUIRES = [ "cryptography==3.4.8", "pip>=8.0.3,<20.3", "python-slugify==4.0.1", - "pyyaml==5.4.1", + "pyyaml==6.0", "requests==2.26.0", "voluptuous==0.12.2", "voluptuous-serialize==2.4.0", From 24509503bb3d7b60df3d77fc2b7643ad3505dc85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 14 Oct 2021 10:00:44 +0200 Subject: [PATCH 0326/1038] Remove snapshot from hassio integration (#57652) --- homeassistant/components/hassio/__init__.py | 34 +------------- homeassistant/components/hassio/const.py | 1 - homeassistant/components/hassio/http.py | 9 +--- homeassistant/components/hassio/services.yaml | 46 ------------------- tests/components/hassio/test_init.py | 27 ++--------- 5 files changed, 7 insertions(+), 110 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index eacf5be5f9f..6a6a0143bf2 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -43,7 +43,6 @@ from .const import ( ATTR_PASSWORD, ATTR_REPOSITORY, ATTR_SLUG, - ATTR_SNAPSHOT, ATTR_URL, ATTR_VERSION, DOMAIN, @@ -87,8 +86,6 @@ SERVICE_ADDON_UPDATE = "addon_update" SERVICE_ADDON_STDIN = "addon_stdin" SERVICE_HOST_SHUTDOWN = "host_shutdown" SERVICE_HOST_REBOOT = "host_reboot" -SERVICE_SNAPSHOT_FULL = "snapshot_full" -SERVICE_SNAPSHOT_PARTIAL = "snapshot_partial" SERVICE_BACKUP_FULL = "backup_full" SERVICE_BACKUP_PARTIAL = "backup_partial" SERVICE_RESTORE_FULL = "restore_full" @@ -116,11 +113,9 @@ SCHEMA_BACKUP_PARTIAL = SCHEMA_BACKUP_FULL.extend( SCHEMA_RESTORE_FULL = vol.Schema( { - vol.Exclusive(ATTR_SLUG, ATTR_SLUG): cv.slug, - vol.Exclusive(ATTR_SNAPSHOT, ATTR_SLUG): cv.slug, + vol.Required(ATTR_SLUG): cv.slug, vol.Optional(ATTR_PASSWORD): cv.string, - }, - cv.has_at_least_one_key(ATTR_SLUG, ATTR_SNAPSHOT), + } ) SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend( @@ -175,18 +170,6 @@ MAP_SERVICE_API = { None, True, ), - SERVICE_SNAPSHOT_FULL: APIEndpointSettings( - "/backups/new/full", - SCHEMA_BACKUP_FULL, - None, - True, - ), - SERVICE_SNAPSHOT_PARTIAL: APIEndpointSettings( - "/backups/new/partial", - SCHEMA_BACKUP_PARTIAL, - None, - True, - ), } @@ -489,22 +472,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: """Handle service calls for Hass.io.""" api_endpoint = MAP_SERVICE_API[service.service] - if "snapshot" in service.service: - _LOGGER.warning( - "The service '%s' is deprecated and will be removed in Home Assistant 2021.11, use '%s' instead", - service.service, - service.service.replace("snapshot", "backup"), - ) data = service.data.copy() addon = data.pop(ATTR_ADDON, None) slug = data.pop(ATTR_SLUG, None) - snapshot = data.pop(ATTR_SNAPSHOT, None) - if snapshot is not None: - _LOGGER.warning( - "Using 'snapshot' is deprecated and will be removed in Home Assistant 2021.11, use 'slug' instead" - ) - slug = snapshot - payload = None # Pass data to Hass.io API diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index 134fba15f70..d78829e0fda 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -15,7 +15,6 @@ ATTR_HOMEASSISTANT = "homeassistant" ATTR_INPUT = "input" ATTR_PANELS = "panels" ATTR_PASSWORD = "password" -ATTR_SNAPSHOT = "snapshot" ATTR_TITLE = "title" ATTR_USERNAME = "username" ATTR_UUID = "uuid" diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index d25a65afee1..2012725c7f4 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -38,15 +38,10 @@ NO_TIMEOUT = re.compile( r"|backups/.+/full" r"|backups/.+/partial" r"|backups/[^/]+/(?:upload|download)" - r"|snapshots/.+/full" - r"|snapshots/.+/partial" - r"|snapshots/[^/]+/(?:upload|download)" r")$" ) -NO_AUTH_ONBOARDING = re.compile( - r"^(?:" r"|supervisor/logs" r"|backups/[^/]+/.+" r"|snapshots/[^/]+/.+" r")$" -) +NO_AUTH_ONBOARDING = re.compile(r"^(?:" r"|supervisor/logs" r"|backups/[^/]+/.+" r")$") NO_AUTH = re.compile( r"^(?:" r"|app/.*" r"|addons/[^/]+/logo" r"|addons/[^/]+/icon" r")$" @@ -89,7 +84,7 @@ class HassIOView(HomeAssistantView): This method is a coroutine. """ headers = _init_header(request) - if path in ("snapshots/new/upload", "backups/new/upload"): + if path == "backups/new/upload": # We need to reuse the full content type that includes the boundary headers[ "Content-Type" diff --git a/homeassistant/components/hassio/services.yaml b/homeassistant/components/hassio/services.yaml index 3e5736c3593..d7137aad2ab 100644 --- a/homeassistant/components/hassio/services.yaml +++ b/homeassistant/components/hassio/services.yaml @@ -66,52 +66,6 @@ host_shutdown: name: Poweroff the host system. description: Poweroff the host system. -snapshot_full: - name: Create a full backup. - description: Create a full backup (deprecated, use backup_full instead). - fields: - name: - name: Name - description: Optional (default = current date and time). - example: "Backup 1" - selector: - text: - password: - name: Password - description: Optional password. - example: "password" - selector: - text: - -snapshot_partial: - name: Create a partial backup. - description: Create a partial backup (deprecated, use backup_partial instead). - fields: - addons: - name: Add-ons - description: Optional list of add-on slugs. - example: ["core_ssh", "core_samba", "core_mosquitto"] - selector: - object: - folders: - name: Folders - description: Optional list of directories. - example: ["homeassistant", "share"] - selector: - object: - name: - name: Name - description: Optional (default = current date and time). - example: "Partial backup 1" - selector: - text: - password: - name: Password - description: Optional password. - example: "password" - selector: - text: - backup_full: name: Create a full backup. description: Create a full backup. diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 6e62545ec68..cfa457695ac 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -301,8 +301,6 @@ async def test_service_register(hassio_env, hass): assert hass.services.has_service("hassio", "host_shutdown") assert hass.services.has_service("hassio", "host_reboot") assert hass.services.has_service("hassio", "host_reboot") - assert hass.services.has_service("hassio", "snapshot_full") - assert hass.services.has_service("hassio", "snapshot_partial") assert hass.services.has_service("hassio", "backup_full") assert hass.services.has_service("hassio", "backup_partial") assert hass.services.has_service("hassio", "restore_full") @@ -353,36 +351,17 @@ async def test_service_calls(hassio_env, hass, aioclient_mock, caplog): "backup_partial", {"addons": ["test"], "folders": ["ssl"], "password": "123456"}, ) - await hass.services.async_call("hassio", "snapshot_full", {}) - await hass.services.async_call( - "hassio", - "snapshot_partial", - {"addons": ["test"], "folders": ["ssl"]}, - ) await hass.async_block_till_done() - assert ( - "The service 'snapshot_full' is deprecated and will be removed in Home Assistant 2021.11, use 'backup_full' instead" - in caplog.text - ) - assert ( - "The service 'snapshot_partial' is deprecated and will be removed in Home Assistant 2021.11, use 'backup_partial' instead" - in caplog.text - ) - assert aioclient_mock.call_count == 14 - assert aioclient_mock.mock_calls[-3][2] == { + assert aioclient_mock.call_count == 12 + assert aioclient_mock.mock_calls[-1][2] == { "addons": ["test"], "folders": ["ssl"], "password": "123456", } await hass.services.async_call("hassio", "restore_full", {"slug": "test"}) - await hass.services.async_call("hassio", "restore_full", {"snapshot": "test"}) await hass.async_block_till_done() - assert ( - "Using 'snapshot' is deprecated and will be removed in Home Assistant 2021.11, use 'slug' instead" - in caplog.text - ) await hass.services.async_call( "hassio", @@ -397,7 +376,7 @@ async def test_service_calls(hassio_env, hass, aioclient_mock, caplog): ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 17 + assert aioclient_mock.call_count == 14 assert aioclient_mock.mock_calls[-1][2] == { "addons": ["test"], "folders": ["ssl"], From 4b3d423767685bdf04ecc82d7ef1f678453c0f6c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 14 Oct 2021 10:04:26 +0200 Subject: [PATCH 0327/1038] Add config and diagnostic entities (#57528) * Add config entity concept * Rename is_config_entity to entity_category * Add test * Add 'diagnostic' entity category --- .../components/config/entity_registry.py | 1 + homeassistant/const.py | 3 ++ homeassistant/helpers/entity.py | 13 ++++++- homeassistant/helpers/entity_platform.py | 1 + homeassistant/helpers/entity_registry.py | 8 ++++ .../components/config/test_entity_registry.py | 37 ++++++++++++------- tests/helpers/test_entity.py | 20 ++++++++++ 7 files changed, 69 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 43196acf319..1040a500f3c 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -177,6 +177,7 @@ def _entry_dict(entry): "name": entry.name, "icon": entry.icon, "platform": entry.platform, + "entity_category": entry.entity_category, } diff --git a/homeassistant/const.py b/homeassistant/const.py index 483109737ed..f4387c9bbce 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -704,3 +704,6 @@ CLOUD_NEVER_EXPOSED_ENTITIES: Final[list[str]] = ["group.all_locks"] # The ID of the Home Assistant Cast App CAST_APP_ID_HOMEASSISTANT: Final = "B12CE3CA" + +ENTITY_CATEGORY_CONFIG: Final = "config" +ENTITY_CATEGORY_DIAGNOSTIC: Final = "diagnostic" diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 1d3b331b8ab..584fa4f0554 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -11,7 +11,7 @@ import logging import math import sys from timeit import default_timer as timer -from typing import Any, TypedDict, final +from typing import Any, Literal, TypedDict, final from homeassistant.config import DATA_CUSTOMIZE from homeassistant.const import ( @@ -180,6 +180,7 @@ class EntityDescription: key: str device_class: str | None = None + entity_category: Literal["config", "diagnostic"] | None = None entity_registry_enabled_default: bool = True force_update: bool = False icon: str | None = None @@ -238,6 +239,7 @@ class Entity(ABC): _attr_context_recent_time: timedelta = timedelta(seconds=5) _attr_device_class: str | None _attr_device_info: DeviceInfo | None = None + _attr_entity_category: str | None _attr_entity_picture: str | None = None _attr_entity_registry_enabled_default: bool _attr_extra_state_attributes: MutableMapping[str, Any] @@ -404,6 +406,15 @@ class Entity(ABC): """Return the attribution.""" return self._attr_attribution + @property + def entity_category(self) -> str | None: + """Return the category of the entity, if any.""" + if hasattr(self, "_attr_entity_category"): + return self._attr_entity_category + if hasattr(self, "entity_description"): + return self.entity_description.entity_category + return None + # DO NOT OVERWRITE # These properties and methods are either managed by Home Assistant or they # are used to perform a very specific function. Overwriting these may diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 831f6c9a4b6..5fb095c4f02 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -501,6 +501,7 @@ class EntityPlatform: unit_of_measurement=entity.unit_of_measurement, original_name=entity.name, original_icon=entity.icon, + entity_category=entity.entity_category, ) entity.registry_entry = entry diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 88233b30df7..bedbdc51785 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -106,6 +106,7 @@ class RegistryEntry: # As set by integration original_name: str | None = attr.ib(default=None) original_icon: str | None = attr.ib(default=None) + entity_category: str | None = attr.ib(default=None) domain: str = attr.ib(init=False, repr=False) @domain.default @@ -256,6 +257,7 @@ class EntityRegistry: unit_of_measurement: str | None = None, original_name: str | None = None, original_icon: str | None = None, + entity_category: str | None = None, ) -> RegistryEntry: """Get entity. Create if it doesn't exist.""" config_entry_id = None @@ -276,6 +278,7 @@ class EntityRegistry: unit_of_measurement=unit_of_measurement or UNDEFINED, original_name=original_name or UNDEFINED, original_icon=original_icon or UNDEFINED, + entity_category=entity_category or UNDEFINED, # When we changed our slugify algorithm, we invalidated some # stored entity IDs with either a __ or ending in _. # Fix introduced in 0.86 (Jan 23, 2019). Next line can be @@ -310,6 +313,7 @@ class EntityRegistry: unit_of_measurement=unit_of_measurement, original_name=original_name, original_icon=original_icon, + entity_category=entity_category, ) self._register_entry(entity) _LOGGER.info("Registered new %s.%s entity: %s", domain, platform, entity_id) @@ -418,6 +422,7 @@ class EntityRegistry: unit_of_measurement: str | None | UndefinedType = UNDEFINED, original_name: str | None | UndefinedType = UNDEFINED, original_icon: str | None | UndefinedType = UNDEFINED, + entity_category: str | None | UndefinedType = UNDEFINED, ) -> RegistryEntry: """Private facing update properties method.""" old = self.entities[entity_id] @@ -438,6 +443,7 @@ class EntityRegistry: ("unit_of_measurement", unit_of_measurement), ("original_name", original_name), ("original_icon", original_icon), + ("entity_category", entity_category), ): if value is not UNDEFINED and value != getattr(old, attr_name): new_values[attr_name] = value @@ -523,6 +529,7 @@ class EntityRegistry: unit_of_measurement=entity.get("unit_of_measurement"), original_name=entity.get("original_name"), original_icon=entity.get("original_icon"), + entity_category=entity.get("entity_category"), ) self.entities = entities @@ -555,6 +562,7 @@ class EntityRegistry: "unit_of_measurement": entry.unit_of_measurement, "original_name": entry.original_name, "original_icon": entry.original_icon, + "entity_category": entry.entity_category, } for entry in self.entities.values() ] diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 3d5861c2db3..3faff1222d4 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -1,6 +1,4 @@ """Test entity_registry API.""" -from collections import OrderedDict - import pytest from homeassistant.components.config import entity_registry @@ -31,18 +29,22 @@ def device_registry(hass): async def test_list_entities(hass, client): """Test list entries.""" - entities = OrderedDict() - entities["test_domain.name"] = RegistryEntry( - entity_id="test_domain.name", - unique_id="1234", - platform="test_platform", - name="Hello World", + mock_registry( + hass, + { + "test_domain.name": RegistryEntry( + entity_id="test_domain.name", + unique_id="1234", + platform="test_platform", + name="Hello World", + ), + "test_domain.no_name": RegistryEntry( + entity_id="test_domain.no_name", + unique_id="6789", + platform="test_platform", + ), + }, ) - entities["test_domain.no_name"] = RegistryEntry( - entity_id="test_domain.no_name", unique_id="6789", platform="test_platform" - ) - - mock_registry(hass, entities) await client.send_json({"id": 5, "type": "config/entity_registry/list"}) msg = await client.receive_json() @@ -57,6 +59,7 @@ async def test_list_entities(hass, client): "name": "Hello World", "icon": None, "platform": "test_platform", + "entity_category": None, }, { "config_entry_id": None, @@ -67,6 +70,7 @@ async def test_list_entities(hass, client): "name": None, "icon": None, "platform": "test_platform", + "entity_category": None, }, ] @@ -108,6 +112,7 @@ async def test_get_entity(hass, client): "original_icon": None, "capabilities": None, "unique_id": "1234", + "entity_category": None, } await client.send_json( @@ -132,6 +137,7 @@ async def test_get_entity(hass, client): "original_icon": None, "capabilities": None, "unique_id": "6789", + "entity_category": None, } @@ -187,6 +193,7 @@ async def test_update_entity(hass, client): "original_icon": None, "capabilities": None, "unique_id": "1234", + "entity_category": None, } } @@ -235,6 +242,7 @@ async def test_update_entity(hass, client): "original_icon": None, "capabilities": None, "unique_id": "1234", + "entity_category": None, }, "reload_delay": 30, } @@ -289,6 +297,7 @@ async def test_update_entity_require_restart(hass, client): "original_icon": None, "capabilities": None, "unique_id": "1234", + "entity_category": None, }, "require_restart": True, } @@ -390,6 +399,7 @@ async def test_update_entity_no_changes(hass, client): "original_icon": None, "capabilities": None, "unique_id": "1234", + "entity_category": None, } } @@ -470,6 +480,7 @@ async def test_update_entity_id(hass, client): "original_icon": None, "capabilities": None, "unique_id": "1234", + "entity_category": None, } } diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index bdb7a2782a3..e334e6d2c56 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -809,3 +809,23 @@ async def test_attribution_attribute(hass): state = hass.states.get(mock_entity.entity_id) assert state.attributes.get(ATTR_ATTRIBUTION) == "Home Assistant" + + +async def test_entity_category_property(hass): + """Test entity category property.""" + mock_entity1 = entity.Entity() + mock_entity1.hass = hass + mock_entity1.entity_description = entity.EntityDescription( + key="abc", entity_category="ignore_me" + ) + mock_entity1.entity_id = "hello.world" + mock_entity1._attr_entity_category = "config" + assert mock_entity1.entity_category == "config" + + mock_entity2 = entity.Entity() + mock_entity2.hass = hass + mock_entity2.entity_description = entity.EntityDescription( + key="abc", entity_category="config" + ) + mock_entity2.entity_id = "hello.world" + assert mock_entity2.entity_category == "config" From d3e24cc1d627009e1a8d022e551cc894b3ce19ca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 13 Oct 2021 22:15:29 -1000 Subject: [PATCH 0328/1038] Bump flux_led to 0.24.5 (#57653) - Fixes fallback to old protocol with asyncio - Changelog: https://github.com/Danielhiversen/flux_led/compare/0.24.4...0.24.5 --- 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 81648541106..b3239ff15b2 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.24.4"], + "requirements": ["flux_led==0.24.5"], "codeowners": ["@icemanch"], "iot_class": "local_polling", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 42577015a37..93ff79cd05a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -649,7 +649,7 @@ fjaraskupan==1.0.1 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.4 +flux_led==0.24.5 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 194544f985c..a868f02c168 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -381,7 +381,7 @@ fjaraskupan==1.0.1 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.4 +flux_led==0.24.5 # homeassistant.components.homekit fnvhash==0.1.0 From debcdc382fa882dce65e8905d3e746d092506ad8 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 14 Oct 2021 10:22:21 +0200 Subject: [PATCH 0329/1038] Late review comments. (#57654) --- tests/components/modbus/test_init.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index b4657d7ee73..25e67960f2a 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -672,12 +672,6 @@ async def test_pymodbus_connect_fail(hass, caplog): assert await async_setup_component(hass, DOMAIN, config) is True -# await hass.async_block_till_done() -# await hass.async_block_till_done() -# assert mock_pb.connect.called -# assert ExceptionMessage in caplog.text - - async def test_delay(hass, mock_pymodbus): """Run test for startup delay.""" From 1dcba4419932262ff90c1333adef4d614f06d137 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Oct 2021 10:23:54 +0200 Subject: [PATCH 0330/1038] Migrate attribution attribute for bbox (#57650) --- homeassistant/components/bbox/sensor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index d2c06f45875..d86dd0c243b 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -14,7 +14,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_MONITORED_VARIABLES, CONF_NAME, DATA_RATE_MEGABITS_PER_SECOND, @@ -117,7 +116,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class BboxUptimeSensor(SensorEntity): """Bbox uptime sensor.""" - _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + _attr_attribution = ATTRIBUTION _attr_device_class = DEVICE_CLASS_TIMESTAMP def __init__(self, bbox_data, name, description: SensorEntityDescription): @@ -138,7 +137,7 @@ class BboxUptimeSensor(SensorEntity): class BboxSensor(SensorEntity): """Implementation of a Bbox sensor.""" - _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + _attr_attribution = ATTRIBUTION def __init__(self, bbox_data, name, description: SensorEntityDescription): """Initialize the sensor.""" From f43bba8cfdfcf7de102a33de13f04bb2b0bbffee Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Oct 2021 10:24:30 +0200 Subject: [PATCH 0331/1038] Migrate attribution attribute for bitcoin (#57651) --- homeassistant/components/bitcoin/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/bitcoin/sensor.py b/homeassistant/components/bitcoin/sensor.py index b66f775eae2..553d0aafa05 100644 --- a/homeassistant/components/bitcoin/sensor.py +++ b/homeassistant/components/bitcoin/sensor.py @@ -13,7 +13,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_CURRENCY, CONF_DISPLAY_OPTIONS, TIME_MINUTES, @@ -165,7 +164,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class BitcoinSensor(SensorEntity): """Representation of a Bitcoin sensor.""" - _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + _attr_attribution = ATTRIBUTION _attr_icon = ICON def __init__(self, data, currency, description: SensorEntityDescription): From 2ec352ce96152e9c373a76b5b09dccd48bf1c3c4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Oct 2021 10:25:19 +0200 Subject: [PATCH 0332/1038] Migrate attribution attribute for Aftership (#57649) --- homeassistant/components/aftership/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/aftership/sensor.py b/homeassistant/components/aftership/sensor.py index a3b41f8314c..f7d89767d54 100644 --- a/homeassistant/components/aftership/sensor.py +++ b/homeassistant/components/aftership/sensor.py @@ -11,7 +11,7 @@ from homeassistant.components.sensor import ( PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, SensorEntity, ) -from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME, HTTP_OK +from homeassistant.const import CONF_API_KEY, CONF_NAME, HTTP_OK from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -109,6 +109,7 @@ async def async_setup_platform( class AfterShipSensor(SensorEntity): """Representation of a AfterShip sensor.""" + _attr_attribution = ATTRIBUTION _attr_native_unit_of_measurement: str = "packages" _attr_icon: str = ICON @@ -191,7 +192,6 @@ class AfterShipSensor(SensorEntity): _LOGGER.debug("Ignoring %s as it has status: %s", name, status) self._attributes = { - ATTR_ATTRIBUTION: ATTRIBUTION, **status_counts, ATTR_TRACKINGS: trackings, } From b28062789f1e0b763e44755a5c9be1f4aa7b6924 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Oct 2021 11:27:09 +0200 Subject: [PATCH 0333/1038] Remove deprecated Lyft integration (#57638) --- .coveragerc | 1 - homeassistant/components/lyft/__init__.py | 1 - homeassistant/components/lyft/manifest.json | 8 - homeassistant/components/lyft/sensor.py | 259 -------------------- requirements_all.txt | 3 - 5 files changed, 272 deletions(-) delete mode 100644 homeassistant/components/lyft/__init__.py delete mode 100644 homeassistant/components/lyft/manifest.json delete mode 100644 homeassistant/components/lyft/sensor.py diff --git a/.coveragerc b/.coveragerc index d09bab53760..cb65ae85c2e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -601,7 +601,6 @@ omit = homeassistant/components/lutron_caseta/scene.py homeassistant/components/lutron_caseta/switch.py homeassistant/components/lw12wifi/light.py - homeassistant/components/lyft/sensor.py homeassistant/components/lyric/__init__.py homeassistant/components/lyric/api.py homeassistant/components/lyric/climate.py diff --git a/homeassistant/components/lyft/__init__.py b/homeassistant/components/lyft/__init__.py deleted file mode 100644 index a7ffe972cc9..00000000000 --- a/homeassistant/components/lyft/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The lyft component.""" diff --git a/homeassistant/components/lyft/manifest.json b/homeassistant/components/lyft/manifest.json deleted file mode 100644 index 784ffa30d6e..00000000000 --- a/homeassistant/components/lyft/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "lyft", - "name": "Lyft", - "documentation": "https://www.home-assistant.io/integrations/lyft", - "requirements": ["lyft_rides==0.2"], - "codeowners": [], - "iot_class": "cloud_polling" -} diff --git a/homeassistant/components/lyft/sensor.py b/homeassistant/components/lyft/sensor.py deleted file mode 100644 index 84e3744a0e2..00000000000 --- a/homeassistant/components/lyft/sensor.py +++ /dev/null @@ -1,259 +0,0 @@ -"""Support for the Lyft API.""" -from datetime import timedelta -import logging - -from lyft_rides.auth import ClientCredentialGrant -from lyft_rides.client import LyftRidesClient -from lyft_rides.errors import APIError -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, TIME_MINUTES -import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -CONF_END_LATITUDE = "end_latitude" -CONF_END_LONGITUDE = "end_longitude" -CONF_PRODUCT_IDS = "product_ids" -CONF_START_LATITUDE = "start_latitude" -CONF_START_LONGITUDE = "start_longitude" - -ICON = "mdi:taxi" - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - vol.Optional(CONF_START_LATITUDE): cv.latitude, - vol.Optional(CONF_START_LONGITUDE): cv.longitude, - vol.Optional(CONF_END_LATITUDE): cv.latitude, - vol.Optional(CONF_END_LONGITUDE): cv.longitude, - vol.Optional(CONF_PRODUCT_IDS): vol.All(cv.ensure_list, [cv.string]), - } -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Lyft sensor.""" - _LOGGER.warning( - "The Lyft integration has been deprecated and will be removed in " - "Home Assistant Core 2021.10" - ) - - auth_flow = ClientCredentialGrant( - client_id=config.get(CONF_CLIENT_ID), - client_secret=config.get(CONF_CLIENT_SECRET), - scopes="public", - is_sandbox_mode=False, - ) - try: - session = auth_flow.get_session() - - timeandpriceest = LyftEstimate( - session, - config.get(CONF_START_LATITUDE, hass.config.latitude), - config.get(CONF_START_LONGITUDE, hass.config.longitude), - config.get(CONF_END_LATITUDE), - config.get(CONF_END_LONGITUDE), - ) - timeandpriceest.fetch_data() - except APIError as exc: - _LOGGER.error("Error setting up Lyft platform: %s", exc) - return False - - wanted_product_ids = config.get(CONF_PRODUCT_IDS) - - dev = [] - for product_id, product in timeandpriceest.products.items(): - if (wanted_product_ids is not None) and (product_id not in wanted_product_ids): - continue - dev.append(LyftSensor("time", timeandpriceest, product_id, product)) - if product.get("estimate") is not None: - dev.append(LyftSensor("price", timeandpriceest, product_id, product)) - add_entities(dev, True) - - -class LyftSensor(SensorEntity): - """Implementation of an Lyft sensor.""" - - def __init__(self, sensorType, products, product_id, product): - """Initialize the Lyft sensor.""" - self.data = products - self._product_id = product_id - self._product = product - self._sensortype = sensorType - self._name = f"{self._product['display_name']} {self._sensortype}" - if "lyft" not in self._name.lower(): - self._name = f"Lyft{self._name}" - if self._sensortype == "time": - self._unit_of_measurement = TIME_MINUTES - elif self._sensortype == "price": - estimate = self._product["estimate"] - if estimate is not None: - self._unit_of_measurement = estimate.get("currency") - self._state = None - - @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 native_unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - params = { - "Product ID": self._product["ride_type"], - "Product display name": self._product["display_name"], - "Vehicle Capacity": self._product["seats"], - } - - if self._product.get("pricing_details") is not None: - pricing_details = self._product["pricing_details"] - params["Base price"] = pricing_details.get("base_charge") - params["Cancellation fee"] = pricing_details.get("cancel_penalty_amount") - params["Minimum price"] = pricing_details.get("cost_minimum") - params["Cost per mile"] = pricing_details.get("cost_per_mile") - params["Cost per minute"] = pricing_details.get("cost_per_minute") - params["Price currency code"] = pricing_details.get("currency") - params["Service fee"] = pricing_details.get("trust_and_service") - - if self._product.get("estimate") is not None: - estimate = self._product["estimate"] - params["Trip distance (in miles)"] = estimate.get( - "estimated_distance_miles" - ) - params["High price estimate (in cents)"] = estimate.get( - "estimated_cost_cents_max" - ) - params["Low price estimate (in cents)"] = estimate.get( - "estimated_cost_cents_min" - ) - params["Trip duration (in seconds)"] = estimate.get( - "estimated_duration_seconds" - ) - - params["Prime Time percentage"] = estimate.get("primetime_percentage") - - if self._product.get("eta") is not None: - eta = self._product["eta"] - params["Pickup time estimate (in seconds)"] = eta.get("eta_seconds") - - return {k: v for k, v in params.items() if v is not None} - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return ICON - - def update(self): - """Get the latest data from the Lyft API and update the states.""" - self.data.update() - try: - self._product = self.data.products[self._product_id] - except KeyError: - return - self._state = None - if self._sensortype == "time": - eta = self._product["eta"] - if (eta is not None) and (eta.get("is_valid_estimate")): - time_estimate = eta.get("eta_seconds") - if time_estimate is None: - return - self._state = int(time_estimate / 60) - elif self._sensortype == "price": - estimate = self._product["estimate"] - if (estimate is not None) and estimate.get("is_valid_estimate"): - self._state = ( - int( - ( - estimate.get("estimated_cost_cents_min", 0) - + estimate.get("estimated_cost_cents_max", 0) - ) - / 2 - ) - / 100 - ) - - -class LyftEstimate: - """The class for handling the time and price estimate.""" - - def __init__( - self, - session, - start_latitude, - start_longitude, - end_latitude=None, - end_longitude=None, - ): - """Initialize the LyftEstimate object.""" - self._session = session - self.start_latitude = start_latitude - self.start_longitude = start_longitude - self.end_latitude = end_latitude - self.end_longitude = end_longitude - self.products = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest product info and estimates from the Lyft API.""" - - try: - self.fetch_data() - except APIError as exc: - _LOGGER.error("Error fetching Lyft data: %s", exc) - - def fetch_data(self): - """Get the latest product info and estimates from the Lyft API.""" - - client = LyftRidesClient(self._session) - - self.products = {} - - products_response = client.get_ride_types( - self.start_latitude, self.start_longitude - ) - - products = products_response.json.get("ride_types") - - for product in products: - self.products[product["ride_type"]] = product - - if self.end_latitude is not None and self.end_longitude is not None: - price_response = client.get_cost_estimates( - self.start_latitude, - self.start_longitude, - self.end_latitude, - self.end_longitude, - ) - - prices = price_response.json.get("cost_estimates", []) - - for price in prices: - product = self.products[price["ride_type"]] - if price.get("is_valid_estimate"): - product["estimate"] = price - - eta_response = client.get_pickup_time_estimates( - self.start_latitude, self.start_longitude - ) - - etas = eta_response.json.get("eta_estimates") - - for eta in etas: - if eta.get("is_valid_estimate"): - self.products[eta["ride_type"]]["eta"] = eta diff --git a/requirements_all.txt b/requirements_all.txt index 93ff79cd05a..1effc0bea1d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -962,9 +962,6 @@ lupupy==0.0.21 # homeassistant.components.lw12wifi lw12==0.9.2 -# homeassistant.components.lyft -lyft_rides==0.2 - # homeassistant.components.nmap_tracker mac-vendor-lookup==0.1.11 From ce186c5935cad0ffef010735655bffcbfbcae6bb Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Thu, 14 Oct 2021 23:43:00 +0800 Subject: [PATCH 0334/1038] Only pass libav logger messages when stream logger is set to debug (#57616) --- homeassistant/components/stream/__init__.py | 29 ++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 1d3a46d0273..d06d60aa48b 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -105,11 +105,34 @@ CONFIG_SCHEMA = vol.Schema( ) +def filter_libav_logging() -> None: + """Filter libav logging to only log when the stream logger is at DEBUG.""" + + stream_debug_enabled = logging.getLogger(__name__).isEnabledFor(logging.DEBUG) + + def libav_filter(record: logging.LogRecord) -> bool: + return stream_debug_enabled + + for logging_namespace in ( + "libav.mp4", + "libav.h264", + "libav.hevc", + "libav.rtsp", + "libav.tcp", + "libav.tls", + "libav.NULL", + ): + logging.getLogger(logging_namespace).addFilter(libav_filter) + + # Set log level to error for libav.mp4 + logging.getLogger("libav.mp4").setLevel(logging.ERROR) + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up stream.""" - # Set log level to error for libav - logging.getLogger("libav").setLevel(logging.ERROR) - logging.getLogger("libav.mp4").setLevel(logging.ERROR) + + # Drop libav log messages if stream logging is above DEBUG + filter_libav_logging() # Keep import here so that we can import stream integration without installing reqs # pylint: disable=import-outside-toplevel From fdc6d9e004d4fcded30063960971b4efb967708a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Oct 2021 18:50:51 +0200 Subject: [PATCH 0335/1038] Add select platform to Tuya (#57674) --- .coveragerc | 1 + homeassistant/components/tuya/const.py | 6 +- homeassistant/components/tuya/select.py | 130 ++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/tuya/select.py diff --git a/.coveragerc b/.coveragerc index cb65ae85c2e..1a5380dd0c9 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1117,6 +1117,7 @@ omit = homeassistant/components/tuya/fan.py homeassistant/components/tuya/light.py homeassistant/components/tuya/scene.py + homeassistant/components/tuya/select.py homeassistant/components/tuya/switch.py homeassistant/components/twentemilieu/const.py homeassistant/components/twentemilieu/sensor.py diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 0efdb10b6a2..51d92b399e3 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -40,6 +40,7 @@ TUYA_SUPPORTED_PRODUCT_CATEGORIES = ( "kg", # Switch "kj", # Air Purifier "kj", # Air Purifier + "kfj", # Coffee maker "kt", # Air conditioner "mcs", # Door Window Sensor "pc", # Power Strip @@ -53,7 +54,7 @@ TUYA_SUPPORTED_PRODUCT_CATEGORIES = ( TUYA_SMART_APP = "tuyaSmart" SMARTLIFE_APP = "smartlife" -PLATFORMS = ["binary_sensor", "climate", "fan", "light", "scene", "switch"] +PLATFORMS = ["binary_sensor", "climate", "fan", "light", "scene", "select", "switch"] class DPCode(str, Enum): @@ -68,6 +69,8 @@ class DPCode(str, Enum): CHILD_LOCK = "child_lock" # Child lock COLOUR_DATA = "colour_data" # Colored light mode COLOUR_DATA_V2 = "colour_data_v2" # Colored light mode + CONCENTRATION_SET = "concentration_set" # Concentration setting + CUP_NUMBER = "cup_number" # NUmber of cups DOORCONTACT_STATE = "doorcontact_state" # Status of door window sensor FAN_DIRECTION = "fan_direction" # Fan direction FAN_SPEED_ENUM = "fan_speed_enum" # Speed mode @@ -77,6 +80,7 @@ class DPCode(str, Enum): HUMIDITY_SET = "humidity_set" # Humidity setting LIGHT = "light" # Light LOCK = "lock" # Lock / Child lock + MATERIAL = "material" # Material MODE = "mode" # Working mode / Mode PUMP_RESET = "pump_reset" # Water pump reset SHAKE = "shake" # Oscillating diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py new file mode 100644 index 00000000000..c3b81787086 --- /dev/null +++ b/homeassistant/components/tuya/select.py @@ -0,0 +1,130 @@ +"""Support for Tuya select.""" +from __future__ import annotations + +from typing import cast + +from tuya_iot import TuyaDevice, TuyaDeviceManager +from tuya_iot.device import TuyaDeviceStatusRange + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +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 . import HomeAssistantTuyaData +from .base import EnumTypeData, TuyaEntity +from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode + +# All descriptions can be found here. Mostly the Enum data types in the +# default instructions set of each category end up being a select. +# https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq +SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { + # Coffee maker + # https://developer.tuya.com/en/docs/iot/categorykfj?id=Kaiuz2p12pc7f + "kfj": ( + SelectEntityDescription( + key=DPCode.CUP_NUMBER, + name="Cups", + icon="mdi:numeric", + ), + SelectEntityDescription( + key=DPCode.CONCENTRATION_SET, + name="Concentration", + icon="mdi:altimeter", + ), + SelectEntityDescription( + key=DPCode.MATERIAL, + name="Material", + ), + SelectEntityDescription( + key=DPCode.MODE, + name="Mode", + ), + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Tuya select dynamically through Tuya discovery.""" + hass_data: HomeAssistantTuyaData = hass.data[DOMAIN][entry.entry_id] + + @callback + def async_discover_device(device_ids: list[str]) -> None: + """Discover and add a discovered Tuya select.""" + entities: list[TuyaSelectEntity] = [] + for device_id in device_ids: + device = hass_data.device_manager.device_map[device_id] + if descriptions := SELECTS.get(device.category): + for description in descriptions: + if ( + description.key in device.function + or description.key in device.status + ): + entities.append( + TuyaSelectEntity( + device, hass_data.device_manager, description + ) + ) + + async_add_entities(entities) + + async_discover_device([*hass_data.device_manager.device_map]) + + entry.async_on_unload( + async_dispatcher_connect(hass, TUYA_DISCOVERY_NEW, async_discover_device) + ) + + +class TuyaSelectEntity(TuyaEntity, SelectEntity): + """Tuya Select Entity.""" + + def __init__( + self, + device: TuyaDevice, + device_manager: TuyaDeviceManager, + description: SelectEntityDescription, + ) -> None: + """Init Tuya sensor.""" + super().__init__(device, device_manager) + self.entity_description = description + self._attr_unique_id = f"{super().unique_id}{description.key}" + + self._attr_opions: list[str] = [] + if status_range := device.status_range.get(description.key): + self._status_range = cast(TuyaDeviceStatusRange, status_range) + + # Extract type data from enum status range, + if self._status_range.type == "Enum": + type_data = EnumTypeData.from_json(self._status_range.values) + self._attr_options = type_data.range + + @property + def name(self) -> str | None: + """Return Tuya device name.""" + if self.entity_description.name is not None: + return f"{self.tuya_device.name} {self.entity_description.name}" + return self.tuya_device.name + + @property + def current_option(self) -> str | None: + """Return the selected entity option to represent the entity state.""" + # Raw value + value = self.tuya_device.status.get(self.entity_description.key) + if value is None or value not in self._attr_options: + return None + + return value + + def select_option(self, option: str) -> None: + """Change the selected option.""" + self._send_command( + [ + { + "code": self.entity_description.key, + "value": option, + } + ] + ) From 488a636aecd0d73da5b2b04e3a337dc8c8fb1e2c Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 14 Oct 2021 13:03:38 -0400 Subject: [PATCH 0336/1038] Clean up unused loggers (#57662) --- homeassistant/auth/permissions/__init__.py | 3 --- homeassistant/components/adax/climate.py | 3 --- homeassistant/components/asuswrt/sensor.py | 3 --- homeassistant/components/climacell/sensor.py | 3 --- homeassistant/components/climacell/weather.py | 3 --- homeassistant/components/co2signal/config_flow.py | 3 --- homeassistant/components/econet/climate.py | 4 ---- homeassistant/components/generic_hygrostat/__init__.py | 4 ---- homeassistant/components/gogogate2/cover.py | 4 ---- homeassistant/components/group/binary_sensor.py | 3 --- homeassistant/components/hyperion/camera.py | 3 --- homeassistant/components/ialarm/alarm_control_panel.py | 4 ---- homeassistant/components/keenetic_ndms2/binary_sensor.py | 4 ---- homeassistant/components/kraken/config_flow.py | 3 --- homeassistant/components/litejet/config_flow.py | 3 --- homeassistant/components/litejet/scene.py | 3 --- homeassistant/components/lyric/api.py | 3 --- homeassistant/components/lyric/config_flow.py | 2 -- homeassistant/components/meteoclimatic/sensor.py | 4 ---- homeassistant/components/modbus/__init__.py | 3 --- homeassistant/components/modbus/binary_sensor.py | 2 -- homeassistant/components/modbus/climate.py | 2 -- homeassistant/components/modbus/cover.py | 2 -- homeassistant/components/modbus/fan.py | 2 -- homeassistant/components/modbus/light.py | 2 -- homeassistant/components/modbus/sensor.py | 2 -- homeassistant/components/modbus/switch.py | 2 -- homeassistant/components/motioneye/camera.py | 3 --- homeassistant/components/motioneye/config_flow.py | 3 --- homeassistant/components/mullvad/config_flow.py | 4 ---- homeassistant/components/myq/cover.py | 4 ---- homeassistant/components/myq/light.py | 4 ---- homeassistant/components/mysensors/config_flow.py | 3 --- homeassistant/components/mysensors/cover.py | 3 --- homeassistant/components/netgear/__init__.py | 4 ---- homeassistant/components/netgear/sensor.py | 5 ----- homeassistant/components/ozw/websocket_api.py | 4 ---- homeassistant/components/panasonic_viera/remote.py | 4 ---- homeassistant/components/plaato/binary_sensor.py | 4 ---- homeassistant/components/plaato/config_flow.py | 4 ---- homeassistant/components/screenlogic/binary_sensor.py | 4 ---- homeassistant/components/screenlogic/sensor.py | 4 ---- homeassistant/components/smarttub/__init__.py | 4 ---- homeassistant/components/smarttub/binary_sensor.py | 4 ---- homeassistant/components/smarttub/climate.py | 4 ---- homeassistant/components/smarttub/config_flow.py | 5 ----- homeassistant/components/smarttub/entity.py | 4 ---- homeassistant/components/smarttub/light.py | 4 ---- homeassistant/components/smarttub/sensor.py | 3 --- homeassistant/components/smarttub/switch.py | 4 ---- homeassistant/components/sonarr/__init__.py | 2 -- homeassistant/components/sonos/binary_sensor.py | 3 --- homeassistant/components/sonos/config_flow.py | 4 ---- homeassistant/components/sonos/sensor.py | 4 ---- homeassistant/components/stream/hls.py | 3 --- homeassistant/components/surepetcare/lock.py | 3 --- homeassistant/components/surepetcare/sensor.py | 3 --- homeassistant/components/syncthing/config_flow.py | 4 ---- homeassistant/components/syncthing/sensor.py | 4 ---- homeassistant/components/syncthru/binary_sensor.py | 4 ---- homeassistant/components/unifi/switch.py | 3 --- homeassistant/components/velbus/climate.py | 4 ---- homeassistant/components/velbus/cover.py | 4 ---- homeassistant/components/velbus/light.py | 4 ---- homeassistant/components/velbus/switch.py | 3 --- homeassistant/components/waze_travel_time/__init__.py | 3 --- homeassistant/components/wemo/binary_sensor.py | 3 --- homeassistant/components/wemo/fan.py | 3 --- homeassistant/components/wemo/light.py | 3 --- homeassistant/components/wemo/switch.py | 3 --- homeassistant/components/xiaomi_miio/binary_sensor.py | 4 ---- homeassistant/components/zha/alarm_control_panel.py | 4 ---- homeassistant/components/zha/core/channels/security.py | 3 --- homeassistant/components/zha/core/registries.py | 2 -- homeassistant/components/zwave_js/triggers/value_updated.py | 3 --- homeassistant/helpers/entity_platform.py | 3 --- tests/components/climacell/test_config_flow.py | 3 --- tests/components/climacell/test_init.py | 4 ---- tests/components/climacell/test_sensor.py | 2 -- tests/components/climacell/test_weather.py | 3 --- tests/components/hyperion/test_camera.py | 2 -- tests/components/litejet/test_light.py | 4 ---- tests/components/litejet/test_switch.py | 4 ---- tests/components/modbus/conftest.py | 2 -- tests/components/motioneye/test_config_flow.py | 3 --- tests/components/motioneye/test_web_hooks.py | 4 ---- tests/components/netgear/test_config_flow.py | 3 --- tests/components/sia/test_config_flow.py | 4 ---- 88 files changed, 292 deletions(-) diff --git a/homeassistant/auth/permissions/__init__.py b/homeassistant/auth/permissions/__init__.py index 694ea2b7379..101c331b842 100644 --- a/homeassistant/auth/permissions/__init__.py +++ b/homeassistant/auth/permissions/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations from collections.abc import Callable -import logging from typing import Any import voluptuous as vol @@ -16,8 +15,6 @@ from .util import test_all POLICY_SCHEMA = vol.Schema({vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA}) -_LOGGER = logging.getLogger(__name__) - class AbstractPermissions: """Default permissions class.""" diff --git a/homeassistant/components/adax/climate.py b/homeassistant/components/adax/climate.py index 1abd83fdbfc..674329aaf6b 100644 --- a/homeassistant/components/adax/climate.py +++ b/homeassistant/components/adax/climate.py @@ -1,7 +1,6 @@ """Support for Adax wifi-enabled home heaters.""" from __future__ import annotations -import logging from typing import Any from adax import Adax @@ -25,8 +24,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ACCOUNT_ID -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index 5392b419bca..4b865bfb0e3 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations from dataclasses import dataclass -import logging from numbers import Real from homeassistant.components.sensor import ( @@ -114,8 +113,6 @@ CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( ), ) -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities diff --git a/homeassistant/components/climacell/sensor.py b/homeassistant/components/climacell/sensor.py index 1ba5bbe3a34..07bc4790b5f 100644 --- a/homeassistant/components/climacell/sensor.py +++ b/homeassistant/components/climacell/sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations from abc import abstractmethod -import logging from pyclimacell.const import CURRENT @@ -21,8 +20,6 @@ from .const import ( ClimaCellSensorEntityDescription, ) -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py index 865c2baa330..cb0783c6bee 100644 --- a/homeassistant/components/climacell/weather.py +++ b/homeassistant/components/climacell/weather.py @@ -4,7 +4,6 @@ from __future__ import annotations from abc import abstractmethod from collections.abc import Mapping from datetime import datetime -import logging from typing import Any from pyclimacell.const import ( @@ -94,8 +93,6 @@ from .const import ( MAX_FORECASTS, ) -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/co2signal/config_flow.py b/homeassistant/components/co2signal/config_flow.py index e3862d6347c..e7f94e4d603 100644 --- a/homeassistant/components/co2signal/config_flow.py +++ b/homeassistant/components/co2signal/config_flow.py @@ -1,7 +1,6 @@ """Config flow for Co2signal integration.""" from __future__ import annotations -import logging from typing import Any import voluptuous as vol @@ -15,8 +14,6 @@ from . import APIRatelimitExceeded, CO2Error, InvalidAuth, UnknownError, get_dat from .const import CONF_COUNTRY_CODE, DOMAIN from .util import get_extra_name -_LOGGER = logging.getLogger(__name__) - TYPE_USE_HOME = "Use home location" TYPE_SPECIFY_COORDINATES = "Specify coordinates" TYPE_SPECIFY_COUNTRY = "Specify country code" diff --git a/homeassistant/components/econet/climate.py b/homeassistant/components/econet/climate.py index fe50855d559..24bac516466 100644 --- a/homeassistant/components/econet/climate.py +++ b/homeassistant/components/econet/climate.py @@ -1,6 +1,4 @@ """Support for Rheem EcoNet thermostats.""" -import logging - from pyeconet.equipment import EquipmentType from pyeconet.equipment.thermostat import ThermostatFanMode, ThermostatOperationMode @@ -28,8 +26,6 @@ from homeassistant.const import ATTR_TEMPERATURE from . import EcoNetEntity from .const import DOMAIN, EQUIPMENT -_LOGGER = logging.getLogger(__name__) - ECONET_STATE_TO_HA = { ThermostatOperationMode.HEATING: HVAC_MODE_HEAT, ThermostatOperationMode.COOLING: HVAC_MODE_COOL, diff --git a/homeassistant/components/generic_hygrostat/__init__.py b/homeassistant/components/generic_hygrostat/__init__.py index 568863adb73..b58c98b1d0d 100644 --- a/homeassistant/components/generic_hygrostat/__init__.py +++ b/homeassistant/components/generic_hygrostat/__init__.py @@ -1,7 +1,5 @@ """The generic_hygrostat component.""" -import logging - import voluptuous as vol from homeassistant.components.humidifier.const import ( @@ -13,8 +11,6 @@ from homeassistant.helpers import config_validation as cv, discovery DOMAIN = "generic_hygrostat" -_LOGGER = logging.getLogger(__name__) - CONF_HUMIDIFIER = "humidifier" CONF_SENSOR = "target_sensor" CONF_MIN_HUMIDITY = "min_humidity" diff --git a/homeassistant/components/gogogate2/cover.py b/homeassistant/components/gogogate2/cover.py index 073c48e55b8..fb5871e8636 100644 --- a/homeassistant/components/gogogate2/cover.py +++ b/homeassistant/components/gogogate2/cover.py @@ -1,8 +1,6 @@ """Support for Gogogate2 garage Doors.""" from __future__ import annotations -import logging - from ismartgate.common import ( AbstractDoor, DoorStatus, @@ -28,8 +26,6 @@ from .common import ( get_data_update_coordinator, ) -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/group/binary_sensor.py b/homeassistant/components/group/binary_sensor.py index 24d6cb86aa1..613b21571de 100644 --- a/homeassistant/components/group/binary_sensor.py +++ b/homeassistant/components/group/binary_sensor.py @@ -1,7 +1,6 @@ """This platform allows several binary sensor to be grouped into one binary sensor.""" from __future__ import annotations -import logging from typing import Any import voluptuous as vol @@ -34,8 +33,6 @@ DEFAULT_NAME = "Binary Sensor Group" CONF_ALL = "all" REG_KEY = f"{BINARY_SENSOR_DOMAIN}_registry" -_LOGGER = logging.getLogger(__name__) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITIES): cv.entities_domain(BINARY_SENSOR_DOMAIN), diff --git a/homeassistant/components/hyperion/camera.py b/homeassistant/components/hyperion/camera.py index 809449543af..d172d00c021 100644 --- a/homeassistant/components/hyperion/camera.py +++ b/homeassistant/components/hyperion/camera.py @@ -8,7 +8,6 @@ import binascii from collections.abc import AsyncGenerator from contextlib import asynccontextmanager import functools -import logging from typing import Any from aiohttp import web @@ -50,8 +49,6 @@ from .const import ( TYPE_HYPERION_CAMERA, ) -_LOGGER = logging.getLogger(__name__) - IMAGE_STREAM_JPG_SENTINEL = "data:image/jpg;base64," diff --git a/homeassistant/components/ialarm/alarm_control_panel.py b/homeassistant/components/ialarm/alarm_control_panel.py index a33162b7afd..fc758dd6175 100644 --- a/homeassistant/components/ialarm/alarm_control_panel.py +++ b/homeassistant/components/ialarm/alarm_control_panel.py @@ -1,6 +1,4 @@ """Interfaces with iAlarm control panels.""" -import logging - from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, @@ -10,8 +8,6 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DATA_COORDINATOR, DOMAIN -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass, entry, async_add_entities) -> None: """Set up a iAlarm alarm control panel based on a config entry.""" diff --git a/homeassistant/components/keenetic_ndms2/binary_sensor.py b/homeassistant/components/keenetic_ndms2/binary_sensor.py index e8f7df02489..aa46c0fab02 100644 --- a/homeassistant/components/keenetic_ndms2/binary_sensor.py +++ b/homeassistant/components/keenetic_ndms2/binary_sensor.py @@ -1,6 +1,4 @@ """The Keenetic Client class.""" -import logging - from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, BinarySensorEntity, @@ -12,8 +10,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import KeeneticRouter from .const import DOMAIN, ROUTER -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities diff --git a/homeassistant/components/kraken/config_flow.py b/homeassistant/components/kraken/config_flow.py index 68443705767..b821f7a5920 100644 --- a/homeassistant/components/kraken/config_flow.py +++ b/homeassistant/components/kraken/config_flow.py @@ -1,7 +1,6 @@ """Config flow for kraken integration.""" from __future__ import annotations -import logging from typing import Any import krakenex @@ -17,8 +16,6 @@ from homeassistant.helpers import config_validation as cv from .const import CONF_TRACKED_ASSET_PAIRS, DEFAULT_SCAN_INTERVAL, DOMAIN from .utils import get_tradable_asset_pairs -_LOGGER = logging.getLogger(__name__) - class KrakenConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for kraken.""" diff --git a/homeassistant/components/litejet/config_flow.py b/homeassistant/components/litejet/config_flow.py index 4f8128bd6dc..3e5422518f4 100644 --- a/homeassistant/components/litejet/config_flow.py +++ b/homeassistant/components/litejet/config_flow.py @@ -1,7 +1,6 @@ """Config flow for the LiteJet lighting system.""" from __future__ import annotations -import logging from typing import Any import pylitejet @@ -16,8 +15,6 @@ import homeassistant.helpers.config_validation as cv from .const import CONF_DEFAULT_TRANSITION, DOMAIN -_LOGGER = logging.getLogger(__name__) - class LiteJetOptionsFlow(config_entries.OptionsFlow): """Handle LiteJet options.""" diff --git a/homeassistant/components/litejet/scene.py b/homeassistant/components/litejet/scene.py index 5ae0aec9559..2f2ab244e1e 100644 --- a/homeassistant/components/litejet/scene.py +++ b/homeassistant/components/litejet/scene.py @@ -1,13 +1,10 @@ """Support for LiteJet scenes.""" -import logging from typing import Any from homeassistant.components.scene import Scene from .const import DOMAIN -_LOGGER = logging.getLogger(__name__) - ATTR_NUMBER = "number" diff --git a/homeassistant/components/lyric/api.py b/homeassistant/components/lyric/api.py index 4d955165174..3b23f802ded 100644 --- a/homeassistant/components/lyric/api.py +++ b/homeassistant/components/lyric/api.py @@ -1,5 +1,4 @@ """API for Honeywell Lyric bound to Home Assistant OAuth.""" -import logging from typing import cast from aiohttp import BasicAuth, ClientSession @@ -8,8 +7,6 @@ from aiolyric.client import LyricClient from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession -_LOGGER = logging.getLogger(__name__) - class ConfigEntryLyricClient(LyricClient): """Provide Honeywell Lyric authentication tied to an OAuth2 based config entry.""" diff --git a/homeassistant/components/lyric/config_flow.py b/homeassistant/components/lyric/config_flow.py index 73040d51e1e..38341113206 100644 --- a/homeassistant/components/lyric/config_flow.py +++ b/homeassistant/components/lyric/config_flow.py @@ -7,8 +7,6 @@ from homeassistant.helpers import config_entry_oauth2_flow from .const import DOMAIN -_LOGGER = logging.getLogger(__name__) - class OAuth2FlowHandler( config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN diff --git a/homeassistant/components/meteoclimatic/sensor.py b/homeassistant/components/meteoclimatic/sensor.py index d3ecb44ce70..e4f7a1525d5 100644 --- a/homeassistant/components/meteoclimatic/sensor.py +++ b/homeassistant/components/meteoclimatic/sensor.py @@ -1,6 +1,4 @@ """Support for Meteoclimatic sensor.""" -import logging - from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION @@ -12,8 +10,6 @@ from homeassistant.helpers.update_coordinator import ( from .const import ATTRIBUTION, DOMAIN, MANUFACTURER, MODEL, SENSOR_TYPES -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index b7a1e9db8e7..720e57605ce 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -1,7 +1,6 @@ """Support for Modbus.""" from __future__ import annotations -import logging from typing import cast import voluptuous as vol @@ -129,8 +128,6 @@ from .validators import ( struct_validator, ) -_LOGGER = logging.getLogger(__name__) - BASE_SCHEMA = vol.Schema({vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string}) diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index d3a8578f47d..171486639f4 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations from datetime import datetime -import logging from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import CONF_BINARY_SENSORS, CONF_NAME, STATE_ON @@ -15,7 +14,6 @@ from . import get_hub from .base_platform import BasePlatform PARALLEL_UPDATES = 1 -_LOGGER = logging.getLogger(__name__) async def async_setup_platform( diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 1b1a09cf9cb..a526e7cc2f9 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -2,7 +2,6 @@ from __future__ import annotations from datetime import datetime -import logging import struct from typing import Any @@ -45,7 +44,6 @@ from .const import ( from .modbus import ModbusHub PARALLEL_UPDATES = 1 -_LOGGER = logging.getLogger(__name__) async def async_setup_platform( diff --git a/homeassistant/components/modbus/cover.py b/homeassistant/components/modbus/cover.py index 805f07bad40..152239e88c7 100644 --- a/homeassistant/components/modbus/cover.py +++ b/homeassistant/components/modbus/cover.py @@ -2,7 +2,6 @@ from __future__ import annotations from datetime import datetime -import logging from typing import Any from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN, CoverEntity @@ -37,7 +36,6 @@ from .const import ( from .modbus import ModbusHub PARALLEL_UPDATES = 1 -_LOGGER = logging.getLogger(__name__) async def async_setup_platform( diff --git a/homeassistant/components/modbus/fan.py b/homeassistant/components/modbus/fan.py index 349ae0d0619..62532cc93ac 100644 --- a/homeassistant/components/modbus/fan.py +++ b/homeassistant/components/modbus/fan.py @@ -1,7 +1,6 @@ """Support for Modbus fans.""" from __future__ import annotations -import logging from typing import Any from homeassistant.components.fan import FanEntity @@ -16,7 +15,6 @@ from .const import CONF_FANS from .modbus import ModbusHub PARALLEL_UPDATES = 1 -_LOGGER = logging.getLogger(__name__) async def async_setup_platform( diff --git a/homeassistant/components/modbus/light.py b/homeassistant/components/modbus/light.py index f0f2541ad0f..cc5936050e8 100644 --- a/homeassistant/components/modbus/light.py +++ b/homeassistant/components/modbus/light.py @@ -1,7 +1,6 @@ """Support for Modbus lights.""" from __future__ import annotations -import logging from typing import Any from homeassistant.components.light import LightEntity @@ -15,7 +14,6 @@ from .base_platform import BaseSwitch from .modbus import ModbusHub PARALLEL_UPDATES = 1 -_LOGGER = logging.getLogger(__name__) async def async_setup_platform( diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 6702e6f22d1..4cf642ca44b 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations from datetime import datetime -import logging from typing import Any from homeassistant.components.sensor import CONF_STATE_CLASS, SensorEntity @@ -17,7 +16,6 @@ from .base_platform import BaseStructPlatform from .modbus import ModbusHub PARALLEL_UPDATES = 1 -_LOGGER = logging.getLogger(__name__) async def async_setup_platform( diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 86cba7c36ff..5844daf648e 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -1,7 +1,6 @@ """Support for Modbus switches.""" from __future__ import annotations -import logging from typing import Any from homeassistant.components.switch import SwitchEntity @@ -15,7 +14,6 @@ from .base_platform import BaseSwitch from .modbus import ModbusHub PARALLEL_UPDATES = 1 -_LOGGER = logging.getLogger(__name__) async def async_setup_platform( diff --git a/homeassistant/components/motioneye/camera.py b/homeassistant/components/motioneye/camera.py index e7ff75812f6..3e8df99e0aa 100644 --- a/homeassistant/components/motioneye/camera.py +++ b/homeassistant/components/motioneye/camera.py @@ -1,7 +1,6 @@ """The motionEye integration.""" from __future__ import annotations -import logging from types import MappingProxyType from typing import Any @@ -49,8 +48,6 @@ from .const import ( TYPE_MOTIONEYE_MJPEG_CAMERA, ) -_LOGGER = logging.getLogger(__name__) - PLATFORMS = ["camera"] diff --git a/homeassistant/components/motioneye/config_flow.py b/homeassistant/components/motioneye/config_flow.py index a5f92a3ce09..a767cf7ecad 100644 --- a/homeassistant/components/motioneye/config_flow.py +++ b/homeassistant/components/motioneye/config_flow.py @@ -1,7 +1,6 @@ """Config flow for motionEye integration.""" from __future__ import annotations -import logging from typing import Any, Dict, cast from motioneye_client.client import ( @@ -36,8 +35,6 @@ from .const import ( DOMAIN, ) -_LOGGER = logging.getLogger(__name__) - class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for motionEye.""" diff --git a/homeassistant/components/mullvad/config_flow.py b/homeassistant/components/mullvad/config_flow.py index 1b330d4f6a3..5b6ef78133f 100644 --- a/homeassistant/components/mullvad/config_flow.py +++ b/homeassistant/components/mullvad/config_flow.py @@ -1,14 +1,10 @@ """Config flow for Mullvad VPN integration.""" -import logging - from mullvad_api import MullvadAPI, MullvadAPIError from homeassistant import config_entries from .const import DOMAIN -_LOGGER = logging.getLogger(__name__) - class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Mullvad VPN.""" diff --git a/homeassistant/components/myq/cover.py b/homeassistant/components/myq/cover.py index e8e06dc3b22..4379137b3f1 100644 --- a/homeassistant/components/myq/cover.py +++ b/homeassistant/components/myq/cover.py @@ -1,6 +1,4 @@ """Support for MyQ-Enabled Garage Doors.""" -import logging - from pymyq.const import DEVICE_TYPE_GATE as MYQ_DEVICE_TYPE_GATE from pymyq.errors import MyQError @@ -17,8 +15,6 @@ from homeassistant.exceptions import HomeAssistantError from . import MyQEntity from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY, MYQ_TO_HASS -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up mysq covers.""" diff --git a/homeassistant/components/myq/light.py b/homeassistant/components/myq/light.py index d8154d7c427..fb3666b62b7 100644 --- a/homeassistant/components/myq/light.py +++ b/homeassistant/components/myq/light.py @@ -1,6 +1,4 @@ """Support for MyQ-Enabled lights.""" -import logging - from pymyq.errors import MyQError from homeassistant.components.light import LightEntity @@ -10,8 +8,6 @@ from homeassistant.exceptions import HomeAssistantError from . import MyQEntity from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY, MYQ_TO_HASS -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up myq lights.""" diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py index 920cb40b7ab..aea15e7b8ae 100644 --- a/homeassistant/components/mysensors/config_flow.py +++ b/homeassistant/components/mysensors/config_flow.py @@ -1,7 +1,6 @@ """Config flow for MySensors.""" from __future__ import annotations -import logging import os from typing import Any @@ -46,8 +45,6 @@ from .const import ( ) from .gateway import MQTT_COMPONENT, is_serial_port, is_socket_address, try_connect -_LOGGER = logging.getLogger(__name__) - def _get_schema_common(user_input: dict[str, str]) -> dict: """Create a schema with options common to all gateway types.""" diff --git a/homeassistant/components/mysensors/cover.py b/homeassistant/components/mysensors/cover.py index bab7a07a867..9219097bea4 100644 --- a/homeassistant/components/mysensors/cover.py +++ b/homeassistant/components/mysensors/cover.py @@ -2,7 +2,6 @@ from __future__ import annotations from enum import Enum, unique -import logging from typing import Any from homeassistant.components import mysensors @@ -16,8 +15,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .helpers import on_unload -_LOGGER = logging.getLogger(__name__) - @unique class CoverState(Enum): diff --git a/homeassistant/components/netgear/__init__.py b/homeassistant/components/netgear/__init__.py index 395773c5fe3..301fb780c1b 100644 --- a/homeassistant/components/netgear/__init__.py +++ b/homeassistant/components/netgear/__init__.py @@ -1,6 +1,4 @@ """Support for Netgear routers.""" -import logging - from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -11,8 +9,6 @@ from .const import DOMAIN, PLATFORMS from .errors import CannotLoginException from .router import NetgearRouter -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up Netgear component.""" diff --git a/homeassistant/components/netgear/sensor.py b/homeassistant/components/netgear/sensor.py index d5b8bfd368e..57ffe6f98f2 100644 --- a/homeassistant/components/netgear/sensor.py +++ b/homeassistant/components/netgear/sensor.py @@ -1,6 +1,4 @@ """Support for Netgear routers.""" -import logging - from homeassistant.components.sensor import ( DEVICE_CLASS_SIGNAL_STRENGTH, SensorEntity, @@ -14,9 +12,6 @@ from homeassistant.helpers.typing import HomeAssistantType from .router import NetgearDeviceEntity, NetgearRouter, async_setup_netgear_entry -_LOGGER = logging.getLogger(__name__) - - SENSOR_TYPES = { "type": SensorEntityDescription( key="type", diff --git a/homeassistant/components/ozw/websocket_api.py b/homeassistant/components/ozw/websocket_api.py index bb55a686db8..45c0a113841 100644 --- a/homeassistant/components/ozw/websocket_api.py +++ b/homeassistant/components/ozw/websocket_api.py @@ -1,6 +1,4 @@ """Web socket API for OpenZWave.""" -import logging - from openzwavemqtt.const import ( ATTR_CODE_SLOT, ATTR_LABEL, @@ -26,8 +24,6 @@ from homeassistant.helpers import config_validation as cv from .const import ATTR_CONFIG_PARAMETER, ATTR_CONFIG_VALUE, DOMAIN, MANAGER from .lock import ATTR_USERCODE -_LOGGER = logging.getLogger(__name__) - DRY_RUN = "dry_run" TYPE = "type" ID = "id" diff --git a/homeassistant/components/panasonic_viera/remote.py b/homeassistant/components/panasonic_viera/remote.py index 8f3fab80215..acd1d49f1be 100644 --- a/homeassistant/components/panasonic_viera/remote.py +++ b/homeassistant/components/panasonic_viera/remote.py @@ -1,6 +1,4 @@ """Remote control support for Panasonic Viera TV.""" -import logging - from homeassistant.components.remote import RemoteEntity from homeassistant.const import CONF_NAME, STATE_ON @@ -15,8 +13,6 @@ from .const import ( DOMAIN, ) -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Panasonic Viera TV Remote from a config entry.""" diff --git a/homeassistant/components/plaato/binary_sensor.py b/homeassistant/components/plaato/binary_sensor.py index 27150692d6f..52213d46791 100644 --- a/homeassistant/components/plaato/binary_sensor.py +++ b/homeassistant/components/plaato/binary_sensor.py @@ -1,7 +1,5 @@ """Support for Plaato Airlock sensors.""" -import logging - from pyplaato.plaato import PlaatoKeg from homeassistant.components.binary_sensor import ( @@ -13,8 +11,6 @@ from homeassistant.components.binary_sensor import ( from .const import CONF_USE_WEBHOOK, COORDINATOR, DOMAIN from .entity import PlaatoEntity -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Plaato from a config entry.""" diff --git a/homeassistant/components/plaato/config_flow.py b/homeassistant/components/plaato/config_flow.py index 79a4657c312..3bd3e9f0063 100644 --- a/homeassistant/components/plaato/config_flow.py +++ b/homeassistant/components/plaato/config_flow.py @@ -1,6 +1,4 @@ """Config flow for Plaato.""" -import logging - from pyplaato.plaato import PlaatoDeviceType import voluptuous as vol @@ -24,8 +22,6 @@ from .const import ( PLACEHOLDER_WEBHOOK_URL, ) -_LOGGER = logging.getLogger(__package__) - class PlaatoConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handles a Plaato config flow.""" diff --git a/homeassistant/components/screenlogic/binary_sensor.py b/homeassistant/components/screenlogic/binary_sensor.py index 649e6925408..f5d95d03be2 100644 --- a/homeassistant/components/screenlogic/binary_sensor.py +++ b/homeassistant/components/screenlogic/binary_sensor.py @@ -1,6 +1,4 @@ """Support for a ScreenLogic Binary Sensor.""" -import logging - from screenlogicpy.const import DATA as SL_DATA, DEVICE_TYPE, EQUIPMENT, ON_OFF from homeassistant.components.binary_sensor import ( @@ -11,8 +9,6 @@ from homeassistant.components.binary_sensor import ( from . import ScreenlogicEntity from .const import DOMAIN -_LOGGER = logging.getLogger(__name__) - SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS = {DEVICE_TYPE.ALARM: DEVICE_CLASS_PROBLEM} diff --git a/homeassistant/components/screenlogic/sensor.py b/homeassistant/components/screenlogic/sensor.py index 7e8a0dbf60b..357e771f6be 100644 --- a/homeassistant/components/screenlogic/sensor.py +++ b/homeassistant/components/screenlogic/sensor.py @@ -1,6 +1,4 @@ """Support for a ScreenLogic Sensor.""" -import logging - from screenlogicpy.const import ( CHEM_DOSING_STATE, DATA as SL_DATA, @@ -17,8 +15,6 @@ from homeassistant.components.sensor import ( from . import ScreenlogicEntity from .const import DOMAIN -_LOGGER = logging.getLogger(__name__) - SUPPORTED_CHEM_SENSORS = ( "calcium_harness", "current_orp", diff --git a/homeassistant/components/smarttub/__init__.py b/homeassistant/components/smarttub/__init__.py index a396b50840d..89ad9222e7a 100644 --- a/homeassistant/components/smarttub/__init__.py +++ b/homeassistant/components/smarttub/__init__.py @@ -1,11 +1,7 @@ """SmartTub integration.""" -import logging - from .const import DOMAIN, SMARTTUB_CONTROLLER from .controller import SmartTubController -_LOGGER = logging.getLogger(__name__) - PLATFORMS = ["binary_sensor", "climate", "light", "sensor", "switch"] diff --git a/homeassistant/components/smarttub/binary_sensor.py b/homeassistant/components/smarttub/binary_sensor.py index 6ddeccadc74..a3422e27d8d 100644 --- a/homeassistant/components/smarttub/binary_sensor.py +++ b/homeassistant/components/smarttub/binary_sensor.py @@ -1,8 +1,6 @@ """Platform for binary sensor integration.""" from __future__ import annotations -import logging - from smarttub import SpaError, SpaReminder import voluptuous as vol @@ -16,8 +14,6 @@ from homeassistant.helpers import entity_platform from .const import ATTR_ERRORS, ATTR_REMINDERS, DOMAIN, SMARTTUB_CONTROLLER from .entity import SmartTubEntity, SmartTubSensorBase -_LOGGER = logging.getLogger(__name__) - # whether the reminder has been snoozed (bool) ATTR_REMINDER_SNOOZED = "snoozed" diff --git a/homeassistant/components/smarttub/climate.py b/homeassistant/components/smarttub/climate.py index be564c84a94..b089098546f 100644 --- a/homeassistant/components/smarttub/climate.py +++ b/homeassistant/components/smarttub/climate.py @@ -1,6 +1,4 @@ """Platform for climate integration.""" -import logging - from smarttub import Spa from homeassistant.components.climate import ClimateEntity @@ -19,8 +17,6 @@ from homeassistant.util.temperature import convert as convert_temperature from .const import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN, SMARTTUB_CONTROLLER from .entity import SmartTubEntity -_LOGGER = logging.getLogger(__name__) - PRESET_DAY = "day" PRESET_MODES = { diff --git a/homeassistant/components/smarttub/config_flow.py b/homeassistant/components/smarttub/config_flow.py index 652aec746a1..88ec38e8d63 100644 --- a/homeassistant/components/smarttub/config_flow.py +++ b/homeassistant/components/smarttub/config_flow.py @@ -1,6 +1,4 @@ """Config flow to configure the SmartTub integration.""" -import logging - from smarttub import LoginFailed import voluptuous as vol @@ -10,9 +8,6 @@ from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from .const import DOMAIN from .controller import SmartTubController -_LOGGER = logging.getLogger(__name__) - - DATA_SCHEMA = vol.Schema( {vol.Required(CONF_EMAIL): str, vol.Required(CONF_PASSWORD): str} ) diff --git a/homeassistant/components/smarttub/entity.py b/homeassistant/components/smarttub/entity.py index 4bf20868ee0..49d5e94d76e 100644 --- a/homeassistant/components/smarttub/entity.py +++ b/homeassistant/components/smarttub/entity.py @@ -1,6 +1,4 @@ """Base classes for SmartTub entities.""" -import logging - import smarttub from homeassistant.helpers.entity import DeviceInfo @@ -12,8 +10,6 @@ from homeassistant.helpers.update_coordinator import ( from .const import DOMAIN from .helpers import get_spa_name -_LOGGER = logging.getLogger(__name__) - class SmartTubEntity(CoordinatorEntity): """Base class for SmartTub entities.""" diff --git a/homeassistant/components/smarttub/light.py b/homeassistant/components/smarttub/light.py index 1e4229ee4e6..8d03879a0a5 100644 --- a/homeassistant/components/smarttub/light.py +++ b/homeassistant/components/smarttub/light.py @@ -1,6 +1,4 @@ """Platform for light integration.""" -import logging - from smarttub import SpaLight from homeassistant.components.light import ( @@ -22,8 +20,6 @@ from .const import ( from .entity import SmartTubEntity from .helpers import get_spa_name -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass, entry, async_add_entities): """Set up entities for any lights in the tub.""" diff --git a/homeassistant/components/smarttub/sensor.py b/homeassistant/components/smarttub/sensor.py index 9922792ba12..690873cc8bf 100644 --- a/homeassistant/components/smarttub/sensor.py +++ b/homeassistant/components/smarttub/sensor.py @@ -1,6 +1,5 @@ """Platform for sensor integration.""" from enum import Enum -import logging import smarttub import voluptuous as vol @@ -11,8 +10,6 @@ from homeassistant.helpers import config_validation as cv, entity_platform from .const import DOMAIN, SMARTTUB_CONTROLLER from .entity import SmartTubSensorBase -_LOGGER = logging.getLogger(__name__) - # the desired duration, in hours, of the cycle ATTR_DURATION = "duration" ATTR_CYCLE_LAST_UPDATED = "cycle_last_updated" diff --git a/homeassistant/components/smarttub/switch.py b/homeassistant/components/smarttub/switch.py index 7cab25e6cf7..4ce8d7a6838 100644 --- a/homeassistant/components/smarttub/switch.py +++ b/homeassistant/components/smarttub/switch.py @@ -1,6 +1,4 @@ """Platform for switch integration.""" -import logging - import async_timeout from smarttub import SpaPump @@ -10,8 +8,6 @@ from .const import API_TIMEOUT, ATTR_PUMPS, DOMAIN, SMARTTUB_CONTROLLER from .entity import SmartTubEntity from .helpers import get_spa_name -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass, entry, async_add_entities): """Set up switch entities for the pumps on the tub.""" diff --git a/homeassistant/components/sonarr/__init__.py b/homeassistant/components/sonarr/__init__.py index f0969d063c1..b2cc13abc37 100644 --- a/homeassistant/components/sonarr/__init__.py +++ b/homeassistant/components/sonarr/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations from datetime import timedelta -import logging from sonarr import Sonarr, SonarrAccessRestricted, SonarrError @@ -30,7 +29,6 @@ from .const import ( PLATFORMS = ["sensor"] SCAN_INTERVAL = timedelta(seconds=30) -_LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/sonos/binary_sensor.py b/homeassistant/components/sonos/binary_sensor.py index 8583132521c..730a6367edd 100644 --- a/homeassistant/components/sonos/binary_sensor.py +++ b/homeassistant/components/sonos/binary_sensor.py @@ -1,7 +1,6 @@ """Entity representing a Sonos power sensor.""" from __future__ import annotations -import logging from typing import Any from homeassistant.components.binary_sensor import ( @@ -14,8 +13,6 @@ from .const import SONOS_CREATE_BATTERY from .entity import SonosEntity from .speaker import SonosSpeaker -_LOGGER = logging.getLogger(__name__) - ATTR_BATTERY_POWER_SOURCE = "power_source" diff --git a/homeassistant/components/sonos/config_flow.py b/homeassistant/components/sonos/config_flow.py index 3fa3bbb8fa8..745d3db3890 100644 --- a/homeassistant/components/sonos/config_flow.py +++ b/homeassistant/components/sonos/config_flow.py @@ -1,6 +1,4 @@ """Config flow for SONOS.""" -import logging - import soco from homeassistant import config_entries @@ -13,8 +11,6 @@ from homeassistant.helpers.typing import DiscoveryInfoType from .const import DATA_SONOS_DISCOVERY_MANAGER, DOMAIN from .helpers import hostname_to_uid -_LOGGER = logging.getLogger(__name__) - async def _async_has_devices(hass: HomeAssistant) -> bool: """Return if there are devices that can be discovered.""" diff --git a/homeassistant/components/sonos/sensor.py b/homeassistant/components/sonos/sensor.py index 1a13e6f55f4..a71ac7cef21 100644 --- a/homeassistant/components/sonos/sensor.py +++ b/homeassistant/components/sonos/sensor.py @@ -1,8 +1,6 @@ """Entity representing a Sonos battery level.""" from __future__ import annotations -import logging - from homeassistant.components.sensor import SensorEntity from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -11,8 +9,6 @@ from .const import SONOS_CREATE_BATTERY from .entity import SonosEntity from .speaker import SonosSpeaker -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Sonos from a config entry.""" diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index 39ea9a5e8c0..50b002b52b4 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -1,7 +1,6 @@ """Provide functionality to stream HLS.""" from __future__ import annotations -import logging from typing import TYPE_CHECKING, cast from aiohttp import web @@ -24,8 +23,6 @@ from .fmp4utils import get_codec_string if TYPE_CHECKING: from . import Stream -_LOGGER = logging.getLogger(__name__) - @callback def async_setup_hls(hass: HomeAssistant) -> str: diff --git a/homeassistant/components/surepetcare/lock.py b/homeassistant/components/surepetcare/lock.py index 8351eea161b..bb55514409a 100644 --- a/homeassistant/components/surepetcare/lock.py +++ b/homeassistant/components/surepetcare/lock.py @@ -1,7 +1,6 @@ """Support for Sure PetCare Flaps locks.""" from __future__ import annotations -import logging from typing import Any from surepy.entities import SurepyEntity @@ -16,8 +15,6 @@ from . import SurePetcareDataCoordinator from .const import DOMAIN from .entity import SurePetcareEntity -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback diff --git a/homeassistant/components/surepetcare/sensor.py b/homeassistant/components/surepetcare/sensor.py index 508ad030922..c0441d0a7fd 100644 --- a/homeassistant/components/surepetcare/sensor.py +++ b/homeassistant/components/surepetcare/sensor.py @@ -1,7 +1,6 @@ """Support for Sure PetCare Flaps/Pets sensors.""" from __future__ import annotations -import logging from typing import cast from surepy.entities import SurepyEntity @@ -23,8 +22,6 @@ from . import SurePetcareDataCoordinator from .const import DOMAIN, SURE_BATT_VOLTAGE_DIFF, SURE_BATT_VOLTAGE_LOW from .entity import SurePetcareEntity -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback diff --git a/homeassistant/components/syncthing/config_flow.py b/homeassistant/components/syncthing/config_flow.py index e6a5c994834..7421a385f08 100644 --- a/homeassistant/components/syncthing/config_flow.py +++ b/homeassistant/components/syncthing/config_flow.py @@ -1,6 +1,4 @@ """Config flow for syncthing integration.""" -import logging - import aiosyncthing import voluptuous as vol @@ -9,8 +7,6 @@ from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL from .const import DEFAULT_URL, DEFAULT_VERIFY_SSL, DOMAIN -_LOGGER = logging.getLogger(__name__) - DATA_SCHEMA = vol.Schema( { vol.Required(CONF_URL, default=DEFAULT_URL): str, diff --git a/homeassistant/components/syncthing/sensor.py b/homeassistant/components/syncthing/sensor.py index 924f8aaf669..e88636b814b 100644 --- a/homeassistant/components/syncthing/sensor.py +++ b/homeassistant/components/syncthing/sensor.py @@ -1,7 +1,5 @@ """Support for monitoring the Syncthing instance.""" -import logging - import aiosyncthing from homeassistant.components.sensor import SensorEntity @@ -23,8 +21,6 @@ from .const import ( STATE_CHANGED_RECEIVED, ) -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Syncthing sensors.""" diff --git a/homeassistant/components/syncthru/binary_sensor.py b/homeassistant/components/syncthru/binary_sensor.py index 7c4bd6fa8d1..1c402fbf836 100644 --- a/homeassistant/components/syncthru/binary_sensor.py +++ b/homeassistant/components/syncthru/binary_sensor.py @@ -1,7 +1,5 @@ """Support for Samsung Printers with SyncThru web interface.""" -import logging - from pysyncthru import SyncThru, SyncthruState from homeassistant.components.binary_sensor import ( @@ -18,8 +16,6 @@ from homeassistant.helpers.update_coordinator import ( from . import device_identifiers from .const import DOMAIN -_LOGGER = logging.getLogger(__name__) - SYNCTHRU_STATE_PROBLEM = { SyncthruState.INVALID: True, SyncthruState.OFFLINE: None, diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index e419e2b4410..eebd5014cb5 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -4,7 +4,6 @@ Support for controlling power supply of clients which are powered over Ethernet Support for controlling network access of clients selected in option flow. Support for controlling deep packet inspection (DPI) restriction groups. """ -import logging from typing import Any from aiounifi.api import SOURCE_EVENT @@ -26,8 +25,6 @@ from .const import ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN from .unifi_client import UniFiClient from .unifi_entity_base import UniFiBase -_LOGGER = logging.getLogger(__name__) - BLOCK_SWITCH = "block" DPI_SWITCH = "dpi" POE_SWITCH = "poe" diff --git a/homeassistant/components/velbus/climate.py b/homeassistant/components/velbus/climate.py index 68d92bf43d0..cdb049266b5 100644 --- a/homeassistant/components/velbus/climate.py +++ b/homeassistant/components/velbus/climate.py @@ -1,6 +1,4 @@ """Support for Velbus thermostat.""" -import logging - from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, @@ -11,8 +9,6 @@ from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from . import VelbusEntity from .const import DOMAIN -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass, entry, async_add_entities): """Set up Velbus switch based on config_entry.""" diff --git a/homeassistant/components/velbus/cover.py b/homeassistant/components/velbus/cover.py index 1003d341c93..53fd32fad34 100644 --- a/homeassistant/components/velbus/cover.py +++ b/homeassistant/components/velbus/cover.py @@ -1,6 +1,4 @@ """Support for Velbus covers.""" -import logging - from homeassistant.components.cover import ( ATTR_POSITION, SUPPORT_CLOSE, @@ -13,8 +11,6 @@ from homeassistant.components.cover import ( from . import VelbusEntity from .const import DOMAIN -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass, entry, async_add_entities): """Set up Velbus switch based on config_entry.""" diff --git a/homeassistant/components/velbus/light.py b/homeassistant/components/velbus/light.py index 482bdb53e94..a252930b49d 100644 --- a/homeassistant/components/velbus/light.py +++ b/homeassistant/components/velbus/light.py @@ -1,6 +1,4 @@ """Support for Velbus light.""" -import logging - from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_FLASH, @@ -16,8 +14,6 @@ from homeassistant.components.light import ( from . import VelbusEntity from .const import DOMAIN -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass, entry, async_add_entities): """Set up Velbus switch based on config_entry.""" diff --git a/homeassistant/components/velbus/switch.py b/homeassistant/components/velbus/switch.py index 6b9609cc857..70c7e1eb457 100644 --- a/homeassistant/components/velbus/switch.py +++ b/homeassistant/components/velbus/switch.py @@ -1,5 +1,4 @@ """Support for Velbus switches.""" -import logging from typing import Any from homeassistant.components.switch import SwitchEntity @@ -7,8 +6,6 @@ from homeassistant.components.switch import SwitchEntity from . import VelbusEntity from .const import DOMAIN -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass, entry, async_add_entities): """Set up Velbus switch based on config_entry.""" diff --git a/homeassistant/components/waze_travel_time/__init__.py b/homeassistant/components/waze_travel_time/__init__.py index 1b9db0e947a..fa605d19c49 100644 --- a/homeassistant/components/waze_travel_time/__init__.py +++ b/homeassistant/components/waze_travel_time/__init__.py @@ -1,6 +1,4 @@ """The waze_travel_time component.""" -import logging - from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import ( @@ -9,7 +7,6 @@ from homeassistant.helpers.entity_registry import ( ) PLATFORMS = ["sensor"] -_LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/wemo/binary_sensor.py b/homeassistant/components/wemo/binary_sensor.py index a7f1824cf4b..1341d5526a3 100644 --- a/homeassistant/components/wemo/binary_sensor.py +++ b/homeassistant/components/wemo/binary_sensor.py @@ -1,6 +1,5 @@ """Support for WeMo binary sensors.""" import asyncio -import logging from pywemo import Insight, Maker @@ -10,8 +9,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DOMAIN as WEMO_DOMAIN from .entity import WemoEntity -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up WeMo binary sensors.""" diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index 501011f841a..00f9b77aa61 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -1,7 +1,6 @@ """Support for WeMo humidifier.""" import asyncio from datetime import timedelta -import logging import math import voluptuous as vol @@ -26,8 +25,6 @@ from .entity import WemoEntity SCAN_INTERVAL = timedelta(seconds=10) PARALLEL_UPDATES = 0 -_LOGGER = logging.getLogger(__name__) - ATTR_CURRENT_HUMIDITY = "current_humidity" ATTR_TARGET_HUMIDITY = "target_humidity" ATTR_FAN_MODE = "fan_mode" diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index 13f375ad726..4339e964b62 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -1,6 +1,5 @@ """Support for Belkin WeMo lights.""" import asyncio -import logging from pywemo.ouimeaux_device import bridge @@ -24,8 +23,6 @@ from .const import DOMAIN as WEMO_DOMAIN from .entity import WemoEntity from .wemo_device import DeviceCoordinator -_LOGGER = logging.getLogger(__name__) - SUPPORT_WEMO = ( SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR | SUPPORT_TRANSITION ) diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index a9ee2579c47..d1240d034b5 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -1,7 +1,6 @@ """Support for WeMo switches.""" import asyncio from datetime import datetime, timedelta -import logging from pywemo import CoffeeMaker, Insight, Maker @@ -16,8 +15,6 @@ from .entity import WemoEntity SCAN_INTERVAL = timedelta(seconds=10) PARALLEL_UPDATES = 0 -_LOGGER = logging.getLogger(__name__) - # The WEMO_ constants below come from pywemo itself ATTR_SENSOR_STATE = "sensor_state" ATTR_SWITCH_MODE = "switch_mode" diff --git a/homeassistant/components/xiaomi_miio/binary_sensor.py b/homeassistant/components/xiaomi_miio/binary_sensor.py index 28553c159fe..a28c449e5cb 100644 --- a/homeassistant/components/xiaomi_miio/binary_sensor.py +++ b/homeassistant/components/xiaomi_miio/binary_sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations from dataclasses import dataclass -import logging from typing import Callable from homeassistant.components.binary_sensor import ( @@ -30,9 +29,6 @@ from .const import ( ) from .device import XiaomiCoordinatedMiioEntity -_LOGGER = logging.getLogger(__name__) - - ATTR_NO_WATER = "no_water" ATTR_POWERSUPPLY_ATTACHED = "powersupply_attached" ATTR_WATER_TANK_DETACHED = "water_tank_detached" diff --git a/homeassistant/components/zha/alarm_control_panel.py b/homeassistant/components/zha/alarm_control_panel.py index dd705e44798..1ba5f1b73f8 100644 --- a/homeassistant/components/zha/alarm_control_panel.py +++ b/homeassistant/components/zha/alarm_control_panel.py @@ -1,6 +1,5 @@ """Alarm control panels on Zigbee Home Automation networks.""" import functools -import logging from zigpy.zcl.clusters.security import IasAce @@ -44,9 +43,6 @@ from .core.helpers import async_get_zha_config_value from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity -_LOGGER = logging.getLogger(__name__) - - STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) IAS_ACE_STATE_MAP = { diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index 0800fee1374..5ef0ee3d9fa 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -7,7 +7,6 @@ https://home-assistant.io/integrations/zha/ from __future__ import annotations import asyncio -import logging from zigpy.exceptions import ZigbeeException from zigpy.zcl.clusters import security @@ -42,8 +41,6 @@ NAME = 0 SIGNAL_ARMED_STATE_CHANGED = "zha_armed_state_changed" SIGNAL_ALARM_TRIGGERED = "zha_armed_triggered" -_LOGGER = logging.getLogger(__name__) - @registries.ZIGBEE_CHANNEL_REGISTRY.register(AceCluster.cluster_id) class IasAce(ZigbeeChannel): diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 1e41c313836..8b2c4d11fbf 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -4,7 +4,6 @@ from __future__ import annotations import collections from collections.abc import Callable import dataclasses -import logging from typing import Dict, List import attr @@ -29,7 +28,6 @@ from . import channels as zha_channels # noqa: F401 pylint: disable=unused-impo from .decorators import CALLABLE_T, DictRegistry, SetRegistry from .typing import ChannelType -_LOGGER = logging.getLogger(__name__) GROUP_ENTITY_DOMAINS = [LIGHT, SWITCH, FAN] PHILLIPS_REMOTE_CLUSTER = 0xFC00 diff --git a/homeassistant/components/zwave_js/triggers/value_updated.py b/homeassistant/components/zwave_js/triggers/value_updated.py index fdf2589073e..7ebdb4f3748 100644 --- a/homeassistant/components/zwave_js/triggers/value_updated.py +++ b/homeassistant/components/zwave_js/triggers/value_updated.py @@ -2,7 +2,6 @@ from __future__ import annotations import functools -import logging import voluptuous as vol from zwave_js_server.const import CommandClass @@ -39,8 +38,6 @@ from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.typing import ConfigType -_LOGGER = logging.getLogger(__name__) - # Platform type should be . PLATFORM_TYPE = f"{DOMAIN}.{__name__.rsplit('.', maxsplit=1)[-1]}" diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 5fb095c4f02..0d966cde313 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -5,7 +5,6 @@ import asyncio from collections.abc import Callable, Coroutine, Iterable from contextvars import ContextVar from datetime import datetime, timedelta -import logging from logging import Logger from types import ModuleType from typing import TYPE_CHECKING, Any, Protocol @@ -59,8 +58,6 @@ PLATFORM_NOT_READY_RETRIES = 10 DATA_ENTITY_PLATFORM = "entity_platform" PLATFORM_NOT_READY_BASE_WAIT_TIME = 30 # seconds -_LOGGER = logging.getLogger(__name__) - class AddEntitiesCallback(Protocol): """Protocol type for EntityPlatform.add_entities callback.""" diff --git a/tests/components/climacell/test_config_flow.py b/tests/components/climacell/test_config_flow.py index faa3748be69..476f2ba3bee 100644 --- a/tests/components/climacell/test_config_flow.py +++ b/tests/components/climacell/test_config_flow.py @@ -1,5 +1,4 @@ """Test the ClimaCell config flow.""" -import logging from unittest.mock import patch from pyclimacell.exceptions import ( @@ -34,8 +33,6 @@ from .const import API_KEY, MIN_CONFIG from tests.common import MockConfigEntry -_LOGGER = logging.getLogger(__name__) - async def test_user_flow_minimum_fields(hass: HomeAssistant) -> None: """Test user config flow with minimum fields.""" diff --git a/tests/components/climacell/test_init.py b/tests/components/climacell/test_init.py index d90a0c00181..5ee50c6d0ec 100644 --- a/tests/components/climacell/test_init.py +++ b/tests/components/climacell/test_init.py @@ -1,6 +1,4 @@ """Tests for Climacell init.""" -import logging - import pytest from homeassistant.components.climacell.config_flow import ( @@ -16,8 +14,6 @@ from .const import API_V3_ENTRY_DATA, MIN_CONFIG, V1_ENTRY_DATA from tests.common import MockConfigEntry -_LOGGER = logging.getLogger(__name__) - async def test_load_and_unload( hass: HomeAssistant, diff --git a/tests/components/climacell/test_sensor.py b/tests/components/climacell/test_sensor.py index c642457b63e..8c075942cea 100644 --- a/tests/components/climacell/test_sensor.py +++ b/tests/components/climacell/test_sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations from datetime import datetime -import logging from typing import Any from unittest.mock import patch @@ -23,7 +22,6 @@ from .const import API_V3_ENTRY_DATA, API_V4_ENTRY_DATA from tests.common import MockConfigEntry -_LOGGER = logging.getLogger(__name__) CC_SENSOR_ENTITY_ID = "sensor.climacell_{}" O3 = "ozone" diff --git a/tests/components/climacell/test_weather.py b/tests/components/climacell/test_weather.py index 90efdea3c8c..e3e15889e44 100644 --- a/tests/components/climacell/test_weather.py +++ b/tests/components/climacell/test_weather.py @@ -2,7 +2,6 @@ from __future__ import annotations from datetime import datetime -import logging from typing import Any from unittest.mock import patch @@ -51,8 +50,6 @@ from .const import API_V3_ENTRY_DATA, API_V4_ENTRY_DATA from tests.common import MockConfigEntry -_LOGGER = logging.getLogger(__name__) - @callback def _enable_entity(hass: HomeAssistant, entity_name: str) -> None: diff --git a/tests/components/hyperion/test_camera.py b/tests/components/hyperion/test_camera.py index daed1ec2cc9..a32663d7725 100644 --- a/tests/components/hyperion/test_camera.py +++ b/tests/components/hyperion/test_camera.py @@ -4,7 +4,6 @@ from __future__ import annotations import asyncio import base64 from collections.abc import Awaitable -import logging from typing import Callable from unittest.mock import AsyncMock, Mock, patch @@ -39,7 +38,6 @@ from . import ( setup_test_config_entry, ) -_LOGGER = logging.getLogger(__name__) TEST_CAMERA_ENTITY_ID = "camera.test_instance_1" TEST_IMAGE_DATA = "TEST DATA" TEST_IMAGE_UPDATE = { diff --git a/tests/components/litejet/test_light.py b/tests/components/litejet/test_light.py index 1961843c8b0..86b3dd84367 100644 --- a/tests/components/litejet/test_light.py +++ b/tests/components/litejet/test_light.py @@ -1,6 +1,4 @@ """The tests for the litejet component.""" -import logging - from homeassistant.components import light from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_TRANSITION from homeassistant.components.litejet.const import CONF_DEFAULT_TRANSITION @@ -8,8 +6,6 @@ from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_O from . import async_init_integration -_LOGGER = logging.getLogger(__name__) - ENTITY_LIGHT = "light.mock_load_1" ENTITY_LIGHT_NUMBER = 1 ENTITY_OTHER_LIGHT = "light.mock_load_2" diff --git a/tests/components/litejet/test_switch.py b/tests/components/litejet/test_switch.py index dfcb9801093..d8fb2ef39c9 100644 --- a/tests/components/litejet/test_switch.py +++ b/tests/components/litejet/test_switch.py @@ -1,13 +1,9 @@ """The tests for the litejet component.""" -import logging - from homeassistant.components import switch from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON from . import async_init_integration -_LOGGER = logging.getLogger(__name__) - ENTITY_SWITCH = "switch.mock_switch_1" ENTITY_SWITCH_NUMBER = 1 ENTITY_OTHER_SWITCH = "switch.mock_switch_2" diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index 9b85d23df1c..4c8df13fbe6 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -21,8 +21,6 @@ TEST_MODBUS_HOST = "modbusHost" TEST_PORT_TCP = 5501 TEST_PORT_SERIAL = "usb01" -_LOGGER = logging.getLogger(__name__) - @dataclass class ReadResult: diff --git a/tests/components/motioneye/test_config_flow.py b/tests/components/motioneye/test_config_flow.py index dffcc4660d8..591bbaa4c7d 100644 --- a/tests/components/motioneye/test_config_flow.py +++ b/tests/components/motioneye/test_config_flow.py @@ -1,5 +1,4 @@ """Test the motionEye config flow.""" -import logging from unittest.mock import AsyncMock, patch from motioneye_client.client import ( @@ -25,8 +24,6 @@ from . import TEST_URL, create_mock_motioneye_client, create_mock_motioneye_conf from tests.common import MockConfigEntry -_LOGGER = logging.getLogger(__name__) - async def test_user_success(hass: HomeAssistant) -> None: """Test successful user flow.""" diff --git a/tests/components/motioneye/test_web_hooks.py b/tests/components/motioneye/test_web_hooks.py index ac84bd405cd..55e7e31d9b9 100644 --- a/tests/components/motioneye/test_web_hooks.py +++ b/tests/components/motioneye/test_web_hooks.py @@ -1,6 +1,5 @@ """Test the motionEye camera web hooks.""" import copy -import logging from typing import Any from unittest.mock import AsyncMock, call, patch @@ -50,9 +49,6 @@ from . import ( from tests.common import async_capture_events -_LOGGER = logging.getLogger(__name__) - - WEB_HOOK_MOTION_DETECTED_QUERY_STRING = ( "camera_id=%t&changed_pixels=%D&despeckle_labels=%Q&event=%v&fps=%{fps}" "&frame_number=%q&height=%h&host=%{host}&motion_center_x=%K&motion_center_y=%L" diff --git a/tests/components/netgear/test_config_flow.py b/tests/components/netgear/test_config_flow.py index ad060b60d36..b81171196c0 100644 --- a/tests/components/netgear/test_config_flow.py +++ b/tests/components/netgear/test_config_flow.py @@ -1,5 +1,4 @@ """Tests for the Netgear config flow.""" -import logging from unittest.mock import Mock, patch from pynetgear import DEFAULT_HOST, DEFAULT_PORT, DEFAULT_USER @@ -19,8 +18,6 @@ from homeassistant.const import ( from tests.common import MockConfigEntry -_LOGGER = logging.getLogger(__name__) - URL = "http://routerlogin.net" SERIAL = "5ER1AL0000001" diff --git a/tests/components/sia/test_config_flow.py b/tests/components/sia/test_config_flow.py index a91822c7846..6b1517601f4 100644 --- a/tests/components/sia/test_config_flow.py +++ b/tests/components/sia/test_config_flow.py @@ -1,5 +1,4 @@ """Test the sia config flow.""" -import logging from unittest.mock import patch import pytest @@ -21,9 +20,6 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry -_LOGGER = logging.getLogger(__name__) - - BASIS_CONFIG_ENTRY_ID = 1 BASIC_CONFIG = { CONF_PORT: 7777, From addb91d49ef5bb109b87455d13b390adaefe8021 Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Thu, 14 Oct 2021 19:54:48 +0200 Subject: [PATCH 0337/1038] Update xknx to version 0.18.10 (#57635) Co-authored-by: Paulus Schoutsen --- homeassistant/components/knx/__init__.py | 10 ++++++++++ homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/knx/conftest.py | 1 + tests/components/knx/test_binary_sensor.py | 23 +++++++++++++++------- 6 files changed, 30 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index e6c1562f37e..e52a79cf25a 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -7,6 +7,7 @@ from typing import Final import voluptuous as vol from xknx import XKNX +from xknx.core import XknxConnectionState from xknx.core.telegram_queue import TelegramQueue from xknx.dpt import DPTArray, DPTBase, DPTBinary from xknx.exceptions import XKNXException @@ -270,6 +271,9 @@ class KNXModule: self.init_xknx() self._knx_event_callback: TelegramQueue.Callback = self.register_callback() + self.xknx.connection_manager.register_connection_state_changed_cb( + self.connection_state_changed_cb + ) def init_xknx(self) -> None: """Initialize XKNX object.""" @@ -352,6 +356,12 @@ class KNXModule: }, ) + async def connection_state_changed_cb(self, state: XknxConnectionState) -> None: + """Call invoked after a KNX connection state change was received.""" + self.connected = state == XknxConnectionState.CONNECTED + if tasks := [device.after_update() for device in self.xknx.devices]: + await asyncio.gather(*tasks) + def register_callback(self) -> TelegramQueue.Callback: """Register callback within XKNX TelegramQueue.""" address_filters = list( diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index c514ec6fe64..aafa6630560 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -2,7 +2,7 @@ "domain": "knx", "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.18.9"], + "requirements": ["xknx==0.18.10"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "silver", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 1effc0bea1d..7eeb386220e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2427,7 +2427,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.18.9 +xknx==0.18.10 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a868f02c168..d3b47554c58 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1395,7 +1395,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.18.9 +xknx==0.18.10 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index 0dc8749830e..5f60f5603aa 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -153,6 +153,7 @@ class KNXTestKit: source_address=IndividualAddress(self.INDIVIDUAL_ADDRESS), ) ) + await self.xknx.telegrams.join() await self.hass.async_block_till_done() async def receive_read( diff --git a/tests/components/knx/test_binary_sensor.py b/tests/components/knx/test_binary_sensor.py index 48b871b85e4..9f9785150c0 100644 --- a/tests/components/knx/test_binary_sensor.py +++ b/tests/components/knx/test_binary_sensor.py @@ -1,4 +1,5 @@ """Test KNX binary sensor.""" +import asyncio from datetime import timedelta from homeassistant.components.knx.const import CONF_STATE_ADDRESS, CONF_SYNC_STATE @@ -126,7 +127,7 @@ async def test_binary_sensor_counter(hass: HomeAssistant, knx: KNXTestKit): { CONF_NAME: "test", CONF_STATE_ADDRESS: "2/2/2", - BinarySensorSchema.CONF_CONTEXT_TIMEOUT: 1, + BinarySensorSchema.CONF_CONTEXT_TIMEOUT: 0.001, CONF_SYNC_STATE: False, }, ] @@ -144,8 +145,9 @@ async def test_binary_sensor_counter(hass: HomeAssistant, knx: KNXTestKit): state = hass.states.get("binary_sensor.test") assert state.state is STATE_OFF assert state.attributes.get("counter") == 0 - async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=0.001)) await hass.async_block_till_done() + await asyncio.sleep(0.002) # state changed twice after context timeout - once to ON with counter 1 and once to counter 0 state = hass.states.get("binary_sensor.test") assert state.state is STATE_ON @@ -153,8 +155,12 @@ async def test_binary_sensor_counter(hass: HomeAssistant, knx: KNXTestKit): # additional async_block_till_done needed event capture await hass.async_block_till_done() assert len(events) == 2 - assert events.pop(0).data.get("new_state").attributes.get("counter") == 1 - assert events.pop(0).data.get("new_state").attributes.get("counter") == 0 + event = events.pop(0).data + assert event.get("new_state").attributes.get("counter") == 1 + assert event.get("old_state").attributes.get("counter") == 0 + event = events.pop(0).data + assert event.get("new_state").attributes.get("counter") == 0 + assert event.get("old_state").attributes.get("counter") == 1 # receive 2 telegrams in context await knx.receive_write("2/2/2", True) @@ -170,9 +176,11 @@ async def test_binary_sensor_counter(hass: HomeAssistant, knx: KNXTestKit): assert state.state is STATE_ON assert state.attributes.get("counter") == 0 await hass.async_block_till_done() - assert len(events) == 2 - assert events.pop(0).data.get("new_state").attributes.get("counter") == 2 - assert events.pop(0).data.get("new_state").attributes.get("counter") == 0 + await hass.async_block_till_done() + assert len(events) == 1 + event = events.pop(0).data + assert event.get("new_state").attributes.get("counter") == 2 + assert event.get("old_state").attributes.get("counter") == 0 async def test_binary_sensor_reset(hass: HomeAssistant, knx: KNXTestKit): @@ -200,6 +208,7 @@ async def test_binary_sensor_reset(hass: HomeAssistant, knx: KNXTestKit): assert state.state is STATE_ON async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) await hass.async_block_till_done() + await hass.async_block_till_done() # state reset after after timeout state = hass.states.get("binary_sensor.test") assert state.state is STATE_OFF From e27e4c356184882ccde9d155c96341eeebab329e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 14 Oct 2021 20:16:56 +0200 Subject: [PATCH 0338/1038] Add support for device configuration URL (#57539) Co-authored-by: Paulus Schoutsen --- .../components/config/device_registry.py | 1 + homeassistant/components/shelly/__init__.py | 2 + homeassistant/helpers/device_registry.py | 10 ++++ homeassistant/helpers/entity.py | 1 + homeassistant/helpers/entity_platform.py | 11 ++++ .../components/config/test_device_registry.py | 2 + tests/components/shelly/conftest.py | 4 +- .../components/shelly/test_device_trigger.py | 2 +- tests/helpers/test_entity_platform.py | 55 ++++++++++++++++++- 9 files changed, 83 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index 1dd8fbe4167..1cc63297352 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -68,6 +68,7 @@ def _entry_dict(entry): """Convert entry to API format.""" return { "area_id": entry.area_id, + "configuration_url": entry.configuration_url, "config_entries": list(entry.config_entries), "connections": list(entry.connections), "disabled_by": entry.disabled_by, diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index cb48f34cba6..da1603e3201 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -399,6 +399,7 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): manufacturer="Shelly", model=aioshelly.const.MODEL_NAMES.get(self.model, self.model), sw_version=sw_version, + configuration_url=f"http://{self.entry.data[CONF_HOST]}", ) self.device_id = entry.id self.device.subscribe_updates(self.async_set_updated_data) @@ -635,6 +636,7 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): manufacturer="Shelly", model=aioshelly.const.MODEL_NAMES.get(self.model, self.model), sw_version=sw_version, + configuration_url=f"http://{self.entry.data[CONF_HOST]}", ) self.device_id = entry.id self.device.subscribe_updates(self.async_set_updated_data) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 9348c112942..b41df3d6aa0 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -55,6 +55,7 @@ class DeviceEntry: area_id: str | None = attr.ib(default=None) config_entries: set[str] = attr.ib(converter=set, factory=set) + configuration_url: str | None = attr.ib(default=None) connections: set[tuple[str, str]] = attr.ib(converter=set, factory=set) disabled_by: str | None = attr.ib( default=None, @@ -244,6 +245,7 @@ class DeviceRegistry: self, *, config_entry_id: str, + configuration_url: str | None | UndefinedType = UNDEFINED, connections: set[tuple[str, str]] | None = None, default_manufacturer: str | None | UndefinedType = UNDEFINED, default_model: str | None | UndefinedType = UNDEFINED, @@ -302,6 +304,7 @@ class DeviceRegistry: device = self._async_update_device( device.id, add_config_entry_id=config_entry_id, + configuration_url=configuration_url, disabled_by=disabled_by, entry_type=entry_type, manufacturer=manufacturer, @@ -326,6 +329,7 @@ class DeviceRegistry: *, add_config_entry_id: str | UndefinedType = UNDEFINED, area_id: str | None | UndefinedType = UNDEFINED, + configuration_url: str | None | UndefinedType = UNDEFINED, disabled_by: str | None | UndefinedType = UNDEFINED, manufacturer: str | None | UndefinedType = UNDEFINED, model: str | None | UndefinedType = UNDEFINED, @@ -342,6 +346,7 @@ class DeviceRegistry: device_id, add_config_entry_id=add_config_entry_id, area_id=area_id, + configuration_url=configuration_url, disabled_by=disabled_by, manufacturer=manufacturer, model=model, @@ -361,6 +366,7 @@ class DeviceRegistry: *, add_config_entry_id: str | UndefinedType = UNDEFINED, area_id: str | None | UndefinedType = UNDEFINED, + configuration_url: str | None | UndefinedType = UNDEFINED, disabled_by: str | None | UndefinedType = UNDEFINED, entry_type: str | None | UndefinedType = UNDEFINED, manufacturer: str | None | UndefinedType = UNDEFINED, @@ -424,6 +430,7 @@ class DeviceRegistry: changes["identifiers"] = new_identifiers for attr_name, value in ( + ("configuration_url", configuration_url), ("disabled_by", disabled_by), ("entry_type", entry_type), ("manufacturer", manufacturer), @@ -514,6 +521,8 @@ class DeviceRegistry: name_by_user=device.get("name_by_user"), # Introduced in 0.119 disabled_by=device.get("disabled_by"), + # Introduced in 2021.11 + configuration_url=device.get("configuration_url"), ) # Introduced in 0.111 for device in data.get("deleted_devices", []): @@ -556,6 +565,7 @@ class DeviceRegistry: "area_id": entry.area_id, "name_by_user": entry.name_by_user, "disabled_by": entry.disabled_by, + "configuration_url": entry.configuration_url, } for entry in self.devices.values() ] diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 584fa4f0554..2ad0f934973 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -158,6 +158,7 @@ def get_unit_of_measurement(hass: HomeAssistant, entity_id: str) -> str | None: class DeviceInfo(TypedDict, total=False): """Entity device information for device registry.""" + configuration_url: str | None connections: set[tuple[str, str]] default_manufacturer: str default_model: str diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 0d966cde313..871ac92e3a6 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -473,6 +473,17 @@ class EntityPlatform: if key in device_info: processed_dev_info[key] = device_info[key] # type: ignore[misc] + if "configuration_url" in device_info: + try: + processed_dev_info["configuration_url"] = cv.url( + device_info["configuration_url"] + ) + except vol.Invalid: + _LOGGER.warning( + "Ignoring invalid device configuration_url '%s'", + device_info["configuration_url"], + ) + try: device = device_registry.async_get_or_create(**processed_dev_info) # type: ignore[arg-type] device_id = device.id diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index 4e10413f14f..0d9170f0a83 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -58,6 +58,7 @@ async def test_list_devices(hass, client, registry): "area_id": None, "name_by_user": None, "disabled_by": None, + "configuration_url": None, }, { "config_entries": ["1234"], @@ -72,6 +73,7 @@ async def test_list_devices(hass, client, registry): "area_id": None, "name_by_user": None, "disabled_by": None, + "configuration_url": None, }, ] diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 9dbba7732ac..a0d4a27bbc4 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -108,7 +108,7 @@ async def coap_wrapper(hass): config_entry = MockConfigEntry( domain=DOMAIN, - data={"sleep_period": 0, "model": "SHSW-25"}, + data={"sleep_period": 0, "model": "SHSW-25", "host": "1.2.3.4"}, unique_id="12345678", ) config_entry.add_to_hass(hass) @@ -140,7 +140,7 @@ async def rpc_wrapper(hass): config_entry = MockConfigEntry( domain=DOMAIN, - data={"sleep_period": 0, "model": "SNSW-001P16EU", "gen": 2}, + data={"sleep_period": 0, "model": "SNSW-001P16EU", "gen": 2, "host": "1.2.3.4"}, unique_id="12345678", ) config_entry.add_to_hass(hass) diff --git a/tests/components/shelly/test_device_trigger.py b/tests/components/shelly/test_device_trigger.py index 41fbad2f8e3..a81662159c2 100644 --- a/tests/components/shelly/test_device_trigger.py +++ b/tests/components/shelly/test_device_trigger.py @@ -109,7 +109,7 @@ async def test_get_triggers_button(hass): config_entry = MockConfigEntry( domain=DOMAIN, - data={"sleep_period": 43200, "model": "SHBTN-1"}, + data={"sleep_period": 43200, "model": "SHBTN-1", "host": "1.2.3.4"}, unique_id="12345678", ) config_entry.add_to_hass(hass) diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 65a46f33cd8..9f213801355 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -832,6 +832,7 @@ async def test_device_info_called(hass): unique_id="qwer", device_info={ "identifiers": {("hue", "1234")}, + "configuration_url": "http://192.168.0.100/config", "connections": {(dr.CONNECTION_NETWORK_MAC, "abcd")}, "manufacturer": "test-manuf", "model": "test-model", @@ -860,13 +861,14 @@ async def test_device_info_called(hass): device = registry.async_get_device({("hue", "1234")}) assert device is not None assert device.identifiers == {("hue", "1234")} + assert device.configuration_url == "http://192.168.0.100/config" assert device.connections == {(dr.CONNECTION_NETWORK_MAC, "abcd")} + assert device.entry_type == "service" assert device.manufacturer == "test-manuf" assert device.model == "test-model" assert device.name == "test-name" - assert device.sw_version == "test-sw" assert device.suggested_area == "Heliport" - assert device.entry_type == "service" + assert device.sw_version == "test-sw" assert device.via_device_id == via.id @@ -916,6 +918,55 @@ async def test_device_info_not_overrides(hass): assert device2.model == "test-model" +async def test_device_info_invalid_url(hass, caplog): + """Test device info is forwarded correctly.""" + registry = dr.async_get(hass) + registry.async_get_or_create( + config_entry_id="123", + connections=set(), + identifiers={("hue", "via-id")}, + manufacturer="manufacturer", + model="via", + ) + + async def async_setup_entry(hass, config_entry, async_add_entities): + """Mock setup entry method.""" + async_add_entities( + [ + # Valid device info, but invalid url + MockEntity( + unique_id="qwer", + device_info={ + "identifiers": {("hue", "1234")}, + "configuration_url": "foo://192.168.0.100/config", + }, + ), + ] + ) + return True + + platform = MockPlatform(async_setup_entry=async_setup_entry) + config_entry = MockConfigEntry(entry_id="super-mock-id") + entity_platform = MockEntityPlatform( + hass, platform_name=config_entry.domain, platform=platform + ) + + assert await entity_platform.async_setup_entry(config_entry) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids()) == 1 + + device = registry.async_get_device({("hue", "1234")}) + assert device is not None + assert device.identifiers == {("hue", "1234")} + assert device.configuration_url is None + + assert ( + "Ignoring invalid device configuration_url 'foo://192.168.0.100/config'" + in caplog.text + ) + + async def test_entity_disabled_by_integration(hass): """Test entity disabled by integration.""" component = EntityComponent(_LOGGER, DOMAIN, hass, timedelta(seconds=20)) From 8ef8838801e0df944f12bb95edf8d01e903aecf2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 14 Oct 2021 20:19:39 +0200 Subject: [PATCH 0339/1038] Correct detection of row_number support for MariaDB (#57663) --- homeassistant/components/recorder/util.py | 4 +++- tests/components/recorder/test_util.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 567164d4325..8277f86e9f9 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -311,7 +311,9 @@ def setup_connection_for_dialect( result = query_on_connection(dbapi_connection, "SELECT VERSION()") version = result[0][0] major, minor, _patch = version.split(".", 2) - if int(major) == 5 and int(minor) < 8: + if (int(major) == 5 and int(minor) < 8) or ( + int(major) == 10 and int(minor) < 2 + ): instance._db_supports_row_number = ( # pylint: disable=[protected-access] False ) diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 8b5de5cff16..ff690e24279 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -125,7 +125,8 @@ async def test_last_run_was_recently_clean(hass): @pytest.mark.parametrize( "mysql_version, db_supports_row_number", [ - ("10.0.0", True), + ("10.2.0", True), + ("10.1.0", False), ("5.8.0", True), ("5.7.0", False), ], From 681b5c48e2f02799a29a8d248161515d2f2899ea Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 14 Oct 2021 12:21:44 -0600 Subject: [PATCH 0340/1038] Ensure Notion device name is stored as a string (#57670) --- homeassistant/components/notion/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index c9f4131be1a..2fb9339955a 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -151,7 +151,7 @@ class NotionEntity(CoordinatorEntity): "identifiers": {(DOMAIN, sensor["hardware_id"])}, "manufacturer": "Silicon Labs", "model": sensor["hardware_revision"], - "name": sensor["name"], + "name": str(sensor["name"]), "sw_version": sensor["firmware_version"], "via_device": (DOMAIN, bridge.get("hardware_id")), } From 7546c766dd540f5c79fee9e4a200cf9688a66a54 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 14 Oct 2021 12:03:39 -0700 Subject: [PATCH 0341/1038] Fix lint issue (#57694) --- homeassistant/helpers/entity_platform.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 871ac92e3a6..51e29647a9e 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -5,7 +5,7 @@ import asyncio from collections.abc import Callable, Coroutine, Iterable from contextvars import ContextVar from datetime import datetime, timedelta -from logging import Logger +from logging import Logger, getLogger from types import ModuleType from typing import TYPE_CHECKING, Any, Protocol @@ -58,6 +58,8 @@ PLATFORM_NOT_READY_RETRIES = 10 DATA_ENTITY_PLATFORM = "entity_platform" PLATFORM_NOT_READY_BASE_WAIT_TIME = 30 # seconds +_LOGGER = getLogger(__name__) + class AddEntitiesCallback(Protocol): """Protocol type for EntityPlatform.add_entities callback.""" From cef34356e2a66c1a51324c8b095183316b612ad4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Oct 2021 21:04:02 +0200 Subject: [PATCH 0342/1038] Add sensor platform to Tuya (#57668) --- .coveragerc | 1 + homeassistant/components/tuya/const.py | 16 +- homeassistant/components/tuya/sensor.py | 187 ++++++++++++++++++++++++ 3 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/tuya/sensor.py diff --git a/.coveragerc b/.coveragerc index 1a5380dd0c9..6274e8526fc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1118,6 +1118,7 @@ omit = homeassistant/components/tuya/light.py homeassistant/components/tuya/scene.py homeassistant/components/tuya/select.py + homeassistant/components/tuya/sensor.py homeassistant/components/tuya/switch.py homeassistant/components/twentemilieu/const.py homeassistant/components/twentemilieu/sensor.py diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 51d92b399e3..fa66d72da4f 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -54,7 +54,16 @@ TUYA_SUPPORTED_PRODUCT_CATEGORIES = ( TUYA_SMART_APP = "tuyaSmart" SMARTLIFE_APP = "smartlife" -PLATFORMS = ["binary_sensor", "climate", "fan", "light", "scene", "select", "switch"] +PLATFORMS = [ + "binary_sensor", + "climate", + "fan", + "light", + "scene", + "select", + "sensor", + "switch", +] class DPCode(str, Enum): @@ -64,6 +73,8 @@ class DPCode(str, Enum): """ ANION = "anion" # Ionizer unit + BATTERY_PERCENTAGE = "battery_percentage" # Battery percentage + BATTERY_STATE = "battery_state" # Battery state BRIGHT_VALUE = "bright_value" # Brightness C_F = "c_f" # Temperature unit switching CHILD_LOCK = "child_lock" # Child lock @@ -71,6 +82,9 @@ class DPCode(str, Enum): COLOUR_DATA_V2 = "colour_data_v2" # Colored light mode CONCENTRATION_SET = "concentration_set" # Concentration setting CUP_NUMBER = "cup_number" # NUmber of cups + CUR_CURRENT = "cur_current" # Actual current + CUR_POWER = "cur_power" # Actual power + CUR_VOLTAGE = "cur_voltage" # Actual voltage DOORCONTACT_STATE = "doorcontact_state" # Status of door window sensor FAN_DIRECTION = "fan_direction" # Fan direction FAN_SPEED_ENUM = "fan_speed_enum" # Speed mode diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py new file mode 100644 index 00000000000..e1ab4867df9 --- /dev/null +++ b/homeassistant/components/tuya/sensor.py @@ -0,0 +1,187 @@ +"""Support for Tuya sensors.""" +from __future__ import annotations + +from typing import cast + +from tuya_iot import TuyaDevice, TuyaDeviceManager +from tuya_iot.device import TuyaDeviceStatusRange + +from homeassistant.components.sensor import ( + DEVICE_CLASS_BATTERY, + STATE_CLASS_MEASUREMENT, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, + PERCENTAGE, +) +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 StateType + +from . import HomeAssistantTuyaData +from .base import EnumTypeData, IntegerTypeData, TuyaEntity +from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode + +# All descriptions can be found here. Mostly the Integer data types in the +# default status set of each category (that don't have a set instruction) +# end up being a sensor. +# https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq +SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { + # Door Window Sensor + # https://developer.tuya.com/en/docs/iot/s?id=K9gf48hm02l8m + "mcs": ( + SensorEntityDescription( + key=DPCode.BATTERY_PERCENTAGE, + name="Battery", + native_unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.BATTERY_STATE, + name="Battery State", + entity_registry_enabled_default=False, + ), + ), + # Switch + # https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s + "kg": ( + SensorEntityDescription( + key=DPCode.CUR_CURRENT, + name="Current", + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key=DPCode.CUR_POWER, + name="Power", + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key=DPCode.CUR_VOLTAGE, + name="Voltage", + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + entity_registry_enabled_default=False, + ), + ), +} + +# Socket (duplicate of `kg`) +# https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s +SENSORS["cz"] = SENSORS["kg"] + +# Power Socket (duplicate of `kg`) +# https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s +SENSORS["pc"] = SENSORS["kg"] + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Tuya sensor dynamically through Tuya discovery.""" + hass_data: HomeAssistantTuyaData = hass.data[DOMAIN][entry.entry_id] + + @callback + def async_discover_device(device_ids: list[str]) -> None: + """Discover and add a discovered Tuya sensor.""" + entities: list[TuyaSensorEntity] = [] + for device_id in device_ids: + device = hass_data.device_manager.device_map[device_id] + if descriptions := SENSORS.get(device.category): + for description in descriptions: + if ( + description.key in device.function + or description.key in device.status + ): + entities.append( + TuyaSensorEntity( + device, hass_data.device_manager, description + ) + ) + + async_add_entities(entities) + + async_discover_device([*hass_data.device_manager.device_map]) + + entry.async_on_unload( + async_dispatcher_connect(hass, TUYA_DISCOVERY_NEW, async_discover_device) + ) + + +class TuyaSensorEntity(TuyaEntity, SensorEntity): + """Tuya Sensor Entity.""" + + _status_range: TuyaDeviceStatusRange | None = None + _type_data: IntegerTypeData | EnumTypeData | None = None + + def __init__( + self, + device: TuyaDevice, + device_manager: TuyaDeviceManager, + description: SensorEntityDescription, + ) -> None: + """Init Tuya sensor.""" + super().__init__(device, device_manager) + self.entity_description = description + self._attr_unique_id = f"{super().unique_id}{description.key}" + + if status_range := device.status_range.get(description.key): + self._status_range = cast(TuyaDeviceStatusRange, status_range) + + # Extract type data from integer status range, + # and determine unit of measurement + if self._status_range.type == "Integer": + self._type_data = IntegerTypeData.from_json(self._status_range.values) + if description.native_unit_of_measurement is None: + self._attr_native_unit_of_measurement = self._type_data.unit + + # Extract type data from enum status range + elif self._status_range.type == "Enum": + self._type_data = EnumTypeData.from_json(self._status_range.values) + + @property + def name(self) -> str | None: + """Return Tuya device name.""" + if self.entity_description.name is not None: + return f"{self.tuya_device.name} {self.entity_description.name}" + return self.tuya_device.name + + @property + def native_value(self) -> StateType: + """Return the value reported by the sensor.""" + # Unknown or unsupported data type + if self._status_range is None or self._status_range.type not in ( + "Integer", + "String", + "Enum", + ): + return None + + # Raw value + value = self.tuya_device.status.get(self.entity_description.key) + if value is None: + return None + + # Scale integer/float value + if isinstance(self._type_data, IntegerTypeData): + return self.scale(value, self._type_data.scale) + + # Unexpected enum value + if ( + isinstance(self._type_data, EnumTypeData) + and value not in self._type_data.range + ): + return None + + # Valid string or enum value + return value From d6d6929e2b17b6dcb739c2cc2d96d17b9ade312b Mon Sep 17 00:00:00 2001 From: "Peter A. Bigot" Date: Thu, 14 Oct 2021 15:14:48 -0500 Subject: [PATCH 0343/1038] Port unmerged fixes from tuya_v2 (#57624) --- homeassistant/components/tuya/light.py | 72 ++++++++++++++++---------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 0dc59faac8e..69e7020f56c 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -114,7 +114,8 @@ class TuyaLightEntity(TuyaEntity, LightEntity): def turn_on(self, **kwargs: Any) -> None: """Turn on or control the light.""" commands = [] - _LOGGER.debug("light kwargs-> %s", kwargs) + work_mode = self._work_mode() + _LOGGER.debug("light kwargs-> %s; work_mode %s", kwargs, work_mode) if ( DPCode.LIGHT in self.tuya_device.status @@ -124,27 +125,11 @@ class TuyaLightEntity(TuyaEntity, LightEntity): else: commands += [{"code": DPCode.SWITCH_LED, "value": True}] - if ATTR_BRIGHTNESS in kwargs: - if self._work_mode().startswith(WORK_MODE_COLOUR): - colour_data = self._get_hsv() - v_range = self._tuya_hsv_v_range() - colour_data["v"] = int( - self.remap(kwargs[ATTR_BRIGHTNESS], 0, 255, v_range[0], v_range[1]) - ) - commands += [ - {"code": self.dp_code_colour, "value": json.dumps(colour_data)} - ] - else: - new_range = self._tuya_brightness_range() - tuya_brightness = int( - self.remap( - kwargs[ATTR_BRIGHTNESS], 0, 255, new_range[0], new_range[1] - ) - ) - commands += [{"code": self.dp_code_bright, "value": tuya_brightness}] + colour_data = self._get_hsv() + v_range = self._tuya_hsv_v_range() + send_colour_data = False if ATTR_HS_COLOR in kwargs: - colour_data = self._get_hsv() # hsv h colour_data["h"] = int(kwargs[ATTR_HS_COLOR][0]) # hsv s @@ -161,16 +146,16 @@ class TuyaLightEntity(TuyaEntity, LightEntity): ) # hsv v ha_v = self.brightness - v_range = self._tuya_hsv_v_range() colour_data["v"] = int(self.remap(ha_v, 0, 255, v_range[0], v_range[1])) commands += [ {"code": self.dp_code_colour, "value": json.dumps(colour_data)} ] - if self.tuya_device.status[DPCode.WORK_MODE] != "colour": - commands += [{"code": DPCode.WORK_MODE, "value": "colour"}] + if work_mode != WORK_MODE_COLOUR: + work_mode = WORK_MODE_COLOUR + commands += [{"code": DPCode.WORK_MODE, "value": work_mode}] - if ATTR_COLOR_TEMP in kwargs: + elif ATTR_COLOR_TEMP in kwargs: # temp color new_range = self._tuya_temp_range() color_temp = self.remap( @@ -190,8 +175,29 @@ class TuyaLightEntity(TuyaEntity, LightEntity): ) commands += [{"code": self.dp_code_bright, "value": int(tuya_brightness)}] - if self.tuya_device.status[DPCode.WORK_MODE] != "white": - commands += [{"code": DPCode.WORK_MODE, "value": "white"}] + if work_mode != WORK_MODE_WHITE: + work_mode = WORK_MODE_WHITE + commands += [{"code": DPCode.WORK_MODE, "value": WORK_MODE_WHITE}] + + if ATTR_BRIGHTNESS in kwargs: + if work_mode == WORK_MODE_COLOUR: + colour_data["v"] = int( + self.remap(kwargs[ATTR_BRIGHTNESS], 0, 255, v_range[0], v_range[1]) + ) + send_colour_data = True + elif work_mode == WORK_MODE_WHITE: + new_range = self._tuya_brightness_range() + tuya_brightness = int( + self.remap( + kwargs[ATTR_BRIGHTNESS], 0, 255, new_range[0], new_range[1] + ) + ) + commands += [{"code": self.dp_code_bright, "value": tuya_brightness}] + + if send_colour_data: + commands += [ + {"code": self.dp_code_colour, "value": json.dumps(colour_data)} + ] self._send_command(commands) @@ -232,6 +238,14 @@ class TuyaLightEntity(TuyaEntity, LightEntity): bright_value = json.loads(bright_item.values) return bright_value.get("min", 0), bright_value.get("max", 255) + @property + def color_mode(self) -> str: + """Return the color_mode of the light.""" + work_mode = self._work_mode() + if work_mode == WORK_MODE_WHITE: + return COLOR_MODE_COLOR_TEMP + return COLOR_MODE_HS + @property def hs_color(self) -> tuple[float, float] | None: """Return the hs_color of the light.""" @@ -320,6 +334,12 @@ class TuyaLightEntity(TuyaEntity, LightEntity): return self.tuya_device.status.get(DPCode.WORK_MODE, "") def _get_hsv(self) -> dict[str, int]: + if ( + self.dp_code_colour not in self.tuya_device.status + or len(self.tuya_device.status[self.dp_code_colour]) == 0 + ): + return {"h": 0, "s": 0, "v": 0} + return json.loads(self.tuya_device.status[self.dp_code_colour]) @property From 0407a56fdfca0a36bda64d2c514e1dd9c97b28cb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Oct 2021 22:15:41 +0200 Subject: [PATCH 0344/1038] Add number platform to Tuya (#57672) --- .coveragerc | 1 + homeassistant/components/tuya/const.py | 7 +- homeassistant/components/tuya/number.py | 151 ++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/tuya/number.py diff --git a/.coveragerc b/.coveragerc index 6274e8526fc..91f4d43c702 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1116,6 +1116,7 @@ omit = homeassistant/components/tuya/const.py homeassistant/components/tuya/fan.py homeassistant/components/tuya/light.py + homeassistant/components/tuya/number.py homeassistant/components/tuya/scene.py homeassistant/components/tuya/select.py homeassistant/components/tuya/sensor.py diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index fa66d72da4f..56f5b584121 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -37,9 +37,9 @@ TUYA_SUPPORTED_PRODUCT_CATEGORIES = ( "fs", # Fan "fwl", # Ambient light "jsq", # Humidifier's light + "kfj", # Coffee Maker "kg", # Switch "kj", # Air Purifier - "kj", # Air Purifier "kfj", # Coffee maker "kt", # Air conditioner "mcs", # Door Window Sensor @@ -48,7 +48,6 @@ TUYA_SUPPORTED_PRODUCT_CATEGORIES = ( "wk", # Thermostat "xdd", # Ceiling Light "xxj", # Diffuser - "xxj", # Diffuser's light ) TUYA_SMART_APP = "tuyaSmart" @@ -59,6 +58,7 @@ PLATFORMS = [ "climate", "fan", "light", + "number", "scene", "select", "sensor", @@ -96,6 +96,7 @@ class DPCode(str, Enum): LOCK = "lock" # Lock / Child lock MATERIAL = "material" # Material MODE = "mode" # Working mode / Mode + POWDER_SET = "powder_set" # Powder PUMP_RESET = "pump_reset" # Water pump reset SHAKE = "shake" # Oscillating SPEED = "speed" # Speed level @@ -129,7 +130,9 @@ class DPCode(str, Enum): TEMPER_ALARM = "temper_alarm" # Tamper alarm UV = "uv" # UV sterilization WARM = "warm" # Heat preservation + WARM_TIME = "warm_time" # Heat preservation time WATER_RESET = "water_reset" # Resetting of water usage days + WATER_SET = "water_set" # Water level WET = "wet" # Humidification WORK_MODE = "work_mode" # Working mode diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py new file mode 100644 index 00000000000..977917f8b0c --- /dev/null +++ b/homeassistant/components/tuya/number.py @@ -0,0 +1,151 @@ +"""Support for Tuya number.""" +from __future__ import annotations + +from typing import cast + +from tuya_iot import TuyaDevice, TuyaDeviceManager +from tuya_iot.device import TuyaDeviceStatusRange + +from homeassistant.components.number import NumberEntity, NumberEntityDescription +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 . import HomeAssistantTuyaData +from .base import IntegerTypeData, TuyaEntity +from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode + +# All descriptions can be found here. Mostly the Integer data types in the +# default instructions set of each category end up being a number. +# https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq +NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { + # Coffee maker + # https://developer.tuya.com/en/docs/iot/categorykfj?id=Kaiuz2p12pc7f + "kfj": ( + NumberEntityDescription( + key=DPCode.WATER_SET, + name="Water Level", + icon="mdi:cup-water", + entity_registry_enabled_default=False, + ), + NumberEntityDescription( + key=DPCode.TEMP_SET, + name="Temperature", + icon="mdi:thermometer", + entity_registry_enabled_default=False, + ), + NumberEntityDescription( + key=DPCode.WARM_TIME, + name="Heat Preservation Time", + icon="mdi:timer", + entity_registry_enabled_default=False, + ), + NumberEntityDescription( + key=DPCode.POWDER_SET, + name="Powder", + entity_registry_enabled_default=False, + ), + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Tuya number dynamically through Tuya discovery.""" + hass_data: HomeAssistantTuyaData = hass.data[DOMAIN][entry.entry_id] + + @callback + def async_discover_device(device_ids: list[str]) -> None: + """Discover and add a discovered Tuya number.""" + entities: list[TuyaNumberEntity] = [] + for device_id in device_ids: + device = hass_data.device_manager.device_map[device_id] + if descriptions := NUMBERS.get(device.category): + for description in descriptions: + if ( + description.key in device.function + or description.key in device.status + ): + entities.append( + TuyaNumberEntity( + device, hass_data.device_manager, description + ) + ) + + async_add_entities(entities) + + async_discover_device([*hass_data.device_manager.device_map]) + + entry.async_on_unload( + async_dispatcher_connect(hass, TUYA_DISCOVERY_NEW, async_discover_device) + ) + + +class TuyaNumberEntity(TuyaEntity, NumberEntity): + """Tuya Number Entity.""" + + _status_range: TuyaDeviceStatusRange | None = None + _type_data: IntegerTypeData | None = None + + def __init__( + self, + device: TuyaDevice, + device_manager: TuyaDeviceManager, + description: NumberEntityDescription, + ) -> None: + """Init Tuya sensor.""" + super().__init__(device, device_manager) + self.entity_description = description + self._attr_unique_id = f"{super().unique_id}{description.key}" + + if status_range := device.status_range.get(description.key): + self._status_range = cast(TuyaDeviceStatusRange, status_range) + + # Extract type data from integer status range, + # and determine unit of measurement + if self._status_range.type == "Integer": + self._type_data = IntegerTypeData.from_json(self._status_range.values) + self._attr_max_value = self._type_data.max + self._attr_min_value = self._type_data.min + self._attr_step = self._type_data.step + if description.unit_of_measurement is None: + self._attr_unit_of_measurement = self._type_data.unit + + @property + def name(self) -> str | None: + """Return Tuya device name.""" + if self.entity_description.name is not None: + return f"{self.tuya_device.name} {self.entity_description.name}" + return self.tuya_device.name + + @property + def value(self) -> float | None: + """Return the entity value to represent the entity state.""" + # Unknown or unsupported data type + if self._status_range is None or self._status_range.type != "Integer": + return None + + # Raw value + value = self.tuya_device.status.get(self.entity_description.key) + + # Scale integer/float value + if value and isinstance(self._type_data, IntegerTypeData): + return self.scale(value, self._type_data.scale) + + return None + + def set_value(self, value: float) -> None: + """Set new value.""" + if self._type_data is None: + raise RuntimeError("Cannot set value, device doesn't provide type data") + + self._send_command( + [ + { + "code": self.entity_description.key, + "value": int(self.scale(value, self._type_data.scale)), + } + ] + ) From 3c11e2a097b886256c7794e3f17d8c42f1d7ffbc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 14 Oct 2021 13:31:01 -0700 Subject: [PATCH 0345/1038] Add entity category to WLED (#57693) --- homeassistant/components/wled/coordinator.py | 1 + homeassistant/components/wled/sensor.py | 8 ++++++++ homeassistant/components/wled/switch.py | 4 ++++ tests/components/wled/test_sensor.py | 10 ++++++++++ tests/components/wled/test_switch.py | 4 ++++ 5 files changed, 27 insertions(+) diff --git a/homeassistant/components/wled/coordinator.py b/homeassistant/components/wled/coordinator.py index 06c1f8b5dc3..6288a3d8ff7 100644 --- a/homeassistant/components/wled/coordinator.py +++ b/homeassistant/components/wled/coordinator.py @@ -25,6 +25,7 @@ class WLEDDataUpdateCoordinator(DataUpdateCoordinator[WLEDDevice]): """Class to manage fetching WLED data from single endpoint.""" keep_master_light: bool + config_entry: ConfigEntry def __init__( self, diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index 48d8443a0a9..b5982798852 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -11,6 +11,7 @@ from homeassistant.const import ( DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TIMESTAMP, ELECTRIC_CURRENT_MILLIAMPERE, + ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ) @@ -50,6 +51,7 @@ class WLEDEstimatedCurrentSensor(WLEDEntity, SensorEntity): _attr_icon = "mdi:power" _attr_native_unit_of_measurement = ELECTRIC_CURRENT_MILLIAMPERE _attr_device_class = DEVICE_CLASS_CURRENT + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED estimated current sensor.""" @@ -76,6 +78,7 @@ class WLEDUptimeSensor(WLEDEntity, SensorEntity): _attr_device_class = DEVICE_CLASS_TIMESTAMP _attr_entity_registry_enabled_default = False + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED uptime sensor.""" @@ -96,6 +99,7 @@ class WLEDFreeHeapSensor(WLEDEntity, SensorEntity): _attr_icon = "mdi:memory" _attr_entity_registry_enabled_default = False _attr_native_unit_of_measurement = DATA_BYTES + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED free heap sensor.""" @@ -115,6 +119,7 @@ class WLEDWifiSignalSensor(WLEDEntity, SensorEntity): _attr_icon = "mdi:wifi" _attr_native_unit_of_measurement = PERCENTAGE _attr_entity_registry_enabled_default = False + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED Wi-Fi signal sensor.""" @@ -136,6 +141,7 @@ class WLEDWifiRSSISensor(WLEDEntity, SensorEntity): _attr_device_class = DEVICE_CLASS_SIGNAL_STRENGTH _attr_native_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT _attr_entity_registry_enabled_default = False + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED Wi-Fi RSSI sensor.""" @@ -156,6 +162,7 @@ class WLEDWifiChannelSensor(WLEDEntity, SensorEntity): _attr_icon = "mdi:wifi" _attr_entity_registry_enabled_default = False + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED Wi-Fi Channel sensor.""" @@ -176,6 +183,7 @@ class WLEDWifiBSSIDSensor(WLEDEntity, SensorEntity): _attr_icon = "mdi:wifi" _attr_entity_registry_enabled_default = False + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED Wi-Fi BSSID sensor.""" diff --git a/homeassistant/components/wled/switch.py b/homeassistant/components/wled/switch.py index b17572f7607..376132c18a7 100644 --- a/homeassistant/components/wled/switch.py +++ b/homeassistant/components/wled/switch.py @@ -5,6 +5,7 @@ from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -42,6 +43,7 @@ class WLEDNightlightSwitch(WLEDEntity, SwitchEntity): """Defines a WLED nightlight switch.""" _attr_icon = "mdi:weather-night" + _attr_entity_category = ENTITY_CATEGORY_CONFIG def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED nightlight switch.""" @@ -78,6 +80,7 @@ class WLEDSyncSendSwitch(WLEDEntity, SwitchEntity): """Defines a WLED sync send switch.""" _attr_icon = "mdi:upload-network-outline" + _attr_entity_category = ENTITY_CATEGORY_CONFIG def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED sync send switch.""" @@ -110,6 +113,7 @@ class WLEDSyncReceiveSwitch(WLEDEntity, SwitchEntity): """Defines a WLED sync receive switch.""" _attr_icon = "mdi:download-network-outline" + _attr_entity_category = ENTITY_CATEGORY_CONFIG def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED sync receive switch.""" diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py index e6e4b130d99..28effd2ff07 100644 --- a/tests/components/wled/test_sensor.py +++ b/tests/components/wled/test_sensor.py @@ -17,6 +17,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, DATA_BYTES, ELECTRIC_CURRENT_MILLIAMPERE, + ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, STATE_UNKNOWN, @@ -106,6 +107,7 @@ async def test_sensors( entry = registry.async_get("sensor.wled_rgb_light_estimated_current") assert entry assert entry.unique_id == "aabbccddeeff_estimated_current" + assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC state = hass.states.get("sensor.wled_rgb_light_uptime") assert state @@ -116,26 +118,31 @@ async def test_sensors( entry = registry.async_get("sensor.wled_rgb_light_uptime") assert entry assert entry.unique_id == "aabbccddeeff_uptime" + assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC state = hass.states.get("sensor.wled_rgb_light_free_memory") assert state assert state.attributes.get(ATTR_ICON) == "mdi:memory" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_BYTES assert state.state == "14600" + assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC entry = registry.async_get("sensor.wled_rgb_light_free_memory") assert entry assert entry.unique_id == "aabbccddeeff_free_heap" + assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC state = hass.states.get("sensor.wled_rgb_light_wifi_signal") assert state assert state.attributes.get(ATTR_ICON) == "mdi:wifi" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.state == "76" + assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC entry = registry.async_get("sensor.wled_rgb_light_wifi_signal") assert entry assert entry.unique_id == "aabbccddeeff_wifi_signal" + assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC state = hass.states.get("sensor.wled_rgb_light_wifi_rssi") assert state @@ -149,6 +156,7 @@ async def test_sensors( entry = registry.async_get("sensor.wled_rgb_light_wifi_rssi") assert entry assert entry.unique_id == "aabbccddeeff_wifi_rssi" + assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC state = hass.states.get("sensor.wled_rgb_light_wifi_channel") assert state @@ -159,6 +167,7 @@ async def test_sensors( entry = registry.async_get("sensor.wled_rgb_light_wifi_channel") assert entry assert entry.unique_id == "aabbccddeeff_wifi_channel" + assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC state = hass.states.get("sensor.wled_rgb_light_wifi_bssid") assert state @@ -169,6 +178,7 @@ async def test_sensors( entry = registry.async_get("sensor.wled_rgb_light_wifi_bssid") assert entry assert entry.unique_id == "aabbccddeeff_wifi_bssid" + assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC @pytest.mark.parametrize( diff --git a/tests/components/wled/test_switch.py b/tests/components/wled/test_switch.py index eb8b8b526e0..7ba86960d2b 100644 --- a/tests/components/wled/test_switch.py +++ b/tests/components/wled/test_switch.py @@ -14,6 +14,7 @@ from homeassistant.components.wled.const import ( from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_ICON, + ENTITY_CATEGORY_CONFIG, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, @@ -43,6 +44,7 @@ async def test_switch_state( entry = entity_registry.async_get("switch.wled_rgb_light_nightlight") assert entry assert entry.unique_id == "aabbccddeeff_nightlight" + assert entry.entity_category == ENTITY_CATEGORY_CONFIG state = hass.states.get("switch.wled_rgb_light_sync_send") assert state @@ -53,6 +55,7 @@ async def test_switch_state( entry = entity_registry.async_get("switch.wled_rgb_light_sync_send") assert entry assert entry.unique_id == "aabbccddeeff_sync_send" + assert entry.entity_category == ENTITY_CATEGORY_CONFIG state = hass.states.get("switch.wled_rgb_light_sync_receive") assert state @@ -63,6 +66,7 @@ async def test_switch_state( entry = entity_registry.async_get("switch.wled_rgb_light_sync_receive") assert entry assert entry.unique_id == "aabbccddeeff_sync_receive" + assert entry.entity_category == ENTITY_CATEGORY_CONFIG async def test_switch_change_state( From a584d7b5c9849ca97fdcd855c933258cd1391a09 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Oct 2021 22:31:12 +0200 Subject: [PATCH 0346/1038] Add service configuration URL to Stookalert (#57697) --- .../components/stookalert/binary_sensor.py | 33 +++++++------------ homeassistant/components/stookalert/const.py | 1 - 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/stookalert/binary_sensor.py b/homeassistant/components/stookalert/binary_sensor.py index 07e89f3c97e..b75790cef8a 100644 --- a/homeassistant/components/stookalert/binary_sensor.py +++ b/homeassistant/components/stookalert/binary_sensor.py @@ -12,26 +12,14 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, - CONF_NAME, -) +from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import ( - ATTR_ENTRY_TYPE, - CONF_PROVINCE, - DOMAIN, - ENTRY_TYPE_SERVICE, - LOGGER, - PROVINCES, -) +from .const import CONF_PROVINCE, DOMAIN, ENTRY_TYPE_SERVICE, LOGGER, PROVINCES DEFAULT_NAME = "Stookalert" ATTRIBUTION = "Data provided by rivm.nl" @@ -90,13 +78,14 @@ class StookalertBinarySensor(BinarySensorEntity): self._client = client self._attr_name = f"Stookalert {entry.data[CONF_PROVINCE]}" self._attr_unique_id = entry.unique_id - self._attr_device_info = { - ATTR_IDENTIFIERS: {(DOMAIN, f"{entry.entry_id}")}, - ATTR_NAME: entry.data[CONF_PROVINCE], - ATTR_MANUFACTURER: "RIVM", - ATTR_MODEL: "Stookalert", - ATTR_ENTRY_TYPE: ENTRY_TYPE_SERVICE, - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"{entry.entry_id}")}, + name=entry.data[CONF_PROVINCE], + manufacturer="RIVM", + model="Stookalert", + entry_type=ENTRY_TYPE_SERVICE, + configuration_url="https://www.rivm.nl/stookalert", + ) def update(self) -> None: """Update the data from the Stookalert handler.""" diff --git a/homeassistant/components/stookalert/const.py b/homeassistant/components/stookalert/const.py index bbd5922b82a..72e39e60048 100644 --- a/homeassistant/components/stookalert/const.py +++ b/homeassistant/components/stookalert/const.py @@ -22,5 +22,4 @@ PROVINCES: Final = ( "Zuid-Holland", ) -ATTR_ENTRY_TYPE: Final = "entry_type" ENTRY_TYPE_SERVICE: Final = "service" From 5382ab8562cb3696e4240d5fbd9e650e7ffd225b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Oct 2021 22:31:23 +0200 Subject: [PATCH 0347/1038] Add device configuration URL to Plugwise (#57696) --- homeassistant/components/plugwise/gateway.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/plugwise/gateway.py b/homeassistant/components/plugwise/gateway.py index 41e3caacbff..976accfdfa0 100644 --- a/homeassistant/components/plugwise/gateway.py +++ b/homeassistant/components/plugwise/gateway.py @@ -192,11 +192,14 @@ class SmileGateway(CoordinatorEntity): @property def device_info(self) -> DeviceInfo: """Return the device information.""" - device_information = { - "identifiers": {(DOMAIN, self._dev_id)}, - "name": self._entity_name, - "manufacturer": "Plugwise", - } + device_information = DeviceInfo( + identifiers={(DOMAIN, self._dev_id)}, + name=self._entity_name, + manufacturer="Plugwise", + ) + + if entry := self.coordinator.config_entry: + device_information["configuration_url"] = f"http://{entry.data[CONF_HOST]}" if self._model is not None: device_information["model"] = self._model.replace("_", " ").title() From 93e15ef88c6e83efd46e46a220882813ee708463 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Oct 2021 22:42:34 +0200 Subject: [PATCH 0348/1038] Add service configuration URL to Spotify (#57701) --- homeassistant/components/spotify/media_player.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 780febf6791..029f7dc9e8b 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -262,13 +262,14 @@ class SpotifyMediaPlayer(MediaPlayerEntity): product = self._me["product"] model = f"Spotify {product}" - return { - "identifiers": {(DOMAIN, self._id)}, - "manufacturer": "Spotify AB", - "model": model, - "name": self._name, - "entry_type": "service", - } + return DeviceInfo( + identifiers={(DOMAIN, self._id)}, + manufacturer="Spotify AB", + model=model, + name=self._name, + entry_type="service", + configuration_url="https://open.spotify.com", + ) @property def state(self) -> str | None: From 7104750008517969130b4cc98245cbb99fb258dd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 14 Oct 2021 13:44:04 -0700 Subject: [PATCH 0349/1038] Bump frontend to 20211014.0 (#57706) --- 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 5b908efb2df..114f68ba720 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20211007.1" + "home-assistant-frontend==20211014.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 762d0fd47ba..857b74a9870 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==3.4.8 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211007.1 +home-assistant-frontend==20211014.0 httpx==0.19.0 ifaddr==0.1.7 jinja2==3.0.2 diff --git a/requirements_all.txt b/requirements_all.txt index 7eeb386220e..1bfb04bdd8e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -807,7 +807,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211007.1 +home-assistant-frontend==20211014.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d3b47554c58..f0b145a330b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -491,7 +491,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211007.1 +home-assistant-frontend==20211014.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 997d014111609d86197a383c80052671b67be527 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 14 Oct 2021 22:51:31 +0200 Subject: [PATCH 0350/1038] Add support for entity categories to NUT entities (#57689) --- homeassistant/components/nut/const.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/homeassistant/components/nut/const.py b/homeassistant/components/nut/const.py index 250d999ce35..0261c0209be 100644 --- a/homeassistant/components/nut/const.py +++ b/homeassistant/components/nut/const.py @@ -16,6 +16,8 @@ from homeassistant.components.sensor import ( from homeassistant.const import ( ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, FREQUENCY_HERTZ, PERCENTAGE, POWER_VOLT_AMPERE, @@ -68,6 +70,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "ups.load": SensorEntityDescription( key="ups.load", @@ -81,6 +84,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Overload Setting", native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", + entity_category=ENTITY_CATEGORY_CONFIG, ), "ups.id": SensorEntityDescription( key="ups.id", @@ -92,18 +96,21 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Load Restart Delay", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", + entity_category=ENTITY_CATEGORY_CONFIG, ), "ups.delay.reboot": SensorEntityDescription( key="ups.delay.reboot", name="UPS Reboot Delay", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", + entity_category=ENTITY_CATEGORY_CONFIG, ), "ups.delay.shutdown": SensorEntityDescription( key="ups.delay.shutdown", name="UPS Shutdown Delay", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", + entity_category=ENTITY_CATEGORY_CONFIG, ), "ups.timer.start": SensorEntityDescription( key="ups.timer.start", @@ -128,16 +135,19 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Self-Test Interval", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", + entity_category=ENTITY_CATEGORY_CONFIG, ), "ups.test.result": SensorEntityDescription( key="ups.test.result", name="Self-Test Result", icon="mdi:information-outline", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "ups.test.date": SensorEntityDescription( key="ups.test.date", name="Self-Test Date", icon="mdi:calendar", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "ups.display.language": SensorEntityDescription( key="ups.display.language", @@ -196,6 +206,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="ups.watchdog.status", name="Watchdog Status", icon="mdi:information-outline", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "ups.start.auto": SensorEntityDescription( key="ups.start.auto", @@ -229,6 +240,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Low Battery Setpoint", native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", + entity_category=ENTITY_CATEGORY_CONFIG, ), "battery.charge.restart": SensorEntityDescription( key="battery.charge.restart", @@ -241,6 +253,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { name="Warning Battery Setpoint", native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", + entity_category=ENTITY_CATEGORY_CONFIG, ), "battery.charger.status": SensorEntityDescription( key="battery.charger.status", @@ -297,12 +310,14 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "battery.runtime": SensorEntityDescription( key="battery.runtime", name="Battery Runtime", native_unit_of_measurement=TIME_SECONDS, icon="mdi:timer-outline", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "battery.runtime.low": SensorEntityDescription( key="battery.runtime.low", @@ -320,6 +335,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { key="battery.alarm.threshold", name="Battery Alarm Threshold", icon="mdi:information-outline", + entity_category=ENTITY_CATEGORY_CONFIG, ), "battery.date": SensorEntityDescription( key="battery.date", From e395e3366348873a6aa226a9a9d975655590ba6b Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 14 Oct 2021 22:52:17 +0200 Subject: [PATCH 0351/1038] Add support for entity categories to Synology DSM entities (#57690) --- homeassistant/components/synology_dsm/const.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index e054a9594a0..5bae7426769 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -26,6 +26,8 @@ from homeassistant.const import ( DATA_TERABYTES, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, TEMP_CELSIUS, ) @@ -108,6 +110,7 @@ UPGRADE_BINARY_SENSORS: tuple[SynologyDSMBinarySensorEntityDescription, ...] = ( key="update_available", name="Update available", device_class=DEVICE_CLASS_UPDATE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ) @@ -126,12 +129,14 @@ STORAGE_DISK_BINARY_SENSORS: tuple[SynologyDSMBinarySensorEntityDescription, ... key="disk_exceed_bad_sector_thr", name="Exceeded Max Bad Sectors", device_class=DEVICE_CLASS_SAFETY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SynologyDSMBinarySensorEntityDescription( api_key=SynoStorage.API_KEY, key="disk_below_remain_life_thr", name="Below Min Remaining Life", device_class=DEVICE_CLASS_SAFETY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ) @@ -305,6 +310,7 @@ STORAGE_VOL_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( name="Average Disk Temp", native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SynologyDSMSensorEntityDescription( api_key=SynoStorage.API_KEY, @@ -313,6 +319,7 @@ STORAGE_VOL_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ) STORAGE_DISK_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( @@ -322,12 +329,14 @@ STORAGE_DISK_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( name="Status (Smart)", icon="mdi:checkbox-marked-circle-outline", entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SynologyDSMSensorEntityDescription( api_key=SynoStorage.API_KEY, key="disk_status", name="Status", icon="mdi:checkbox-marked-circle-outline", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SynologyDSMSensorEntityDescription( api_key=SynoStorage.API_KEY, @@ -336,6 +345,7 @@ STORAGE_DISK_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ) @@ -347,6 +357,7 @@ INFORMATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SynologyDSMSensorEntityDescription( api_key=SynoDSMInformation.API_KEY, @@ -354,6 +365,7 @@ INFORMATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = ( name="last boot", device_class=DEVICE_CLASS_TIMESTAMP, entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ) @@ -364,5 +376,6 @@ SURVEILLANCE_SWITCH: tuple[SynologyDSMSwitchEntityDescription, ...] = ( key="home_mode", name="home mode", icon="mdi:home-account", + entity_category=ENTITY_CATEGORY_CONFIG, ), ) From a6aff613d7f28e1a1d381c8e14b36f4322369167 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 14 Oct 2021 22:53:05 +0200 Subject: [PATCH 0352/1038] Optimize update calls for AVM Fritz!Smarthome devices (#57579) --- homeassistant/components/fritzbox/__init__.py | 4 ++-- tests/components/fritzbox/test_binary_sensor.py | 8 ++++---- tests/components/fritzbox/test_climate.py | 12 ++++++------ tests/components/fritzbox/test_sensor.py | 10 +++++----- tests/components/fritzbox/test_switch.py | 10 +++++----- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 8d354f655f6..36b51a630ea 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -66,9 +66,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: devices = fritz.get_devices() data = {} + fritz.update_devices() for device in devices: - device.update() - # assume device as unavailable, see #55799 if ( device.has_powermeter @@ -78,6 +77,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: and device.power <= 0 and device.energy <= 0 ): + LOGGER.debug("Assume device %s as unavailable", device.name) device.present = False data[device.ain] = device diff --git a/tests/components/fritzbox/test_binary_sensor.py b/tests/components/fritzbox/test_binary_sensor.py index cb76109e0ff..f010c8499ea 100644 --- a/tests/components/fritzbox/test_binary_sensor.py +++ b/tests/components/fritzbox/test_binary_sensor.py @@ -70,14 +70,14 @@ async def test_update(hass: HomeAssistant, fritz: Mock): hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz ) - assert device.update.call_count == 1 + assert fritz().update_devices.call_count == 1 assert fritz().login.call_count == 1 next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) await hass.async_block_till_done() - assert device.update.call_count == 2 + assert fritz().update_devices.call_count == 2 assert fritz().login.call_count == 1 @@ -89,12 +89,12 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock): hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz ) - assert device.update.call_count == 1 + assert fritz().update_devices.call_count == 1 assert fritz().login.call_count == 1 next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) await hass.async_block_till_done() - assert device.update.call_count == 2 + assert fritz().update_devices.call_count == 2 assert fritz().login.call_count == 1 diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index 30ee7130fea..627c82f617a 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -134,7 +134,7 @@ async def test_update(hass: HomeAssistant, fritz: Mock): await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) - assert device.update.call_count == 2 + assert fritz().update_devices.call_count == 2 assert state assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 19 assert state.attributes[ATTR_TEMPERATURE] == 20 @@ -143,19 +143,19 @@ async def test_update(hass: HomeAssistant, fritz: Mock): async def test_update_error(hass: HomeAssistant, fritz: Mock): """Test update with error.""" device = FritzDeviceClimateMock() - device.update.side_effect = HTTPError("Boom") + fritz().update_devices.side_effect = HTTPError("Boom") assert not await setup_config_entry( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz ) - assert device.update.call_count == 1 + assert fritz().update_devices.call_count == 1 assert fritz().login.call_count == 1 next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) await hass.async_block_till_done() - assert device.update.call_count == 2 + assert fritz().update_devices.call_count == 2 assert fritz().login.call_count == 2 @@ -299,7 +299,7 @@ async def test_preset_mode_update(hass: HomeAssistant, fritz: Mock): await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) - assert device.update.call_count == 2 + assert fritz().update_devices.call_count == 2 assert state assert state.attributes[ATTR_PRESET_MODE] == PRESET_COMFORT @@ -310,6 +310,6 @@ async def test_preset_mode_update(hass: HomeAssistant, fritz: Mock): await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) - assert device.update.call_count == 3 + assert fritz().update_devices.call_count == 3 assert state assert state.attributes[ATTR_PRESET_MODE] == PRESET_ECO diff --git a/tests/components/fritzbox/test_sensor.py b/tests/components/fritzbox/test_sensor.py index f7a3ef9ae2a..42cf90bba58 100644 --- a/tests/components/fritzbox/test_sensor.py +++ b/tests/components/fritzbox/test_sensor.py @@ -70,30 +70,30 @@ async def test_update(hass: HomeAssistant, fritz: Mock): assert await setup_config_entry( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz ) - assert device.update.call_count == 1 + assert fritz().update_devices.call_count == 1 assert fritz().login.call_count == 1 next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) await hass.async_block_till_done() - assert device.update.call_count == 2 + assert fritz().update_devices.call_count == 2 assert fritz().login.call_count == 1 async def test_update_error(hass: HomeAssistant, fritz: Mock): """Test update with error.""" device = FritzDeviceSensorMock() - device.update.side_effect = HTTPError("Boom") + fritz().update_devices.side_effect = HTTPError("Boom") assert not await setup_config_entry( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz ) - assert device.update.call_count == 1 + assert fritz().update_devices.call_count == 1 assert fritz().login.call_count == 1 next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) await hass.async_block_till_done() - assert device.update.call_count == 2 + assert fritz().update_devices.call_count == 2 assert fritz().login.call_count == 2 diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index fb7221262d3..73bb7a1110b 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -114,32 +114,32 @@ async def test_update(hass: HomeAssistant, fritz: Mock): assert await setup_config_entry( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz ) - assert device.update.call_count == 1 + assert fritz().update_devices.call_count == 1 assert fritz().login.call_count == 1 next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) await hass.async_block_till_done() - assert device.update.call_count == 2 + assert fritz().update_devices.call_count == 2 assert fritz().login.call_count == 1 async def test_update_error(hass: HomeAssistant, fritz: Mock): """Test update with error.""" device = FritzDeviceSwitchMock() - device.update.side_effect = HTTPError("Boom") + fritz().update_devices.side_effect = HTTPError("Boom") assert not await setup_config_entry( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz ) - assert device.update.call_count == 1 + assert fritz().update_devices.call_count == 1 assert fritz().login.call_count == 1 next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) await hass.async_block_till_done() - assert device.update.call_count == 2 + assert fritz().update_devices.call_count == 2 assert fritz().login.call_count == 2 From 445c7301f8da1986d1c5b589f1409c9daa4e0202 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 14 Oct 2021 22:55:31 +0200 Subject: [PATCH 0353/1038] Skip auto-update when fqdn and ssl-verfiy is used for Synology DSM (#57568) --- .../components/synology_dsm/config_flow.py | 21 ++++++++++++- .../synology_dsm/test_config_flow.py | 30 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index ed0ee8e9125..a40dfd87744 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -1,6 +1,7 @@ """Config flow to configure the Synology DSM integration.""" from __future__ import annotations +from ipaddress import ip_address import logging from typing import Any from urllib.parse import urlparse @@ -92,6 +93,14 @@ def _ordered_shared_schema( } +def _is_valid_ip(text: str) -> bool: + try: + ip_address(text) + except ValueError: + return False + return True + + class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" @@ -245,7 +254,17 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): if not existing_entry: self._abort_if_unique_id_configured() - if existing_entry and existing_entry.data[CONF_HOST] != parsed_url.hostname: + fqdn_with_ssl_verification = ( + existing_entry + and not _is_valid_ip(existing_entry.data[CONF_HOST]) + and existing_entry.data[CONF_VERIFY_SSL] + ) + + if ( + existing_entry + and existing_entry.data[CONF_HOST] != parsed_url.hostname + and not fqdn_with_ssl_verification + ): _LOGGER.debug( "Update host from '%s' to '%s' for NAS '%s' via SSDP discovery", existing_entry.data[CONF_HOST], diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py index cc761a4b06a..435ed3bdb4b 100644 --- a/tests/components/synology_dsm/test_config_flow.py +++ b/tests/components/synology_dsm/test_config_flow.py @@ -423,6 +423,7 @@ async def test_reconfig_ssdp(hass: HomeAssistant, service: MagicMock): domain=DOMAIN, data={ CONF_HOST: "wrong_host", + CONF_VERIFY_SSL: VERIFY_SSL, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_MAC: MACS, @@ -443,6 +444,34 @@ async def test_reconfig_ssdp(hass: HomeAssistant, service: MagicMock): assert result["reason"] == "reconfigure_successful" +async def test_skip_reconfig_ssdp(hass: HomeAssistant, service: MagicMock): + """Test re-configuration of already existing entry by ssdp.""" + + MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "wrong_host", + CONF_VERIFY_SSL: True, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_MAC: MACS, + }, + unique_id=SERIAL, + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_SSDP}, + data={ + ssdp.ATTR_SSDP_LOCATION: "http://192.168.1.5:5000", + ssdp.ATTR_UPNP_FRIENDLY_NAME: "mydsm", + ssdp.ATTR_UPNP_SERIAL: "001132XXXX59", # Existing in MACS[0], but SSDP does not have `-` + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + async def test_existing_ssdp(hass: HomeAssistant, service: MagicMock): """Test abort of already existing entry by ssdp.""" @@ -450,6 +479,7 @@ async def test_existing_ssdp(hass: HomeAssistant, service: MagicMock): domain=DOMAIN, data={ CONF_HOST: "192.168.1.5", + CONF_VERIFY_SSL: VERIFY_SSL, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_MAC: MACS, From f8ea2f9b0875032dfa3120c6d5ab1ae554b67a15 Mon Sep 17 00:00:00 2001 From: Peter Nijssen Date: Thu, 14 Oct 2021 23:11:07 +0200 Subject: [PATCH 0354/1038] Bump spiderpy to 1.4.3 (#57675) --- homeassistant/components/spider/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/spider/manifest.json b/homeassistant/components/spider/manifest.json index ced19db39c7..d25d2c97901 100644 --- a/homeassistant/components/spider/manifest.json +++ b/homeassistant/components/spider/manifest.json @@ -2,7 +2,7 @@ "domain": "spider", "name": "Itho Daalderop Spider", "documentation": "https://www.home-assistant.io/integrations/spider", - "requirements": ["spiderpy==1.4.2"], + "requirements": ["spiderpy==1.4.3"], "codeowners": ["@peternijssen"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 1bfb04bdd8e..1436035f5ad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2202,7 +2202,7 @@ speak2mary==1.4.0 speedtest-cli==2.1.3 # homeassistant.components.spider -spiderpy==1.4.2 +spiderpy==1.4.3 # homeassistant.components.spotify spotipy==2.18.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f0b145a330b..8ea29165078 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1269,7 +1269,7 @@ speak2mary==1.4.0 speedtest-cli==2.1.3 # homeassistant.components.spider -spiderpy==1.4.2 +spiderpy==1.4.3 # homeassistant.components.spotify spotipy==2.18.0 From 9c7dc5865c8b5a568c5f5e8bb675c25000a748cc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 14 Oct 2021 14:11:57 -0700 Subject: [PATCH 0355/1038] Add url to CO2signal (#57703) --- homeassistant/components/co2signal/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index a4c1062e2c6..6e35a6d751b 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -109,6 +109,7 @@ class CO2Sensor(update_coordinator.CoordinatorEntity[CO2SignalResponse], SensorE ATTR_NAME: "CO2 signal", ATTR_MANUFACTURER: "Tmrow.com", "entry_type": "service", + "configuration_url": "https://www.electricitymap.org/", } self._attr_unique_id = ( f"{coordinator.entry_id}_{description.unique_id or description.key}" From 2e5af5d8e23f1dd322969ca827329ab1b53417b3 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 14 Oct 2021 23:13:01 +0200 Subject: [PATCH 0356/1038] Add configuration url to Synology DSM (#57704) --- .../components/synology_dsm/__init__.py | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index a4b03ecce3d..a8e35178be4 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -362,6 +362,10 @@ class SynoApi: """Initialize the API wrapper class.""" self._hass = hass self._entry = entry + if entry.data.get(CONF_SSL): + self.config_url = f"https://{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}" + else: + self.config_url = f"http://{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}" # DSM APIs self.dsm: SynologyDSM = None @@ -609,13 +613,14 @@ class SynologyDSMBaseEntity(CoordinatorEntity): @property def device_info(self) -> DeviceInfo: """Return the device information.""" - return { - "identifiers": {(DOMAIN, self._api.information.serial)}, - "name": "Synology NAS", - "manufacturer": "Synology", - "model": self._api.information.model, - "sw_version": self._api.information.version_string, - } + return DeviceInfo( + identifiers={(DOMAIN, self._api.information.serial)}, + name="Synology NAS", + manufacturer="Synology", + model=self._api.information.model, + sw_version=self._api.information.version_string, + configuration_url=self._api.config_url, + ) async def async_added_to_hass(self) -> None: """Register entity for updates from API.""" @@ -677,13 +682,12 @@ class SynologyDSMDeviceEntity(SynologyDSMBaseEntity): @property def device_info(self) -> DeviceInfo: """Return the device information.""" - return { - "identifiers": { - (DOMAIN, f"{self._api.information.serial}_{self._device_id}") - }, - "name": f"Synology NAS ({self._device_name} - {self._device_type})", - "manufacturer": self._device_manufacturer, - "model": self._device_model, - "sw_version": self._device_firmware, - "via_device": (DOMAIN, self._api.information.serial), - } + return DeviceInfo( + identifiers={(DOMAIN, f"{self._api.information.serial}_{self._device_id}")}, + name=f"Synology NAS ({self._device_name} - {self._device_type})", + manufacturer=self._device_manufacturer, + model=self._device_model, + sw_version=self._device_firmware, + via_device=(DOMAIN, self._api.information.serial), + configuration_url=self._api.config_url, + ) From 2601d71f5df2ac959d6a053ba069c0ef3346fcef Mon Sep 17 00:00:00 2001 From: David Le Brun Date: Thu, 14 Oct 2021 23:15:16 +0200 Subject: [PATCH 0357/1038] Add state_class to EnOcean sensors (#57666) --- homeassistant/components/enocean/sensor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/enocean/sensor.py b/homeassistant/components/enocean/sensor.py index ccf01eec448..ca0f5e95109 100644 --- a/homeassistant/components/enocean/sensor.py +++ b/homeassistant/components/enocean/sensor.py @@ -5,6 +5,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, SensorEntity, SensorEntityDescription, ) @@ -44,6 +45,7 @@ SENSOR_DESC_TEMPERATURE = SensorEntityDescription( native_unit_of_measurement=TEMP_CELSIUS, icon="mdi:thermometer", device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) SENSOR_DESC_HUMIDITY = SensorEntityDescription( @@ -52,6 +54,7 @@ SENSOR_DESC_HUMIDITY = SensorEntityDescription( native_unit_of_measurement=PERCENTAGE, icon="mdi:water-percent", device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ) SENSOR_DESC_POWER = SensorEntityDescription( @@ -60,6 +63,7 @@ SENSOR_DESC_POWER = SensorEntityDescription( native_unit_of_measurement=POWER_WATT, icon="mdi:power-plug", device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ) SENSOR_DESC_WINDOWHANDLE = SensorEntityDescription( From 4745e58a925cb6b1fe7dfaed4bef00e66f6f1c7f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 14 Oct 2021 16:13:12 -0600 Subject: [PATCH 0358/1038] Remove long-term statistics from IQVIA forecast sensor (#57687) --- homeassistant/components/iqvia/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index ab311800bdf..187816f5f9f 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -117,7 +117,6 @@ INDEX_SENSOR_DESCRIPTIONS = ( key=TYPE_ASTHMA_TOMORROW, name="Asthma Index: Tomorrow", icon="mdi:flower", - state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=TYPE_DISEASE_TODAY, From 3127074f76c216466afcf8a9558e644e48730281 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 15 Oct 2021 00:17:00 +0200 Subject: [PATCH 0359/1038] Add entity category to Shelly (#57705) --- .../components/shelly/binary_sensor.py | 21 +++++++++++++++---- homeassistant/components/shelly/entity.py | 18 ++++++++++++++++ homeassistant/components/shelly/sensor.py | 8 +++++++ 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index 46e5468c079..6ebea1456a1 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -18,6 +18,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -41,16 +42,24 @@ from .utils import ( SENSORS: Final = { ("device", "overtemp"): BlockAttributeDescription( - name="Overheating", device_class=DEVICE_CLASS_PROBLEM + name="Overheating", + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ("device", "overpower"): BlockAttributeDescription( - name="Overpowering", device_class=DEVICE_CLASS_PROBLEM + name="Overpowering", + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ("light", "overpower"): BlockAttributeDescription( - name="Overpowering", device_class=DEVICE_CLASS_PROBLEM + name="Overpowering", + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ("relay", "overpower"): BlockAttributeDescription( - name="Overpowering", device_class=DEVICE_CLASS_PROBLEM + name="Overpowering", + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ("sensor", "dwIsOpened"): BlockAttributeDescription( name="Door", @@ -106,6 +115,7 @@ REST_SENSORS: Final = { value=lambda status, _: status["cloud"]["connected"], device_class=DEVICE_CLASS_CONNECTIVITY, default_enabled=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "fwupdate": RestAttributeDescription( name="Firmware Update", @@ -116,6 +126,7 @@ REST_SENSORS: Final = { "latest_stable_version": status["update"]["new_version"], "installed_version": status["update"]["old_version"], }, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), } @@ -134,6 +145,7 @@ RPC_SENSORS: Final = { name="Cloud", device_class=DEVICE_CLASS_CONNECTIVITY, default_enabled=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "fwupdate": RpcAttributeDescription( key="sys", @@ -145,6 +157,7 @@ RPC_SENSORS: Final = { "latest_stable_version": status.get("stable", {"version": ""})["version"], "beta_version": status.get("beta", {"version": ""})["version"], }, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), } diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 0fe25884f00..db00e68c976 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -237,6 +237,7 @@ class BlockAttributeDescription: # Callable (settings, block), return true if entity should be removed removal_condition: Callable[[dict, Block], bool] | None = None extra_state_attributes: Callable[[Block], dict | None] | None = None + entity_category: str | None = None @dataclass @@ -255,6 +256,7 @@ class RpcAttributeDescription: available: Callable[[dict], bool] | None = None removal_condition: Callable[[dict, str], bool] | None = None extra_state_attributes: Callable[[dict], dict | None] | None = None + entity_category: str | None = None @dataclass @@ -269,6 +271,7 @@ class RestAttributeDescription: state_class: str | None = None default_enabled: bool = True extra_state_attributes: Callable[[dict], dict | None] | None = None + entity_category: str | None = None class ShellyBlockEntity(entity.Entity): @@ -469,6 +472,11 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): return self.description.extra_state_attributes(self.block) + @property + def entity_category(self) -> str | None: + """Return category of entity.""" + return self.description.entity_category + class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity): """Class to load info from REST.""" @@ -541,6 +549,11 @@ class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity): return self.description.extra_state_attributes(self.wrapper.device.status) + @property + def entity_category(self) -> str | None: + """Return category of entity.""" + return self.description.entity_category + class ShellyRpcAttributeEntity(ShellyRpcEntity, entity.Entity): """Helper class to represent a rpc attribute.""" @@ -599,6 +612,11 @@ class ShellyRpcAttributeEntity(ShellyRpcEntity, entity.Entity): self.wrapper.device.status[self.key][self.sub_key] ) + @property + def entity_category(self) -> str | None: + """Return category of entity.""" + return self.description.entity_category + class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEntity): """Represent a shelly sleeping block attribute entity.""" diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 9ee0712aaef..10e5977a51a 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, + ENTITY_CATEGORY_DIAGNOSTIC, LIGHT_LUX, PERCENTAGE, POWER_WATT, @@ -45,6 +46,7 @@ SENSORS: Final = { state_class=sensor.STATE_CLASS_MEASUREMENT, removal_condition=lambda settings, _: settings.get("external_power") == 1, available=lambda block: cast(int, block.battery) != -1, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ("device", "deviceTemp"): BlockAttributeDescription( name="Device Temperature", @@ -53,6 +55,7 @@ SENSORS: Final = { device_class=sensor.DEVICE_CLASS_TEMPERATURE, state_class=sensor.STATE_CLASS_MEASUREMENT, default_enabled=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ("emeter", "current"): BlockAttributeDescription( name="Current", @@ -205,6 +208,7 @@ SENSORS: Final = { extra_state_attributes=lambda block: { "Operational hours": round(cast(int, block.totalWorkTime) / 3600, 1) }, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ("adc", "adc"): BlockAttributeDescription( name="ADC", @@ -229,12 +233,14 @@ REST_SENSORS: Final = { device_class=sensor.DEVICE_CLASS_SIGNAL_STRENGTH, state_class=sensor.STATE_CLASS_MEASUREMENT, default_enabled=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "uptime": RestAttributeDescription( name="Uptime", value=lambda status, last: get_device_uptime(status["uptime"], last), device_class=sensor.DEVICE_CLASS_TIMESTAMP, default_enabled=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), } @@ -286,6 +292,7 @@ RPC_SENSORS: Final = { device_class=sensor.DEVICE_CLASS_SIGNAL_STRENGTH, state_class=sensor.STATE_CLASS_MEASUREMENT, default_enabled=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "uptime": RpcAttributeDescription( key="sys", @@ -294,6 +301,7 @@ RPC_SENSORS: Final = { value=get_device_uptime, device_class=sensor.DEVICE_CLASS_TIMESTAMP, default_enabled=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), } From abf6720cd3d12d5bed3194754481aa39933c349e Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 14 Oct 2021 18:20:08 -0400 Subject: [PATCH 0360/1038] Add strict typing to goalzero (#57680) --- .strict-typing | 1 + homeassistant/components/goalzero/__init__.py | 2 +- homeassistant/components/goalzero/binary_sensor.py | 4 +++- homeassistant/components/goalzero/sensor.py | 4 +++- homeassistant/components/goalzero/switch.py | 8 +++++--- mypy.ini | 11 +++++++++++ 6 files changed, 24 insertions(+), 6 deletions(-) diff --git a/.strict-typing b/.strict-typing index 7710f637090..907c51442a1 100644 --- a/.strict-typing +++ b/.strict-typing @@ -48,6 +48,7 @@ homeassistant.components.frontend.* homeassistant.components.fritz.* homeassistant.components.geo_location.* homeassistant.components.gios.* +homeassistant.components.goalzero.* homeassistant.components.group.* homeassistant.components.guardian.* homeassistant.components.history.* diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py index 7a179c46210..53daa29de8a 100644 --- a/homeassistant/components/goalzero/__init__.py +++ b/homeassistant/components/goalzero/__init__.py @@ -54,7 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except exceptions.ConnectError as ex: raise ConfigEntryNotReady(f"Failed to connect to device: {ex}") from ex - async def async_update_data(): + async def async_update_data() -> None: """Fetch data from API endpoint.""" try: await api.get_state() diff --git a/homeassistant/components/goalzero/binary_sensor.py b/homeassistant/components/goalzero/binary_sensor.py index 21eecc678ad..0e61c7178bb 100644 --- a/homeassistant/components/goalzero/binary_sensor.py +++ b/homeassistant/components/goalzero/binary_sensor.py @@ -1,6 +1,8 @@ """Support for Goal Zero Yeti Sensors.""" from __future__ import annotations +from typing import cast + from homeassistant.components.binary_sensor import ( DEVICE_CLASS_BATTERY_CHARGING, DEVICE_CLASS_CONNECTIVITY, @@ -81,4 +83,4 @@ class YetiBinarySensor(YetiEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return if the service is on.""" - return self.api.data.get(self.entity_description.key) == 1 + return cast(bool, self.api.data.get(self.entity_description.key) == 1) diff --git a/homeassistant/components/goalzero/sensor.py b/homeassistant/components/goalzero/sensor.py index 957891e67ed..bbf3fba753f 100644 --- a/homeassistant/components/goalzero/sensor.py +++ b/homeassistant/components/goalzero/sensor.py @@ -1,6 +1,8 @@ """Support for Goal Zero Yeti Sensors.""" from __future__ import annotations +from typing import cast + from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, @@ -170,4 +172,4 @@ class YetiSensor(YetiEntity, SensorEntity): @property def native_value(self) -> str: """Return the state.""" - return self.api.data.get(self.entity_description.key) + return cast(str, self.api.data.get(self.entity_description.key)) diff --git a/homeassistant/components/goalzero/switch.py b/homeassistant/components/goalzero/switch.py index 767c728e62b..2932413465a 100644 --- a/homeassistant/components/goalzero/switch.py +++ b/homeassistant/components/goalzero/switch.py @@ -1,6 +1,8 @@ """Support for Goal Zero Yeti Switches.""" from __future__ import annotations +from typing import Any, cast + from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME @@ -65,15 +67,15 @@ class YetiSwitch(YetiEntity, SwitchEntity): @property def is_on(self) -> bool: """Return state of the switch.""" - return self.api.data.get(self.entity_description.key) + return cast(bool, self.api.data.get(self.entity_description.key)) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" payload = {self.entity_description.key: 0} await self.api.post_state(payload=payload) self.coordinator.async_set_updated_data(data=payload) - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" payload = {self.entity_description.key: 1} await self.api.post_state(payload=payload) diff --git a/mypy.ini b/mypy.ini index 440f410d0ab..636fbfe5287 100644 --- a/mypy.ini +++ b/mypy.ini @@ -539,6 +539,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.goalzero.*] +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.group.*] check_untyped_defs = true disallow_incomplete_defs = true From cc6030cff211c1af7790d33ea030826d2f522d51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 15 Oct 2021 00:31:46 +0200 Subject: [PATCH 0361/1038] Add configuration_url to Uptime Robot (#57709) --- homeassistant/components/uptimerobot/entity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/uptimerobot/entity.py b/homeassistant/components/uptimerobot/entity.py index 6944750ab66..f78e2665ffe 100644 --- a/homeassistant/components/uptimerobot/entity.py +++ b/homeassistant/components/uptimerobot/entity.py @@ -33,6 +33,7 @@ class UptimeRobotEntity(CoordinatorEntity): "manufacturer": "Uptime Robot Team", "entry_type": "service", "model": self.monitor.type.name, + "configuration_url": f"https://uptimerobot.com/dashboard#{self.monitor.id}", } self._attr_extra_state_attributes = { ATTR_TARGET: self.monitor.url, From 9280215d69a1953eff6e914a3304352dfd22a2fc Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 15 Oct 2021 00:32:17 +0200 Subject: [PATCH 0362/1038] push motionblinds to 0.5.6 (#57707) --- 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 b8e8add912d..6ad29e4257e 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.5.5"], + "requirements": ["motionblinds==0.5.6"], "codeowners": ["@starkillerOG"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 1436035f5ad..e5aed307008 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1008,7 +1008,7 @@ minio==4.0.9 mitemp_bt==0.0.3 # homeassistant.components.motion_blinds -motionblinds==0.5.5 +motionblinds==0.5.6 # homeassistant.components.motioneye motioneye-client==0.3.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8ea29165078..b40b0ca3f43 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -597,7 +597,7 @@ millheater==0.6.2 minio==4.0.9 # homeassistant.components.motion_blinds -motionblinds==0.5.5 +motionblinds==0.5.6 # homeassistant.components.motioneye motioneye-client==0.3.11 From efb6300359150de8dd24bb3f239c4638cd27ba9b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 15 Oct 2021 00:35:09 +0200 Subject: [PATCH 0363/1038] Add service configuration URL to Verisure (#57713) --- .../verisure/alarm_control_panel.py | 13 ++--- .../components/verisure/binary_sensor.py | 30 ++++++----- homeassistant/components/verisure/camera.py | 17 ++++--- homeassistant/components/verisure/lock.py | 17 ++++--- homeassistant/components/verisure/sensor.py | 51 ++++++++++--------- homeassistant/components/verisure/switch.py | 17 ++++--- 6 files changed, 77 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py index 176ca9444c1..e9cb2f49842 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -42,12 +42,13 @@ class VerisureAlarm(CoordinatorEntity, AlarmControlPanelEntity): @property def device_info(self) -> DeviceInfo: """Return device information about this entity.""" - return { - "name": "Verisure Alarm", - "manufacturer": "Verisure", - "model": "VBox", - "identifiers": {(DOMAIN, self.coordinator.entry.data[CONF_GIID])}, - } + return DeviceInfo( + name="Verisure Alarm", + manufacturer="Verisure", + model="VBox", + identifiers={(DOMAIN, self.coordinator.entry.data[CONF_GIID])}, + configuration_url="https://mypages.verisure.com", + ) @property def unique_id(self) -> str: diff --git a/homeassistant/components/verisure/binary_sensor.py b/homeassistant/components/verisure/binary_sensor.py index 4d9b1e84770..d2bdd05a9ac 100644 --- a/homeassistant/components/verisure/binary_sensor.py +++ b/homeassistant/components/verisure/binary_sensor.py @@ -54,14 +54,15 @@ class VerisureDoorWindowSensor(CoordinatorEntity, BinarySensorEntity): def device_info(self) -> DeviceInfo: """Return device information about this entity.""" area = self.coordinator.data["door_window"][self.serial_number]["area"] - return { - "name": area, - "suggested_area": area, - "manufacturer": "Verisure", - "model": "Shock Sensor Detector", - "identifiers": {(DOMAIN, self.serial_number)}, - "via_device": (DOMAIN, self.coordinator.entry.data[CONF_GIID]), - } + return DeviceInfo( + name=area, + suggested_area=area, + manufacturer="Verisure", + model="Shock Sensor Detector", + identifiers={(DOMAIN, self.serial_number)}, + via_device=(DOMAIN, self.coordinator.entry.data[CONF_GIID]), + configuration_url="https://mypages.verisure.com", + ) @property def is_on(self) -> bool: @@ -95,12 +96,13 @@ class VerisureEthernetStatus(CoordinatorEntity, BinarySensorEntity): @property def device_info(self) -> DeviceInfo: """Return device information about this entity.""" - return { - "name": "Verisure Alarm", - "manufacturer": "Verisure", - "model": "VBox", - "identifiers": {(DOMAIN, self.coordinator.entry.data[CONF_GIID])}, - } + return DeviceInfo( + name="Verisure Alarm", + manufacturer="Verisure", + model="VBox", + identifiers={(DOMAIN, self.coordinator.entry.data[CONF_GIID])}, + configuration_url="https://mypages.verisure.com", + ) @property def is_on(self) -> bool: diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index 455d7070a8b..5335865e128 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -70,14 +70,15 @@ class VerisureSmartcam(CoordinatorEntity, Camera): def device_info(self) -> DeviceInfo: """Return device information about this entity.""" area = self.coordinator.data["cameras"][self.serial_number]["area"] - return { - "name": area, - "suggested_area": area, - "manufacturer": "Verisure", - "model": "SmartCam", - "identifiers": {(DOMAIN, self.serial_number)}, - "via_device": (DOMAIN, self.coordinator.entry.data[CONF_GIID]), - } + return DeviceInfo( + name=area, + suggested_area=area, + manufacturer="Verisure", + model="SmartCam", + identifiers={(DOMAIN, self.serial_number)}, + via_device=(DOMAIN, self.coordinator.entry.data[CONF_GIID]), + configuration_url="https://mypages.verisure.com", + ) def camera_image( self, width: int | None = None, height: int | None = None diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index e645bf3f8c1..c5be4162d1f 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -79,14 +79,15 @@ class VerisureDoorlock(CoordinatorEntity, LockEntity): def device_info(self) -> DeviceInfo: """Return device information about this entity.""" area = self.coordinator.data["locks"][self.serial_number]["area"] - return { - "name": area, - "suggested_area": area, - "manufacturer": "Verisure", - "model": "Lockguard Smartlock", - "identifiers": {(DOMAIN, self.serial_number)}, - "via_device": (DOMAIN, self.coordinator.entry.data[CONF_GIID]), - } + return DeviceInfo( + name=area, + suggested_area=area, + manufacturer="Verisure", + model="Lockguard Smartlock", + identifiers={(DOMAIN, self.serial_number)}, + via_device=(DOMAIN, self.coordinator.entry.data[CONF_GIID]), + configuration_url="https://mypages.verisure.com", + ) @property def available(self) -> bool: diff --git a/homeassistant/components/verisure/sensor.py b/homeassistant/components/verisure/sensor.py index cdeddd8d6e4..2075dbe4a97 100644 --- a/homeassistant/components/verisure/sensor.py +++ b/homeassistant/components/verisure/sensor.py @@ -74,14 +74,15 @@ class VerisureThermometer(CoordinatorEntity, SensorEntity): "deviceType" ) area = self.coordinator.data["climate"][self.serial_number]["deviceArea"] - return { - "name": area, - "suggested_area": area, - "manufacturer": "Verisure", - "model": DEVICE_TYPE_NAME.get(device_type, device_type), - "identifiers": {(DOMAIN, self.serial_number)}, - "via_device": (DOMAIN, self.coordinator.entry.data[CONF_GIID]), - } + return DeviceInfo( + name=area, + suggested_area=area, + manufacturer="Verisure", + model=DEVICE_TYPE_NAME.get(device_type, device_type), + identifiers={(DOMAIN, self.serial_number)}, + via_device=(DOMAIN, self.coordinator.entry.data[CONF_GIID]), + configuration_url="https://mypages.verisure.com", + ) @property def native_value(self) -> str | None: @@ -127,14 +128,15 @@ class VerisureHygrometer(CoordinatorEntity, SensorEntity): "deviceType" ) area = self.coordinator.data["climate"][self.serial_number]["deviceArea"] - return { - "name": area, - "suggested_area": area, - "manufacturer": "Verisure", - "model": DEVICE_TYPE_NAME.get(device_type, device_type), - "identifiers": {(DOMAIN, self.serial_number)}, - "via_device": (DOMAIN, self.coordinator.entry.data[CONF_GIID]), - } + return DeviceInfo( + name=area, + suggested_area=area, + manufacturer="Verisure", + model=DEVICE_TYPE_NAME.get(device_type, device_type), + identifiers={(DOMAIN, self.serial_number)}, + via_device=(DOMAIN, self.coordinator.entry.data[CONF_GIID]), + configuration_url="https://mypages.verisure.com", + ) @property def native_value(self) -> str | None: @@ -176,14 +178,15 @@ class VerisureMouseDetection(CoordinatorEntity, SensorEntity): def device_info(self) -> DeviceInfo: """Return device information about this entity.""" area = self.coordinator.data["mice"][self.serial_number]["area"] - return { - "name": area, - "suggested_area": area, - "manufacturer": "Verisure", - "model": "Mouse detector", - "identifiers": {(DOMAIN, self.serial_number)}, - "via_device": (DOMAIN, self.coordinator.entry.data[CONF_GIID]), - } + return DeviceInfo( + name=area, + suggested_area=area, + manufacturer="Verisure", + model="Mouse detector", + identifiers={(DOMAIN, self.serial_number)}, + via_device=(DOMAIN, self.coordinator.entry.data[CONF_GIID]), + configuration_url="https://mypages.verisure.com", + ) @property def native_value(self) -> str | None: diff --git a/homeassistant/components/verisure/switch.py b/homeassistant/components/verisure/switch.py index f428f70cda6..73abeaaf5ce 100644 --- a/homeassistant/components/verisure/switch.py +++ b/homeassistant/components/verisure/switch.py @@ -49,14 +49,15 @@ class VerisureSmartplug(CoordinatorEntity, SwitchEntity): def device_info(self) -> DeviceInfo: """Return device information about this entity.""" area = self.coordinator.data["smart_plugs"][self.serial_number]["area"] - return { - "name": area, - "suggested_area": area, - "manufacturer": "Verisure", - "model": "SmartPlug", - "identifiers": {(DOMAIN, self.serial_number)}, - "via_device": (DOMAIN, self.coordinator.entry.data[CONF_GIID]), - } + return DeviceInfo( + name=area, + suggested_area=area, + manufacturer="Verisure", + model="SmartPlug", + identifiers={(DOMAIN, self.serial_number)}, + via_device=(DOMAIN, self.coordinator.entry.data[CONF_GIID]), + configuration_url="https://mypages.verisure.com", + ) @property def is_on(self) -> bool: From 4a20d28ec60d008898e94185a09ab96ef60d5217 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 15 Oct 2021 00:37:31 +0200 Subject: [PATCH 0364/1038] Add service configuration URL to Speedtest.net (#57715) --- homeassistant/components/speedtestdotnet/sensor.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/speedtestdotnet/sensor.py b/homeassistant/components/speedtestdotnet/sensor.py index fa9cd137ba1..06f180a570f 100644 --- a/homeassistant/components/speedtestdotnet/sensor.py +++ b/homeassistant/components/speedtestdotnet/sensor.py @@ -8,6 +8,7 @@ from homeassistant.components.speedtestdotnet import SpeedTestDataCoordinator 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 AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import StateType @@ -60,11 +61,12 @@ class SpeedtestSensor(CoordinatorEntity, RestoreEntity, SensorEntity): self._attr_unique_id = description.key self._state: StateType = None self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} - self._attr_device_info = { - "identifiers": {(DOMAIN, self.coordinator.config_entry.entry_id)}, - "name": DEFAULT_NAME, - "entry_type": "service", - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.coordinator.config_entry.entry_id)}, + name=DEFAULT_NAME, + entry_type="service", + configuration_url="https://www.speedtest.net/", + ) @property def native_value(self) -> StateType: From a9737865ae74c1d6620a0cf65a2520c17e364c85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 15 Oct 2021 00:46:06 +0200 Subject: [PATCH 0365/1038] Fix platform typo in Tuya const (#57716) --- homeassistant/components/tuya/config_flow.py | 4 ++-- homeassistant/components/tuya/const.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py index bcde364ae1b..08a913992a3 100644 --- a/homeassistant/components/tuya/config_flow.py +++ b/homeassistant/components/tuya/config_flow.py @@ -23,7 +23,7 @@ from .const import ( TUYA_COUNTRIES, TUYA_RESPONSE_CODE, TUYA_RESPONSE_MSG, - TUYA_RESPONSE_PLATFROM_URL, + TUYA_RESPONSE_PLATFORM_URL, TUYA_RESPONSE_RESULT, TUYA_RESPONSE_SUCCESS, TUYA_SMART_APP, @@ -97,7 +97,7 @@ class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if response.get(TUYA_RESPONSE_SUCCESS, False): if endpoint := response.get(TUYA_RESPONSE_RESULT, {}).get( - TUYA_RESPONSE_PLATFROM_URL + TUYA_RESPONSE_PLATFORM_URL ): data[CONF_ENDPOINT] = endpoint diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 56f5b584121..0bc71a478b4 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -23,7 +23,7 @@ TUYA_RESPONSE_CODE = "code" TUYA_RESPONSE_RESULT = "result" TUYA_RESPONSE_MSG = "msg" TUYA_RESPONSE_SUCCESS = "success" -TUYA_RESPONSE_PLATFROM_URL = "platform_url" +TUYA_RESPONSE_PLATFORM_URL = "platform_url" TUYA_SUPPORTED_PRODUCT_CATEGORIES = ( "bh", # Smart Kettle From e077fb13ce87b9b894e38150607df3633cbf073c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 15 Oct 2021 00:46:26 +0200 Subject: [PATCH 0366/1038] Add device configuration URL to WLED (#57692) --- homeassistant/components/wled/models.py | 22 ++++++++-------------- tests/components/wled/conftest.py | 1 + 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/wled/models.py b/homeassistant/components/wled/models.py index de8628fc755..93b04b9f49b 100644 --- a/homeassistant/components/wled/models.py +++ b/homeassistant/components/wled/models.py @@ -1,11 +1,4 @@ """Models for WLED.""" -from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, - ATTR_SW_VERSION, -) from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -21,10 +14,11 @@ class WLEDEntity(CoordinatorEntity): @property def device_info(self) -> DeviceInfo: """Return device information about this WLED device.""" - return { - ATTR_IDENTIFIERS: {(DOMAIN, self.coordinator.data.info.mac_address)}, - ATTR_NAME: self.coordinator.data.info.name, - ATTR_MANUFACTURER: self.coordinator.data.info.brand, - ATTR_MODEL: self.coordinator.data.info.product, - ATTR_SW_VERSION: self.coordinator.data.info.version, - } + return DeviceInfo( + identifiers={(DOMAIN, self.coordinator.data.info.mac_address)}, + name=self.coordinator.data.info.name, + manufacturer=self.coordinator.data.info.brand, + model=self.coordinator.data.info.product, + sw_version=self.coordinator.data.info.version, + configuration_url=f"http://{self.coordinator.wled.host}", + ) diff --git a/tests/components/wled/conftest.py b/tests/components/wled/conftest.py index 527dc84af59..708bbf46834 100644 --- a/tests/components/wled/conftest.py +++ b/tests/components/wled/conftest.py @@ -57,6 +57,7 @@ def mock_wled(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None wled = wled_mock.return_value wled.update.return_value = device wled.connected = False + wled.host = "127.0.0.1" yield wled From c243dca58e013ca5e140c2484be7751d9b16469f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 15 Oct 2021 00:50:09 +0200 Subject: [PATCH 0367/1038] Change name from Uptime Robot to UptimeRobot (#57714) --- homeassistant/components/uptimerobot/__init__.py | 6 +++--- homeassistant/components/uptimerobot/binary_sensor.py | 8 ++++---- homeassistant/components/uptimerobot/config_flow.py | 4 ++-- homeassistant/components/uptimerobot/const.py | 4 ++-- homeassistant/components/uptimerobot/entity.py | 4 ++-- homeassistant/components/uptimerobot/manifest.json | 2 +- homeassistant/components/uptimerobot/strings.json | 4 ++-- tests/components/uptimerobot/__init__.py | 2 +- tests/components/uptimerobot/common.py | 6 +++--- tests/components/uptimerobot/test_binary_sensor.py | 4 ++-- tests/components/uptimerobot/test_config_flow.py | 10 +++++----- tests/components/uptimerobot/test_init.py | 2 +- 12 files changed, 28 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/uptimerobot/__init__.py b/homeassistant/components/uptimerobot/__init__.py index 4eaef45c4d2..c4b2eb54b2f 100644 --- a/homeassistant/components/uptimerobot/__init__.py +++ b/homeassistant/components/uptimerobot/__init__.py @@ -1,4 +1,4 @@ -"""The Uptime Robot integration.""" +"""The UptimeRobot integration.""" from __future__ import annotations from pyuptimerobot import ( @@ -24,7 +24,7 @@ from .const import API_ATTR_OK, COORDINATOR_UPDATE_INTERVAL, DOMAIN, LOGGER, PLA async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Uptime Robot from a config entry.""" + """Set up UptimeRobot from a config entry.""" hass.data.setdefault(DOMAIN, {}) uptime_robot_api = UptimeRobot( entry.data[CONF_API_KEY], async_get_clientsession(hass) @@ -55,7 +55,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class UptimeRobotDataUpdateCoordinator(DataUpdateCoordinator): - """Data update coordinator for Uptime Robot.""" + """Data update coordinator for UptimeRobot.""" def __init__( self, diff --git a/homeassistant/components/uptimerobot/binary_sensor.py b/homeassistant/components/uptimerobot/binary_sensor.py index ac0dc0c1186..ac40f1f4788 100644 --- a/homeassistant/components/uptimerobot/binary_sensor.py +++ b/homeassistant/components/uptimerobot/binary_sensor.py @@ -1,4 +1,4 @@ -"""A platform that to monitor Uptime Robot monitors.""" +"""UptimeRobot binary_sensor platform.""" from __future__ import annotations import voluptuous as vol @@ -31,7 +31,7 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Uptime Robot binary_sensor platform.""" + """Set up the UptimeRobot binary_sensor platform.""" hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=config @@ -42,7 +42,7 @@ async def async_setup_platform( async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the Uptime Robot binary_sensors.""" + """Set up the UptimeRobot binary_sensors.""" coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( [ @@ -61,7 +61,7 @@ async def async_setup_entry( class UptimeRobotBinarySensor(UptimeRobotEntity, BinarySensorEntity): - """Representation of a Uptime Robot binary sensor.""" + """Representation of a UptimeRobot binary sensor.""" @property def is_on(self) -> bool: diff --git a/homeassistant/components/uptimerobot/config_flow.py b/homeassistant/components/uptimerobot/config_flow.py index 1e8bec992ad..00d85ff3687 100644 --- a/homeassistant/components/uptimerobot/config_flow.py +++ b/homeassistant/components/uptimerobot/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow for Uptime Robot integration.""" +"""Config flow for UptimeRobot integration.""" from __future__ import annotations from pyuptimerobot import ( @@ -23,7 +23,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): str}) class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for Uptime Robot.""" + """Handle a config flow for UptimeRobot.""" VERSION = 1 diff --git a/homeassistant/components/uptimerobot/const.py b/homeassistant/components/uptimerobot/const.py index 7f3655b75cf..1c9bbe3e00b 100644 --- a/homeassistant/components/uptimerobot/const.py +++ b/homeassistant/components/uptimerobot/const.py @@ -1,4 +1,4 @@ -"""Constants for the Uptime Robot integration.""" +"""Constants for the UptimeRobot integration.""" from __future__ import annotations from datetime import timedelta @@ -13,7 +13,7 @@ COORDINATOR_UPDATE_INTERVAL: timedelta = timedelta(seconds=10) DOMAIN: Final = "uptimerobot" PLATFORMS: Final = ["binary_sensor"] -ATTRIBUTION: Final = "Data provided by Uptime Robot" +ATTRIBUTION: Final = "Data provided by UptimeRobot" ATTR_TARGET: Final = "target" diff --git a/homeassistant/components/uptimerobot/entity.py b/homeassistant/components/uptimerobot/entity.py index f78e2665ffe..8dde12c6091 100644 --- a/homeassistant/components/uptimerobot/entity.py +++ b/homeassistant/components/uptimerobot/entity.py @@ -23,14 +23,14 @@ class UptimeRobotEntity(CoordinatorEntity): description: EntityDescription, monitor: UptimeRobotMonitor, ) -> None: - """Initialize Uptime Robot entities.""" + """Initialize UptimeRobot entities.""" super().__init__(coordinator) self.entity_description = description self._monitor = monitor self._attr_device_info = { "identifiers": {(DOMAIN, str(self.monitor.id))}, "name": self.monitor.friendly_name, - "manufacturer": "Uptime Robot Team", + "manufacturer": "UptimeRobot Team", "entry_type": "service", "model": self.monitor.type.name, "configuration_url": f"https://uptimerobot.com/dashboard#{self.monitor.id}", diff --git a/homeassistant/components/uptimerobot/manifest.json b/homeassistant/components/uptimerobot/manifest.json index 66b1dc9abe4..8f9a9d74103 100644 --- a/homeassistant/components/uptimerobot/manifest.json +++ b/homeassistant/components/uptimerobot/manifest.json @@ -1,6 +1,6 @@ { "domain": "uptimerobot", - "name": "Uptime Robot", + "name": "UptimeRobot", "documentation": "https://www.home-assistant.io/integrations/uptimerobot", "requirements": [ "pyuptimerobot==21.9.0" diff --git a/homeassistant/components/uptimerobot/strings.json b/homeassistant/components/uptimerobot/strings.json index 094130b470d..2946f2e2d5d 100644 --- a/homeassistant/components/uptimerobot/strings.json +++ b/homeassistant/components/uptimerobot/strings.json @@ -2,14 +2,14 @@ "config": { "step": { "user": { - "description": "You need to supply a read-only API key from Uptime Robot", + "description": "You need to supply a read-only API key from UptimeRobot", "data": { "api_key": "[%key:common::config_flow::data::api_key%]" } }, "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", - "description": "You need to supply a new read-only API key from Uptime Robot", + "description": "You need to supply a new read-only API key from UptimeRobot", "data": { "api_key": "[%key:common::config_flow::data::api_key%]" } diff --git a/tests/components/uptimerobot/__init__.py b/tests/components/uptimerobot/__init__.py index b8f18655820..cd83dd8ee74 100644 --- a/tests/components/uptimerobot/__init__.py +++ b/tests/components/uptimerobot/__init__.py @@ -1 +1 @@ -"""Tests for the Uptime Robot integration.""" +"""Tests for the UptimeRobot integration.""" diff --git a/tests/components/uptimerobot/common.py b/tests/components/uptimerobot/common.py index aa241ce5a92..224d2d32911 100644 --- a/tests/components/uptimerobot/common.py +++ b/tests/components/uptimerobot/common.py @@ -1,4 +1,4 @@ -"""Common constants and functions for Uptime Robot tests.""" +"""Common constants and functions for UptimeRobot tests.""" from __future__ import annotations from enum import Enum @@ -61,7 +61,7 @@ def mock_uptimerobot_api_response( status: APIStatus = APIStatus.OK, key: MockApiResponseKey = MockApiResponseKey.MONITORS, ) -> UptimeRobotApiResponse: - """Mock API response for Uptime Robot.""" + """Mock API response for UptimeRobot.""" return UptimeRobotApiResponse.from_dict( { "stat": {"error": APIStatus.FAIL}.get(key, status), @@ -77,7 +77,7 @@ def mock_uptimerobot_api_response( async def setup_uptimerobot_integration(hass: HomeAssistant) -> MockConfigEntry: - """Set up the Uptime Robot integration.""" + """Set up the UptimeRobot integration.""" mock_entry = MockConfigEntry(**MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA) mock_entry.add_to_hass(hass) diff --git a/tests/components/uptimerobot/test_binary_sensor.py b/tests/components/uptimerobot/test_binary_sensor.py index 13bb3b342e9..f86ab9eef14 100644 --- a/tests/components/uptimerobot/test_binary_sensor.py +++ b/tests/components/uptimerobot/test_binary_sensor.py @@ -1,4 +1,4 @@ -"""Test Uptime Robot binary_sensor.""" +"""Test UptimeRobot binary_sensor.""" from unittest.mock import patch @@ -53,7 +53,7 @@ async def test_config_import(hass: HomeAssistant) -> None: async def test_presentation(hass: HomeAssistant) -> None: - """Test the presenstation of Uptime Robot binary_sensors.""" + """Test the presenstation of UptimeRobot binary_sensors.""" await setup_uptimerobot_integration(hass) entity = hass.states.get(UPTIMEROBOT_TEST_ENTITY) diff --git a/tests/components/uptimerobot/test_config_flow.py b/tests/components/uptimerobot/test_config_flow.py index 8c5225ad38c..074179761d9 100644 --- a/tests/components/uptimerobot/test_config_flow.py +++ b/tests/components/uptimerobot/test_config_flow.py @@ -1,4 +1,4 @@ -"""Test the Uptime Robot config flow.""" +"""Test the UptimeRobot config flow.""" from unittest.mock import patch import pytest @@ -196,7 +196,7 @@ async def test_user_unique_id_already_exists( async def test_reauthentication( hass: HomeAssistant, ) -> None: - """Test Uptime Robot reauthentication.""" + """Test UptimeRobot reauthentication.""" old_entry = MockConfigEntry(**MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA) old_entry.add_to_hass(hass) @@ -235,7 +235,7 @@ async def test_reauthentication( async def test_reauthentication_failure( hass: HomeAssistant, ) -> None: - """Test Uptime Robot reauthentication failure.""" + """Test UptimeRobot reauthentication failure.""" old_entry = MockConfigEntry(**MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA) old_entry.add_to_hass(hass) @@ -275,7 +275,7 @@ async def test_reauthentication_failure( async def test_reauthentication_failure_no_existing_entry( hass: HomeAssistant, ) -> None: - """Test Uptime Robot reauthentication with no existing entry.""" + """Test UptimeRobot reauthentication with no existing entry.""" old_entry = MockConfigEntry( **{**MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA, "unique_id": None} ) @@ -316,7 +316,7 @@ async def test_reauthentication_failure_no_existing_entry( async def test_reauthentication_failure_account_not_matching( hass: HomeAssistant, ) -> None: - """Test Uptime Robot reauthentication failure when using another account.""" + """Test UptimeRobot reauthentication failure when using another account.""" old_entry = MockConfigEntry(**MOCK_UPTIMEROBOT_CONFIG_ENTRY_DATA) old_entry.add_to_hass(hass) diff --git a/tests/components/uptimerobot/test_init.py b/tests/components/uptimerobot/test_init.py index 43f78e7a19f..3a11319f230 100644 --- a/tests/components/uptimerobot/test_init.py +++ b/tests/components/uptimerobot/test_init.py @@ -1,4 +1,4 @@ -"""Test the Uptime Robot init.""" +"""Test the UptimeRobot init.""" from unittest.mock import patch from pytest import LogCaptureFixture From e34aed743cf1a1448166a990bfdeefd654f0a452 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 15 Oct 2021 01:25:44 +0200 Subject: [PATCH 0368/1038] Xiaomi Miio appropriatly raise ConfigEntryAuthFailed/ConfigEntryNotReady (#54696) * Add reties to cloud login * push to version 0.4 of micloud * distinguish between authentication error and socket errors * raise from error * Update homeassistant/components/xiaomi_miio/gateway.py Co-authored-by: Franck Nijhof * move ConfigEntryNotReady to connect function * remove unused import * also add ConfigEntryNotReady for device * catch exceptions in config flow * fixes * bring tests back to 100% * add missing catch exception * add test * fix black * Update homeassistant/components/xiaomi_miio/device.py Co-authored-by: Teemu R. * Update homeassistant/components/xiaomi_miio/gateway.py Co-authored-by: Teemu R. * Update tests/components/xiaomi_miio/test_config_flow.py Co-authored-by: Teemu R. * fix tests * define specific exceptions * fix styling * fix tests * use proper DeviceException * Revert "use proper DeviceException" This reverts commit 0bd16135387cd6d9e563cd62ac147d0a25c577f3. * use appropriate side-effect * remove unused returns * Update homeassistant/components/xiaomi_miio/const.py Co-authored-by: Martin Hjelmare * remove unused returns Co-authored-by: Franck Nijhof Co-authored-by: Teemu R. Co-authored-by: Martin Hjelmare --- .../components/xiaomi_miio/__init__.py | 20 ++++--- .../components/xiaomi_miio/config_flow.py | 24 +++++++- homeassistant/components/xiaomi_miio/const.py | 10 ++++ .../components/xiaomi_miio/device.py | 15 ++--- .../components/xiaomi_miio/gateway.py | 41 ++++++------- .../components/xiaomi_miio/manifest.json | 2 +- .../components/xiaomi_miio/strings.json | 1 + .../xiaomi_miio/translations/en.json | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../xiaomi_miio/test_config_flow.py | 60 ++++++++++++++++++- 11 files changed, 129 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 5382adb8d96..4788c3220bf 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -35,6 +35,7 @@ from miio.gateway.gateway import GatewayException from homeassistant import config_entries, core from homeassistant.const import CONF_HOST, CONF_TOKEN from homeassistant.core import callback +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -65,6 +66,8 @@ from .const import ( MODELS_PURIFIER_MIOT, MODELS_SWITCH, MODELS_VACUUM, + AuthException, + SetupException, ) from .gateway import ConnectXiaomiGateway @@ -100,10 +103,9 @@ async def async_setup_entry( ): """Set up the Xiaomi Miio components from a config entry.""" hass.data.setdefault(DOMAIN, {}) - if entry.data[ - CONF_FLOW_TYPE - ] == CONF_GATEWAY and not await async_setup_gateway_entry(hass, entry): - return False + if entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY: + await async_setup_gateway_entry(hass, entry) + return True return bool( entry.data[CONF_FLOW_TYPE] != CONF_DEVICE @@ -362,8 +364,12 @@ async def async_setup_gateway_entry( # Connect to gateway gateway = ConnectXiaomiGateway(hass, entry) - if not await gateway.async_connect_gateway(host, token): - return False + try: + await gateway.async_connect_gateway(host, token) + except AuthException as error: + raise ConfigEntryAuthFailed() from error + except SetupException as error: + raise ConfigEntryNotReady() from error gateway_info = gateway.gateway_info gateway_model = f"{gateway_info.model}-{gateway_info.hardware_version}" @@ -416,8 +422,6 @@ async def async_setup_gateway_entry( hass.config_entries.async_forward_entry_setup(entry, platform) ) - return True - async def async_setup_device_entry( hass: core.HomeAssistant, entry: config_entries.ConfigEntry diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index 790a82a0411..5256b37ccda 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -3,6 +3,7 @@ import logging from re import search from micloud import MiCloud +from micloud.micloudexception import MiCloudAccessDenied import voluptuous as vol from homeassistant import config_entries @@ -28,6 +29,8 @@ from .const import ( MODELS_ALL_DEVICES, MODELS_GATEWAY, SERVER_COUNTRY_CODES, + AuthException, + SetupException, ) from .device import ConnectXiaomiDevice @@ -230,8 +233,13 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) miio_cloud = MiCloud(cloud_username, cloud_password) - if not await self.hass.async_add_executor_job(miio_cloud.login): + try: + if not await self.hass.async_add_executor_job(miio_cloud.login): + errors["base"] = "cloud_login_error" + except MiCloudAccessDenied: errors["base"] = "cloud_login_error" + + if errors: return self.async_show_form( step_id="cloud", data_schema=DEVICE_CLOUD_CONFIG, errors=errors ) @@ -320,14 +328,24 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Try to connect to a Xiaomi Device. connect_device_class = ConnectXiaomiDevice(self.hass) - await connect_device_class.async_connect_device(self.host, self.token) + try: + await connect_device_class.async_connect_device(self.host, self.token) + except AuthException: + if self.model is None: + errors["base"] = "wrong_token" + except SetupException: + if self.model is None: + errors["base"] = "cannot_connect" + device_info = connect_device_class.device_info if self.model is None and device_info is not None: self.model = device_info.model - if self.model is None: + if self.model is None and not errors: errors["base"] = "cannot_connect" + + if errors: return self.async_show_form( step_id="connect", data_schema=DEVICE_MODEL_CONFIG, errors=errors ) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 4adc2d287dd..e27fb4d2110 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -37,6 +37,16 @@ SUCCESS = ["ok"] SERVER_COUNTRY_CODES = ["cn", "de", "i2", "ru", "sg", "us"] DEFAULT_CLOUD_COUNTRY = "cn" + +# Exceptions +class AuthException(Exception): + """Exception indicating an authentication error.""" + + +class SetupException(Exception): + """Exception indicating a failure during setup.""" + + # Fan Models MODEL_AIRPURIFIER_2H = "zhimi.airpurifier.mc2" MODEL_AIRPURIFIER_2S = "zhimi.airpurifier.mc1" diff --git a/homeassistant/components/xiaomi_miio/device.py b/homeassistant/components/xiaomi_miio/device.py index aa81d8c23b6..be9c1151aa5 100644 --- a/homeassistant/components/xiaomi_miio/device.py +++ b/homeassistant/components/xiaomi_miio/device.py @@ -7,12 +7,11 @@ import logging from construct.core import ChecksumError from miio import Device, DeviceException -from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import CONF_MAC, CONF_MODEL, DOMAIN +from .const import CONF_MAC, CONF_MODEL, DOMAIN, AuthException, SetupException _LOGGER = logging.getLogger(__name__) @@ -48,14 +47,11 @@ class ConnectXiaomiDevice: ) except DeviceException as error: if isinstance(error.__cause__, ChecksumError): - raise ConfigEntryAuthFailed(error) from error + raise AuthException(error) from error - _LOGGER.error( - "DeviceException during setup of xiaomi device with host %s: %s", - host, - error, - ) - return False + raise SetupException( + f"DeviceException during setup of xiaomi device with host {host}" + ) from error _LOGGER.debug( "%s %s %s detected", @@ -63,7 +59,6 @@ class ConnectXiaomiDevice: self._device_info.firmware_version, self._device_info.hardware_version, ) - return True class XiaomiMiioEntity(Entity): diff --git a/homeassistant/components/xiaomi_miio/gateway.py b/homeassistant/components/xiaomi_miio/gateway.py index 8b7a5c77a17..c873a56fb44 100644 --- a/homeassistant/components/xiaomi_miio/gateway.py +++ b/homeassistant/components/xiaomi_miio/gateway.py @@ -3,10 +3,10 @@ import logging from construct.core import ChecksumError from micloud import MiCloud +from micloud.micloudexception import MiCloudAccessDenied from miio import DeviceException, gateway from miio.gateway.gateway import GATEWAY_MODEL_EU -from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -17,6 +17,8 @@ from .const import ( CONF_CLOUD_SUBDEVICES, CONF_CLOUD_USERNAME, DOMAIN, + AuthException, + SetupException, ) _LOGGER = logging.getLogger(__name__) @@ -59,8 +61,7 @@ class ConnectXiaomiGateway: self._cloud_password = self._config_entry.data.get(CONF_CLOUD_PASSWORD) self._cloud_country = self._config_entry.data.get(CONF_CLOUD_COUNTRY) - if not await self._hass.async_add_executor_job(self.connect_gateway): - return False + await self._hass.async_add_executor_job(self.connect_gateway) _LOGGER.debug( "%s %s %s detected", @@ -68,7 +69,6 @@ class ConnectXiaomiGateway: self._gateway_info.firmware_version, self._gateway_info.hardware_version, ) - return True def connect_gateway(self): """Connect the gateway in a way that can called by async_add_executor_job.""" @@ -78,14 +78,11 @@ class ConnectXiaomiGateway: self._gateway_info = self._gateway_device.info() except DeviceException as error: if isinstance(error.__cause__, ChecksumError): - raise ConfigEntryAuthFailed(error) from error + raise AuthException(error) from error - _LOGGER.error( - "DeviceException during setup of xiaomi gateway with host %s: %s", - self._host, - error, - ) - return False + raise SetupException( + "DeviceException during setup of xiaomi gateway with host {self._host}" + ) from error # get the connected sub devices use_cloud = self._use_cloud or self._gateway_info.model == GATEWAY_MODEL_EU @@ -109,27 +106,27 @@ class ConnectXiaomiGateway: or self._cloud_password is None or self._cloud_country is None ): - raise ConfigEntryAuthFailed( + raise AuthException( "Missing cloud credentials in Xiaomi Miio configuration" ) try: miio_cloud = MiCloud(self._cloud_username, self._cloud_password) if not miio_cloud.login(): - raise ConfigEntryAuthFailed( - "Could not login to Xiaomi Miio Cloud, check the credentials" + raise SetupException( + "Failed to login to Xiaomi Miio Cloud during setup of Xiaomi" + " gateway with host {self._host}", ) devices_raw = miio_cloud.get_devices(self._cloud_country) self._gateway_device.get_devices_from_dict(devices_raw) + except MiCloudAccessDenied as error: + raise AuthException( + "Could not login to Xiaomi Miio Cloud, check the credentials" + ) from error except DeviceException as error: - _LOGGER.error( - "DeviceException during setup of xiaomi gateway with host %s: %s", - self._host, - error, - ) - return False - - return True + raise SetupException( + f"DeviceException during setup of xiaomi gateway with host {self._host}" + ) from error class XiaomiGatewayDevice(CoordinatorEntity, Entity): diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 28f3c2da0c5..37c6b8f8a09 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -3,7 +3,7 @@ "name": "Xiaomi Miio", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", - "requirements": ["construct==2.10.56", "micloud==0.3", "python-miio==0.5.8"], + "requirements": ["construct==2.10.56", "micloud==0.4", "python-miio==0.5.8"], "codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"], "zeroconf": ["_miio._udp.local."], "iot_class": "local_polling" diff --git a/homeassistant/components/xiaomi_miio/strings.json b/homeassistant/components/xiaomi_miio/strings.json index 129f6f1ecbf..1331d22e933 100644 --- a/homeassistant/components/xiaomi_miio/strings.json +++ b/homeassistant/components/xiaomi_miio/strings.json @@ -9,6 +9,7 @@ }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "wrong_token": "Checksum error, wrong token", "unknown_device": "The device model is not known, not able to setup the device using config flow.", "cloud_no_devices": "No devices found in this Xiaomi Miio cloud account.", "cloud_credentials_incomplete": "Cloud credentials incomplete, please fill in username, password and country", diff --git a/homeassistant/components/xiaomi_miio/translations/en.json b/homeassistant/components/xiaomi_miio/translations/en.json index 84593a3edc1..9ad0063df58 100644 --- a/homeassistant/components/xiaomi_miio/translations/en.json +++ b/homeassistant/components/xiaomi_miio/translations/en.json @@ -9,6 +9,7 @@ }, "error": { "cannot_connect": "Failed to connect", + "wrong_token": "Checksum error, wrong token", "cloud_credentials_incomplete": "Cloud credentials incomplete, please fill in username, password and country", "cloud_login_error": "Could not login to Xiaomi Miio Cloud, check the credentials.", "cloud_no_devices": "No devices found in this Xiaomi Miio cloud account.", diff --git a/requirements_all.txt b/requirements_all.txt index e5aed307008..23ac2a8dd71 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -993,7 +993,7 @@ meteofrance-api==1.0.2 mficlient==0.3.0 # homeassistant.components.xiaomi_miio -micloud==0.3 +micloud==0.4 # homeassistant.components.miflora miflora==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b40b0ca3f43..05d686cc856 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -588,7 +588,7 @@ meteofrance-api==1.0.2 mficlient==0.3.0 # homeassistant.components.xiaomi_miio -micloud==0.3 +micloud==0.4 # homeassistant.components.mill millheater==0.6.2 diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index 5e7c0351c14..24aa5ac04e4 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -1,6 +1,8 @@ """Test the Xiaomi Miio config flow.""" from unittest.mock import Mock, patch +from construct.core import ChecksumError +from micloud.micloudexception import MiCloudAccessDenied from miio import DeviceException import pytest @@ -300,6 +302,23 @@ async def test_config_flow_gateway_cloud_login_error(hass): assert result["step_id"] == "cloud" assert result["errors"] == {"base": "cloud_login_error"} + with patch( + "homeassistant.components.xiaomi_miio.config_flow.MiCloud.login", + side_effect=MiCloudAccessDenied({}), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + const.CONF_CLOUD_USERNAME: TEST_CLOUD_USER, + const.CONF_CLOUD_PASSWORD: TEST_CLOUD_PASS, + const.CONF_CLOUD_COUNTRY: TEST_CLOUD_COUNTRY, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "cloud" + assert result["errors"] == {"base": "cloud_login_error"} + async def test_config_flow_gateway_cloud_no_devices(hass): """Test a failed config flow using cloud with no devices.""" @@ -540,8 +559,8 @@ async def test_import_flow_success(hass): } -async def test_config_flow_step_device_manual_model_succes(hass): - """Test config flow, device connection error, manual model.""" +async def test_config_flow_step_device_manual_model_error(hass): + """Test config flow, device connection error, model None.""" result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -561,7 +580,7 @@ async def test_config_flow_step_device_manual_model_succes(hass): with patch( "homeassistant.components.xiaomi_miio.device.Device.info", - side_effect=DeviceException({}), + return_value=get_mock_info(model=None), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -572,6 +591,41 @@ async def test_config_flow_step_device_manual_model_succes(hass): assert result["step_id"] == "connect" assert result["errors"] == {"base": "cannot_connect"} + +async def test_config_flow_step_device_manual_model_succes(hass): + """Test config flow, device connection error, manual model.""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "cloud" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {const.CONF_MANUAL: True}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "manual" + assert result["errors"] == {} + + error = DeviceException({}) + error.__cause__ = ChecksumError({}) + with patch( + "homeassistant.components.xiaomi_miio.device.Device.info", + side_effect=error, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "connect" + assert result["errors"] == {"base": "wrong_token"} + overwrite_model = const.MODELS_VACUUM[0] with patch( From b0ff28ceb41e9d555be98ce47edd24b638bc1648 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 15 Oct 2021 02:31:25 +0200 Subject: [PATCH 0369/1038] Add entity category to Xiaomi Miio (#57719) --- .../components/xiaomi_miio/binary_sensor.py | 7 ++++++ .../components/xiaomi_miio/number.py | 10 +++++++- .../components/xiaomi_miio/select.py | 2 ++ .../components/xiaomi_miio/sensor.py | 24 +++++++++++++++++++ .../components/xiaomi_miio/switch.py | 9 +++++++ 5 files changed, 51 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/binary_sensor.py b/homeassistant/components/xiaomi_miio/binary_sensor.py index a28c449e5cb..d33059c20ef 100644 --- a/homeassistant/components/xiaomi_miio/binary_sensor.py +++ b/homeassistant/components/xiaomi_miio/binary_sensor.py @@ -11,6 +11,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from . import VacuumCoordinatorDataAttributes from .const import ( @@ -50,6 +51,7 @@ BINARY_SENSOR_TYPES = ( key=ATTR_NO_WATER, name="Water Tank Empty", icon="mdi:water-off-outline", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), XiaomiMiioBinarySensorDescription( key=ATTR_WATER_TANK_DETACHED, @@ -57,11 +59,13 @@ BINARY_SENSOR_TYPES = ( icon="mdi:car-coolant-level", device_class=DEVICE_CLASS_CONNECTIVITY, value=lambda value: not value, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), XiaomiMiioBinarySensorDescription( key=ATTR_POWERSUPPLY_ATTACHED, name="Power Supply", device_class=DEVICE_CLASS_PLUG, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ) @@ -75,6 +79,7 @@ VACUUM_SENSORS = { parent_key=VacuumCoordinatorDataAttributes.status, entity_registry_enabled_default=True, device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ATTR_WATER_BOX_ATTACHED: XiaomiMiioBinarySensorDescription( key=ATTR_WATER_BOX_ATTACHED, @@ -83,6 +88,7 @@ VACUUM_SENSORS = { parent_key=VacuumCoordinatorDataAttributes.status, entity_registry_enabled_default=True, device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ATTR_WATER_SHORTAGE: XiaomiMiioBinarySensorDescription( key=ATTR_WATER_SHORTAGE, @@ -91,6 +97,7 @@ VACUUM_SENSORS = { parent_key=VacuumCoordinatorDataAttributes.status, entity_registry_enabled_default=True, device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), } diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index a915ce57847..1461f33add6 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from enum import Enum from homeassistant.components.number import NumberEntity, NumberEntityDescription -from homeassistant.const import DEGREE, TIME_MINUTES +from homeassistant.const import DEGREE, ENTITY_CATEGORY_CONFIG, TIME_MINUTES from homeassistant.core import callback from .const import ( @@ -108,6 +108,7 @@ NUMBER_TYPES = { step=10, available_with_device_off=False, method="async_set_motor_speed", + entity_category=ENTITY_CATEGORY_CONFIG, ), FEATURE_SET_FAVORITE_LEVEL: XiaomiMiioNumberDescription( key=ATTR_FAVORITE_LEVEL, @@ -117,6 +118,7 @@ NUMBER_TYPES = { max_value=17, step=1, method="async_set_favorite_level", + entity_category=ENTITY_CATEGORY_CONFIG, ), FEATURE_SET_FAN_LEVEL: XiaomiMiioNumberDescription( key=ATTR_FAN_LEVEL, @@ -126,6 +128,7 @@ NUMBER_TYPES = { max_value=3, step=1, method="async_set_fan_level", + entity_category=ENTITY_CATEGORY_CONFIG, ), FEATURE_SET_VOLUME: XiaomiMiioNumberDescription( key=ATTR_VOLUME, @@ -135,6 +138,7 @@ NUMBER_TYPES = { max_value=100, step=1, method="async_set_volume", + entity_category=ENTITY_CATEGORY_CONFIG, ), FEATURE_SET_OSCILLATION_ANGLE: XiaomiMiioNumberDescription( key=ATTR_OSCILLATION_ANGLE, @@ -145,6 +149,7 @@ NUMBER_TYPES = { max_value=120, step=1, method="async_set_oscillation_angle", + entity_category=ENTITY_CATEGORY_CONFIG, ), FEATURE_SET_DELAY_OFF_COUNTDOWN: XiaomiMiioNumberDescription( key=ATTR_DELAY_OFF_COUNTDOWN, @@ -155,6 +160,7 @@ NUMBER_TYPES = { max_value=480, step=1, method="async_set_delay_off_countdown", + entity_category=ENTITY_CATEGORY_CONFIG, ), FEATURE_SET_LED_BRIGHTNESS_LEVEL: XiaomiMiioNumberDescription( key=ATTR_LED_BRIGHTNESS_LEVEL, @@ -164,6 +170,7 @@ NUMBER_TYPES = { max_value=8, step=1, method="async_set_led_brightness_level", + entity_category=ENTITY_CATEGORY_CONFIG, ), FEATURE_SET_FAVORITE_RPM: XiaomiMiioNumberDescription( key=ATTR_FAVORITE_RPM, @@ -174,6 +181,7 @@ NUMBER_TYPES = { max_value=2300, step=10, method="async_set_favorite_rpm", + entity_category=ENTITY_CATEGORY_CONFIG, ), } diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index 9e9cdcec1ae..2753fb09786 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -12,6 +12,7 @@ from miio.airpurifier_miot import LedBrightness as AirpurifierMiotLedBrightness from miio.fan import LedBrightness as FanLedBrightness from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import callback from .const import ( @@ -63,6 +64,7 @@ SELECTOR_TYPES = { icon="mdi:brightness-6", device_class="xiaomi_miio__led_brightness", options=("bright", "dim", "off"), + entity_category=ENTITY_CATEGORY_CONFIG, ), } diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 764976820bb..c41b6a1d00d 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -38,6 +38,7 @@ from homeassistant.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, + ENTITY_CATEGORY_DIAGNOSTIC, LIGHT_LUX, PERCENTAGE, POWER_WATT, @@ -172,6 +173,7 @@ SENSOR_TYPES = { native_unit_of_measurement=PERCENTAGE, icon="mdi:water-check", state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ATTR_ACTUAL_SPEED: XiaomiMiioSensorDescription( key=ATTR_ACTUAL_SPEED, @@ -179,6 +181,7 @@ SENSOR_TYPES = { native_unit_of_measurement="rpm", icon="mdi:fast-forward", state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ATTR_MOTOR_SPEED: XiaomiMiioSensorDescription( key=ATTR_MOTOR_SPEED, @@ -186,6 +189,7 @@ SENSOR_TYPES = { native_unit_of_measurement="rpm", icon="mdi:fast-forward", state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ATTR_MOTOR2_SPEED: XiaomiMiioSensorDescription( key=ATTR_MOTOR2_SPEED, @@ -193,6 +197,7 @@ SENSOR_TYPES = { native_unit_of_measurement="rpm", icon="mdi:fast-forward", state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ATTR_USE_TIME: XiaomiMiioSensorDescription( key=ATTR_USE_TIME, @@ -201,6 +206,7 @@ SENSOR_TYPES = { icon="mdi:progress-clock", state_class=STATE_CLASS_TOTAL_INCREASING, entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ATTR_ILLUMINANCE: XiaomiMiioSensorDescription( key=ATTR_ILLUMINANCE, @@ -236,6 +242,7 @@ SENSOR_TYPES = { icon="mdi:air-filter", state_class=STATE_CLASS_MEASUREMENT, attributes=("filter_type",), + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ATTR_FILTER_USE: XiaomiMiioSensorDescription( key=ATTR_FILTER_HOURS_USED, @@ -243,6 +250,7 @@ SENSOR_TYPES = { native_unit_of_measurement=TIME_HOURS, icon="mdi:clock-outline", state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ATTR_CARBON_DIOXIDE: XiaomiMiioSensorDescription( key=ATTR_CARBON_DIOXIDE, @@ -258,6 +266,7 @@ SENSOR_TYPES = { device_class=DEVICE_CLASS_GAS, state_class=STATE_CLASS_TOTAL_INCREASING, entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ATTR_BATTERY: XiaomiMiioSensorDescription( key=ATTR_BATTERY, @@ -265,6 +274,7 @@ SENSOR_TYPES = { native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_BATTERY, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), } @@ -391,6 +401,7 @@ VACUUM_SENSORS = { device_class=DEVICE_CLASS_TIMESTAMP, parent_key=VacuumCoordinatorDataAttributes.dnd_status, entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), f"dnd_{ATTR_DND_END}": XiaomiMiioSensorDescription( key=ATTR_DND_END, @@ -399,6 +410,7 @@ VACUUM_SENSORS = { device_class=DEVICE_CLASS_TIMESTAMP, parent_key=VacuumCoordinatorDataAttributes.dnd_status, entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), f"last_clean_{ATTR_LAST_CLEAN_START}": XiaomiMiioSensorDescription( key=ATTR_LAST_CLEAN_START, @@ -406,6 +418,7 @@ VACUUM_SENSORS = { name="Last Clean Start", device_class=DEVICE_CLASS_TIMESTAMP, parent_key=VacuumCoordinatorDataAttributes.last_clean_details, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), f"last_clean_{ATTR_LAST_CLEAN_END}": XiaomiMiioSensorDescription( key=ATTR_LAST_CLEAN_END, @@ -413,6 +426,7 @@ VACUUM_SENSORS = { device_class=DEVICE_CLASS_TIMESTAMP, parent_key=VacuumCoordinatorDataAttributes.last_clean_details, name="Last Clean End", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), f"last_clean_{ATTR_LAST_CLEAN_TIME}": XiaomiMiioSensorDescription( native_unit_of_measurement=TIME_SECONDS, @@ -420,6 +434,7 @@ VACUUM_SENSORS = { key=ATTR_LAST_CLEAN_TIME, parent_key=VacuumCoordinatorDataAttributes.last_clean_details, name="Last Clean Duration", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), f"last_clean_{ATTR_LAST_CLEAN_AREA}": XiaomiMiioSensorDescription( native_unit_of_measurement=AREA_SQUARE_METERS, @@ -427,6 +442,7 @@ VACUUM_SENSORS = { key=ATTR_LAST_CLEAN_AREA, parent_key=VacuumCoordinatorDataAttributes.last_clean_details, name="Last Clean Area", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), f"clean_history_{ATTR_CLEAN_HISTORY_TOTAL_DURATION}": XiaomiMiioSensorDescription( native_unit_of_measurement=TIME_SECONDS, @@ -435,6 +451,7 @@ VACUUM_SENSORS = { parent_key=VacuumCoordinatorDataAttributes.clean_history_status, name="Total duration", entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), f"clean_history_{ATTR_CLEAN_HISTORY_TOTAL_AREA}": XiaomiMiioSensorDescription( native_unit_of_measurement=AREA_SQUARE_METERS, @@ -443,6 +460,7 @@ VACUUM_SENSORS = { parent_key=VacuumCoordinatorDataAttributes.clean_history_status, name="Total Clean Area", entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), f"clean_history_{ATTR_CLEAN_HISTORY_COUNT}": XiaomiMiioSensorDescription( native_unit_of_measurement="", @@ -452,6 +470,7 @@ VACUUM_SENSORS = { parent_key=VacuumCoordinatorDataAttributes.clean_history_status, name="Total Clean Count", entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), f"clean_history_{ATTR_CLEAN_HISTORY_DUST_COLLECTION_COUNT}": XiaomiMiioSensorDescription( native_unit_of_measurement="", @@ -461,6 +480,7 @@ VACUUM_SENSORS = { parent_key=VacuumCoordinatorDataAttributes.clean_history_status, name="Total Dust Collection Count", entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), f"consumable_{ATTR_CONSUMABLE_STATUS_MAIN_BRUSH_LEFT}": XiaomiMiioSensorDescription( native_unit_of_measurement=TIME_SECONDS, @@ -469,6 +489,7 @@ VACUUM_SENSORS = { parent_key=VacuumCoordinatorDataAttributes.consumable_status, name="Main Brush Left", entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), f"consumable_{ATTR_CONSUMABLE_STATUS_SIDE_BRUSH_LEFT}": XiaomiMiioSensorDescription( native_unit_of_measurement=TIME_SECONDS, @@ -477,6 +498,7 @@ VACUUM_SENSORS = { parent_key=VacuumCoordinatorDataAttributes.consumable_status, name="Side Brush Left", entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), f"consumable_{ATTR_CONSUMABLE_STATUS_FILTER_LEFT}": XiaomiMiioSensorDescription( native_unit_of_measurement=TIME_SECONDS, @@ -485,6 +507,7 @@ VACUUM_SENSORS = { parent_key=VacuumCoordinatorDataAttributes.consumable_status, name="Filter Left", entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), f"consumable_{ATTR_CONSUMABLE_STATUS_SENSOR_DIRTY_LEFT}": XiaomiMiioSensorDescription( native_unit_of_measurement=TIME_SECONDS, @@ -493,6 +516,7 @@ VACUUM_SENSORS = { parent_key=VacuumCoordinatorDataAttributes.consumable_status, name="Sensor Dirty Left", entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), } diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index fd9d4053313..6f68e5652db 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -22,6 +22,7 @@ from homeassistant.const import ( ATTR_TEMPERATURE, CONF_HOST, CONF_TOKEN, + ENTITY_CATEGORY_CONFIG, ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -206,6 +207,7 @@ SWITCH_TYPES = ( icon="mdi:volume-high", method_on="async_set_buzzer_on", method_off="async_set_buzzer_off", + entity_category=ENTITY_CATEGORY_CONFIG, ), XiaomiMiioSwitchDescription( key=ATTR_CHILD_LOCK, @@ -214,6 +216,7 @@ SWITCH_TYPES = ( icon="mdi:lock", method_on="async_set_child_lock_on", method_off="async_set_child_lock_off", + entity_category=ENTITY_CATEGORY_CONFIG, ), XiaomiMiioSwitchDescription( key=ATTR_DRY, @@ -222,6 +225,7 @@ SWITCH_TYPES = ( icon="mdi:hair-dryer", method_on="async_set_dry_on", method_off="async_set_dry_off", + entity_category=ENTITY_CATEGORY_CONFIG, ), XiaomiMiioSwitchDescription( key=ATTR_CLEAN, @@ -231,6 +235,7 @@ SWITCH_TYPES = ( method_on="async_set_clean_on", method_off="async_set_clean_off", available_with_device_off=False, + entity_category=ENTITY_CATEGORY_CONFIG, ), XiaomiMiioSwitchDescription( key=ATTR_LED, @@ -239,6 +244,7 @@ SWITCH_TYPES = ( icon="mdi:led-outline", method_on="async_set_led_on", method_off="async_set_led_off", + entity_category=ENTITY_CATEGORY_CONFIG, ), XiaomiMiioSwitchDescription( key=ATTR_LEARN_MODE, @@ -247,6 +253,7 @@ SWITCH_TYPES = ( icon="mdi:school-outline", method_on="async_set_learn_mode_on", method_off="async_set_learn_mode_off", + entity_category=ENTITY_CATEGORY_CONFIG, ), XiaomiMiioSwitchDescription( key=ATTR_AUTO_DETECT, @@ -254,6 +261,7 @@ SWITCH_TYPES = ( name="Auto Detect", method_on="async_set_auto_detect_on", method_off="async_set_auto_detect_off", + entity_category=ENTITY_CATEGORY_CONFIG, ), XiaomiMiioSwitchDescription( key=ATTR_IONIZER, @@ -262,6 +270,7 @@ SWITCH_TYPES = ( icon="mdi:shimmer", method_on="async_set_ionizer_on", method_off="async_set_ionizer_off", + entity_category=ENTITY_CATEGORY_CONFIG, ), ) From e7e88d6a1915093d103ea1c4ee55aec1b7d89e94 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 15 Oct 2021 02:46:25 +0200 Subject: [PATCH 0370/1038] Add entity category to Tractive (#57720) --- homeassistant/components/tractive/binary_sensor.py | 3 ++- homeassistant/components/tractive/sensor.py | 2 ++ homeassistant/components/tractive/switch.py | 4 ++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tractive/binary_sensor.py b/homeassistant/components/tractive/binary_sensor.py index 1686fb0af9b..dfd28eed98d 100644 --- a/homeassistant/components/tractive/binary_sensor.py +++ b/homeassistant/components/tractive/binary_sensor.py @@ -9,7 +9,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_BATTERY_CHARGING +from homeassistant.const import ATTR_BATTERY_CHARGING, ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -77,6 +77,7 @@ SENSOR_TYPE = BinarySensorEntityDescription( key=ATTR_BATTERY_CHARGING, name="Battery Charging", device_class=DEVICE_CLASS_BATTERY_CHARGING, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ) diff --git a/homeassistant/components/tractive/sensor.py b/homeassistant/components/tractive/sensor.py index f81bcc6f869..b9afbeba757 100644 --- a/homeassistant/components/tractive/sensor.py +++ b/homeassistant/components/tractive/sensor.py @@ -9,6 +9,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_BATTERY_LEVEL, DEVICE_CLASS_BATTERY, + ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, TIME_MINUTES, ) @@ -134,6 +135,7 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_BATTERY, entity_class=TractiveHardwareSensor, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), TractiveSensorEntityDescription( key=ATTR_MINUTES_ACTIVE, diff --git a/homeassistant/components/tractive/switch.py b/homeassistant/components/tractive/switch.py index e31b380e794..e606b68779e 100644 --- a/homeassistant/components/tractive/switch.py +++ b/homeassistant/components/tractive/switch.py @@ -9,6 +9,7 @@ from aiotractive.exceptions import TractiveError from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -49,18 +50,21 @@ SWITCH_TYPES: tuple[TractiveSwitchEntityDescription, ...] = ( name="Tracker Buzzer", icon="mdi:volume-high", method="async_set_buzzer", + entity_category=ENTITY_CATEGORY_CONFIG, ), TractiveSwitchEntityDescription( key=ATTR_LED, name="Tracker LED", icon="mdi:led-on", method="async_set_led", + entity_category=ENTITY_CATEGORY_CONFIG, ), TractiveSwitchEntityDescription( key=ATTR_LIVE_TRACKING, name="Live Tracking", icon="mdi:map-marker-path", method="async_set_live_tracking", + entity_category=ENTITY_CATEGORY_CONFIG, ), ) From 9000e5b2d9b68b6df6c944342405ba3e6b8a5449 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 15 Oct 2021 03:48:28 +0300 Subject: [PATCH 0371/1038] Fix Shelly humidity sensor available condition (#57721) --- homeassistant/components/shelly/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 10e5977a51a..f61df56eaa7 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -185,7 +185,7 @@ SENSORS: Final = { value=lambda value: round(value, 1), device_class=sensor.DEVICE_CLASS_HUMIDITY, state_class=sensor.STATE_CLASS_MEASUREMENT, - available=lambda block: cast(int, block.extTemp) != 999, + available=lambda block: cast(int, block.humidity) != 999, ), ("sensor", "luminosity"): BlockAttributeDescription( name="Luminosity", From e34fb4cfb9c2820cc7fa99b420356c2a963932fd Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 15 Oct 2021 03:10:24 +0200 Subject: [PATCH 0372/1038] Add entity category to Brother (#57728) --- homeassistant/components/brother/const.py | 30 ++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/brother/const.py b/homeassistant/components/brother/const.py index 8e34f9f983b..a91d84103e1 100644 --- a/homeassistant/components/brother/const.py +++ b/homeassistant/components/brother/const.py @@ -7,7 +7,11 @@ from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, SensorEntityDescription, ) -from homeassistant.const import DEVICE_CLASS_TIMESTAMP, PERCENTAGE +from homeassistant.const import ( + DEVICE_CLASS_TIMESTAMP, + ENTITY_CATEGORY_DIAGNOSTIC, + PERCENTAGE, +) ATTR_BELT_UNIT_REMAINING_LIFE: Final = "belt_unit_remaining_life" ATTR_BLACK_DRUM_COUNTER: Final = "black_drum_counter" @@ -82,6 +86,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( key=ATTR_STATUS, icon="mdi:printer", name=ATTR_STATUS.title(), + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_PAGE_COUNTER, @@ -89,6 +94,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_PAGE_COUNTER.replace("_", " ").title(), native_unit_of_measurement=UNIT_PAGES, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_BW_COUNTER, @@ -96,6 +102,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_BW_COUNTER.replace("_", " ").title(), native_unit_of_measurement=UNIT_PAGES, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_COLOR_COUNTER, @@ -103,6 +110,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_COLOR_COUNTER.replace("_", " ").title(), native_unit_of_measurement=UNIT_PAGES, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_DUPLEX_COUNTER, @@ -110,6 +118,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_DUPLEX_COUNTER.replace("_", " ").title(), native_unit_of_measurement=UNIT_PAGES, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_DRUM_REMAINING_LIFE, @@ -117,6 +126,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_DRUM_REMAINING_LIFE.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_BLACK_DRUM_REMAINING_LIFE, @@ -124,6 +134,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_BLACK_DRUM_REMAINING_LIFE.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_CYAN_DRUM_REMAINING_LIFE, @@ -131,6 +142,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_CYAN_DRUM_REMAINING_LIFE.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_MAGENTA_DRUM_REMAINING_LIFE, @@ -138,6 +150,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_MAGENTA_DRUM_REMAINING_LIFE.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_YELLOW_DRUM_REMAINING_LIFE, @@ -145,6 +158,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_YELLOW_DRUM_REMAINING_LIFE.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_BELT_UNIT_REMAINING_LIFE, @@ -152,6 +166,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_BELT_UNIT_REMAINING_LIFE.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_FUSER_REMAINING_LIFE, @@ -159,6 +174,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_FUSER_REMAINING_LIFE.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_LASER_REMAINING_LIFE, @@ -166,6 +182,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_LASER_REMAINING_LIFE.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_PF_KIT_1_REMAINING_LIFE, @@ -173,6 +190,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_PF_KIT_1_REMAINING_LIFE.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_PF_KIT_MP_REMAINING_LIFE, @@ -180,6 +198,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_BLACK_TONER_REMAINING, @@ -187,6 +206,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_BLACK_TONER_REMAINING.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_CYAN_TONER_REMAINING, @@ -194,6 +214,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_CYAN_TONER_REMAINING.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_MAGENTA_TONER_REMAINING, @@ -201,6 +222,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_MAGENTA_TONER_REMAINING.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_YELLOW_TONER_REMAINING, @@ -208,6 +230,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_YELLOW_TONER_REMAINING.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_BLACK_INK_REMAINING, @@ -215,6 +238,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_BLACK_INK_REMAINING.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_CYAN_INK_REMAINING, @@ -222,6 +246,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_CYAN_INK_REMAINING.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_MAGENTA_INK_REMAINING, @@ -229,6 +254,7 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_MAGENTA_INK_REMAINING.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_YELLOW_INK_REMAINING, @@ -236,11 +262,13 @@ SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( name=ATTR_YELLOW_INK_REMAINING.replace("_", " ").title(), native_unit_of_measurement=PERCENTAGE, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_UPTIME, name=ATTR_UPTIME.title(), entity_registry_enabled_default=False, device_class=DEVICE_CLASS_TIMESTAMP, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ) From eed9f674022cc61f876bcda134fbd881c31f862b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 15 Oct 2021 03:27:40 +0200 Subject: [PATCH 0373/1038] Add service configuration URL to MQTT (#57731) --- homeassistant/components/mqtt/abbreviations.py | 1 + homeassistant/components/mqtt/mixins.py | 5 +++++ tests/components/mqtt/test_common.py | 4 ++++ tests/components/mqtt/test_init.py | 14 ++++++++++++++ 4 files changed, 24 insertions(+) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index f3dcca9cfd5..fa7344f82c5 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -22,6 +22,7 @@ ABBREVIATIONS = { "clr_temp_cmd_tpl": "color_temp_command_template", "bat_lev_t": "battery_level_topic", "bat_lev_tpl": "battery_level_template", + "cu": "configuration_url", "chrg_t": "charging_topic", "chrg_tpl": "charging_template", "clrm": "color_mode", diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 6c29938c75d..0ba699c6229 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -68,6 +68,7 @@ CONF_SW_VERSION = "sw_version" CONF_VIA_DEVICE = "via_device" CONF_DEPRECATED_VIA_HUB = "via_hub" CONF_SUGGESTED_AREA = "suggested_area" +CONF_CONFIGURATION_URL = "configuration_url" MQTT_ATTRIBUTES_BLOCKED = { "assumed_state", @@ -154,6 +155,7 @@ MQTT_ENTITY_DEVICE_INFO_SCHEMA = vol.All( vol.Optional(CONF_SW_VERSION): cv.string, vol.Optional(CONF_VIA_DEVICE): cv.string, vol.Optional(CONF_SUGGESTED_AREA): cv.string, + vol.Optional(CONF_CONFIGURATION_URL): cv.url, } ), validate_device_has_at_least_one_identifier, @@ -531,6 +533,9 @@ def device_info_from_config(config): if CONF_SUGGESTED_AREA in config: info["suggested_area"] = config[CONF_SUGGESTED_AREA] + if CONF_CONFIGURATION_URL in config: + info["configuration_url"] = config[CONF_CONFIGURATION_URL] + return info diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index c3a4022fdd8..0458ad14d51 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -22,6 +22,7 @@ DEFAULT_CONFIG_DEVICE_INFO_ID = { "model": "Glass", "sw_version": "0.1-beta", "suggested_area": "default_area", + "configuration_url": "http://example.com", } DEFAULT_CONFIG_DEVICE_INFO_MAC = { @@ -31,6 +32,7 @@ DEFAULT_CONFIG_DEVICE_INFO_MAC = { "model": "Glass", "sw_version": "0.1-beta", "suggested_area": "default_area", + "configuration_url": "http://example.com", } @@ -771,6 +773,7 @@ async def help_test_entity_device_info_with_identifier(hass, mqtt_mock, domain, assert device.model == "Glass" assert device.sw_version == "0.1-beta" assert device.suggested_area == "default_area" + assert device.configuration_url == "http://example.com" async def help_test_entity_device_info_with_connection(hass, mqtt_mock, domain, config): @@ -799,6 +802,7 @@ async def help_test_entity_device_info_with_connection(hass, mqtt_mock, domain, assert device.model == "Glass" assert device.sw_version == "0.1-beta" assert device.suggested_area == "default_area" + assert device.configuration_url == "http://example.com" async def help_test_entity_device_info_remove(hass, mqtt_mock, domain, config): diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index dfdd316cda9..7c6d482c7ec 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -301,6 +301,7 @@ def test_entity_device_info_schema(): "name": "Beer", "model": "Glass", "sw_version": "0.1-beta", + "configuration_url": "http://example.com", } ) # full device info with via_device @@ -316,6 +317,7 @@ def test_entity_device_info_schema(): "model": "Glass", "sw_version": "0.1-beta", "via_device": "test-hub", + "configuration_url": "http://example.com", } ) # no identifiers @@ -334,6 +336,18 @@ def test_entity_device_info_schema(): {"identifiers": [], "connections": [], "name": "Beer"} ) + # not an valid URL + with pytest.raises(vol.Invalid): + MQTT_ENTITY_DEVICE_INFO_SCHEMA( + { + "manufacturer": "Whatever", + "name": "Beer", + "model": "Glass", + "sw_version": "0.1-beta", + "configuration_url": "fake://link", + } + ) + async def test_receiving_non_utf8_message_gets_logged( hass, mqtt_mock, calls, record_calls, caplog From e232bdc0827c06ab03c61a4675c1ddcd6f02f9d5 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 15 Oct 2021 05:02:37 +0300 Subject: [PATCH 0374/1038] Add Shelly "installed version" extra state attribute to Gen2 firmware update sensor (#57722) --- homeassistant/components/shelly/binary_sensor.py | 3 ++- homeassistant/components/shelly/entity.py | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index 6ebea1456a1..d7e4983df77 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -153,8 +153,9 @@ RPC_SENSORS: Final = { name="Firmware Update", device_class=DEVICE_CLASS_UPDATE, default_enabled=False, - extra_state_attributes=lambda status: { + extra_state_attributes=lambda status, shelly: { "latest_stable_version": status.get("stable", {"version": ""})["version"], + "installed_version": shelly["ver"], "beta_version": status.get("beta", {"version": ""})["version"], }, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index db00e68c976..0a610545180 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -255,7 +255,7 @@ class RpcAttributeDescription: default_enabled: bool = True available: Callable[[dict], bool] | None = None removal_condition: Callable[[dict, str], bool] | None = None - extra_state_attributes: Callable[[dict], dict | None] | None = None + extra_state_attributes: Callable[[dict, dict], dict | None] | None = None entity_category: str | None = None @@ -608,8 +608,11 @@ class ShellyRpcAttributeEntity(ShellyRpcEntity, entity.Entity): if self.description.extra_state_attributes is None: return None + assert self.wrapper.device.shelly + return self.description.extra_state_attributes( - self.wrapper.device.status[self.key][self.sub_key] + self.wrapper.device.status[self.key][self.sub_key], + self.wrapper.device.shelly, ) @property From 148d2480ac173120218db8ab32eb92eb00a8a3b8 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 15 Oct 2021 04:32:24 +0200 Subject: [PATCH 0375/1038] Add configuration url to Pi hole (#57718) --- homeassistant/components/pi_hole/__init__.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index 5c679a4839a..5e960a1f70d 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -177,8 +177,14 @@ class PiHoleEntity(CoordinatorEntity): @property def device_info(self) -> DeviceInfo: """Return the device information of the entity.""" - return { - "identifiers": {(DOMAIN, self._server_unique_id)}, - "name": self._name, - "manufacturer": "Pi-hole", - } + if self.api.tls: + config_url = f"https://{self.api.host}/{self.api.location}" + else: + config_url = f"http://{self.api.host}/{self.api.location}" + + return DeviceInfo( + identifiers={(DOMAIN, self._server_unique_id)}, + name=self._name, + manufacturer="Pi-hole", + configuration_url=config_url, + ) From e7ac734d01d76a8c88c1b37d3e5f0bf8c1ed4bd3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Oct 2021 17:51:12 -1000 Subject: [PATCH 0376/1038] Add configuration_url to gogogate2 (#57739) --- homeassistant/components/gogogate2/common.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/gogogate2/common.py b/homeassistant/components/gogogate2/common.py index 5d190034028..776b90e1c5e 100644 --- a/homeassistant/components/gogogate2/common.py +++ b/homeassistant/components/gogogate2/common.py @@ -18,6 +18,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -95,13 +96,18 @@ class GoGoGate2Entity(CoordinatorEntity): def device_info(self): """Device info for the controller.""" data = self.coordinator.data - return { + info: DeviceInfo = { "identifiers": {(DOMAIN, self._config_entry.unique_id)}, "name": self._config_entry.title, "manufacturer": MANUFACTURER, "model": data.model, "sw_version": data.firmwareversion, } + if data.model.startswith("ismartgate"): + info[ + "configuration_url" + ] = f"https://{self._config_entry.unique_id}.isgaccess.com" + return info def get_data_update_coordinator( From bcff2b78581785d1a0e02a00a49b4d43bf0b9a21 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Oct 2021 17:52:26 -1000 Subject: [PATCH 0377/1038] Add configuration url to nexia (#57740) --- homeassistant/components/nexia/__init__.py | 26 +++----------- .../components/nexia/binary_sensor.py | 9 +++-- homeassistant/components/nexia/climate.py | 9 ++--- homeassistant/components/nexia/const.py | 2 -- homeassistant/components/nexia/coordinator.py | 36 +++++++++++++++++++ homeassistant/components/nexia/entity.py | 1 + homeassistant/components/nexia/scene.py | 8 ++--- homeassistant/components/nexia/sensor.py | 8 ++--- 8 files changed, 56 insertions(+), 43 deletions(-) create mode 100644 homeassistant/components/nexia/coordinator.py diff --git a/homeassistant/components/nexia/__init__.py b/homeassistant/components/nexia/__init__.py index 6059a639019..827bff50d34 100644 --- a/homeassistant/components/nexia/__init__.py +++ b/homeassistant/components/nexia/__init__.py @@ -1,5 +1,4 @@ """Support for Nexia / Trane XL Thermostats.""" -from datetime import timedelta from functools import partial import logging @@ -12,17 +11,15 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import CONF_BRAND, DOMAIN, NEXIA_DEVICE, PLATFORMS, UPDATE_COORDINATOR +from .const import CONF_BRAND, DOMAIN, PLATFORMS +from .coordinator import NexiaDataUpdateCoordinator from .util import is_invalid_auth_code _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = cv.deprecated(DOMAIN) -DEFAULT_UPDATE_RATE = 120 - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Configure the base Nexia device for Home Assistant.""" @@ -57,23 +54,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("HTTP error from Nexia service: %s", http_ex) raise ConfigEntryNotReady from http_ex - async def _async_update_data(): - """Fetch data from API endpoint.""" - return await hass.async_add_executor_job(nexia_home.update) - - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - name="Nexia update", - update_method=_async_update_data, - update_interval=timedelta(seconds=DEFAULT_UPDATE_RATE), - ) - - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { - NEXIA_DEVICE: nexia_home, - UPDATE_COORDINATOR: coordinator, - } + coordinator = NexiaDataUpdateCoordinator(hass, nexia_home) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator hass.config_entries.async_setup_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/nexia/binary_sensor.py b/homeassistant/components/nexia/binary_sensor.py index 9adf47ee2c5..63fb98c9a0b 100644 --- a/homeassistant/components/nexia/binary_sensor.py +++ b/homeassistant/components/nexia/binary_sensor.py @@ -1,17 +1,16 @@ """Support for Nexia / Trane XL Thermostats.""" from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.nexia.coordinator import NexiaDataUpdateCoordinator -from .const import DOMAIN, NEXIA_DEVICE, UPDATE_COORDINATOR +from .const import DOMAIN from .entity import NexiaThermostatEntity async def async_setup_entry(hass, config_entry, async_add_entities): """Set up sensors for a Nexia device.""" - - nexia_data = hass.data[DOMAIN][config_entry.entry_id] - nexia_home = nexia_data[NEXIA_DEVICE] - coordinator = nexia_data[UPDATE_COORDINATOR] + coordinator: NexiaDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + nexia_home = coordinator.nexia_home entities = [] for thermostat_id in nexia_home.get_thermostat_ids(): diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index e27a1816a8e..68fafab1718 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -47,11 +47,10 @@ from .const import ( ATTR_HUMIDIFY_SUPPORTED, ATTR_ZONE_STATUS, DOMAIN, - NEXIA_DEVICE, SIGNAL_THERMOSTAT_UPDATE, SIGNAL_ZONE_UPDATE, - UPDATE_COORDINATOR, ) +from .coordinator import NexiaDataUpdateCoordinator from .entity import NexiaThermostatZoneEntity from .util import percent_conv @@ -90,10 +89,8 @@ NEXIA_TO_HA_HVAC_MODE_MAP = { async def async_setup_entry(hass, config_entry, async_add_entities): """Set up climate for a Nexia device.""" - - nexia_data = hass.data[DOMAIN][config_entry.entry_id] - nexia_home = nexia_data[NEXIA_DEVICE] - coordinator = nexia_data[UPDATE_COORDINATOR] + coordinator: NexiaDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + nexia_home = coordinator.nexia_home platform = entity_platform.async_get_current_platform() diff --git a/homeassistant/components/nexia/const.py b/homeassistant/components/nexia/const.py index 22b24c3b764..4b076805e71 100644 --- a/homeassistant/components/nexia/const.py +++ b/homeassistant/components/nexia/const.py @@ -9,7 +9,6 @@ NOTIFICATION_TITLE = "Nexia Setup" CONF_BRAND = "brand" -NEXIA_DEVICE = "device" NEXIA_SCAN_INTERVAL = "scan_interval" DOMAIN = "nexia" @@ -25,7 +24,6 @@ ATTR_DEHUMIDIFY_SUPPORTED = "dehumidify_supported" ATTR_HUMIDIFY_SETPOINT = "humidify_setpoint" ATTR_DEHUMIDIFY_SETPOINT = "dehumidify_setpoint" -UPDATE_COORDINATOR = "update_coordinator" MANUFACTURER = "Trane" diff --git a/homeassistant/components/nexia/coordinator.py b/homeassistant/components/nexia/coordinator.py new file mode 100644 index 00000000000..7f92ca9354b --- /dev/null +++ b/homeassistant/components/nexia/coordinator.py @@ -0,0 +1,36 @@ +"""Component to embed TP-Link smart home devices.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from nexia.home import NexiaHome + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_UPDATE_RATE = 120 + + +class NexiaDataUpdateCoordinator(DataUpdateCoordinator): + """DataUpdateCoordinator for nexia homes.""" + + def __init__( + self, + hass: HomeAssistant, + nexia_home: NexiaHome, + ) -> None: + """Initialize DataUpdateCoordinator for the nexia home.""" + self.nexia_home = nexia_home + super().__init__( + hass, + _LOGGER, + name="Nexia update", + update_interval=timedelta(seconds=DEFAULT_UPDATE_RATE), + ) + + async def _async_update_data(self) -> None: + """Fetch data from API endpoint.""" + return await self.hass.async_add_executor_job(self.nexia_home.update) diff --git a/homeassistant/components/nexia/entity.py b/homeassistant/components/nexia/entity.py index fc69c7ef389..87e06fb05cf 100644 --- a/homeassistant/components/nexia/entity.py +++ b/homeassistant/components/nexia/entity.py @@ -57,6 +57,7 @@ class NexiaThermostatEntity(NexiaEntity): "model": self._thermostat.get_model(), "sw_version": self._thermostat.get_firmware(), "manufacturer": MANUFACTURER, + "configuration_url": self.coordinator.nexia_home.root_url, } async def async_added_to_hass(self): diff --git a/homeassistant/components/nexia/scene.py b/homeassistant/components/nexia/scene.py index 495a8fb4d3a..3eb64fc8d0f 100644 --- a/homeassistant/components/nexia/scene.py +++ b/homeassistant/components/nexia/scene.py @@ -5,7 +5,8 @@ from typing import Any from homeassistant.components.scene import Scene from homeassistant.helpers.event import async_call_later -from .const import ATTR_DESCRIPTION, DOMAIN, NEXIA_DEVICE, UPDATE_COORDINATOR +from .const import ATTR_DESCRIPTION, DOMAIN +from .coordinator import NexiaDataUpdateCoordinator from .entity import NexiaEntity SCENE_ACTIVATION_TIME = 5 @@ -13,10 +14,9 @@ SCENE_ACTIVATION_TIME = 5 async def async_setup_entry(hass, config_entry, async_add_entities): """Set up automations for a Nexia device.""" + coordinator: NexiaDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + nexia_home = coordinator.nexia_home - nexia_data = hass.data[DOMAIN][config_entry.entry_id] - nexia_home = nexia_data[NEXIA_DEVICE] - coordinator = nexia_data[UPDATE_COORDINATOR] entities = [] # Automation switches diff --git a/homeassistant/components/nexia/sensor.py b/homeassistant/components/nexia/sensor.py index 6e44b8c9883..15479df310b 100644 --- a/homeassistant/components/nexia/sensor.py +++ b/homeassistant/components/nexia/sensor.py @@ -11,7 +11,8 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) -from .const import DOMAIN, NEXIA_DEVICE, UPDATE_COORDINATOR +from .const import DOMAIN +from .coordinator import NexiaDataUpdateCoordinator from .entity import NexiaThermostatEntity, NexiaThermostatZoneEntity from .util import percent_conv @@ -19,9 +20,8 @@ from .util import percent_conv async def async_setup_entry(hass, config_entry, async_add_entities): """Set up sensors for a Nexia device.""" - nexia_data = hass.data[DOMAIN][config_entry.entry_id] - nexia_home = nexia_data[NEXIA_DEVICE] - coordinator = nexia_data[UPDATE_COORDINATOR] + coordinator: NexiaDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + nexia_home = coordinator.nexia_home entities = [] # Thermostat / System Sensors From faf5c2eb40df5fbd11f6a7d7a8ad40f52f09216a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Oct 2021 17:53:35 -1000 Subject: [PATCH 0378/1038] Add discovery support for single channel magichome controllers (#57736) --- homeassistant/components/flux_led/manifest.json | 9 +++++---- homeassistant/generated/dhcp.py | 10 ++++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index b3239ff15b2..1c5a6d16100 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -28,12 +28,13 @@ "hostname": "lwip*" }, { - "macaddress": "2462AB*", - "hostname": "zengge_35*" + "hostname": "zengge_35_*" }, { - "macaddress": "70039F*", - "hostname": "zengge_0e*" + "hostname": "zengge_0e_*" + }, + { + "hostname": "zengge_00_*" }, { "macaddress": "C82E47*", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 69ca9b422d7..eb22c45cef7 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -98,13 +98,15 @@ DHCP = [ }, { "domain": "flux_led", - "macaddress": "2462AB*", - "hostname": "zengge_35*" + "hostname": "zengge_35_*" }, { "domain": "flux_led", - "macaddress": "70039F*", - "hostname": "zengge_0e*" + "hostname": "zengge_0e_*" + }, + { + "domain": "flux_led", + "hostname": "zengge_00_*" }, { "domain": "flux_led", From 4417ffb4074e73b9fb32b6ea5db5b28ffe45dc21 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 15 Oct 2021 07:09:59 +0200 Subject: [PATCH 0379/1038] COnvert DATA_TYPE to enum. (#57699) --- homeassistant/components/modbus/__init__.py | 47 +++++------- .../components/modbus/base_platform.py | 4 +- homeassistant/components/modbus/climate.py | 19 ++--- homeassistant/components/modbus/const.py | 35 +++++---- homeassistant/components/modbus/validators.py | 63 +++++++--------- tests/components/modbus/test_climate.py | 13 ++-- tests/components/modbus/test_init.py | 24 +++---- tests/components/modbus/test_sensor.py | 72 +++++++++---------- 8 files changed, 122 insertions(+), 155 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 720e57605ce..c1e64716d21 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -96,20 +96,6 @@ from .const import ( CONF_TARGET_TEMP, CONF_VERIFY, CONF_WRITE_TYPE, - DATA_TYPE_CUSTOM, - DATA_TYPE_FLOAT, - DATA_TYPE_FLOAT16, - DATA_TYPE_FLOAT32, - DATA_TYPE_FLOAT64, - DATA_TYPE_INT, - DATA_TYPE_INT16, - DATA_TYPE_INT32, - DATA_TYPE_INT64, - DATA_TYPE_STRING, - DATA_TYPE_UINT, - DATA_TYPE_UINT16, - DATA_TYPE_UINT32, - DATA_TYPE_UINT64, DEFAULT_HUB, DEFAULT_SCAN_INTERVAL, DEFAULT_TEMP_UNIT, @@ -118,6 +104,7 @@ from .const import ( SERIAL, TCP, UDP, + DataType, ) from .modbus import ModbusHub, async_modbus_setup from .validators import ( @@ -153,23 +140,23 @@ BASE_STRUCT_SCHEMA = BASE_COMPONENT_SCHEMA.extend( ] ), vol.Optional(CONF_COUNT): cv.positive_int, - vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_INT): vol.In( + vol.Optional(CONF_DATA_TYPE, default=DataType.INT): vol.In( [ - DATA_TYPE_INT16, - DATA_TYPE_INT32, - DATA_TYPE_INT64, - DATA_TYPE_UINT16, - DATA_TYPE_UINT32, - DATA_TYPE_UINT64, - DATA_TYPE_FLOAT16, - DATA_TYPE_FLOAT32, - DATA_TYPE_FLOAT64, - DATA_TYPE_STRING, - DATA_TYPE_INT, - DATA_TYPE_UINT, - DATA_TYPE_FLOAT, - DATA_TYPE_STRING, - DATA_TYPE_CUSTOM, + DataType.INT16, + DataType.INT32, + DataType.INT64, + DataType.UINT16, + DataType.UINT32, + DataType.UINT64, + DataType.FLOAT16, + DataType.FLOAT32, + DataType.FLOAT64, + DataType.STRING, + DataType.INT, + DataType.UINT, + DataType.FLOAT, + DataType.STRING, + DataType.CUSTOM, ] ), vol.Optional(CONF_STRUCTURE): cv.string, diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index 95f8d33b366..1c4ebe2237d 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -53,9 +53,9 @@ from .const import ( CONF_SWAP_WORD_BYTE, CONF_VERIFY, CONF_WRITE_TYPE, - DATA_TYPE_STRING, SIGNAL_START_ENTITY, SIGNAL_STOP_ENTITY, + DataType, ) from .modbus import ModbusHub @@ -165,7 +165,7 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): registers = self._swap_registers(registers) byte_string = b"".join([x.to_bytes(2, byteorder="big") for x in registers]) - if self._data_type == DATA_TYPE_STRING: + if self._data_type == DataType.STRING: return byte_string.decode() val = struct.unpack(self._structure, byte_string) diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index a526e7cc2f9..268d7bfd3df 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -34,12 +34,7 @@ from .const import ( CONF_MIN_TEMP, CONF_STEP, CONF_TARGET_TEMP, - DATA_TYPE_INT16, - DATA_TYPE_INT32, - DATA_TYPE_INT64, - DATA_TYPE_UINT16, - DATA_TYPE_UINT32, - DATA_TYPE_UINT64, + DataType, ) from .modbus import ModbusHub @@ -113,12 +108,12 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): float(kwargs[ATTR_TEMPERATURE]) - self._offset ) / self._scale if self._data_type in ( - DATA_TYPE_INT16, - DATA_TYPE_INT32, - DATA_TYPE_INT64, - DATA_TYPE_UINT16, - DATA_TYPE_UINT32, - DATA_TYPE_UINT64, + DataType.INT16, + DataType.INT32, + DataType.INT64, + DataType.UINT16, + DataType.UINT32, + DataType.UINT64, ): target_temperature = int(target_temperature) as_bytes = struct.pack(self._structure, target_temperature) diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index d3240565982..610782b5733 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -1,4 +1,6 @@ """Constants used in modbus integration.""" +from enum import Enum + from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.climate.const import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.cover import DOMAIN as COVER_DOMAIN @@ -75,21 +77,26 @@ ATTR_VALUE = "value" ATTR_STATE = "state" ATTR_TEMPERATURE = "temperature" + # data types -DATA_TYPE_CUSTOM = "custom" -DATA_TYPE_FLOAT = "float" -DATA_TYPE_INT = "int" -DATA_TYPE_UINT = "uint" -DATA_TYPE_STRING = "string" -DATA_TYPE_INT16 = "int16" -DATA_TYPE_INT32 = "int32" -DATA_TYPE_INT64 = "int64" -DATA_TYPE_UINT16 = "uint16" -DATA_TYPE_UINT32 = "uint32" -DATA_TYPE_UINT64 = "uint64" -DATA_TYPE_FLOAT16 = "float16" -DATA_TYPE_FLOAT32 = "float32" -DATA_TYPE_FLOAT64 = "float64" +class DataType(str, Enum): + """Data types used by sensor etc.""" + + CUSTOM = "custom" + FLOAT = "float" # deprecated + INT = "int" # deprecated + UINT = "uint" # deprecated + STRING = "string" + INT16 = "int16" + INT32 = "int32" + INT64 = "int64" + UINT16 = "uint16" + UINT32 = "uint32" + UINT64 = "uint64" + FLOAT16 = "float16" + FLOAT32 = "float32" + FLOAT64 = "float64" + # call types CALL_TYPE_COIL = "coil" diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index 3d13178bccc..ca0f370b562 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -30,57 +30,44 @@ from .const import ( CONF_SWAP_BYTE, CONF_SWAP_NONE, CONF_WRITE_TYPE, - DATA_TYPE_CUSTOM, - DATA_TYPE_FLOAT, - DATA_TYPE_FLOAT16, - DATA_TYPE_FLOAT32, - DATA_TYPE_FLOAT64, - DATA_TYPE_INT, - DATA_TYPE_INT16, - DATA_TYPE_INT32, - DATA_TYPE_INT64, - DATA_TYPE_STRING, - DATA_TYPE_UINT, - DATA_TYPE_UINT16, - DATA_TYPE_UINT32, - DATA_TYPE_UINT64, DEFAULT_HUB, DEFAULT_SCAN_INTERVAL, PLATFORMS, SERIAL, + DataType, ) _LOGGER = logging.getLogger(__name__) OLD_DATA_TYPES = { - DATA_TYPE_INT: { - 1: DATA_TYPE_INT16, - 2: DATA_TYPE_INT32, - 4: DATA_TYPE_INT64, + DataType.INT: { + 1: DataType.INT16, + 2: DataType.INT32, + 4: DataType.INT64, }, - DATA_TYPE_UINT: { - 1: DATA_TYPE_UINT16, - 2: DATA_TYPE_UINT32, - 4: DATA_TYPE_UINT64, + DataType.UINT: { + 1: DataType.UINT16, + 2: DataType.UINT32, + 4: DataType.UINT64, }, - DATA_TYPE_FLOAT: { - 1: DATA_TYPE_FLOAT16, - 2: DATA_TYPE_FLOAT32, - 4: DATA_TYPE_FLOAT64, + DataType.FLOAT: { + 1: DataType.FLOAT16, + 2: DataType.FLOAT32, + 4: DataType.FLOAT64, }, } ENTRY = namedtuple("ENTRY", ["struct_id", "register_count"]) DEFAULT_STRUCT_FORMAT = { - DATA_TYPE_INT16: ENTRY("h", 1), - DATA_TYPE_INT32: ENTRY("i", 2), - DATA_TYPE_INT64: ENTRY("q", 4), - DATA_TYPE_UINT16: ENTRY("H", 1), - DATA_TYPE_UINT32: ENTRY("I", 2), - DATA_TYPE_UINT64: ENTRY("Q", 4), - DATA_TYPE_FLOAT16: ENTRY("e", 1), - DATA_TYPE_FLOAT32: ENTRY("f", 2), - DATA_TYPE_FLOAT64: ENTRY("d", 4), - DATA_TYPE_STRING: ENTRY("s", 1), + DataType.INT16: ENTRY("h", 1), + DataType.INT32: ENTRY("i", 2), + DataType.INT64: ENTRY("q", 4), + DataType.UINT16: ENTRY("H", 1), + DataType.UINT32: ENTRY("I", 2), + DataType.UINT64: ENTRY("Q", 4), + DataType.FLOAT16: ENTRY("e", 1), + DataType.FLOAT32: ENTRY("f", 2), + DataType.FLOAT64: ENTRY("d", 4), + DataType.STRING: ENTRY("s", 1), } @@ -92,7 +79,7 @@ def struct_validator(config: dict[str, Any]) -> dict[str, Any]: name = config[CONF_NAME] structure = config.get(CONF_STRUCTURE) swap_type = config.get(CONF_SWAP) - if data_type in (DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT): + if data_type in (DataType.INT, DataType.UINT, DataType.FLOAT): error = f"{name} with {data_type} is not valid, trying to convert" _LOGGER.warning(error) try: @@ -101,7 +88,7 @@ def struct_validator(config: dict[str, Any]) -> dict[str, Any]: except KeyError as exp: error = f"{name} cannot convert automatically {data_type}" raise vol.Invalid(error) from exp - if config[CONF_DATA_TYPE] != DATA_TYPE_CUSTOM: + if config[CONF_DATA_TYPE] != DataType.CUSTOM: if structure: error = f"{name} structure: cannot be mixed with {data_type}" raise vol.Invalid(error) diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index 187c049b069..5fc61fed52d 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -8,10 +8,7 @@ from homeassistant.components.modbus.const import ( CONF_DATA_TYPE, CONF_LAZY_ERROR, CONF_TARGET_TEMP, - DATA_TYPE_FLOAT32, - DATA_TYPE_FLOAT64, - DATA_TYPE_INT16, - DATA_TYPE_INT32, + DataType, ) from homeassistant.const import ( ATTR_TEMPERATURE, @@ -128,7 +125,7 @@ async def test_service_climate_update(hass, mock_modbus, mock_ha): CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, CONF_SLAVE: 10, - CONF_DATA_TYPE: DATA_TYPE_INT16, + CONF_DATA_TYPE: DataType.INT16, } ] }, @@ -143,7 +140,7 @@ async def test_service_climate_update(hass, mock_modbus, mock_ha): CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, CONF_SLAVE: 10, - CONF_DATA_TYPE: DATA_TYPE_INT32, + CONF_DATA_TYPE: DataType.INT32, } ] }, @@ -158,7 +155,7 @@ async def test_service_climate_update(hass, mock_modbus, mock_ha): CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, CONF_SLAVE: 10, - CONF_DATA_TYPE: DATA_TYPE_FLOAT32, + CONF_DATA_TYPE: DataType.FLOAT32, } ] }, @@ -173,7 +170,7 @@ async def test_service_climate_update(hass, mock_modbus, mock_ha): CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, CONF_SLAVE: 10, - CONF_DATA_TYPE: DATA_TYPE_FLOAT64, + CONF_DATA_TYPE: DataType.FLOAT64, } ] }, diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 25e67960f2a..08b9540d507 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -46,9 +46,6 @@ from homeassistant.components.modbus.const import ( CONF_SWAP, CONF_SWAP_BYTE, CONF_SWAP_WORD, - DATA_TYPE_CUSTOM, - DATA_TYPE_INT, - DATA_TYPE_STRING, DEFAULT_SCAN_INTERVAL, MODBUS_DOMAIN as DOMAIN, RTUOVERTCP, @@ -59,6 +56,7 @@ from homeassistant.components.modbus.const import ( SERVICE_WRITE_REGISTER, TCP, UDP, + DataType, ) from homeassistant.components.modbus.validators import ( duplicate_entity_validator, @@ -142,23 +140,23 @@ async def test_number_validator(): { CONF_NAME: TEST_ENTITY_NAME, CONF_COUNT: 2, - CONF_DATA_TYPE: DATA_TYPE_STRING, + CONF_DATA_TYPE: DataType.STRING, }, { CONF_NAME: TEST_ENTITY_NAME, CONF_COUNT: 2, - CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_DATA_TYPE: DataType.INT, }, { CONF_NAME: TEST_ENTITY_NAME, CONF_COUNT: 2, - CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_DATA_TYPE: DataType.INT, CONF_SWAP: CONF_SWAP_BYTE, }, { CONF_NAME: TEST_ENTITY_NAME, CONF_COUNT: 2, - CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_DATA_TYPE: DataType.CUSTOM, CONF_STRUCTURE: ">i", CONF_SWAP: CONF_SWAP_BYTE, }, @@ -178,36 +176,36 @@ async def test_ok_struct_validator(do_config): { CONF_NAME: TEST_ENTITY_NAME, CONF_COUNT: 8, - CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_DATA_TYPE: DataType.INT, }, { CONF_NAME: TEST_ENTITY_NAME, CONF_COUNT: 8, - CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_DATA_TYPE: DataType.CUSTOM, }, { CONF_NAME: TEST_ENTITY_NAME, CONF_COUNT: 8, - CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_DATA_TYPE: DataType.CUSTOM, CONF_STRUCTURE: "no good", }, { CONF_NAME: TEST_ENTITY_NAME, CONF_COUNT: 20, - CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_DATA_TYPE: DataType.CUSTOM, CONF_STRUCTURE: ">f", }, { CONF_NAME: TEST_ENTITY_NAME, CONF_COUNT: 1, - CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_DATA_TYPE: DataType.CUSTOM, CONF_STRUCTURE: ">f", CONF_SWAP: CONF_SWAP_WORD, }, { CONF_NAME: TEST_ENTITY_NAME, CONF_COUNT: 1, - CONF_DATA_TYPE: DATA_TYPE_STRING, + CONF_DATA_TYPE: DataType.STRING, CONF_STRUCTURE: ">f", CONF_SWAP: CONF_SWAP_WORD, }, diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index b979ac0435e..6bc4a352d93 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -14,11 +14,7 @@ from homeassistant.components.modbus.const import ( CONF_SWAP_NONE, CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE, - DATA_TYPE_CUSTOM, - DATA_TYPE_FLOAT, - DATA_TYPE_INT, - DATA_TYPE_STRING, - DATA_TYPE_UINT, + DataType, ) from homeassistant.components.sensor import ( CONF_STATE_CLASS, @@ -148,7 +144,7 @@ async def test_config_sensor(hass, mock_modbus): CONF_ADDRESS: 1234, CONF_COUNT: 8, CONF_PRECISION: 2, - CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_DATA_TYPE: DataType.CUSTOM, CONF_STRUCTURE: ">no struct", }, ] @@ -163,7 +159,7 @@ async def test_config_sensor(hass, mock_modbus): CONF_ADDRESS: 1234, CONF_COUNT: 2, CONF_PRECISION: 2, - CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_DATA_TYPE: DataType.CUSTOM, CONF_STRUCTURE: ">4f", }, ] @@ -176,7 +172,7 @@ async def test_config_sensor(hass, mock_modbus): { CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, - CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_DATA_TYPE: DataType.CUSTOM, CONF_COUNT: 4, CONF_SWAP: CONF_SWAP_NONE, CONF_STRUCTURE: "invalid", @@ -191,7 +187,7 @@ async def test_config_sensor(hass, mock_modbus): { CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, - CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_DATA_TYPE: DataType.CUSTOM, CONF_COUNT: 4, CONF_SWAP: CONF_SWAP_NONE, CONF_STRUCTURE: "", @@ -206,7 +202,7 @@ async def test_config_sensor(hass, mock_modbus): { CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, - CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_DATA_TYPE: DataType.CUSTOM, CONF_COUNT: 4, CONF_SWAP: CONF_SWAP_NONE, CONF_STRUCTURE: "1s", @@ -221,7 +217,7 @@ async def test_config_sensor(hass, mock_modbus): { CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 1234, - CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_DATA_TYPE: DataType.CUSTOM, CONF_COUNT: 1, CONF_STRUCTURE: "2s", CONF_SWAP: CONF_SWAP_WORD, @@ -258,7 +254,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ( { CONF_COUNT: 1, - CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_DATA_TYPE: DataType.INT, CONF_SCALE: 1, CONF_OFFSET: 0, CONF_PRECISION: 0, @@ -276,7 +272,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ( { CONF_COUNT: 1, - CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_DATA_TYPE: DataType.INT, CONF_SCALE: 1, CONF_OFFSET: 13, CONF_PRECISION: 0, @@ -288,7 +284,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ( { CONF_COUNT: 1, - CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_DATA_TYPE: DataType.INT, CONF_SCALE: 3, CONF_OFFSET: 13, CONF_PRECISION: 0, @@ -300,7 +296,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ( { CONF_COUNT: 1, - CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_DATA_TYPE: DataType.UINT, CONF_SCALE: 3, CONF_OFFSET: 13, CONF_PRECISION: 4, @@ -312,7 +308,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ( { CONF_COUNT: 1, - CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_DATA_TYPE: DataType.INT, CONF_SCALE: 1.5, CONF_OFFSET: 0, CONF_PRECISION: 0, @@ -324,7 +320,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ( { CONF_COUNT: 1, - CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_DATA_TYPE: DataType.INT, CONF_SCALE: "1.5", CONF_OFFSET: "5", CONF_PRECISION: "1", @@ -336,7 +332,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ( { CONF_COUNT: 1, - CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_DATA_TYPE: DataType.INT, CONF_SCALE: 2.4, CONF_OFFSET: 0, CONF_PRECISION: 2, @@ -348,7 +344,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ( { CONF_COUNT: 1, - CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_DATA_TYPE: DataType.INT, CONF_SCALE: 1, CONF_OFFSET: -10.3, CONF_PRECISION: 1, @@ -360,7 +356,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ( { CONF_COUNT: 2, - CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_DATA_TYPE: DataType.INT, CONF_SCALE: 1, CONF_OFFSET: 0, CONF_PRECISION: 0, @@ -372,7 +368,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ( { CONF_COUNT: 2, - CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_DATA_TYPE: DataType.UINT, CONF_SCALE: 1, CONF_OFFSET: 0, CONF_PRECISION: 0, @@ -384,7 +380,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ( { CONF_COUNT: 4, - CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_DATA_TYPE: DataType.UINT, CONF_SCALE: 1, CONF_OFFSET: 0, CONF_PRECISION: 0, @@ -396,7 +392,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ( { CONF_COUNT: 4, - CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_DATA_TYPE: DataType.UINT, CONF_SCALE: 2, CONF_OFFSET: 3, CONF_PRECISION: 0, @@ -408,7 +404,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ( { CONF_COUNT: 4, - CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_DATA_TYPE: DataType.UINT, CONF_SCALE: 2.0, CONF_OFFSET: 3.0, CONF_PRECISION: 0, @@ -421,7 +417,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl { CONF_COUNT: 2, CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, - CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_DATA_TYPE: DataType.UINT, CONF_SCALE: 1, CONF_OFFSET: 0, CONF_PRECISION: 0, @@ -434,7 +430,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl { CONF_COUNT: 2, CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, - CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_DATA_TYPE: DataType.UINT, CONF_SCALE: 1, CONF_OFFSET: 0, CONF_PRECISION: 0, @@ -447,7 +443,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl { CONF_COUNT: 2, CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, - CONF_DATA_TYPE: DATA_TYPE_FLOAT, + CONF_DATA_TYPE: DataType.FLOAT, CONF_SCALE: 1, CONF_OFFSET: 0, CONF_PRECISION: 5, @@ -460,7 +456,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl { CONF_COUNT: 8, CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, - CONF_DATA_TYPE: DATA_TYPE_STRING, + CONF_DATA_TYPE: DataType.STRING, CONF_SCALE: 1, CONF_OFFSET: 0, CONF_PRECISION: 0, @@ -473,7 +469,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl { CONF_COUNT: 8, CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, - CONF_DATA_TYPE: DATA_TYPE_STRING, + CONF_DATA_TYPE: DataType.STRING, CONF_SCALE: 1, CONF_OFFSET: 0, CONF_PRECISION: 0, @@ -486,7 +482,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl { CONF_COUNT: 2, CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, - CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_DATA_TYPE: DataType.UINT, CONF_SCALE: 1, CONF_OFFSET: 0, CONF_PRECISION: 0, @@ -498,7 +494,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ( { CONF_COUNT: 1, - CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_DATA_TYPE: DataType.INT, CONF_SWAP: CONF_SWAP_NONE, }, [0x0102], @@ -508,7 +504,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ( { CONF_COUNT: 1, - CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_DATA_TYPE: DataType.INT, CONF_SWAP: CONF_SWAP_BYTE, }, [0x0201], @@ -518,7 +514,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ( { CONF_COUNT: 2, - CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_DATA_TYPE: DataType.INT, CONF_SWAP: CONF_SWAP_BYTE, }, [0x0102, 0x0304], @@ -528,7 +524,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ( { CONF_COUNT: 2, - CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_DATA_TYPE: DataType.INT, CONF_SWAP: CONF_SWAP_WORD, }, [0x0102, 0x0304], @@ -538,7 +534,7 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl ( { CONF_COUNT: 2, - CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_DATA_TYPE: DataType.INT, CONF_SWAP: CONF_SWAP_WORD_BYTE, }, [0x0102, 0x0304], @@ -610,7 +606,7 @@ async def test_lazy_error_sensor(hass, mock_do_cycle, start_expect, end_expect): { CONF_COUNT: 8, CONF_PRECISION: 2, - CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_DATA_TYPE: DataType.CUSTOM, CONF_STRUCTURE: ">4f", }, # floats: 7.931250095367432, 10.600000381469727, @@ -622,7 +618,7 @@ async def test_lazy_error_sensor(hass, mock_do_cycle, start_expect, end_expect): { CONF_COUNT: 4, CONF_PRECISION: 0, - CONF_DATA_TYPE: DATA_TYPE_CUSTOM, + CONF_DATA_TYPE: DataType.CUSTOM, CONF_STRUCTURE: ">2i", }, [0x0000, 0x0100, 0x0000, 0x0032], @@ -632,7 +628,7 @@ async def test_lazy_error_sensor(hass, mock_do_cycle, start_expect, end_expect): { CONF_COUNT: 1, CONF_PRECISION: 0, - CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_DATA_TYPE: DataType.INT, }, [0x0101], "257", From 12b692287518087143def2668ab27a6ef374dbd1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 14 Oct 2021 23:17:40 -0700 Subject: [PATCH 0380/1038] Add entity category to cloud (#57747) --- homeassistant/components/cloud/binary_sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/cloud/binary_sensor.py b/homeassistant/components/cloud/binary_sensor.py index f96bda4ce1b..a27364c715f 100644 --- a/homeassistant/components/cloud/binary_sensor.py +++ b/homeassistant/components/cloud/binary_sensor.py @@ -5,6 +5,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, BinarySensorEntity, ) +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DISPATCHER_REMOTE_UPDATE, DOMAIN @@ -28,6 +29,7 @@ class CloudRemoteBinary(BinarySensorEntity): _attr_device_class = DEVICE_CLASS_CONNECTIVITY _attr_should_poll = False _attr_unique_id = "cloud-remote-ui-connectivity" + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__(self, cloud): """Initialize the binary sensor.""" From 49b07224bf481759128bad1d8149b704c0c48596 Mon Sep 17 00:00:00 2001 From: Steffen Ronalter Date: Fri, 15 Oct 2021 08:23:26 +0200 Subject: [PATCH 0381/1038] Add onewire support for DS2413 (#55921) --- .../components/onewire/binary_sensor.py | 9 ++++ homeassistant/components/onewire/switch.py | 9 ++++ tests/components/onewire/const.py | 52 +++++++++++++++++++ 3 files changed, 70 insertions(+) diff --git a/homeassistant/components/onewire/binary_sensor.py b/homeassistant/components/onewire/binary_sensor.py index 0b78988f7e1..0a57e0c1b19 100644 --- a/homeassistant/components/onewire/binary_sensor.py +++ b/homeassistant/components/onewire/binary_sensor.py @@ -57,6 +57,15 @@ DEVICE_BINARY_SENSORS: dict[str, tuple[OneWireBinarySensorEntityDescription, ... ) for id in DEVICE_KEYS_0_7 ), + "3A": tuple( + OneWireBinarySensorEntityDescription( + key=f"sensed.{id}", + entity_registry_enabled_default=False, + name=f"Sensed {id}", + read_mode=READ_MODE_BOOL, + ) + for id in DEVICE_KEYS_A_B + ), } diff --git a/homeassistant/components/onewire/switch.py b/homeassistant/components/onewire/switch.py index e8e790feab4..aadc1315712 100644 --- a/homeassistant/components/onewire/switch.py +++ b/homeassistant/components/onewire/switch.py @@ -84,6 +84,15 @@ DEVICE_SWITCHES: dict[str, tuple[OneWireEntityDescription, ...]] = { for id in DEVICE_KEYS_0_7 ] ), + "3A": tuple( + OneWireSwitchEntityDescription( + key=f"PIO.{id}", + entity_registry_enabled_default=False, + name=f"PIO {id}", + read_mode=READ_MODE_BOOL, + ) + for id in DEVICE_KEYS_A_B + ), } LOGGER = logging.getLogger(__name__) diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index 9c37442e2f7..7a39e70d4bc 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -656,6 +656,57 @@ MOCK_OWPROXY_DEVICES = { }, ], }, + "3A.111111111111": { + "inject_reads": [ + b"DS2413", # read device type + ], + "device_info": { + ATTR_IDENTIFIERS: {(DOMAIN, "3A.111111111111")}, + ATTR_MANUFACTURER: MANUFACTURER, + ATTR_MODEL: "DS2413", + ATTR_NAME: "3A.111111111111", + }, + BINARY_SENSOR_DOMAIN: [ + { + "entity_id": "binary_sensor.3a_111111111111_sensed_a", + "unique_id": "/3A.111111111111/sensed.A", + "injected_value": b" 1", + "result": STATE_ON, + ATTR_UNIT_OF_MEASUREMENT: None, + ATTR_DEVICE_CLASS: None, + "disabled": True, + }, + { + "entity_id": "binary_sensor.3a_111111111111_sensed_b", + "unique_id": "/3A.111111111111/sensed.B", + "injected_value": b" 0", + "result": STATE_OFF, + ATTR_UNIT_OF_MEASUREMENT: None, + ATTR_DEVICE_CLASS: None, + "disabled": True, + }, + ], + SWITCH_DOMAIN: [ + { + "entity_id": "switch.3a_111111111111_pio_a", + "unique_id": "/3A.111111111111/PIO.A", + "injected_value": b" 1", + "result": STATE_ON, + ATTR_UNIT_OF_MEASUREMENT: None, + ATTR_DEVICE_CLASS: None, + "disabled": True, + }, + { + "entity_id": "switch.3a_111111111111_pio_b", + "unique_id": "/3A.111111111111/PIO.B", + "injected_value": b" 0", + "result": STATE_OFF, + ATTR_UNIT_OF_MEASUREMENT: None, + ATTR_DEVICE_CLASS: None, + "disabled": True, + }, + ], + }, "3B.111111111111": { "inject_reads": [ b"DS1825", # read device type @@ -940,6 +991,7 @@ MOCK_SYSBUS_DEVICES = { ], }, "29-111111111111": {SENSOR_DOMAIN: []}, + "3A-111111111111": {SENSOR_DOMAIN: []}, "3B-111111111111": { "device_info": { ATTR_IDENTIFIERS: {(DOMAIN, "3B-111111111111")}, From 8dbff0b4b3fce88ded310b514a7fe646576afea7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 15 Oct 2021 00:04:43 -0700 Subject: [PATCH 0382/1038] Fix WLED exception on close (#57752) --- homeassistant/components/wled/coordinator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/wled/coordinator.py b/homeassistant/components/wled/coordinator.py index 6288a3d8ff7..9dbd02d65c3 100644 --- a/homeassistant/components/wled/coordinator.py +++ b/homeassistant/components/wled/coordinator.py @@ -92,6 +92,7 @@ class WLEDDataUpdateCoordinator(DataUpdateCoordinator[WLEDDevice]): async def close_websocket(_) -> None: """Close WebSocket connection.""" + self.unsub = None await self.wled.disconnect() # Clean disconnect WebSocket on Home Assistant shutdown From 727d8c7a371c8aa2c16c1a7533853e2d8a019474 Mon Sep 17 00:00:00 2001 From: avee87 <6134677+avee87@users.noreply.github.com> Date: Fri, 15 Oct 2021 08:04:55 +0100 Subject: [PATCH 0383/1038] Fix signature for hassio.restore_partial service (#57735) --- homeassistant/components/hassio/services.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/homeassistant/components/hassio/services.yaml b/homeassistant/components/hassio/services.yaml index d7137aad2ab..6b77a180c09 100644 --- a/homeassistant/components/hassio/services.yaml +++ b/homeassistant/components/hassio/services.yaml @@ -118,6 +118,7 @@ restore_full: fields: slug: name: Slug + required: true description: Slug of backup to restore from. selector: text: @@ -132,6 +133,12 @@ restore_partial: name: Restore from partial backup. description: Restore from partial backup. fields: + slug: + name: Slug + required: true + description: Slug of backup to restore from. + selector: + text: homeassistant: name: Home Assistant settings description: Restore Home Assistant @@ -149,3 +156,9 @@ restore_partial: example: ["core_ssh", "core_samba", "core_mosquitto"] selector: object: + password: + name: Password + description: Optional password. + example: "password" + selector: + text: From 8b422a7bd59663f4a965bfbd0a26ac0d6777a1f0 Mon Sep 17 00:00:00 2001 From: avee87 <6134677+avee87@users.noreply.github.com> Date: Fri, 15 Oct 2021 08:26:54 +0100 Subject: [PATCH 0384/1038] Add documentation link for Supervisor integration (#57733) --- homeassistant/components/hassio/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hassio/manifest.json b/homeassistant/components/hassio/manifest.json index aaa5b3669ad..27cc1eaf735 100644 --- a/homeassistant/components/hassio/manifest.json +++ b/homeassistant/components/hassio/manifest.json @@ -1,7 +1,7 @@ { "domain": "hassio", "name": "Home Assistant Supervisor", - "documentation": "https://www.home-assistant.io/hassio", + "documentation": "https://www.home-assistant.io/integrations/hassio", "dependencies": ["http"], "after_dependencies": ["panel_custom"], "codeowners": ["@home-assistant/supervisor"], From f6c6ec357813caed364c1de132ddad3a751c7267 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 15 Oct 2021 10:07:25 +0200 Subject: [PATCH 0385/1038] Centralize entity naming for Tuya entities (#57755) --- homeassistant/components/tuya/base.py | 5 +++++ homeassistant/components/tuya/binary_sensor.py | 7 ------- homeassistant/components/tuya/number.py | 7 ------- homeassistant/components/tuya/select.py | 7 ------- homeassistant/components/tuya/sensor.py | 7 ------- homeassistant/components/tuya/switch.py | 5 ----- 6 files changed, 5 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index 2be80f53776..0e8f8add2fa 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -58,6 +58,11 @@ class TuyaEntity(Entity): @property def name(self) -> str | None: """Return Tuya device name.""" + if ( + hasattr(self, "entity_description") + and self.entity_description.name is not None + ): + return f"{self.tuya_device.name} {self.entity_description.name}" return self.tuya_device.name @property diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index 4596b8638a4..a78e5362f36 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -85,13 +85,6 @@ class TuyaBinarySensorEntity(TuyaEntity, BinarySensorEntity): self.entity_description = description self._attr_unique_id = f"{super().unique_id}{description.key}" - @property - def name(self) -> str | None: - """Return Tuya device name.""" - if self.entity_description.name is not None: - return f"{self.tuya_device.name} {self.entity_description.name}" - return self.tuya_device.name - @property def is_on(self) -> bool: """Return true if sensor is on.""" diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index 977917f8b0c..f31929f8f69 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -113,13 +113,6 @@ class TuyaNumberEntity(TuyaEntity, NumberEntity): if description.unit_of_measurement is None: self._attr_unit_of_measurement = self._type_data.unit - @property - def name(self) -> str | None: - """Return Tuya device name.""" - if self.entity_description.name is not None: - return f"{self.tuya_device.name} {self.entity_description.name}" - return self.tuya_device.name - @property def value(self) -> float | None: """Return the entity value to represent the entity state.""" diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index c3b81787086..a71e3db5125 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -101,13 +101,6 @@ class TuyaSelectEntity(TuyaEntity, SelectEntity): type_data = EnumTypeData.from_json(self._status_range.values) self._attr_options = type_data.range - @property - def name(self) -> str | None: - """Return Tuya device name.""" - if self.entity_description.name is not None: - return f"{self.tuya_device.name} {self.entity_description.name}" - return self.tuya_device.name - @property def current_option(self) -> str | None: """Return the selected entity option to represent the entity state.""" diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index e1ab4867df9..54543ba984d 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -149,13 +149,6 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity): elif self._status_range.type == "Enum": self._type_data = EnumTypeData.from_json(self._status_range.values) - @property - def name(self) -> str | None: - """Return Tuya device name.""" - if self.entity_description.name is not None: - return f"{self.tuya_device.name} {self.entity_description.name}" - return self.tuya_device.name - @property def native_value(self) -> StateType: """Return the value reported by the sensor.""" diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index c66674de715..b001da78f9f 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -309,11 +309,6 @@ class TuyaSwitchEntity(TuyaEntity, SwitchEntity): self.entity_description = description self._attr_unique_id = f"{super().unique_id}{description.key}" - @property - def name(self) -> str | None: - """Return Tuya device name.""" - return f"{self.tuya_device.name} {self.entity_description.name}" - @property def is_on(self) -> bool: """Return true if switch is on.""" From 38688e5263e35e613131c895ff7c687667d9bb54 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 15 Oct 2021 10:08:00 +0200 Subject: [PATCH 0386/1038] Add configuration URL to Brother (#57726) --- homeassistant/components/brother/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index 8dd150b48bf..94b777147c3 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -5,6 +5,7 @@ from typing import Any, cast from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -38,6 +39,7 @@ async def async_setup_entry( "manufacturer": ATTR_MANUFACTURER, "model": coordinator.data.model, "sw_version": getattr(coordinator.data, "firmware", None), + "configuration_url": f"http://{entry.data[CONF_HOST]}/", } for description in SENSOR_TYPES: From aed6eee1ff4b257b3f921e850a027fac3143b071 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 15 Oct 2021 10:19:27 +0200 Subject: [PATCH 0387/1038] Minor tweak of entityfilter typing (#57756) --- homeassistant/helpers/entityfilter.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/helpers/entityfilter.py b/homeassistant/helpers/entityfilter.py index 727231dde00..522f789b163 100644 --- a/homeassistant/helpers/entityfilter.py +++ b/homeassistant/helpers/entityfilter.py @@ -118,23 +118,25 @@ def _test_against_patterns(patterns: list[re.Pattern[str]], entity_id: str) -> b return False -# It's safe since we don't modify it. And None causes typing warnings -# pylint: disable=dangerous-default-value def generate_filter( include_domains: list[str], include_entities: list[str], exclude_domains: list[str], exclude_entities: list[str], - include_entity_globs: list[str] = [], - exclude_entity_globs: list[str] = [], + include_entity_globs: list[str] | None = None, + exclude_entity_globs: list[str] | None = None, ) -> Callable[[str], bool]: """Return a function that will filter entities based on the args.""" include_d = set(include_domains) include_e = set(include_entities) exclude_d = set(exclude_domains) exclude_e = set(exclude_entities) - include_eg_set = set(include_entity_globs) - exclude_eg_set = set(exclude_entity_globs) + include_eg_set = ( + set(include_entity_globs) if include_entity_globs is not None else set() + ) + exclude_eg_set = ( + set(exclude_entity_globs) if exclude_entity_globs is not None else set() + ) include_eg = list(map(_glob_to_re, include_eg_set)) exclude_eg = list(map(_glob_to_re, exclude_eg_set)) From 19d812602e906bff71ecb1831505f3ad2bdae870 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 15 Oct 2021 04:27:26 -0400 Subject: [PATCH 0388/1038] Activate strict typing for nfandroidtv (#57743) Co-authored-by: Martin Hjelmare --- .strict-typing | 1 + .../components/nfandroidtv/config_flow.py | 11 +++--- .../components/nfandroidtv/notify.py | 34 ++++++++++++++----- mypy.ini | 11 ++++++ 4 files changed, 44 insertions(+), 13 deletions(-) diff --git a/.strict-typing b/.strict-typing index 907c51442a1..5b0bbd569d7 100644 --- a/.strict-typing +++ b/.strict-typing @@ -75,6 +75,7 @@ homeassistant.components.neato.* homeassistant.components.nest.* homeassistant.components.netatmo.* homeassistant.components.network.* +homeassistant.components.nfandroidtv.* homeassistant.components.no_ip.* homeassistant.components.notify.* homeassistant.components.notion.* diff --git a/homeassistant/components/nfandroidtv/config_flow.py b/homeassistant/components/nfandroidtv/config_flow.py index 0f7cffcff4b..defa4467f3a 100644 --- a/homeassistant/components/nfandroidtv/config_flow.py +++ b/homeassistant/components/nfandroidtv/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from notifications_android_tv.notifications import ConnectError, Notifications import voluptuous as vol @@ -18,7 +19,9 @@ _LOGGER = logging.getLogger(__name__) class NFAndroidTVFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for NFAndroidTV.""" - async def async_step_user(self, user_input=None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initiated by the user.""" errors = {} @@ -50,7 +53,7 @@ class NFAndroidTVFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, import_config): + async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: """Import a config entry from configuration.yaml.""" for entry in self._async_current_entries(): if entry.data[CONF_HOST] == import_config[CONF_HOST]: @@ -63,7 +66,7 @@ class NFAndroidTVFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user(import_config) - async def _async_try_connect(self, host): + async def _async_try_connect(self, host: str) -> str | None: """Try connecting to Android TV / Fire TV.""" try: await self.hass.async_add_executor_job(Notifications, host) @@ -73,4 +76,4 @@ class NFAndroidTVFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") return "unknown" - return + return None diff --git a/homeassistant/components/nfandroidtv/notify.py b/homeassistant/components/nfandroidtv/notify.py index 0f15b152038..c769770ae43 100644 --- a/homeassistant/components/nfandroidtv/notify.py +++ b/homeassistant/components/nfandroidtv/notify.py @@ -1,5 +1,8 @@ """Notifications for Android TV notification service.""" +from __future__ import annotations + import logging +from typing import Any, BinaryIO from notifications_android_tv import Notifications import requests @@ -16,6 +19,7 @@ from homeassistant.components.notify import ( from homeassistant.const import ATTR_ICON, CONF_HOST, CONF_TIMEOUT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( ATTR_COLOR, @@ -63,7 +67,11 @@ PLATFORM_SCHEMA = cv.deprecated( ) -async def async_get_service(hass: HomeAssistant, config, discovery_info=None): +async def async_get_service( + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, +) -> NFAndroidTVNotificationService: """Get the NFAndroidTV notification service.""" if discovery_info is not None: notify = await hass.async_add_executor_job( @@ -86,15 +94,15 @@ class NFAndroidTVNotificationService(BaseNotificationService): def __init__( self, notify: Notifications, - is_allowed_path, - ): + is_allowed_path: Any, + ) -> None: """Initialize the service.""" self.notify = notify self.is_allowed_path = is_allowed_path - def send_message(self, message="", **kwargs): + def send_message(self, message: str, **kwargs: Any) -> None: """Send a message to a Android TV device.""" - data = kwargs.get(ATTR_DATA) + data: dict | None = kwargs.get(ATTR_DATA) title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) duration = None fontsize = None @@ -107,7 +115,9 @@ class NFAndroidTVNotificationService(BaseNotificationService): if data: if ATTR_DURATION in data: try: - duration = int(data.get(ATTR_DURATION)) + duration = int( + data.get(ATTR_DURATION, Notifications.DEFAULT_DURATION) + ) except ValueError: _LOGGER.warning( "Invalid duration-value: %s", str(data.get(ATTR_DURATION)) @@ -152,7 +162,7 @@ class NFAndroidTVNotificationService(BaseNotificationService): if filedata is not None: if ATTR_ICON in filedata: icon = self.load_file( - url=filedata.get(ATTR_ICON), + url=filedata[ATTR_ICON], local_path=filedata.get(ATTR_FILE_PATH), username=filedata.get(ATTR_FILE_USERNAME), password=filedata.get(ATTR_FILE_PASSWORD), @@ -179,14 +189,20 @@ class NFAndroidTVNotificationService(BaseNotificationService): ) def load_file( - self, url=None, local_path=None, username=None, password=None, auth=None - ): + self, + url: str | None = None, + local_path: str | None = None, + username: str | None = None, + password: str | None = None, + auth: str | None = None, + ) -> bytes | BinaryIO | None: """Load image/document/etc from a local path or URL.""" try: if url is not None: # Check whether authentication parameters are provided if username is not None and password is not None: # Use digest or basic authentication + auth_: HTTPDigestAuth | HTTPBasicAuth if ATTR_FILE_AUTH_DIGEST == auth: auth_ = HTTPDigestAuth(username, password) else: diff --git a/mypy.ini b/mypy.ini index 636fbfe5287..5335cde9be2 100644 --- a/mypy.ini +++ b/mypy.ini @@ -836,6 +836,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.nfandroidtv.*] +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.no_ip.*] check_untyped_defs = true disallow_incomplete_defs = true From 6e6313272da8633a1ebd822029a35e33ea0a1f77 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 15 Oct 2021 10:33:20 +0200 Subject: [PATCH 0389/1038] Move Tuya value scaling into IntegerTypeData (#57757) --- homeassistant/components/tuya/base.py | 24 +++++++++++++++++----- homeassistant/components/tuya/climate.py | 26 ++++++++++-------------- homeassistant/components/tuya/number.py | 4 ++-- homeassistant/components/tuya/sensor.py | 2 +- 4 files changed, 33 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index 0e8f8add2fa..8d1d8d26f94 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -26,6 +26,25 @@ class IntegerTypeData: scale: float step: float + @property + def max_scaled(self) -> float: + """Return the max scaled.""" + return self.scale_value(self.max) + + @property + def min_scaled(self) -> float: + """Return the min scaled.""" + return self.scale_value(self.min) + + @property + def step_scaled(self) -> float: + """Return the step scaled.""" + return self.scale_value(self.step) + + def scale_value(self, value: float | int) -> float: + """Scale a value.""" + return value * 1.0 / (10 ** self.scale) + @classmethod def from_json(cls, data: str) -> IntegerTypeData: """Load JSON string and return a IntegerTypeData object.""" @@ -96,8 +115,3 @@ class TuyaEntity(Entity): "Sending commands for device %s: %s", self.tuya_device.id, commands ) self.tuya_device_manager.send_commands(self.tuya_device.id, commands) - - @staticmethod - def scale(value: float | int, scale: float | int) -> float: - """Scale a value.""" - return value * 1.0 / (10 ** scale) diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index f667eed4e93..d05d063df05 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -188,11 +188,9 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): ) self._attr_supported_features |= SUPPORT_TARGET_TEMPERATURE self._set_temperature_type = type_data - self._attr_max_temp = self.scale(type_data.max, type_data.scale) - self._attr_min_temp = self.scale(type_data.min, type_data.scale) - self._attr_target_temperature_step = self.scale( - type_data.step, type_data.scale - ) + self._attr_max_temp = type_data.max_scaled + self._attr_min_temp = type_data.min_scaled + self._attr_target_temperature_step = type_data.step_scaled # Determine dpcode to use for getting the current temperature if all( @@ -243,8 +241,8 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): device.status_range[DPCode.HUMIDITY_SET].values ) self._set_humidity_type = type_data - self._attr_min_humidity = int(self.scale(type_data.max, type_data.scale)) - self._attr_max_humidity = int(self.scale(type_data.min, type_data.scale)) + self._attr_min_humidity = int(type_data.min_scaled) + self._attr_max_humidity = int(type_data.max_scaled) # Determine dpcode to use for getting the current humidity if ( @@ -324,7 +322,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): [ { "code": self._set_humidity_dpcode, - "value": self.scale(humidity, self._set_humidity_type.scale), + "value": self._set_humidity_type.scale_value(humidity), } ] ) @@ -366,9 +364,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): { "code": self._set_temperature_dpcode, "value": round( - self.scale( - kwargs["temperature"], self._set_temperature_type.scale - ) + self._set_temperature_type.scale_value(kwargs["temperature"]) ), } ] @@ -387,7 +383,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): if temperature is None: return None - return self.scale(temperature, self._current_temperature_type.scale) + return self._current_temperature_type.scale_value(temperature) @property def current_humidity(self) -> int | None: @@ -399,7 +395,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): if humidity is None: return None - return round(self.scale(humidity, self._current_humidity_type.scale)) + return round(self._current_humidity_type.scale_value(humidity)) @property def target_temperature(self) -> float | None: @@ -411,7 +407,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): if temperature is None: return None - return self.scale(temperature, self._set_temperature_type.scale) + return self._set_temperature_type.scale_value(temperature) @property def target_humidity(self) -> int | None: @@ -423,7 +419,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): if humidity is None: return None - return round(self.scale(humidity, self._set_humidity_type.scale)) + return round(self._set_humidity_type.scale_value(humidity)) @property def hvac_mode(self) -> str: diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index f31929f8f69..d048bc214b1 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -125,7 +125,7 @@ class TuyaNumberEntity(TuyaEntity, NumberEntity): # Scale integer/float value if value and isinstance(self._type_data, IntegerTypeData): - return self.scale(value, self._type_data.scale) + return self._type_data.scale_value(value) return None @@ -138,7 +138,7 @@ class TuyaNumberEntity(TuyaEntity, NumberEntity): [ { "code": self.entity_description.key, - "value": int(self.scale(value, self._type_data.scale)), + "value": self._type_data.scale_value(value), } ] ) diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 54543ba984d..c56c9a851b4 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -167,7 +167,7 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity): # Scale integer/float value if isinstance(self._type_data, IntegerTypeData): - return self.scale(value, self._type_data.scale) + return self._type_data.scale_value(value) # Unexpected enum value if ( From b7c52d04852e13c44fd0ee10529ead86c643579a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Oct 2021 23:23:32 -1000 Subject: [PATCH 0390/1038] Add configuration url to rachio (#57738) --- homeassistant/components/rachio/entity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/rachio/entity.py b/homeassistant/components/rachio/entity.py index ec1807b04c2..879427ba2c0 100644 --- a/homeassistant/components/rachio/entity.py +++ b/homeassistant/components/rachio/entity.py @@ -38,4 +38,5 @@ class RachioDevice(Entity): "name": self._controller.name, "model": self._controller.model, "manufacturer": DEFAULT_NAME, + "configuration_url": "https://app.rach.io", } From b97d5a703ccfcdb2a00ea8745f71f273691b8446 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 15 Oct 2021 11:33:30 +0200 Subject: [PATCH 0391/1038] Renamed variables in Tuya (#57759) --- homeassistant/components/tuya/base.py | 24 ++++----- .../components/tuya/binary_sensor.py | 2 +- homeassistant/components/tuya/climate.py | 45 ++++++++--------- homeassistant/components/tuya/fan.py | 50 +++++++++---------- homeassistant/components/tuya/light.py | 44 ++++++++-------- homeassistant/components/tuya/number.py | 2 +- homeassistant/components/tuya/select.py | 2 +- homeassistant/components/tuya/sensor.py | 2 +- homeassistant/components/tuya/switch.py | 2 +- 9 files changed, 85 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index 8d1d8d26f94..66b497c2f4e 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -71,8 +71,8 @@ class TuyaEntity(Entity): def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> None: """Init TuyaHaEntity.""" self._attr_unique_id = f"tuya.{device.id}" - self.tuya_device = device - self.tuya_device_manager = device_manager + self.device = device + self.device_manager = device_manager @property def name(self) -> str | None: @@ -81,37 +81,35 @@ class TuyaEntity(Entity): hasattr(self, "entity_description") and self.entity_description.name is not None ): - return f"{self.tuya_device.name} {self.entity_description.name}" - return self.tuya_device.name + return f"{self.device.name} {self.entity_description.name}" + return self.device.name @property def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" return DeviceInfo( - identifiers={(DOMAIN, self.tuya_device.id)}, + identifiers={(DOMAIN, self.device.id)}, manufacturer="Tuya", - name=self.tuya_device.name, - model=self.tuya_device.product_name, + name=self.device.name, + model=self.device.product_name, ) @property def available(self) -> bool: """Return if the device is available.""" - return self.tuya_device.online + return self.device.online async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" self.async_on_remove( async_dispatcher_connect( self.hass, - f"{TUYA_HA_SIGNAL_UPDATE_ENTITY}_{self.tuya_device.id}", + f"{TUYA_HA_SIGNAL_UPDATE_ENTITY}_{self.device.id}", self.async_write_ha_state, ) ) def _send_command(self, commands: list[dict[str, Any]]) -> None: """Send command to the device.""" - _LOGGER.debug( - "Sending commands for device %s: %s", self.tuya_device.id, commands - ) - self.tuya_device_manager.send_commands(self.tuya_device.id, commands) + _LOGGER.debug("Sending commands for device %s: %s", self.device.id, commands) + self.device_manager.send_commands(self.device.id, commands) diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index a78e5362f36..0a6f0aed053 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -88,4 +88,4 @@ class TuyaBinarySensorEntity(TuyaEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return true if sensor is on.""" - return self.tuya_device.status.get(self.entity_description.key, False) + return self.device.status.get(self.entity_description.key, False) diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index d05d063df05..3d678b84fd0 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -251,7 +251,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): ): self._current_humidity_dpcode = DPCode.HUMIDITY_CURRENT self._current_humidity_type = IntegerTypeData.from_json( - self.tuya_device.status_range[DPCode.HUMIDITY_CURRENT].values + self.device.status_range[DPCode.HUMIDITY_CURRENT].values ) # Determine dpcode to use for getting the current humidity @@ -261,7 +261,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): ): self._current_humidity_dpcode = DPCode.HUMIDITY_CURRENT self._current_humidity_type = IntegerTypeData.from_json( - self.tuya_device.status_range[DPCode.HUMIDITY_CURRENT].values + self.device.status_range[DPCode.HUMIDITY_CURRENT].values ) # Determine fan modes @@ -271,12 +271,12 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): ): self._attr_supported_features |= SUPPORT_FAN_MODE self._attr_fan_modes = EnumTypeData.from_json( - self.tuya_device.status_range[DPCode.FAN_SPEED_ENUM].values + self.device.status_range[DPCode.FAN_SPEED_ENUM].values ).range # Determine swing modes if any( - dpcode in self.tuya_device.function + dpcode in self.device.function for dpcode in ( DPCode.SHAKE, DPCode.SWING, @@ -287,15 +287,15 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): self._attr_supported_features |= SUPPORT_SWING_MODE self._attr_swing_modes = [SWING_OFF] if any( - dpcode in self.tuya_device.function + dpcode in self.device.function for dpcode in (DPCode.SHAKE, DPCode.SWING) ): self._attr_swing_modes.append(SWING_ON) - if DPCode.SWITCH_HORIZONTAL in self.tuya_device.function: + if DPCode.SWITCH_HORIZONTAL in self.device.function: self._attr_swing_modes.append(SWING_HORIZONTAL) - if DPCode.SWITCH_VERTICAL in self.tuya_device.function: + if DPCode.SWITCH_VERTICAL in self.device.function: self._attr_swing_modes.append(SWING_VERTICAL) def set_hvac_mode(self, hvac_mode: str) -> None: @@ -379,7 +379,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): ): return None - temperature = self.tuya_device.status.get(self._current_temperature_dpcode) + temperature = self.device.status.get(self._current_temperature_dpcode) if temperature is None: return None @@ -391,7 +391,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): if self._current_humidity_dpcode is None or self._current_humidity_type is None: return None - humidity = self.tuya_device.status.get(self._current_humidity_dpcode) + humidity = self.device.status.get(self._current_humidity_dpcode) if humidity is None: return None @@ -403,7 +403,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): if self._set_temperature_dpcode is None or self._set_temperature_type is None: return None - temperature = self.tuya_device.status.get(self._set_temperature_dpcode) + temperature = self.device.status.get(self._set_temperature_dpcode) if temperature is None: return None @@ -415,7 +415,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): if self._set_humidity_dpcode is None or self._set_humidity_type is None: return None - humidity = self.tuya_device.status.get(self._set_humidity_dpcode) + humidity = self.device.status.get(self._set_humidity_dpcode) if humidity is None: return None @@ -426,34 +426,33 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): """Return hvac mode.""" # If the switch off, hvac mode is off as well. Unless the switch # the switch is on or doesn't exists of course... - if not self.tuya_device.status.get(DPCode.SWITCH, True): + if not self.device.status.get(DPCode.SWITCH, True): return HVAC_MODE_OFF - if DPCode.MODE not in self.tuya_device.function: - if self.tuya_device.status.get(DPCode.SWITCH, False): + if DPCode.MODE not in self.device.function: + if self.device.status.get(DPCode.SWITCH, False): return self.entity_description.switch_only_hvac_mode return HVAC_MODE_OFF - if self.tuya_device.status.get(DPCode.MODE) is not None: - return TUYA_HVAC_TO_HA[self.tuya_device.status[DPCode.MODE]] + if self.device.status.get(DPCode.MODE) is not None: + return TUYA_HVAC_TO_HA[self.device.status[DPCode.MODE]] return HVAC_MODE_OFF @property def fan_mode(self) -> str | None: """Return fan mode.""" - return self.tuya_device.status.get(DPCode.FAN_SPEED_ENUM) + return self.device.status.get(DPCode.FAN_SPEED_ENUM) @property def swing_mode(self) -> str: """Return swing mode.""" if any( - self.tuya_device.status.get(dpcode) - for dpcode in (DPCode.SHAKE, DPCode.SWING) + self.device.status.get(dpcode) for dpcode in (DPCode.SHAKE, DPCode.SWING) ): return SWING_ON - horizontal = self.tuya_device.status.get(DPCode.SWITCH_HORIZONTAL) - vertical = self.tuya_device.status.get(DPCode.SWITCH_VERTICAL) + horizontal = self.device.status.get(DPCode.SWITCH_HORIZONTAL) + vertical = self.device.status.get(DPCode.SWITCH_VERTICAL) if horizontal and vertical: return SWING_BOTH if horizontal: @@ -465,7 +464,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): def turn_on(self) -> None: """Turn the device on, retaining current HVAC (if supported).""" - if DPCode.SWITCH in self.tuya_device.function: + if DPCode.SWITCH in self.device.function: self._send_command([{"code": DPCode.SWITCH, "value": True}]) return @@ -478,7 +477,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): def turn_off(self) -> None: """Turn the device on, retaining current HVAC (if supported).""" - if DPCode.SWITCH in self.tuya_device.function: + if DPCode.SWITCH in self.device.function: self._send_command([{"code": DPCode.SWITCH, "value": False}]) return diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index 35b893f9103..99d28f1b998 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -65,9 +65,9 @@ class TuyaFanEntity(TuyaEntity, FanEntity): super().__init__(device, device_manager) self.ha_preset_modes = [] - if DPCode.MODE in self.tuya_device.function: + if DPCode.MODE in self.device.function: self.ha_preset_modes = json.loads( - self.tuya_device.function[DPCode.MODE].values + self.device.function[DPCode.MODE].values ).get("range", []) # Air purifier fan can be controlled either via the ranged values or via the enum. @@ -76,18 +76,18 @@ class TuyaFanEntity(TuyaEntity, FanEntity): # Range is used for e.g. Concept CA3000 self.air_purifier_speed_range_len = 0 self.air_purifier_speed_range_enum = [] - if self.tuya_device.category == "kj" and ( - DPCode.FAN_SPEED_ENUM in self.tuya_device.function - or DPCode.SPEED in self.tuya_device.function + if self.device.category == "kj" and ( + DPCode.FAN_SPEED_ENUM in self.device.function + or DPCode.SPEED in self.device.function ): - if DPCode.FAN_SPEED_ENUM in self.tuya_device.function: + if DPCode.FAN_SPEED_ENUM in self.device.function: self.dp_code_speed_enum = DPCode.FAN_SPEED_ENUM else: self.dp_code_speed_enum = DPCode.SPEED - data = json.loads( - self.tuya_device.function[self.dp_code_speed_enum].values - ).get("range") + data = json.loads(self.device.function[self.dp_code_speed_enum].values).get( + "range" + ) if data: self.air_purifier_speed_range_len = len(data) self.air_purifier_speed_range_enum = data @@ -102,7 +102,7 @@ class TuyaFanEntity(TuyaEntity, FanEntity): def set_percentage(self, percentage: int) -> None: """Set the speed of the fan, as a percentage.""" - if self.tuya_device.category == "kj": + if self.device.category == "kj": value_in_range = percentage_to_ordered_list_item( self.air_purifier_speed_range_enum, percentage ) @@ -140,19 +140,19 @@ class TuyaFanEntity(TuyaEntity, FanEntity): @property def is_on(self) -> bool: """Return true if fan is on.""" - return self.tuya_device.status.get(DPCode.SWITCH, False) + return self.device.status.get(DPCode.SWITCH, False) @property def current_direction(self) -> str: """Return the current direction of the fan.""" - if self.tuya_device.status[DPCode.FAN_DIRECTION]: + if self.device.status[DPCode.FAN_DIRECTION]: return DIRECTION_FORWARD return DIRECTION_REVERSE @property def oscillating(self) -> bool: """Return true if the fan is oscillating.""" - return self.tuya_device.status.get(DPCode.SWITCH_HORIZONTAL, False) + return self.device.status.get(DPCode.SWITCH_HORIZONTAL, False) @property def preset_modes(self) -> list[str]: @@ -162,7 +162,7 @@ class TuyaFanEntity(TuyaEntity, FanEntity): @property def preset_mode(self) -> str: """Return the current preset_mode.""" - return self.tuya_device.status[DPCode.MODE] + return self.device.status[DPCode.MODE] @property def percentage(self) -> int | None: @@ -171,24 +171,24 @@ class TuyaFanEntity(TuyaEntity, FanEntity): return 0 if ( - self.tuya_device.category == "kj" + self.device.category == "kj" and self.air_purifier_speed_range_len > 1 and not self.air_purifier_speed_range_enum - and DPCode.FAN_SPEED_ENUM in self.tuya_device.status + and DPCode.FAN_SPEED_ENUM in self.device.status ): # if air-purifier speed enumeration is supported we will prefer it. return ordered_list_item_to_percentage( self.air_purifier_speed_range_enum, - self.tuya_device.status[DPCode.FAN_SPEED_ENUM], + self.device.status[DPCode.FAN_SPEED_ENUM], ) # some type may not have the fan_speed_percent key - return self.tuya_device.status.get(DPCode.FAN_SPEED_PERCENT) + return self.device.status.get(DPCode.FAN_SPEED_PERCENT) @property def speed_count(self) -> int: """Return the number of speeds the fan supports.""" - if self.tuya_device.category == "kj": + if self.device.category == "kj": return self.air_purifier_speed_range_len return super().speed_count @@ -196,19 +196,19 @@ class TuyaFanEntity(TuyaEntity, FanEntity): def supported_features(self): """Flag supported features.""" supports = 0 - if DPCode.MODE in self.tuya_device.status: + if DPCode.MODE in self.device.status: supports |= SUPPORT_PRESET_MODE - if DPCode.FAN_SPEED_PERCENT in self.tuya_device.status: + if DPCode.FAN_SPEED_PERCENT in self.device.status: supports |= SUPPORT_SET_SPEED - if DPCode.SWITCH_HORIZONTAL in self.tuya_device.status: + if DPCode.SWITCH_HORIZONTAL in self.device.status: supports |= SUPPORT_OSCILLATE - if DPCode.FAN_DIRECTION in self.tuya_device.status: + if DPCode.FAN_DIRECTION in self.device.status: supports |= SUPPORT_DIRECTION # Air Purifier specific if ( - DPCode.SPEED in self.tuya_device.status - or DPCode.FAN_SPEED_ENUM in self.tuya_device.status + DPCode.SPEED in self.device.status + or DPCode.FAN_SPEED_ENUM in self.device.status ): supports |= SUPPORT_SET_SPEED return supports diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 69e7020f56c..9758fdefc80 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -109,7 +109,7 @@ class TuyaLightEntity(TuyaEntity, LightEntity): @property def is_on(self) -> bool: """Return true if light is on.""" - return self.tuya_device.status.get(DPCode.SWITCH_LED, False) + return self.device.status.get(DPCode.SWITCH_LED, False) def turn_on(self, **kwargs: Any) -> None: """Turn on or control the light.""" @@ -118,8 +118,8 @@ class TuyaLightEntity(TuyaEntity, LightEntity): _LOGGER.debug("light kwargs-> %s; work_mode %s", kwargs, work_mode) if ( - DPCode.LIGHT in self.tuya_device.status - and DPCode.SWITCH_LED not in self.tuya_device.status + DPCode.LIGHT in self.device.status + and DPCode.SWITCH_LED not in self.device.status ): commands += [{"code": DPCode.LIGHT, "value": True}] else: @@ -204,8 +204,8 @@ class TuyaLightEntity(TuyaEntity, LightEntity): def turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" if ( - DPCode.LIGHT in self.tuya_device.status - and DPCode.SWITCH_LED not in self.tuya_device.status + DPCode.LIGHT in self.device.status + and DPCode.SWITCH_LED not in self.device.status ): commands = [{"code": DPCode.LIGHT, "value": False}] else: @@ -216,10 +216,10 @@ class TuyaLightEntity(TuyaEntity, LightEntity): def brightness(self) -> int | None: """Return the brightness of the light.""" old_range = self._tuya_brightness_range() - brightness = self.tuya_device.status.get(self.dp_code_bright, 0) + brightness = self.device.status.get(self.dp_code_bright, 0) if self._work_mode().startswith(WORK_MODE_COLOUR): - colour_json = self.tuya_device.status.get(self.dp_code_colour) + colour_json = self.device.status.get(self.dp_code_colour) if not colour_json: return None colour_data = json.loads(colour_json) @@ -230,9 +230,9 @@ class TuyaLightEntity(TuyaEntity, LightEntity): return int(self.remap(brightness, old_range[0], old_range[1], 0, 255)) def _tuya_brightness_range(self) -> tuple[int, int]: - if self.dp_code_bright not in self.tuya_device.status: + if self.dp_code_bright not in self.device.status: return 0, 255 - bright_item = self.tuya_device.function.get(self.dp_code_bright) + bright_item = self.device.function.get(self.dp_code_bright) if not bright_item: return 0, 255 bright_value = json.loads(bright_item.values) @@ -249,7 +249,7 @@ class TuyaLightEntity(TuyaEntity, LightEntity): @property def hs_color(self) -> tuple[float, float] | None: """Return the hs_color of the light.""" - colour_json = self.tuya_device.status.get(self.dp_code_colour) + colour_json = self.device.status.get(self.dp_code_colour) if not colour_json: return None colour_data = json.loads(colour_json) @@ -266,7 +266,7 @@ class TuyaLightEntity(TuyaEntity, LightEntity): def color_temp(self) -> int: """Return the color_temp of the light.""" new_range = self._tuya_temp_range() - tuya_color_temp = self.tuya_device.status.get(self.dp_code_temp, 0) + tuya_color_temp = self.device.status.get(self.dp_code_temp, 0) return ( self.max_mireds - self.remap( @@ -290,7 +290,7 @@ class TuyaLightEntity(TuyaEntity, LightEntity): return MIREDS_MAX def _tuya_temp_range(self) -> tuple[int, int]: - temp_item = self.tuya_device.function.get(self.dp_code_temp) + temp_item = self.device.function.get(self.dp_code_temp) if not temp_item: return 0, 255 temp_value = json.loads(temp_item.values) @@ -312,13 +312,13 @@ class TuyaLightEntity(TuyaEntity, LightEntity): return 0, 255 def _tuya_hsv_function(self) -> dict[str, dict] | None: - hsv_item = self.tuya_device.function.get(self.dp_code_colour) + hsv_item = self.device.function.get(self.dp_code_colour) if not hsv_item: return None hsv_data = json.loads(hsv_item.values) if hsv_data: return hsv_data - colour_json = self.tuya_device.status.get(self.dp_code_colour) + colour_json = self.device.status.get(self.dp_code_colour) if not colour_json: return None colour_data = json.loads(colour_json) @@ -331,30 +331,30 @@ class TuyaLightEntity(TuyaEntity, LightEntity): return DEFAULT_HSV def _work_mode(self) -> str: - return self.tuya_device.status.get(DPCode.WORK_MODE, "") + return self.device.status.get(DPCode.WORK_MODE, "") def _get_hsv(self) -> dict[str, int]: if ( - self.dp_code_colour not in self.tuya_device.status - or len(self.tuya_device.status[self.dp_code_colour]) == 0 + self.dp_code_colour not in self.device.status + or len(self.device.status[self.dp_code_colour]) == 0 ): return {"h": 0, "s": 0, "v": 0} - return json.loads(self.tuya_device.status[self.dp_code_colour]) + return json.loads(self.device.status[self.dp_code_colour]) @property def supported_color_modes(self) -> set[str] | None: """Flag supported color modes.""" color_modes = [COLOR_MODE_ONOFF] - if self.dp_code_bright in self.tuya_device.status: + if self.dp_code_bright in self.device.status: color_modes.append(COLOR_MODE_BRIGHTNESS) - if self.dp_code_temp in self.tuya_device.status: + if self.dp_code_temp in self.device.status: color_modes.append(COLOR_MODE_COLOR_TEMP) if ( - self.dp_code_colour in self.tuya_device.status - and len(self.tuya_device.status[self.dp_code_colour]) > 0 + self.dp_code_colour in self.device.status + and len(self.device.status[self.dp_code_colour]) > 0 ): color_modes.append(COLOR_MODE_HS) return set(color_modes) diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index d048bc214b1..91ad3f0e6ba 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -121,7 +121,7 @@ class TuyaNumberEntity(TuyaEntity, NumberEntity): return None # Raw value - value = self.tuya_device.status.get(self.entity_description.key) + value = self.device.status.get(self.entity_description.key) # Scale integer/float value if value and isinstance(self._type_data, IntegerTypeData): diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index a71e3db5125..2b033757bc9 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -105,7 +105,7 @@ class TuyaSelectEntity(TuyaEntity, SelectEntity): def current_option(self) -> str | None: """Return the selected entity option to represent the entity state.""" # Raw value - value = self.tuya_device.status.get(self.entity_description.key) + value = self.device.status.get(self.entity_description.key) if value is None or value not in self._attr_options: return None diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index c56c9a851b4..8d36a9b1207 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -161,7 +161,7 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity): return None # Raw value - value = self.tuya_device.status.get(self.entity_description.key) + value = self.device.status.get(self.entity_description.key) if value is None: return None diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index b001da78f9f..e00fea62c7a 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -312,7 +312,7 @@ class TuyaSwitchEntity(TuyaEntity, SwitchEntity): @property def is_on(self) -> bool: """Return true if switch is on.""" - return self.tuya_device.status.get(self.entity_description.key, False) + return self.device.status.get(self.entity_description.key, False) def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" From f02522783a250ebb7051219c75da17de42c186c0 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 15 Oct 2021 11:42:21 +0200 Subject: [PATCH 0392/1038] Add entity category to Nettigo Air Monitor entities (#57698) --- homeassistant/components/nam/const.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/nam/const.py b/homeassistant/components/nam/const.py index d4781fa0c49..8f1ba356fc9 100644 --- a/homeassistant/components/nam/const.py +++ b/homeassistant/components/nam/const.py @@ -20,6 +20,7 @@ from homeassistant.const import ( DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, + ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, PRESSURE_HPA, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, @@ -214,11 +215,13 @@ SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( device_class=DEVICE_CLASS_SIGNAL_STRENGTH, entity_registry_enabled_default=False, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=ATTR_UPTIME, name=f"{DEFAULT_NAME} Uptime", device_class=DEVICE_CLASS_TIMESTAMP, entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ) From cb625d1c7a2e2b5b59622f3940afa5b4ba0ffd68 Mon Sep 17 00:00:00 2001 From: gjong Date: Fri, 15 Oct 2021 12:24:51 +0200 Subject: [PATCH 0393/1038] Fix Youless state class for power total sensor (#57758) --- homeassistant/components/youless/sensor.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/youless/sensor.py b/homeassistant/components/youless/sensor.py index 24983bb567b..d4465bd0c09 100644 --- a/homeassistant/components/youless/sensor.py +++ b/homeassistant/components/youless/sensor.py @@ -5,6 +5,7 @@ from youless_api.youless_sensor import YoulessSensor from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL, STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) @@ -40,9 +41,9 @@ async def async_setup_entry( async_add_entities( [ GasSensor(coordinator, device), - PowerMeterSensor(coordinator, device, "low"), - PowerMeterSensor(coordinator, device, "high"), - PowerMeterSensor(coordinator, device, "total"), + PowerMeterSensor(coordinator, device, "low", STATE_CLASS_TOTAL_INCREASING), + PowerMeterSensor(coordinator, device, "high", STATE_CLASS_TOTAL_INCREASING), + PowerMeterSensor(coordinator, device, "total", STATE_CLASS_TOTAL), CurrentPowerSensor(coordinator, device), DeliveryMeterSensor(coordinator, device, "low"), DeliveryMeterSensor(coordinator, device, "high"), @@ -168,7 +169,11 @@ class PowerMeterSensor(YoulessBaseSensor): _attr_state_class = STATE_CLASS_TOTAL_INCREASING def __init__( - self, coordinator: DataUpdateCoordinator, device: str, dev_type: str + self, + coordinator: DataUpdateCoordinator, + device: str, + dev_type: str, + state_class: str, ) -> None: """Instantiate a power meter sensor.""" super().__init__( @@ -177,6 +182,7 @@ class PowerMeterSensor(YoulessBaseSensor): self._device = device self._type = dev_type self._attr_name = f"Power {dev_type}" + self._attr_state_class = state_class @property def get_sensor(self) -> YoulessSensor | None: From 0e0430ba36a72d4550d64cc62881283eda241393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 15 Oct 2021 12:33:10 +0200 Subject: [PATCH 0394/1038] Remove YAML import from UptimeRobot (#57761) --- .../components/uptimerobot/binary_sensor.py | 26 +------- .../components/uptimerobot/config_flow.py | 18 ------ .../uptimerobot/test_binary_sensor.py | 30 ---------- .../uptimerobot/test_config_flow.py | 60 ------------------- 4 files changed, 1 insertion(+), 133 deletions(-) diff --git a/homeassistant/components/uptimerobot/binary_sensor.py b/homeassistant/components/uptimerobot/binary_sensor.py index ac40f1f4788..09ce3262d81 100644 --- a/homeassistant/components/uptimerobot/binary_sensor.py +++ b/homeassistant/components/uptimerobot/binary_sensor.py @@ -1,43 +1,19 @@ """UptimeRobot binary_sensor platform.""" from __future__ import annotations -import voluptuous as vol - from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, - PLATFORM_SCHEMA, BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_API_KEY +from homeassistant.config_entries import ConfigEntry 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 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN from .entity import UptimeRobotEntity -PLATFORM_SCHEMA = cv.deprecated( - vol.All(PLATFORM_SCHEMA.extend({vol.Required(CONF_API_KEY): cv.string})) -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the UptimeRobot binary_sensor platform.""" - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) - ) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback diff --git a/homeassistant/components/uptimerobot/config_flow.py b/homeassistant/components/uptimerobot/config_flow.py index 00d85ff3687..c91b08ca12f 100644 --- a/homeassistant/components/uptimerobot/config_flow.py +++ b/homeassistant/components/uptimerobot/config_flow.py @@ -111,21 +111,3 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="reauth_confirm", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) - - async def async_step_import(self, import_config: ConfigType) -> FlowResult: - """Import a config entry from configuration.yaml.""" - for entry in self._async_current_entries(): - if entry.data[CONF_API_KEY] == import_config[CONF_API_KEY]: - LOGGER.warning( - "Already configured. This YAML configuration has already been imported. Please remove it" - ) - return self.async_abort(reason="already_configured") - - imported_config = {CONF_API_KEY: import_config[CONF_API_KEY]} - - _, account = await self._validate_input(imported_config) - if account: - await self.async_set_unique_id(str(account.user_id)) - self._abort_if_unique_id_configured() - return self.async_create_entry(title=account.email, data=imported_config) - return self.async_abort(reason="unknown") diff --git a/tests/components/uptimerobot/test_binary_sensor.py b/tests/components/uptimerobot/test_binary_sensor.py index f86ab9eef14..8a5c4f623e0 100644 --- a/tests/components/uptimerobot/test_binary_sensor.py +++ b/tests/components/uptimerobot/test_binary_sensor.py @@ -8,50 +8,20 @@ from homeassistant.components.binary_sensor import DEVICE_CLASS_CONNECTIVITY from homeassistant.components.uptimerobot.const import ( ATTRIBUTION, COORDINATOR_UPDATE_INTERVAL, - DOMAIN, ) from homeassistant.const import STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from homeassistant.util import dt from .common import ( - MOCK_UPTIMEROBOT_API_KEY, MOCK_UPTIMEROBOT_MONITOR, UPTIMEROBOT_TEST_ENTITY, - MockApiResponseKey, - mock_uptimerobot_api_response, setup_uptimerobot_integration, ) from tests.common import async_fire_time_changed -async def test_config_import(hass: HomeAssistant) -> None: - """Test importing YAML configuration.""" - config = { - "binary_sensor": { - "platform": DOMAIN, - "api_key": MOCK_UPTIMEROBOT_API_KEY, - } - } - with patch( - "pyuptimerobot.UptimeRobot.async_get_account_details", - return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ACCOUNT), - ), patch( - "pyuptimerobot.UptimeRobot.async_get_monitors", - return_value=mock_uptimerobot_api_response(), - ): - assert await async_setup_component(hass, "binary_sensor", config) - await hass.async_block_till_done() - - config_entries = hass.config_entries.async_entries(DOMAIN) - - assert len(config_entries) == 1 - config_entry = config_entries[0] - assert config_entry.source == "import" - - async def test_presentation(hass: HomeAssistant) -> None: """Test the presenstation of UptimeRobot binary_sensors.""" await setup_uptimerobot_integration(hass) diff --git a/tests/components/uptimerobot/test_config_flow.py b/tests/components/uptimerobot/test_config_flow.py index 074179761d9..7daa59df111 100644 --- a/tests/components/uptimerobot/test_config_flow.py +++ b/tests/components/uptimerobot/test_config_flow.py @@ -102,66 +102,6 @@ async def test_form_api_error(hass: HomeAssistant, caplog: LogCaptureFixture) -> assert "test error from API." in caplog.text -async def test_flow_import( - hass: HomeAssistant, -) -> None: - """Test an import flow.""" - with patch( - "pyuptimerobot.UptimeRobot.async_get_account_details", - return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ACCOUNT), - ), patch( - "homeassistant.components.uptimerobot.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={"platform": DOMAIN, CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, - ) - await hass.async_block_till_done() - - assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["data"] == {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY} - - with patch( - "pyuptimerobot.UptimeRobot.async_get_account_details", - return_value=mock_uptimerobot_api_response(key=MockApiResponseKey.ACCOUNT), - ), patch( - "homeassistant.components.uptimerobot.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={"platform": DOMAIN, CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, - ) - await hass.async_block_till_done() - - assert len(mock_setup_entry.mock_calls) == 0 - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - with patch( - "pyuptimerobot.UptimeRobot.async_get_account_details", - return_value=mock_uptimerobot_api_response( - key=MockApiResponseKey.ACCOUNT, data={} - ), - ), patch( - "homeassistant.components.uptimerobot.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={"platform": DOMAIN, CONF_API_KEY: "12345"}, - ) - await hass.async_block_till_done() - - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "unknown" - - async def test_user_unique_id_already_exists( hass: HomeAssistant, ) -> None: From f8dbcb953c8abec020da44bc1b581832405a17ba Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Fri, 15 Oct 2021 07:04:26 -0400 Subject: [PATCH 0395/1038] Swap order of int template helper kwargs (#57729) * swap order of int kwargs * Add binary and kwargless base tests --- homeassistant/helpers/template.py | 4 ++-- tests/helpers/test_template.py | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index b12ebca53c9..4a7676960b4 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1463,7 +1463,7 @@ def forgiving_float_filter(value, default=_SENTINEL): return default -def forgiving_int(value, default=_SENTINEL, base=10): +def forgiving_int(value, base=10, default=_SENTINEL): """Try to convert value to an int, and warn if it fails.""" result = jinja2.filters.do_int(value, default=default, base=base) if result is _SENTINEL: @@ -1472,7 +1472,7 @@ def forgiving_int(value, default=_SENTINEL, base=10): return result -def forgiving_int_filter(value, default=_SENTINEL, base=10): +def forgiving_int_filter(value, base=10, default=_SENTINEL): """Try to convert value to an int, and warn if it fails.""" result = jinja2.filters.do_int(value, default=default, base=base) if result is _SENTINEL: diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 4be9d527d31..d6a25496bbf 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -248,9 +248,14 @@ def test_int_filter(hass): hass.states.async_set("sensor.temperature", "0x10") assert render(hass, "{{ states.sensor.temperature.state | int(base=16) }}") == 16 + assert render(hass, "{{ states.sensor.temperature.state | int(16) }}") == 16 + + hass.states.async_set("sensor.temperature", "1111") + assert render(hass, "{{ states.sensor.temperature.state | int(base=2) }}") == 15 + assert render(hass, "{{ states.sensor.temperature.state | int(2) }}") == 15 assert render(hass, "{{ 'bad' | int }}") == 0 - assert render(hass, "{{ 'bad' | int(1) }}") == 1 + assert render(hass, "{{ 'bad' | int(10, 1) }}") == 1 assert render(hass, "{{ 'bad' | int(default=1) }}") == 1 @@ -262,9 +267,14 @@ def test_int_function(hass): hass.states.async_set("sensor.temperature", "0x10") assert render(hass, "{{ int(states.sensor.temperature.state, base=16) }}") == 16 + assert render(hass, "{{ int(states.sensor.temperature.state, 16) }}") == 16 + + hass.states.async_set("sensor.temperature", "1111") + assert render(hass, "{{ int(states.sensor.temperature.state, base=2) }}") == 15 + assert render(hass, "{{ int(states.sensor.temperature.state, 2) }}") == 15 assert render(hass, "{{ int('bad') }}") == "bad" - assert render(hass, "{{ int('bad', 1) }}") == 1 + assert render(hass, "{{ int('bad', 10, 1) }}") == 1 assert render(hass, "{{ int('bad', default=1) }}") == 1 From 1eebe451544f95c2fcb395055fdc2d736617885e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 15 Oct 2021 14:28:30 +0200 Subject: [PATCH 0396/1038] Add support for entity categories to MQTT entities (#57656) * Add support for entity categories to MQTT entities * Improve test * Apply suggestions from code review Co-authored-by: Paulus Schoutsen * Update homeassistant/components/mqtt/mixins.py Co-authored-by: Paul Monigatti Co-authored-by: Paulus Schoutsen Co-authored-by: Paul Monigatti --- homeassistant/components/mqtt/mixins.py | 17 ++++++++-- homeassistant/const.py | 1 + homeassistant/helpers/entity.py | 14 ++++++++- tests/components/mqtt/test_common.py | 41 +++++++++++++++++++++++++ tests/components/mqtt/test_sensor.py | 7 +++++ 5 files changed, 77 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 0ba699c6229..1965cb77e53 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -8,14 +8,20 @@ import logging import voluptuous as vol -from homeassistant.const import CONF_DEVICE, CONF_ICON, CONF_NAME, CONF_UNIQUE_ID +from homeassistant.const import ( + CONF_DEVICE, + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_NAME, + CONF_UNIQUE_ID, +) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA, Entity from homeassistant.helpers.typing import ConfigType from . import DATA_MQTT, debug_info, publish, subscription @@ -76,6 +82,7 @@ MQTT_ATTRIBUTES_BLOCKED = { "context_recent_time", "device_class", "device_info", + "entity_category", "entity_picture", "entity_registry_enabled_default", "extra_state_attributes", @@ -165,6 +172,7 @@ MQTT_ENTITY_COMMON_SCHEMA = MQTT_AVAILABILITY_SCHEMA.extend( { vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_ENABLED_BY_DEFAULT, default=True): cv.boolean, + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_JSON_ATTRS_TOPIC): valid_subscribe_topic, vol.Optional(CONF_JSON_ATTRS_TEMPLATE): cv.template, @@ -630,6 +638,11 @@ class MqttEntity( """Return if the entity should be enabled when first added to the entity registry.""" return self._config[CONF_ENABLED_BY_DEFAULT] + @property + def entity_category(self) -> str | None: + """Return the entity category if any.""" + return self._config.get(CONF_ENTITY_CATEGORY) + @property def icon(self): """Return icon of the entity if any.""" diff --git a/homeassistant/const.py b/homeassistant/const.py index f4387c9bbce..1c3029a9d34 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -102,6 +102,7 @@ CONF_EFFECT: Final = "effect" CONF_ELEVATION: Final = "elevation" CONF_EMAIL: Final = "email" CONF_ENTITIES: Final = "entities" +CONF_ENTITY_CATEGORY: Final = "entity_category" CONF_ENTITY_ID: Final = "entity_id" CONF_ENTITY_NAMESPACE: Final = "entity_namespace" CONF_ENTITY_PICTURE_TEMPLATE: Final = "entity_picture_template" diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 2ad0f934973..a05d2c7c2fa 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -11,7 +11,9 @@ import logging import math import sys from timeit import default_timer as timer -from typing import Any, Literal, TypedDict, final +from typing import Any, Final, Literal, TypedDict, final + +import voluptuous as vol from homeassistant.config import DATA_CUSTOMIZE from homeassistant.const import ( @@ -24,6 +26,8 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, DEVICE_DEFAULT_NAME, + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, @@ -52,6 +56,14 @@ SOURCE_PLATFORM_CONFIG = "platform_config" FLOAT_PRECISION = abs(int(math.floor(math.log10(abs(sys.float_info.epsilon))))) - 1 +ENTITY_CATEGORIES: Final[list[str]] = [ + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, +] + +ENTITY_CATEGORIES_SCHEMA: Final = vol.In(ENTITY_CATEGORIES) + + @callback @bind_hass def entity_sources(hass: HomeAssistant) -> dict[str, dict[str, str]]: diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 0458ad14d51..72f5f236d28 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -1217,3 +1217,44 @@ async def help_test_entity_disabled_by_default(hass, mqtt_mock, domain, config): assert not ent_registry.async_get_entity_id(domain, mqtt.DOMAIN, "veryunique1") assert not ent_registry.async_get_entity_id(domain, mqtt.DOMAIN, "veryunique2") assert not dev_registry.async_get_device({("mqtt", "helloworld")}) + + +async def help_test_entity_category(hass, mqtt_mock, domain, config): + """Test device registry remove.""" + # Add device settings to config + config = copy.deepcopy(config[domain]) + config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) + + ent_registry = er.async_get(hass) + + # Discover an entity without entity category + unique_id = "veryunique1" + config["unique_id"] = unique_id + data = json.dumps(config) + async_fire_mqtt_message(hass, f"homeassistant/{domain}/{unique_id}/config", data) + await hass.async_block_till_done() + entity_id = ent_registry.async_get_entity_id(domain, mqtt.DOMAIN, unique_id) + assert hass.states.get(entity_id) + entry = ent_registry.async_get(entity_id) + assert entry.entity_category is None + + # Discover an entity with entity category set to "config" + unique_id = "veryunique2" + config["entity_category"] = "config" + config["unique_id"] = unique_id + data = json.dumps(config) + async_fire_mqtt_message(hass, f"homeassistant/{domain}/{unique_id}/config", data) + await hass.async_block_till_done() + entity_id = ent_registry.async_get_entity_id(domain, mqtt.DOMAIN, unique_id) + assert hass.states.get(entity_id) + entry = ent_registry.async_get(entity_id) + assert entry.entity_category == "config" + + # Discover an entity with entity category set to "no_such_category" + unique_id = "veryunique3" + config["entity_category"] = "no_such_category" + config["unique_id"] = unique_id + data = json.dumps(config) + async_fire_mqtt_message(hass, f"homeassistant/{domain}/{unique_id}/config", data) + await hass.async_block_till_done() + assert not ent_registry.async_get_entity_id(domain, mqtt.DOMAIN, unique_id) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 46c06f0d3b3..b7120b99f6e 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -29,6 +29,7 @@ from .test_common import ( help_test_discovery_update_attr, help_test_discovery_update_availability, help_test_discovery_update_unchanged, + help_test_entity_category, help_test_entity_debug_info, help_test_entity_debug_info_max_messages, help_test_entity_debug_info_message, @@ -838,6 +839,12 @@ async def test_entity_disabled_by_default(hass, mqtt_mock): ) +@pytest.mark.no_fail_on_log_exception +async def test_entity_category(hass, mqtt_mock): + """Test entity category.""" + await help_test_entity_category(hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG) + + async def test_value_template_with_entity_id(hass, mqtt_mock): """Test the access to attributes in value_template via the entity_id.""" assert await async_setup_component( From 6881ab58d1b4fb7c9c26bdc4d99e92bbcc9e1590 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 15 Oct 2021 16:27:12 +0200 Subject: [PATCH 0397/1038] Add renault last activity sensors (#57750) * Add battery and location last_activity sensors * Drop state attributes * Drop ATTR_LAST_UPDATE * Adjust tests * Mark new sensors as disabled_default * Add default_disabled attribute * Add context managers * Adjust tests for disabled entities --- .../components/renault/renault_entities.py | 27 +----- homeassistant/components/renault/sensor.py | 33 +++++++- tests/components/renault/__init__.py | 65 +++++++++++---- tests/components/renault/const.py | 82 ++++++++++--------- .../components/renault/test_binary_sensor.py | 3 - .../components/renault/test_device_tracker.py | 3 - tests/components/renault/test_select.py | 3 - tests/components/renault/test_sensor.py | 61 ++++++++++++-- 8 files changed, 180 insertions(+), 97 deletions(-) diff --git a/homeassistant/components/renault/renault_entities.py b/homeassistant/components/renault/renault_entities.py index e0aae72298b..2ea823c25c2 100644 --- a/homeassistant/components/renault/renault_entities.py +++ b/homeassistant/components/renault/renault_entities.py @@ -1,14 +1,12 @@ """Base classes for Renault entities.""" from __future__ import annotations -from collections.abc import Mapping from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Optional, cast +from typing import Optional, cast from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from homeassistant.util.dt import as_utc, parse_datetime from .renault_coordinator import T from .renault_vehicle import RenaultVehicleProxy @@ -26,9 +24,6 @@ class RenaultEntityDescription(EntityDescription, RenaultRequiredKeysMixin): """Class describing Renault entities.""" -ATTR_LAST_UPDATE = "last_update" - - class RenaultDataEntity(CoordinatorEntity[Optional[T]], Entity): """Implementation of a Renault entity with a data coordinator.""" @@ -51,23 +46,3 @@ class RenaultDataEntity(CoordinatorEntity[Optional[T]], Entity): if self.coordinator.data is None: return None return cast(StateType, getattr(self.coordinator.data, key)) - - @property - def extra_state_attributes(self) -> Mapping[str, Any] | None: - """Return the state attributes of this entity.""" - last_update: str | None = None - if self.entity_description.coordinator == "battery": - last_update = cast(str, self._get_data_attr("timestamp")) - elif self.entity_description.coordinator == "location": - last_update = cast(str, self._get_data_attr("lastUpdateTime")) - if last_update: - return {ATTR_LAST_UPDATE: _convert_to_utc_string(last_update)} - return None - - -def _convert_to_utc_string(value: str) -> str: - """Convert date to UTC iso format.""" - original_dt = parse_datetime(value) - if TYPE_CHECKING: - assert original_dt is not None - return as_utc(original_dt).isoformat() diff --git a/homeassistant/components/renault/sensor.py b/homeassistant/components/renault/sensor.py index bcdb01a05f3..e8e26e06d6c 100644 --- a/homeassistant/components/renault/sensor.py +++ b/homeassistant/components/renault/sensor.py @@ -3,13 +3,14 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import cast +from typing import TYPE_CHECKING, cast from renault_api.kamereon.enums import ChargeState, PlugState from renault_api.kamereon.models import ( KamereonVehicleBatteryStatusData, KamereonVehicleCockpitData, KamereonVehicleHvacStatusData, + KamereonVehicleLocationData, ) from homeassistant.components.sensor import ( @@ -25,6 +26,7 @@ from homeassistant.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, ELECTRIC_CURRENT_AMPERE, ENERGY_KILO_WATT_HOUR, LENGTH_KILOMETERS, @@ -37,6 +39,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType +from homeassistant.util.dt import as_utc, parse_datetime from .const import DEVICE_CLASS_CHARGE_STATE, DEVICE_CLASS_PLUG_STATE, DOMAIN from .renault_coordinator import T @@ -148,6 +151,14 @@ def _get_rounded_value(entity: RenaultSensor[T]) -> float: return round(cast(float, entity.data)) +def _get_utc_value(entity: RenaultSensor[T]) -> str: + """Return the UTC value of this entity.""" + original_dt = parse_datetime(cast(str, entity.data)) + if TYPE_CHECKING: + assert original_dt is not None + return as_utc(original_dt).isoformat() + + SENSOR_TYPES: tuple[RenaultSensorEntityDescription, ...] = ( RenaultSensorEntityDescription( key="battery_level", @@ -242,6 +253,16 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription, ...] = ( native_unit_of_measurement=TEMP_CELSIUS, state_class=STATE_CLASS_MEASUREMENT, ), + RenaultSensorEntityDescription( + key="battery_last_activity", + coordinator="battery", + device_class=DEVICE_CLASS_TIMESTAMP, + data_key="timestamp", + entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], + entity_registry_enabled_default=False, + name="Battery Last Activity", + value_lambda=_get_utc_value, + ), RenaultSensorEntityDescription( key="mileage", coordinator="cockpit", @@ -287,4 +308,14 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription, ...] = ( native_unit_of_measurement=TEMP_CELSIUS, state_class=STATE_CLASS_MEASUREMENT, ), + RenaultSensorEntityDescription( + key="location_last_activity", + coordinator="location", + device_class=DEVICE_CLASS_TIMESTAMP, + data_key="lastUpdateTime", + entity_class=RenaultSensor[KamereonVehicleLocationData], + entity_registry_enabled_default=False, + name="Location Last Activity", + value_lambda=_get_utc_value, + ), ) diff --git a/tests/components/renault/__init__.py b/tests/components/renault/__init__.py index bbca3a74139..e95978222f6 100644 --- a/tests/components/renault/__init__.py +++ b/tests/components/renault/__init__.py @@ -1,6 +1,7 @@ """Tests for the Renault integration.""" from __future__ import annotations +import contextlib from types import MappingProxyType from typing import Any from unittest.mock import patch @@ -97,11 +98,11 @@ async def setup_renault_integration_simple(hass: HomeAssistant): return config_entry -async def setup_renault_integration_vehicle(hass: HomeAssistant, vehicle_type: str): - """Create the Renault integration.""" - config_entry = get_mock_config_entry() - config_entry.add_to_hass(hass) - +@contextlib.contextmanager +def patch_fixtures( + hass: HomeAssistant, config_entry: MockConfigEntry, vehicle_type: str +): + """Mock fixtures.""" renault_account = RenaultAccount( config_entry.unique_id, websession=aiohttp_client.async_get_clientsession(hass), @@ -141,19 +142,26 @@ async def setup_renault_integration_vehicle(hass: HomeAssistant, vehicle_type: s "renault_api.renault_vehicle.RenaultVehicle.get_location", return_value=mock_fixtures["location"], ): + yield + + +async def setup_renault_integration_vehicle(hass: HomeAssistant, vehicle_type: str): + """Create the Renault integration.""" + config_entry = get_mock_config_entry() + config_entry.add_to_hass(hass) + + with patch_fixtures(hass, config_entry, vehicle_type): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() return config_entry -async def setup_renault_integration_vehicle_with_no_data( - hass: HomeAssistant, vehicle_type: str +@contextlib.contextmanager +def patch_fixtures_with_no_data( + hass: HomeAssistant, config_entry: MockConfigEntry, vehicle_type: str ): - """Create the Renault integration.""" - config_entry = get_mock_config_entry() - config_entry.add_to_hass(hass) - + """Mock fixtures.""" renault_account = RenaultAccount( config_entry.unique_id, websession=aiohttp_client.async_get_clientsession(hass), @@ -193,19 +201,31 @@ async def setup_renault_integration_vehicle_with_no_data( "renault_api.renault_vehicle.RenaultVehicle.get_location", return_value=mock_fixtures["location"], ): + yield + + +async def setup_renault_integration_vehicle_with_no_data( + hass: HomeAssistant, vehicle_type: str +): + """Create the Renault integration.""" + config_entry = get_mock_config_entry() + config_entry.add_to_hass(hass) + + with patch_fixtures_with_no_data(hass, config_entry, vehicle_type): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() return config_entry -async def setup_renault_integration_vehicle_with_side_effect( - hass: HomeAssistant, vehicle_type: str, side_effect: Any +@contextlib.contextmanager +def patch_fixtures_with_side_effect( + hass: HomeAssistant, + config_entry: MockConfigEntry, + vehicle_type: str, + side_effect: Any, ): - """Create the Renault integration.""" - config_entry = get_mock_config_entry() - config_entry.add_to_hass(hass) - + """Mock fixtures.""" renault_account = RenaultAccount( config_entry.unique_id, websession=aiohttp_client.async_get_clientsession(hass), @@ -244,6 +264,17 @@ async def setup_renault_integration_vehicle_with_side_effect( "renault_api.renault_vehicle.RenaultVehicle.get_location", side_effect=side_effect, ): + yield + + +async def setup_renault_integration_vehicle_with_side_effect( + hass: HomeAssistant, vehicle_type: str, side_effect: Any +): + """Create the Renault integration.""" + config_entry = get_mock_config_entry() + config_entry.add_to_hass(hass) + + with patch_fixtures_with_side_effect(hass, config_entry, vehicle_type, side_effect): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py index f9fb765dab3..2bcab8ef47f 100644 --- a/tests/components/renault/const.py +++ b/tests/components/renault/const.py @@ -13,7 +13,6 @@ from homeassistant.components.renault.const import ( DEVICE_CLASS_PLUG_STATE, DOMAIN, ) -from homeassistant.components.renault.renault_entities import ATTR_LAST_UPDATE from homeassistant.components.select import DOMAIN as SELECT_DOMAIN from homeassistant.components.select.const import ATTR_OPTIONS from homeassistant.components.sensor import ( @@ -33,6 +32,7 @@ from homeassistant.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, ELECTRIC_CURRENT_AMPERE, ENERGY_KILO_WATT_HOUR, LENGTH_KILOMETERS, @@ -53,10 +53,7 @@ FIXED_ATTRIBUTES = ( ATTR_STATE_CLASS, ATTR_UNIT_OF_MEASUREMENT, ) -DYNAMIC_ATTRIBUTES = ( - ATTR_ICON, - ATTR_LAST_UPDATE, -) +DYNAMIC_ATTRIBUTES = (ATTR_ICON,) ICON_FOR_EMPTY_VALUES = { "select.charge_mode": "mdi:calendar-remove", @@ -100,14 +97,12 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777999_plugged_in", "result": STATE_ON, ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG, - ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", }, { "entity_id": "binary_sensor.charging", "unique_id": "vf1aaaaa555777999_charging", "result": STATE_ON, ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY_CHARGING, - ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", }, ], DEVICE_TRACKER_DOMAIN: [], @@ -127,7 +122,6 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777999_battery_autonomy", "result": "141", ATTR_ICON: "mdi:ev-station", - ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, @@ -136,7 +130,6 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777999_battery_available_energy", "result": "31", ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, }, @@ -145,16 +138,21 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777999_battery_level", "result": "60", ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY, - ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, + { + "entity_id": "sensor.battery_last_activity", + "unique_id": "vf1aaaaa555777999_battery_last_activity", + "result": "2020-01-12T21:40:16+00:00", + "default_disabled": True, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + }, { "entity_id": "sensor.battery_temperature", "unique_id": "vf1aaaaa555777999_battery_temperature", "result": "20", ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, @@ -164,14 +162,12 @@ MOCK_VEHICLES = { "result": "charge_in_progress", ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_STATE, ATTR_ICON: "mdi:flash", - ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", }, { "entity_id": "sensor.charging_power", "unique_id": "vf1aaaaa555777999_charging_power", "result": "0.027", ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, - ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT, }, @@ -180,7 +176,6 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777999_charging_remaining_time", "result": "145", ATTR_ICON: "mdi:timer", - ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, }, @@ -206,7 +201,6 @@ MOCK_VEHICLES = { "result": "plugged", ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG_STATE, ATTR_ICON: "mdi:power-plug", - ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", }, ], }, @@ -237,14 +231,12 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777999_plugged_in", "result": STATE_OFF, ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG, - ATTR_LAST_UPDATE: "2020-11-17T08:06:48+00:00", }, { "entity_id": "binary_sensor.charging", "unique_id": "vf1aaaaa555777999_charging", "result": STATE_OFF, ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY_CHARGING, - ATTR_LAST_UPDATE: "2020-11-17T08:06:48+00:00", }, ], DEVICE_TRACKER_DOMAIN: [ @@ -253,7 +245,6 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777999_location", "result": STATE_NOT_HOME, ATTR_ICON: "mdi:car", - ATTR_LAST_UPDATE: "2020-02-18T16:58:38+00:00", } ], SELECT_DOMAIN: [ @@ -272,7 +263,6 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777999_battery_autonomy", "result": "128", ATTR_ICON: "mdi:ev-station", - ATTR_LAST_UPDATE: "2020-11-17T08:06:48+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, @@ -281,7 +271,6 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777999_battery_available_energy", "result": "0", ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_LAST_UPDATE: "2020-11-17T08:06:48+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, }, @@ -290,16 +279,21 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777999_battery_level", "result": "50", ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY, - ATTR_LAST_UPDATE: "2020-11-17T08:06:48+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, + { + "entity_id": "sensor.battery_last_activity", + "unique_id": "vf1aaaaa555777999_battery_last_activity", + "result": "2020-11-17T08:06:48+00:00", + "default_disabled": True, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + }, { "entity_id": "sensor.battery_temperature", "unique_id": "vf1aaaaa555777999_battery_temperature", "result": STATE_UNKNOWN, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_LAST_UPDATE: "2020-11-17T08:06:48+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, @@ -309,14 +303,12 @@ MOCK_VEHICLES = { "result": "charge_error", ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_STATE, ATTR_ICON: "mdi:flash-off", - ATTR_LAST_UPDATE: "2020-11-17T08:06:48+00:00", }, { "entity_id": "sensor.charging_power", "unique_id": "vf1aaaaa555777999_charging_power", "result": STATE_UNKNOWN, ATTR_DEVICE_CLASS: DEVICE_CLASS_CURRENT, - ATTR_LAST_UPDATE: "2020-11-17T08:06:48+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, }, @@ -325,7 +317,6 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777999_charging_remaining_time", "result": STATE_UNKNOWN, ATTR_ICON: "mdi:timer", - ATTR_LAST_UPDATE: "2020-11-17T08:06:48+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, }, @@ -343,7 +334,13 @@ MOCK_VEHICLES = { "result": "unplugged", ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG_STATE, ATTR_ICON: "mdi:power-plug-off", - ATTR_LAST_UPDATE: "2020-11-17T08:06:48+00:00", + }, + { + "entity_id": "sensor.location_last_activity", + "unique_id": "vf1aaaaa555777999_location_last_activity", + "result": "2020-02-18T16:58:38+00:00", + "default_disabled": True, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, }, ], }, @@ -374,14 +371,12 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777123_plugged_in", "result": STATE_ON, ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG, - ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", }, { "entity_id": "binary_sensor.charging", "unique_id": "vf1aaaaa555777123_charging", "result": STATE_ON, ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY_CHARGING, - ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", }, ], DEVICE_TRACKER_DOMAIN: [ @@ -390,7 +385,6 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777123_location", "result": STATE_NOT_HOME, ATTR_ICON: "mdi:car", - ATTR_LAST_UPDATE: "2020-02-18T16:58:38+00:00", } ], SELECT_DOMAIN: [ @@ -409,7 +403,6 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777123_battery_autonomy", "result": "141", ATTR_ICON: "mdi:ev-station", - ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, @@ -418,7 +411,6 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777123_battery_available_energy", "result": "31", ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, }, @@ -427,16 +419,21 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777123_battery_level", "result": "60", ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY, - ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, + { + "entity_id": "sensor.battery_last_activity", + "unique_id": "vf1aaaaa555777123_battery_last_activity", + "result": "2020-01-12T21:40:16+00:00", + "default_disabled": True, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + }, { "entity_id": "sensor.battery_temperature", "unique_id": "vf1aaaaa555777123_battery_temperature", "result": "20", ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, @@ -446,14 +443,12 @@ MOCK_VEHICLES = { "result": "charge_in_progress", ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_STATE, ATTR_ICON: "mdi:flash", - ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", }, { "entity_id": "sensor.charging_power", "unique_id": "vf1aaaaa555777123_charging_power", "result": "27.0", ATTR_DEVICE_CLASS: DEVICE_CLASS_CURRENT, - ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, }, @@ -462,7 +457,6 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777123_charging_remaining_time", "result": "145", ATTR_ICON: "mdi:timer", - ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, }, @@ -496,7 +490,13 @@ MOCK_VEHICLES = { "result": "plugged", ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG_STATE, ATTR_ICON: "mdi:power-plug", - ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", + }, + { + "entity_id": "sensor.location_last_activity", + "unique_id": "vf1aaaaa555777123_location_last_activity", + "result": "2020-02-18T16:58:38+00:00", + "default_disabled": True, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, }, ], }, @@ -526,7 +526,6 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777123_location", "result": STATE_NOT_HOME, ATTR_ICON: "mdi:car", - ATTR_LAST_UPDATE: "2020-02-18T16:58:38+00:00", } ], SELECT_DOMAIN: [], @@ -555,6 +554,13 @@ MOCK_VEHICLES = { ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, + { + "entity_id": "sensor.location_last_activity", + "unique_id": "vf1aaaaa555777123_location_last_activity", + "result": "2020-02-18T16:58:38+00:00", + "default_disabled": True, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + }, ], }, } diff --git a/tests/components/renault/test_binary_sensor.py b/tests/components/renault/test_binary_sensor.py index d0fb6e544ad..10b9f768501 100644 --- a/tests/components/renault/test_binary_sensor.py +++ b/tests/components/renault/test_binary_sensor.py @@ -5,7 +5,6 @@ import pytest from renault_api.kamereon import exceptions from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.renault.renault_entities import ATTR_LAST_UPDATE from homeassistant.const import ATTR_ICON, STATE_OFF, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant @@ -75,7 +74,6 @@ async def test_binary_sensor_empty(hass: HomeAssistant, vehicle_type: str): assert state.attributes.get(attr) == expected_entity.get(attr) # Check dynamic attributes: assert state.attributes.get(ATTR_ICON) == get_no_data_icon(expected_entity) - assert ATTR_LAST_UPDATE not in state.attributes @pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) @@ -112,7 +110,6 @@ async def test_binary_sensor_errors(hass: HomeAssistant, vehicle_type: str): assert state.attributes.get(attr) == expected_entity.get(attr) # Check dynamic attributes: assert state.attributes.get(ATTR_ICON) == get_no_data_icon(expected_entity) - assert ATTR_LAST_UPDATE not in state.attributes async def test_binary_sensor_access_denied(hass): diff --git a/tests/components/renault/test_device_tracker.py b/tests/components/renault/test_device_tracker.py index 2232af18a22..54f25e4e8cd 100644 --- a/tests/components/renault/test_device_tracker.py +++ b/tests/components/renault/test_device_tracker.py @@ -5,7 +5,6 @@ import pytest from renault_api.kamereon import exceptions from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN -from homeassistant.components.renault.renault_entities import ATTR_LAST_UPDATE from homeassistant.const import ATTR_ICON, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant @@ -75,7 +74,6 @@ async def test_device_tracker_empty(hass: HomeAssistant, vehicle_type: str): assert state.attributes.get(attr) == expected_entity.get(attr) # Check dynamic attributes: assert state.attributes.get(ATTR_ICON) == get_no_data_icon(expected_entity) - assert ATTR_LAST_UPDATE not in state.attributes @pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) @@ -112,7 +110,6 @@ async def test_device_tracker_errors(hass: HomeAssistant, vehicle_type: str): assert state.attributes.get(attr) == expected_entity.get(attr) # Check dynamic attributes: assert state.attributes.get(ATTR_ICON) == get_no_data_icon(expected_entity) - assert ATTR_LAST_UPDATE not in state.attributes async def test_device_tracker_access_denied(hass: HomeAssistant): diff --git a/tests/components/renault/test_select.py b/tests/components/renault/test_select.py index 0bc7c4ddc68..5090658464d 100644 --- a/tests/components/renault/test_select.py +++ b/tests/components/renault/test_select.py @@ -4,7 +4,6 @@ from unittest.mock import patch import pytest from renault_api.kamereon import exceptions, schemas -from homeassistant.components.renault.renault_entities import ATTR_LAST_UPDATE from homeassistant.components.select import DOMAIN as SELECT_DOMAIN from homeassistant.components.select.const import ATTR_OPTION, SERVICE_SELECT_OPTION from homeassistant.const import ( @@ -81,7 +80,6 @@ async def test_select_empty(hass: HomeAssistant, vehicle_type: str): assert state.attributes.get(attr) == expected_entity.get(attr) # Check dynamic attributes: assert state.attributes.get(ATTR_ICON) == get_no_data_icon(expected_entity) - assert ATTR_LAST_UPDATE not in state.attributes @pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) @@ -118,7 +116,6 @@ async def test_select_errors(hass: HomeAssistant, vehicle_type: str): assert state.attributes.get(attr) == expected_entity.get(attr) # Check dynamic attributes: assert state.attributes.get(ATTR_ICON) == get_no_data_icon(expected_entity) - assert ATTR_LAST_UPDATE not in state.attributes async def test_select_access_denied(hass: HomeAssistant): diff --git a/tests/components/renault/test_sensor.py b/tests/components/renault/test_sensor.py index e3c758f088a..79d53d896d3 100644 --- a/tests/components/renault/test_sensor.py +++ b/tests/components/renault/test_sensor.py @@ -4,7 +4,6 @@ from unittest.mock import patch import pytest from renault_api.kamereon import exceptions -from homeassistant.components.renault.renault_entities import ATTR_LAST_UPDATE from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ATTR_ICON, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant @@ -12,6 +11,9 @@ from homeassistant.core import HomeAssistant from . import ( check_device_registry, get_no_data_icon, + patch_fixtures, + patch_fixtures_with_no_data, + patch_fixtures_with_side_effect, setup_renault_integration_vehicle, setup_renault_integration_vehicle_with_no_data, setup_renault_integration_vehicle_with_side_effect, @@ -29,7 +31,7 @@ async def test_sensors(hass: HomeAssistant, vehicle_type: str): device_registry = mock_device_registry(hass) with patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): - await setup_renault_integration_vehicle(hass, vehicle_type) + config_entry = await setup_renault_integration_vehicle(hass, vehicle_type) await hass.async_block_till_done() mock_vehicle = MOCK_VEHICLES[vehicle_type] @@ -37,6 +39,21 @@ async def test_sensors(hass: HomeAssistant, vehicle_type: str): expected_entities = mock_vehicle[SENSOR_DOMAIN] assert len(entity_registry.entities) == len(expected_entities) + + # Ensure all entities are enabled + for expected_entity in expected_entities: + if expected_entity.get("default_disabled"): + entity_id = expected_entity["entity_id"] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry.disabled + assert registry_entry.disabled_by == "integration" + entity_registry.async_update_entity(entity_id, **{"disabled_by": None}) + with patch( + "homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN] + ), patch_fixtures(hass, config_entry, vehicle_type): + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + for expected_entity in expected_entities: entity_id = expected_entity["entity_id"] registry_entry = entity_registry.entities.get(entity_id) @@ -56,7 +73,9 @@ async def test_sensor_empty(hass: HomeAssistant, vehicle_type: str): device_registry = mock_device_registry(hass) with patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): - await setup_renault_integration_vehicle_with_no_data(hass, vehicle_type) + config_entry = await setup_renault_integration_vehicle_with_no_data( + hass, vehicle_type + ) await hass.async_block_till_done() mock_vehicle = MOCK_VEHICLES[vehicle_type] @@ -64,6 +83,21 @@ async def test_sensor_empty(hass: HomeAssistant, vehicle_type: str): expected_entities = mock_vehicle[SENSOR_DOMAIN] assert len(entity_registry.entities) == len(expected_entities) + + # Ensure all entities are enabled + for expected_entity in expected_entities: + if expected_entity.get("default_disabled"): + entity_id = expected_entity["entity_id"] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry.disabled + assert registry_entry.disabled_by == "integration" + entity_registry.async_update_entity(entity_id, **{"disabled_by": None}) + with patch( + "homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN] + ), patch_fixtures_with_no_data(hass, config_entry, vehicle_type): + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + for expected_entity in expected_entities: entity_id = expected_entity["entity_id"] registry_entry = entity_registry.entities.get(entity_id) @@ -75,7 +109,6 @@ async def test_sensor_empty(hass: HomeAssistant, vehicle_type: str): assert state.attributes.get(attr) == expected_entity.get(attr) # Check dynamic attributes: assert state.attributes.get(ATTR_ICON) == get_no_data_icon(expected_entity) - assert ATTR_LAST_UPDATE not in state.attributes @pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) @@ -91,7 +124,7 @@ async def test_sensor_errors(hass: HomeAssistant, vehicle_type: str): ) with patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): - await setup_renault_integration_vehicle_with_side_effect( + config_entry = await setup_renault_integration_vehicle_with_side_effect( hass, vehicle_type, invalid_upstream_exception ) await hass.async_block_till_done() @@ -101,6 +134,23 @@ async def test_sensor_errors(hass: HomeAssistant, vehicle_type: str): expected_entities = mock_vehicle[SENSOR_DOMAIN] assert len(entity_registry.entities) == len(expected_entities) + + # Ensure all entities are enabled + for expected_entity in expected_entities: + if expected_entity.get("default_disabled"): + entity_id = expected_entity["entity_id"] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry.disabled + assert registry_entry.disabled_by == "integration" + entity_registry.async_update_entity(entity_id, **{"disabled_by": None}) + with patch( + "homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN] + ), patch_fixtures_with_side_effect( + hass, config_entry, vehicle_type, invalid_upstream_exception + ): + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + for expected_entity in expected_entities: entity_id = expected_entity["entity_id"] registry_entry = entity_registry.entities.get(entity_id) @@ -112,7 +162,6 @@ async def test_sensor_errors(hass: HomeAssistant, vehicle_type: str): assert state.attributes.get(attr) == expected_entity.get(attr) # Check dynamic attributes: assert state.attributes.get(ATTR_ICON) == get_no_data_icon(expected_entity) - assert ATTR_LAST_UPDATE not in state.attributes async def test_sensor_access_denied(hass: HomeAssistant): From 2b379433557b0c7c8a24442f09fff712e2f9546f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 15 Oct 2021 17:46:43 +0200 Subject: [PATCH 0398/1038] Don't add indirectly referenced config entities to service calls (#57671) --- .../components/homeassistant/__init__.py | 2 +- homeassistant/components/homekit/__init__.py | 2 +- homeassistant/helpers/service.py | 25 +++++++++++-------- tests/helpers/test_service.py | 24 ++++++++++++++++++ 4 files changed, 40 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index 2314d2b0c1b..8c31859b8e0 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -61,7 +61,7 @@ async def async_setup(hass: ha.HomeAssistant, config: ConfigType) -> bool: # no async def async_handle_turn_service(service): """Handle calls to homeassistant.turn_on/off.""" - referenced = await async_extract_referenced_entity_ids(hass, service) + referenced = async_extract_referenced_entity_ids(hass, service) all_referenced = referenced.referenced | referenced.indirectly_referenced # Generic turn on/off method requires entity id diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 8fc68ca641c..9ebc29a683f 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -396,7 +396,7 @@ def _async_register_events_and_services(hass: HomeAssistant): async def async_handle_homekit_unpair(service): """Handle unpair HomeKit service call.""" - referenced = await async_extract_referenced_entity_ids(hass, service) + referenced = async_extract_referenced_entity_ids(hass, service) dev_reg = device_registry.async_get(hass) for device_id in referenced.referenced_devices: dev_reg_ent = dev_reg.async_get(device_id) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 002d6447441..800d494fdbb 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -20,6 +20,8 @@ from homeassistant.const import ( CONF_SERVICE_DATA, CONF_SERVICE_TEMPLATE, CONF_TARGET, + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, ENTITY_MATCH_ALL, ENTITY_MATCH_NONE, ) @@ -279,9 +281,7 @@ async def async_extract_entities( if data_ent_id == ENTITY_MATCH_ALL: return [entity for entity in entities if entity.available] - referenced = await async_extract_referenced_entity_ids( - hass, service_call, expand_group - ) + referenced = async_extract_referenced_entity_ids(hass, service_call, expand_group) combined = referenced.referenced | referenced.indirectly_referenced found = [] @@ -310,9 +310,7 @@ async def async_extract_entity_ids( Will convert group entity ids to the entity ids it represents. """ - referenced = await async_extract_referenced_entity_ids( - hass, service_call, expand_group - ) + referenced = async_extract_referenced_entity_ids(hass, service_call, expand_group) return referenced.referenced | referenced.indirectly_referenced @@ -322,7 +320,7 @@ def _has_match(ids: str | list | None) -> bool: @bind_hass -async def async_extract_referenced_entity_ids( +def async_extract_referenced_entity_ids( hass: HomeAssistant, service_call: ServiceCall, expand_group: bool = True ) -> SelectedEntities: """Extract referenced entity IDs from a service call.""" @@ -363,6 +361,13 @@ async def async_extract_referenced_entity_ids( return selected for ent_entry in ent_reg.entities.values(): + # Do not add config or diagnostic entities referenced by areas or devices + if ent_entry.entity_category in ( + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, + ): + continue + if ( # when area matches the target area ent_entry.area_id in selector.area_ids @@ -384,9 +389,7 @@ async def async_extract_config_entry_ids( hass: HomeAssistant, service_call: ServiceCall, expand_group: bool = True ) -> set: """Extract referenced config entry ids from a service call.""" - referenced = await async_extract_referenced_entity_ids( - hass, service_call, expand_group - ) + referenced = async_extract_referenced_entity_ids(hass, service_call, expand_group) ent_reg = entity_registry.async_get(hass) dev_reg = device_registry.async_get(hass) config_entry_ids: set[str] = set() @@ -545,7 +548,7 @@ async def entity_service_call( all_referenced: set[str] | None = None else: # A set of entities we're trying to target. - referenced = await async_extract_referenced_entity_ids(hass, call, True) + referenced = async_extract_referenced_entity_ids(hass, call, True) all_referenced = referenced.referenced | referenced.indirectly_referenced # If the service function is a string, we'll pass it the service call data diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 7e18547145b..16f7ad4825a 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -113,12 +113,26 @@ def area_mock(hass): platform="test", area_id="own-area", ) + config_entity_in_own_area = ent_reg.RegistryEntry( + entity_id="light.config_in_own_area", + unique_id="config-in-own-area-id", + platform="test", + area_id="own-area", + entity_category="config", + ) entity_in_area = ent_reg.RegistryEntry( entity_id="light.in_area", unique_id="in-area-id", platform="test", device_id=device_in_area.id, ) + config_entity_in_area = ent_reg.RegistryEntry( + entity_id="light.config_in_area", + unique_id="config-in-area-id", + platform="test", + device_id=device_in_area.id, + entity_category="config", + ) entity_in_other_area = ent_reg.RegistryEntry( entity_id="light.in_other_area", unique_id="in-area-a-id", @@ -139,6 +153,13 @@ def area_mock(hass): platform="test", device_id=device_no_area.id, ) + config_entity_no_area = ent_reg.RegistryEntry( + entity_id="light.config_no_area", + unique_id="config-no-area-id", + platform="test", + device_id=device_no_area.id, + entity_category="config", + ) entity_diff_area = ent_reg.RegistryEntry( entity_id="light.diff_area", unique_id="diff-area-id", @@ -163,10 +184,13 @@ def area_mock(hass): hass, { entity_in_own_area.entity_id: entity_in_own_area, + config_entity_in_own_area.entity_id: config_entity_in_own_area, entity_in_area.entity_id: entity_in_area, + config_entity_in_area.entity_id: config_entity_in_area, entity_in_other_area.entity_id: entity_in_other_area, entity_assigned_to_area.entity_id: entity_assigned_to_area, entity_no_area.entity_id: entity_no_area, + config_entity_no_area.entity_id: config_entity_no_area, entity_diff_area.entity_id: entity_diff_area, entity_in_area_a.entity_id: entity_in_area_a, entity_in_area_b.entity_id: entity_in_area_b, From 7c1ba8be3db30f7a68c27bbe5ead6fb19792a183 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 15 Oct 2021 18:09:24 +0200 Subject: [PATCH 0399/1038] Don't expose config or diagnostic entities to Amazon Alexa by default (#57770) --- .../components/alexa/smart_home_http.py | 22 +++++++++++++++++-- tests/helpers/test_entityfilter.py | 21 ++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alexa/smart_home_http.py b/homeassistant/components/alexa/smart_home_http.py index 41738c824fb..6d6b9f54533 100644 --- a/homeassistant/components/alexa/smart_home_http.py +++ b/homeassistant/components/alexa/smart_home_http.py @@ -3,7 +3,13 @@ import logging from homeassistant import core from homeassistant.components.http.view import HomeAssistantView -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, +) +from homeassistant.helpers import entity_registry as er from .auth import Auth from .config import AbstractConfig @@ -60,7 +66,19 @@ class AlexaConfig(AbstractConfig): def should_expose(self, entity_id): """If an entity should be exposed.""" - return self._config[CONF_FILTER](entity_id) + if not self._config[CONF_FILTER].empty_filter: + return self._config[CONF_FILTER](entity_id) + + entity_registry = er.async_get(self.hass) + registry_entry = entity_registry.async_get(entity_id) + if registry_entry: + auxiliary_entity = registry_entry.entity_category in ( + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, + ) + else: + auxiliary_entity = False + return not auxiliary_entity @core.callback def async_invalidate_access_token(self): diff --git a/tests/helpers/test_entityfilter.py b/tests/helpers/test_entityfilter.py index 5bc37216f81..5d28295e3a0 100644 --- a/tests/helpers/test_entityfilter.py +++ b/tests/helpers/test_entityfilter.py @@ -205,6 +205,24 @@ def test_no_domain_case4c(): assert testfilter("sun.sun") is False +def test_filter_schema_empty(): + """Test filter schema.""" + conf = {} + filt = FILTER_SCHEMA(conf) + conf.update( + { + "include_domains": [], + "include_entities": [], + "exclude_domains": [], + "exclude_entities": [], + "include_entity_globs": [], + "exclude_entity_globs": [], + } + ) + assert filt.config == conf + assert filt.empty_filter + + def test_filter_schema(): """Test filter schema.""" conf = { @@ -216,6 +234,7 @@ def test_filter_schema(): filt = FILTER_SCHEMA(conf) conf.update({"include_entity_globs": [], "exclude_entity_globs": []}) assert filt.config == conf + assert not filt.empty_filter def test_filter_schema_with_globs(): @@ -230,6 +249,7 @@ def test_filter_schema_with_globs(): } filt = FILTER_SCHEMA(conf) assert filt.config == conf + assert not filt.empty_filter def test_filter_schema_include_exclude(): @@ -248,3 +268,4 @@ def test_filter_schema_include_exclude(): } filt = INCLUDE_EXCLUDE_FILTER_SCHEMA(conf) assert filt.config == conf + assert not filt.empty_filter From 8b33aa3702e51b0d3b13c17669b45899c367402a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 15 Oct 2021 18:12:34 +0200 Subject: [PATCH 0400/1038] Don't expose config or diagnostic entities to Google Assistant (#57669) --- .../components/google_assistant/http.py | 21 ++++++++++++++-- .../google_assistant/test_google_assistant.py | 24 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 61768ff2be8..d5489aad05a 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -12,9 +12,12 @@ import jwt from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( CLOUD_NEVER_EXPOSED_ENTITIES, + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, HTTP_INTERNAL_SERVER_ERROR, HTTP_UNAUTHORIZED, ) +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import dt as dt_util @@ -112,16 +115,30 @@ class GoogleConfig(AbstractConfig): if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: return False + entity_registry = er.async_get(self.hass) + registry_entry = entity_registry.async_get(state.entity_id) + if registry_entry: + auxiliary_entity = registry_entry.entity_category in ( + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, + ) + else: + auxiliary_entity = False + explicit_expose = self.entity_config.get(state.entity_id, {}).get(CONF_EXPOSE) domain_exposed_by_default = ( expose_by_default and state.domain in exposed_domains ) - # Expose an entity if the entity's domain is exposed by default and + # Expose an entity by default if the entity's domain is exposed by default + # and the entity is not a config or diagnostic entity + entity_exposed_by_default = domain_exposed_by_default and not auxiliary_entity + + # Expose an entity if the entity's is exposed by default and # the configuration doesn't explicitly exclude it from being # exposed, or if the entity is explicitly exposed - is_default_exposed = domain_exposed_by_default and explicit_expose is not False + is_default_exposed = entity_exposed_by_default and explicit_expose is not False return is_default_exposed or explicit_expose diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 397b4c309a7..c17a05ddb3d 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -22,6 +22,8 @@ from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from . import DEMO_DEVICES +from tests.common import mock_registry + API_PASSWORD = "test1234" PROJECT_ID = "hasstest-1234" @@ -123,6 +125,28 @@ def hass_fixture(loop, hass): async def test_sync_request(hass_fixture, assistant_client, auth_header): """Test a sync request.""" + + entity_registry = mock_registry(hass_fixture) + + entity_entry1 = entity_registry.async_get_or_create( + "switch", + "test", + "switch_config_id", + suggested_object_id="config_switch", + entity_category="config", + ) + entity_entry2 = entity_registry.async_get_or_create( + "switch", + "test", + "switch_diagnostic_id", + suggested_object_id="diagnostic_switch", + entity_category="diagnostic", + ) + + # These should not show up in the sync request + hass_fixture.states.async_set(entity_entry1.entity_id, "on") + hass_fixture.states.async_set(entity_entry2.entity_id, "something_else") + reqid = "5711642932632160983" data = {"requestId": reqid, "inputs": [{"intent": "action.devices.SYNC"}]} result = await assistant_client.post( From 0f2b5ea28e8f2daa0dcbb26ae21add6702c34168 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 15 Oct 2021 18:35:32 +0200 Subject: [PATCH 0401/1038] Don't expose config or diagnostic entities to cloud (#57771) --- .../components/cloud/alexa_config.py | 27 ++++++++++++---- .../components/cloud/google_config.py | 27 ++++++++++++---- tests/components/cloud/test_alexa_config.py | 28 +++++++++++++++- tests/components/cloud/test_google_config.py | 32 +++++++++++++++++-- 4 files changed, 99 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 43ef0ee62da..c14b4d7a4e7 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -15,9 +15,14 @@ from homeassistant.components.alexa import ( errors as alexa_errors, state_report as alexa_state_report, ) -from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, HTTP_BAD_REQUEST +from homeassistant.const import ( + CLOUD_NEVER_EXPOSED_ENTITIES, + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, + HTTP_BAD_REQUEST, +) from homeassistant.core import HomeAssistant, callback, split_entity_id -from homeassistant.helpers import entity_registry, start +from homeassistant.helpers import entity_registry as er, start from homeassistant.helpers.event import async_call_later from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -110,7 +115,7 @@ class AlexaConfig(alexa_config.AbstractConfig): self._prefs.async_listen_updates(self._async_prefs_updated) self.hass.bus.async_listen( - entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, + er.EVENT_ENTITY_REGISTRY_UPDATED, self._handle_entity_registry_updated, ) @@ -128,13 +133,23 @@ class AlexaConfig(alexa_config.AbstractConfig): if entity_expose is not None: return entity_expose + entity_registry = er.async_get(self.hass) + registry_entry = entity_registry.async_get(entity_id) + if registry_entry: + auxiliary_entity = registry_entry.entity_category in ( + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, + ) + else: + auxiliary_entity = False + default_expose = self._prefs.alexa_default_expose # Backwards compat if default_expose is None: - return True + return not auxiliary_entity - return split_entity_id(entity_id)[0] in default_expose + return not auxiliary_entity and split_entity_id(entity_id)[0] in default_expose @callback def async_invalidate_access_token(self): @@ -340,7 +355,7 @@ class AlexaConfig(alexa_config.AbstractConfig): elif action == "remove": to_remove.append(entity_id) elif action == "update" and bool( - set(event.data["changes"]) & entity_registry.ENTITY_DESCRIBING_ATTRIBUTES + set(event.data["changes"]) & er.ENTITY_DESCRIBING_ATTRIBUTES ): to_update.append(entity_id) if "old_entity_id" in event.data: diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index f1783771f2f..bfb6510fcda 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -7,9 +7,14 @@ from hass_nabucasa.google_report_state import ErrorResponse from homeassistant.components.google_assistant.const import DOMAIN as GOOGLE_DOMAIN from homeassistant.components.google_assistant.helpers import AbstractConfig -from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, HTTP_OK +from homeassistant.const import ( + CLOUD_NEVER_EXPOSED_ENTITIES, + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, + HTTP_OK, +) from homeassistant.core import CoreState, split_entity_id -from homeassistant.helpers import entity_registry, start +from homeassistant.helpers import entity_registry as er, start from homeassistant.setup import async_setup_component from .const import ( @@ -104,7 +109,7 @@ class CloudGoogleConfig(AbstractConfig): self._prefs.async_listen_updates(self._async_prefs_updated) self.hass.bus.async_listen( - entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, + er.EVENT_ENTITY_REGISTRY_UPDATED, self._handle_entity_registry_updated, ) @@ -126,13 +131,23 @@ class CloudGoogleConfig(AbstractConfig): if entity_expose is not None: return entity_expose + entity_registry = er.async_get(self.hass) + registry_entry = entity_registry.async_get(entity_id) + if registry_entry: + auxiliary_entity = registry_entry.entity_category in ( + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, + ) + else: + auxiliary_entity = False + default_expose = self._prefs.google_default_expose # Backwards compat if default_expose is None: - return True + return not auxiliary_entity - return split_entity_id(entity_id)[0] in default_expose + return not auxiliary_entity and split_entity_id(entity_id)[0] in default_expose @property def agent_user_id(self): @@ -215,7 +230,7 @@ class CloudGoogleConfig(AbstractConfig): # Only consider entity registry updates if info relevant for Google has changed if event.data["action"] == "update" and not bool( - set(event.data["changes"]) & entity_registry.ENTITY_DESCRIBING_ATTRIBUTES + set(event.data["changes"]) & er.ENTITY_DESCRIBING_ATTRIBUTES ): return diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index 60ef992dafb..9b0075db09d 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -8,7 +8,7 @@ from homeassistant.components.cloud import ALEXA_SCHEMA, alexa_config from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED from homeassistant.util.dt import utcnow -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, mock_registry @pytest.fixture() @@ -19,6 +19,23 @@ def cloud_stub(): async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs, cloud_stub): """Test Alexa config should expose using prefs.""" + entity_registry = mock_registry(hass) + + entity_entry1 = entity_registry.async_get_or_create( + "switch", + "test", + "switch_config_id", + suggested_object_id="config_switch", + entity_category="config", + ) + entity_entry2 = entity_registry.async_get_or_create( + "switch", + "test", + "switch_diagnostic_id", + suggested_object_id="diagnostic_switch", + entity_category="diagnostic", + ) + entity_conf = {"should_expose": False} await cloud_prefs.async_update( alexa_entity_configs={"light.kitchen": entity_conf}, @@ -31,11 +48,20 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs, cloud_stub): await conf.async_initialize() assert not conf.should_expose("light.kitchen") + assert not conf.should_expose(entity_entry1.entity_id) + assert not conf.should_expose(entity_entry2.entity_id) + entity_conf["should_expose"] = True assert conf.should_expose("light.kitchen") + # config and diagnostic entities should not be exposed + assert not conf.should_expose(entity_entry1.entity_id) + assert not conf.should_expose(entity_entry2.entity_id) entity_conf["should_expose"] = None assert conf.should_expose("light.kitchen") + # config and diagnostic entities should not be exposed + assert not conf.should_expose(entity_entry1.entity_id) + assert not conf.should_expose(entity_entry2.entity_id) assert "alexa" not in hass.config.components await cloud_prefs.async_update( diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index a80ccaccd6c..61d93b5bc85 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -11,7 +11,7 @@ from homeassistant.core import CoreState, State from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED from homeassistant.util.dt import utcnow -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, mock_registry @pytest.fixture @@ -215,8 +215,25 @@ async def test_sync_google_on_home_assistant_start(hass, mock_cloud_login, cloud assert len(mock_sync.mock_calls) == 1 -async def test_google_config_expose_entity_prefs(mock_conf, cloud_prefs): +async def test_google_config_expose_entity_prefs(hass, mock_conf, cloud_prefs): """Test Google config should expose using prefs.""" + entity_registry = mock_registry(hass) + + entity_entry1 = entity_registry.async_get_or_create( + "switch", + "test", + "switch_config_id", + suggested_object_id="config_switch", + entity_category="config", + ) + entity_entry2 = entity_registry.async_get_or_create( + "switch", + "test", + "switch_diagnostic_id", + suggested_object_id="diagnostic_switch", + entity_category="diagnostic", + ) + entity_conf = {"should_expose": False} await cloud_prefs.async_update( google_entity_configs={"light.kitchen": entity_conf}, @@ -224,13 +241,24 @@ async def test_google_config_expose_entity_prefs(mock_conf, cloud_prefs): ) state = State("light.kitchen", "on") + state_config = State(entity_entry1.entity_id, "on") + state_diagnostic = State(entity_entry2.entity_id, "on") assert not mock_conf.should_expose(state) + assert not mock_conf.should_expose(state_config) + assert not mock_conf.should_expose(state_diagnostic) + entity_conf["should_expose"] = True assert mock_conf.should_expose(state) + # config and diagnostic entities should not be exposed + assert not mock_conf.should_expose(state_config) + assert not mock_conf.should_expose(state_diagnostic) entity_conf["should_expose"] = None assert mock_conf.should_expose(state) + # config and diagnostic entities should not be exposed + assert not mock_conf.should_expose(state_config) + assert not mock_conf.should_expose(state_diagnostic) await cloud_prefs.async_update( google_default_expose=["sensor"], From 5aba8a7c8116d7e9a92a3e954d487455300d51e7 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 15 Oct 2021 18:36:26 +0200 Subject: [PATCH 0402/1038] Fix modem_callerid test warning (#57760) --- tests/components/modem_callerid/__init__.py | 5 +++-- tests/components/modem_callerid/test_init.py | 8 +++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/components/modem_callerid/__init__.py b/tests/components/modem_callerid/__init__.py index 2ff0e87c9cd..9564f2b662f 100644 --- a/tests/components/modem_callerid/__init__.py +++ b/tests/components/modem_callerid/__init__.py @@ -11,15 +11,16 @@ CONF_DATA = {CONF_DEVICE: DEFAULT_PORT} IMPORT_DATA = {"sensor": {"platform": "modem_callerid"}} -def _patch_init_modem(mocked_modem): +def _patch_init_modem(): return patch( "homeassistant.components.modem_callerid.PhoneModem", - return_value=mocked_modem, + autospec=True, ) def _patch_config_flow_modem(mocked_modem): return patch( "homeassistant.components.modem_callerid.config_flow.PhoneModem", + autospec=True, return_value=mocked_modem, ) diff --git a/tests/components/modem_callerid/test_init.py b/tests/components/modem_callerid/test_init.py index b288fb7dc9f..f467ca5af51 100644 --- a/tests/components/modem_callerid/test_init.py +++ b/tests/components/modem_callerid/test_init.py @@ -1,5 +1,5 @@ """Test Modem Caller ID integration.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import patch from phone_modem import exceptions @@ -19,8 +19,7 @@ async def test_setup_config(hass: HomeAssistant): data=CONF_DATA, ) entry.add_to_hass(hass) - mocked_modem = AsyncMock() - with _patch_init_modem(mocked_modem): + with _patch_init_modem(): await hass.config_entries.async_setup(entry.entry_id) assert entry.state == ConfigEntryState.LOADED @@ -50,8 +49,7 @@ async def test_unload_config_entry(hass: HomeAssistant): data=CONF_DATA, ) entry.add_to_hass(hass) - mocked_modem = AsyncMock() - with _patch_init_modem(mocked_modem): + with _patch_init_modem(): await hass.config_entries.async_setup(entry.entry_id) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.state is ConfigEntryState.LOADED From 427f2a085b904d796833cfd812990013d07de0cb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 15 Oct 2021 06:37:13 -1000 Subject: [PATCH 0403/1038] Reconnect and retry yeelight commands after previous wifi drop out (#57741) --- homeassistant/components/yeelight/__init__.py | 17 ++++--- homeassistant/components/yeelight/light.py | 48 +++++++++++------- .../components/yeelight/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/yeelight/test_init.py | 50 +++++++++++++++++++ 6 files changed, 92 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 64fa7b01f28..a1463daed12 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -6,7 +6,6 @@ import contextlib from datetime import timedelta from ipaddress import IPv4Address, IPv6Address import logging -import socket from urllib.parse import urlparse from async_upnp_client.search import SsdpSearchListener @@ -163,9 +162,6 @@ UPDATE_REQUEST_PROPERTIES = [ "active_mode", ] -BULB_NETWORK_EXCEPTIONS = (socket.error,) -BULB_EXCEPTIONS = (BulbException, asyncio.TimeoutError, *BULB_NETWORK_EXCEPTIONS) - PLATFORMS = ["binary_sensor", "light"] @@ -270,7 +266,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: device = await _async_get_device(hass, entry.data[CONF_HOST], entry) await _async_initialize(hass, entry, device) - except BULB_EXCEPTIONS as ex: + except (asyncio.TimeoutError, OSError, BulbException) as ex: raise ConfigEntryNotReady from ex hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -594,13 +590,20 @@ class YeelightDevice: self._available = True if not self._initialized: self._initialized = True - except BULB_NETWORK_EXCEPTIONS as ex: + except OSError as ex: if self._available: # just inform once _LOGGER.error( "Unable to update device %s, %s: %s", self._host, self.name, ex ) self._available = False - except BULB_EXCEPTIONS as ex: + except asyncio.TimeoutError as ex: + _LOGGER.debug( + "timed out while trying to update device %s, %s: %s", + self._host, + self.name, + ex, + ) + except BulbException as ex: _LOGGER.debug( "Unable to update device %s, %s: %s", self._host, self.name, ex ) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 67c9dc2ba07..e40fd2726b2 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -1,6 +1,7 @@ """Light platform support for yeelight.""" from __future__ import annotations +import asyncio import logging import math @@ -8,6 +9,7 @@ import voluptuous as vol import yeelight from yeelight import Bulb, Flow, RGBTransition, SleepTransition, flows from yeelight.enums import BulbType, LightType, PowerMode, SceneClass +from yeelight.main import BulbException from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -51,8 +53,6 @@ from . import ( ATTR_COUNT, ATTR_MODE_MUSIC, ATTR_TRANSITIONS, - BULB_EXCEPTIONS, - BULB_NETWORK_EXCEPTIONS, CONF_FLOW_PARAMS, CONF_MODE_MUSIC, CONF_NIGHTLIGHT_SWITCH, @@ -243,23 +243,33 @@ def _async_cmd(func): """Define a wrapper to catch exceptions from the bulb.""" async def _async_wrap(self, *args, **kwargs): - try: - _LOGGER.debug("Calling %s with %s %s", func, args, kwargs) - return await func(self, *args, **kwargs) - except BULB_NETWORK_EXCEPTIONS as ex: - # A network error happened, the bulb is likely offline now - self.device.async_mark_unavailable() - self.async_state_changed() - exc_message = str(ex) or type(ex) - raise HomeAssistantError( - f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {exc_message}" - ) from ex - except BULB_EXCEPTIONS as ex: - # The bulb likely responded but had an error - exc_message = str(ex) or type(ex) - raise HomeAssistantError( - f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {exc_message}" - ) from ex + for attempts in range(2): + try: + _LOGGER.debug("Calling %s with %s %s", func, args, kwargs) + return await func(self, *args, **kwargs) + except asyncio.TimeoutError as ex: + # The wifi likely dropped, so we want to retry once since + # python-yeelight will auto reconnect + exc_message = str(ex) or type(ex) + if attempts == 0: + continue + raise HomeAssistantError( + f"Timed out when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {exc_message}" + ) from ex + except OSError as ex: + # A network error happened, the bulb is likely offline now + self.device.async_mark_unavailable() + self.async_state_changed() + exc_message = str(ex) or type(ex) + raise HomeAssistantError( + f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {exc_message}" + ) from ex + except BulbException as ex: + # The bulb likely responded but had an error + exc_message = str(ex) or type(ex) + raise HomeAssistantError( + f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {exc_message}" + ) from ex return _async_wrap diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 632fdf426f2..4682215092b 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,7 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.7.7", "async-upnp-client==0.22.8"], + "requirements": ["yeelight==0.7.8", "async-upnp-client==0.22.8"], "codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"], "config_flow": true, "dependencies": ["network"], diff --git a/requirements_all.txt b/requirements_all.txt index 23ac2a8dd71..e855c20caac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2447,7 +2447,7 @@ yalesmartalarmclient==0.3.4 yalexs==1.1.13 # homeassistant.components.yeelight -yeelight==0.7.7 +yeelight==0.7.8 # homeassistant.components.yeelightsunflower yeelightsunflower==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 05d686cc856..64bb1109159 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1412,7 +1412,7 @@ yalesmartalarmclient==0.3.4 yalexs==1.1.13 # homeassistant.components.yeelight -yeelight==0.7.7 +yeelight==0.7.8 # homeassistant.components.youless youless-api==0.14 diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index 7ddb2845ac8..73d2543f9d4 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -1,7 +1,9 @@ """Test Yeelight.""" +import asyncio from datetime import timedelta from unittest.mock import AsyncMock, patch +import pytest from yeelight import BulbException, BulbType from yeelight.aio import KEY_CONNECTED @@ -507,3 +509,51 @@ async def test_connection_dropped_resyncs_properties(hass: HomeAssistant): ) await hass.async_block_till_done() assert len(mocked_bulb.async_get_properties.mock_calls) == 2 + + +async def test_oserror_on_first_update_results_in_unavailable(hass: HomeAssistant): + """Test that an OSError on first update results in unavailable.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=ID, + data={CONF_HOST: "127.0.0.1"}, + options={CONF_NAME: "Test name"}, + ) + config_entry.add_to_hass(hass) + mocked_bulb = _mocked_bulb() + mocked_bulb.async_get_properties = AsyncMock(side_effect=OSError) + + with _patch_discovery(), _patch_discovery_timeout(), _patch_discovery_interval(), patch( + f"{MODULE}.AsyncBulb", return_value=mocked_bulb + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("light.test_name").state == STATE_UNAVAILABLE + + +@pytest.mark.parametrize("exception", [BulbException, asyncio.TimeoutError]) +async def test_non_oserror_exception_on_first_update( + hass: HomeAssistant, exception: Exception +): + """Test that an exceptions other than OSError on first update do not result in unavailable. + + The unavailable state will come as a push update in this case + """ + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=ID, + data={CONF_HOST: "127.0.0.1"}, + options={CONF_NAME: "Test name"}, + ) + config_entry.add_to_hass(hass) + mocked_bulb = _mocked_bulb() + mocked_bulb.async_get_properties = AsyncMock(side_effect=exception) + + with _patch_discovery(), _patch_discovery_timeout(), _patch_discovery_interval(), patch( + f"{MODULE}.AsyncBulb", return_value=mocked_bulb + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("light.test_name").state != STATE_UNAVAILABLE From fb5d117df40282ec745f4eedb7c7429d157743a7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 15 Oct 2021 06:37:23 -1000 Subject: [PATCH 0404/1038] Always send color/temp when switching from an effect in yeelight (#57745) --- homeassistant/components/yeelight/light.py | 18 +++++-- tests/components/yeelight/test_light.py | 59 ++++++++++++++++++++++ 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index e40fd2726b2..abe1285f609 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -631,7 +631,11 @@ class YeelightGenericLight(YeelightEntity, LightEntity): """Set bulb's color.""" if not hs_color or COLOR_MODE_HS not in self.supported_color_modes: return - if self.color_mode == COLOR_MODE_HS and self.hs_color == hs_color: + if ( + not self.device.is_color_flow_enabled + and self.color_mode == COLOR_MODE_HS + and self.hs_color == hs_color + ): _LOGGER.debug("HS already set to: %s", hs_color) # Already set, and since we get pushed updates # we avoid setting it again to ensure we do not @@ -648,7 +652,11 @@ class YeelightGenericLight(YeelightEntity, LightEntity): """Set bulb's color.""" if not rgb or COLOR_MODE_RGB not in self.supported_color_modes: return - if self.color_mode == COLOR_MODE_RGB and self.rgb_color == rgb: + if ( + not self.device.is_color_flow_enabled + and self.color_mode == COLOR_MODE_RGB + and self.rgb_color == rgb + ): _LOGGER.debug("RGB already set to: %s", rgb) # Already set, and since we get pushed updates # we avoid setting it again to ensure we do not @@ -667,7 +675,11 @@ class YeelightGenericLight(YeelightEntity, LightEntity): return temp_in_k = mired_to_kelvin(colortemp) - if self.color_mode == COLOR_MODE_COLOR_TEMP and self.color_temp == colortemp: + if ( + not self.device.is_color_flow_enabled + and self.color_mode == COLOR_MODE_COLOR_TEMP + and self.color_temp == colortemp + ): _LOGGER.debug("Color temp already set to: %s", temp_in_k) # Already set, and since we get pushed updates # we avoid setting it again to ensure we do not diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index 9c5a76e4a4b..42ac3675548 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -625,6 +625,22 @@ async def test_state_already_set_avoid_ratelimit(hass: HomeAssistant): assert mocked_bulb.async_set_brightness.mock_calls == [] mocked_bulb.async_set_rgb.reset_mock() + mocked_bulb.last_properties["flowing"] = "1" + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_RGB_COLOR: (red, green, blue)}, + blocking=True, + ) + assert mocked_bulb.async_set_hsv.mock_calls == [] + assert mocked_bulb.async_set_rgb.mock_calls == [ + call(255, 0, 0, duration=350, light_type=ANY) + ] + assert mocked_bulb.async_set_color_temp.mock_calls == [] + assert mocked_bulb.async_set_brightness.mock_calls == [] + mocked_bulb.async_set_rgb.reset_mock() + mocked_bulb.last_properties["flowing"] = "0" + await hass.services.async_call( "light", SERVICE_TURN_ON, @@ -666,6 +682,22 @@ async def test_state_already_set_avoid_ratelimit(hass: HomeAssistant): assert mocked_bulb.async_set_color_temp.mock_calls == [] assert mocked_bulb.async_set_brightness.mock_calls == [] + mocked_bulb.last_properties["flowing"] = "1" + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_COLOR_TEMP: 250}, + blocking=True, + ) + assert mocked_bulb.async_set_hsv.mock_calls == [] + assert mocked_bulb.async_set_rgb.mock_calls == [] + assert mocked_bulb.async_set_color_temp.mock_calls == [ + call(4000, duration=350, light_type=ANY) + ] + assert mocked_bulb.async_set_brightness.mock_calls == [] + mocked_bulb.async_set_color_temp.reset_mock() + mocked_bulb.last_properties["flowing"] = "0" + mocked_bulb.last_properties["color_mode"] = 3 # This last change should generate a call even though # the color mode is the same since the HSV has changed @@ -681,6 +713,33 @@ async def test_state_already_set_avoid_ratelimit(hass: HomeAssistant): assert mocked_bulb.async_set_rgb.mock_calls == [] assert mocked_bulb.async_set_color_temp.mock_calls == [] assert mocked_bulb.async_set_brightness.mock_calls == [] + mocked_bulb.async_set_hsv.reset_mock() + + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_HS_COLOR: (100, 35)}, + blocking=True, + ) + assert mocked_bulb.async_set_hsv.mock_calls == [] + assert mocked_bulb.async_set_rgb.mock_calls == [] + assert mocked_bulb.async_set_color_temp.mock_calls == [] + assert mocked_bulb.async_set_brightness.mock_calls == [] + + mocked_bulb.last_properties["flowing"] = "1" + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_HS_COLOR: (100, 35)}, + blocking=True, + ) + assert mocked_bulb.async_set_hsv.mock_calls == [ + call(100.0, 35.0, duration=350, light_type=ANY) + ] + assert mocked_bulb.async_set_rgb.mock_calls == [] + assert mocked_bulb.async_set_color_temp.mock_calls == [] + assert mocked_bulb.async_set_brightness.mock_calls == [] + mocked_bulb.last_properties["flowing"] = "0" async def test_device_types(hass: HomeAssistant, caplog): From 42803e6ac08e3224ee4deca72a253ff20b56aefb Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 15 Oct 2021 18:40:27 +0200 Subject: [PATCH 0405/1038] Clean startup of modbus by moving service schemas (#57763) --- homeassistant/components/modbus/__init__.py | 35 --------------- homeassistant/components/modbus/const.py | 2 +- homeassistant/components/modbus/modbus.py | 50 ++++++++++++--------- 3 files changed, 31 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index c1e64716d21..dc53b849861 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -49,11 +49,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType from .const import ( - ATTR_ADDRESS, - ATTR_HUB, - ATTR_STATE, - ATTR_UNIT, - ATTR_VALUE, CALL_TYPE_COIL, CALL_TYPE_DISCRETE, CALL_TYPE_REGISTER_HOLDING, @@ -334,33 +329,6 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -SERVICE_WRITE_REGISTER_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_HUB, default=DEFAULT_HUB): cv.string, - vol.Required(ATTR_UNIT): cv.positive_int, - vol.Required(ATTR_ADDRESS): cv.positive_int, - vol.Required(ATTR_VALUE): vol.Any( - cv.positive_int, vol.All(cv.ensure_list, [cv.positive_int]) - ), - } -) - -SERVICE_WRITE_COIL_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_HUB, default=DEFAULT_HUB): cv.string, - vol.Required(ATTR_UNIT): cv.positive_int, - vol.Required(ATTR_ADDRESS): cv.positive_int, - vol.Required(ATTR_STATE): vol.Any( - cv.boolean, vol.All(cv.ensure_list, [cv.boolean]) - ), - } -) -SERVICE_STOP_START_SCHEMA = vol.Schema( - { - vol.Required(ATTR_HUB): cv.string, - } -) - def get_hub(hass: HomeAssistant, name: str) -> ModbusHub: """Return modbus hub with name.""" @@ -372,7 +340,4 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return await async_modbus_setup( hass, config, - SERVICE_WRITE_REGISTER_SCHEMA, - SERVICE_WRITE_COIL_SCHEMA, - SERVICE_STOP_START_SCHEMA, ) diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index 610782b5733..2594408bd74 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -69,6 +69,7 @@ SERIAL = "serial" TCP = "tcp" UDP = "udp" + # service call attributes ATTR_ADDRESS = "address" ATTR_HUB = "hub" @@ -78,7 +79,6 @@ ATTR_STATE = "state" ATTR_TEMPERATURE = "temperature" -# data types class DataType(str, Enum): """Data types used by sensor etc.""" diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index e5c08b2e1ed..d53556b392a 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -30,6 +30,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import HomeAssistant, ServiceCall, callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import Event, async_call_later @@ -124,9 +125,6 @@ PYMODBUS_CALL = [ async def async_modbus_setup( hass: HomeAssistant, config: ConfigType, - service_write_register_schema: vol.Schema, - service_write_coil_schema: vol.Schema, - service_stop_start_schema: vol.Schema, ) -> bool: """Set up Modbus component.""" @@ -173,13 +171,6 @@ async def async_modbus_setup( unit, address, int(float(value)), CALL_TYPE_WRITE_REGISTER ) - hass.services.async_register( - DOMAIN, - SERVICE_WRITE_REGISTER, - async_write_register, - schema=service_write_register_schema, - ) - async def async_write_coil(service: ServiceCall) -> None: """Write Modbus coil.""" unit = service.data[ATTR_UNIT] @@ -193,9 +184,25 @@ async def async_modbus_setup( else: await hub.async_pymodbus_call(unit, address, state, CALL_TYPE_WRITE_COIL) - hass.services.async_register( - DOMAIN, SERVICE_WRITE_COIL, async_write_coil, schema=service_write_coil_schema - ) + for x_write in ( + (SERVICE_WRITE_REGISTER, async_write_register, ATTR_VALUE, cv.positive_int), + (SERVICE_WRITE_COIL, async_write_coil, ATTR_STATE, cv.boolean), + ): + hass.services.async_register( + DOMAIN, + x_write[0], + x_write[1], + schema=vol.Schema( + { + vol.Optional(ATTR_HUB, default=DEFAULT_HUB): cv.string, + vol.Required(ATTR_UNIT): cv.positive_int, + vol.Required(ATTR_ADDRESS): cv.positive_int, + vol.Required(x_write[2]): vol.Any( + cv.positive_int, vol.All(cv.ensure_list, [x_write[3]]) + ), + } + ), + ) async def async_stop_hub(service: ServiceCall) -> None: """Stop Modbus hub.""" @@ -203,19 +210,22 @@ async def async_modbus_setup( hub = hub_collect[service.data[ATTR_HUB]] await hub.async_close() - hass.services.async_register( - DOMAIN, SERVICE_STOP, async_stop_hub, schema=service_stop_start_schema - ) - async def async_restart_hub(service: ServiceCall) -> None: """Restart Modbus hub.""" async_dispatcher_send(hass, SIGNAL_START_ENTITY) hub = hub_collect[service.data[ATTR_HUB]] await hub.async_restart() - hass.services.async_register( - DOMAIN, SERVICE_RESTART, async_restart_hub, schema=service_stop_start_schema - ) + for x_service in ( + (SERVICE_STOP, async_stop_hub), + (SERVICE_RESTART, async_restart_hub), + ): + hass.services.async_register( + DOMAIN, + x_service[0], + x_service[1], + schema=vol.Schema({vol.Required(ATTR_HUB): cv.string}), + ) return True From f8d0f76721340f174333d180931586c537fce227 Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 15 Oct 2021 09:45:04 -0700 Subject: [PATCH 0406/1038] Add device class to temperature sensors for octoprint (#56997) --- homeassistant/components/octoprint/sensor.py | 43 +++++++++++--------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/octoprint/sensor.py b/homeassistant/components/octoprint/sensor.py index 5b2b0af494c..d456813a4ff 100644 --- a/homeassistant/components/octoprint/sensor.py +++ b/homeassistant/components/octoprint/sensor.py @@ -3,8 +3,8 @@ import logging import requests -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity +from homeassistant.const import DEVICE_CLASS_TEMPERATURE, PERCENTAGE, TEMP_CELSIUS from . import DOMAIN as COMPONENT_DOMAIN, SENSOR_TYPES @@ -44,27 +44,28 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for tool in tools: for temp_type in types: new_sensor = OctoPrintSensor( - octoprint_api, - temp_type, - temp_type, - name, - SENSOR_TYPES[octo_type][3], - SENSOR_TYPES[octo_type][0], - SENSOR_TYPES[octo_type][1], - tool, + api=octoprint_api, + condition=temp_type, + sensor_type=temp_type, + sensor_name=name, + unit=SENSOR_TYPES[octo_type][3], + endpoint=SENSOR_TYPES[octo_type][0], + group=SENSOR_TYPES[octo_type][1], + tool=tool, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) devices.append(new_sensor) else: new_sensor = OctoPrintSensor( - octoprint_api, - octo_type, - SENSOR_TYPES[octo_type][2], - name, - SENSOR_TYPES[octo_type][3], - SENSOR_TYPES[octo_type][0], - SENSOR_TYPES[octo_type][1], - None, - SENSOR_TYPES[octo_type][4], + api=octoprint_api, + condition=octo_type, + sensor_type=SENSOR_TYPES[octo_type][2], + sensor_name=name, + unit=SENSOR_TYPES[octo_type][3], + endpoint=SENSOR_TYPES[octo_type][0], + group=SENSOR_TYPES[octo_type][1], + icon=SENSOR_TYPES[octo_type][4], ) devices.append(new_sensor) add_entities(devices, True) @@ -84,6 +85,8 @@ class OctoPrintSensor(SensorEntity): group, tool=None, icon=None, + device_class=None, + state_class=None, ): """Initialize a new OctoPrint sensor.""" self.sensor_name = sensor_name @@ -99,6 +102,8 @@ class OctoPrintSensor(SensorEntity): self.api_group = group self.api_tool = tool self._icon = icon + self._attr_device_class = device_class + self._attr_state_class = state_class _LOGGER.debug("Created OctoPrint sensor %r", self) @property From 19443b474ccce974db08bbe31ddb9f26e70f9499 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 15 Oct 2021 19:02:12 +0200 Subject: [PATCH 0407/1038] Merge bmw_connected_drive metric and imperial sensor types (#56910) --- .../components/bmw_connected_drive/sensor.py | 732 +++++++++--------- 1 file changed, 345 insertions(+), 387 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 104a2eb78d9..1faa858e9d8 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -1,4 +1,6 @@ """Support for reading vehicle status from BMW connected drive portal.""" +from __future__ import annotations + import logging from bimmer_connected.const import SERVICE_ALL_TRIPS, SERVICE_LAST_TRIP, SERVICE_STATUS @@ -27,384 +29,343 @@ from .const import CONF_ACCOUNT, DATA_ENTRIES _LOGGER = logging.getLogger(__name__) -ATTR_TO_HA_METRIC = { - # "": [, , , ], - "mileage": ["mdi:speedometer", None, LENGTH_KILOMETERS, True], - "remaining_range_total": ["mdi:map-marker-distance", None, LENGTH_KILOMETERS, True], - "remaining_range_electric": [ - "mdi:map-marker-distance", +SENSOR_TYPES: dict[str, tuple[str | None, str | None, str | None, str | None, bool]] = { + # "": (, , , , ), + # --- Generic --- + "charging_time_remaining": ( + "mdi:update", None, - LENGTH_KILOMETERS, + TIME_HOURS, + TIME_HOURS, True, - ], - "remaining_range_fuel": ["mdi:map-marker-distance", None, LENGTH_KILOMETERS, True], - "max_range_electric": ["mdi:map-marker-distance", None, LENGTH_KILOMETERS, True], - "remaining_fuel": ["mdi:gas-station", None, VOLUME_LITERS, True], - # LastTrip attributes - "average_combined_consumption": [ - "mdi:flash", + ), + "charging_status": ( + "mdi:battery-charging", + None, + None, None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", True, - ], - "average_electric_consumption": [ - "mdi:power-plug-outline", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - True, - ], - "average_recuperation": [ - "mdi:recycle-variant", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - True, - ], - "electric_distance": ["mdi:map-marker-distance", None, LENGTH_KILOMETERS, True], - "saved_fuel": ["mdi:fuel", None, VOLUME_LITERS, False], - "total_distance": ["mdi:map-marker-distance", None, LENGTH_KILOMETERS, True], - # AllTrips attributes - "average_combined_consumption_community_average": [ - "mdi:flash", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - False, - ], - "average_combined_consumption_community_high": [ - "mdi:flash", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - False, - ], - "average_combined_consumption_community_low": [ - "mdi:flash", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - False, - ], - "average_combined_consumption_user_average": [ - "mdi:flash", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - True, - ], - "average_electric_consumption_community_average": [ - "mdi:power-plug-outline", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - False, - ], - "average_electric_consumption_community_high": [ - "mdi:power-plug-outline", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - False, - ], - "average_electric_consumption_community_low": [ - "mdi:power-plug-outline", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - False, - ], - "average_electric_consumption_user_average": [ - "mdi:power-plug-outline", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - True, - ], - "average_recuperation_community_average": [ - "mdi:recycle-variant", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - False, - ], - "average_recuperation_community_high": [ - "mdi:recycle-variant", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - False, - ], - "average_recuperation_community_low": [ - "mdi:recycle-variant", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - False, - ], - "average_recuperation_user_average": [ - "mdi:recycle-variant", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - True, - ], - "chargecycle_range_community_average": [ - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - False, - ], - "chargecycle_range_community_high": [ - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - False, - ], - "chargecycle_range_community_low": [ - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - False, - ], - "chargecycle_range_user_average": [ - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - True, - ], - "chargecycle_range_user_current_charge_cycle": [ - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - True, - ], - "chargecycle_range_user_high": [ - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - True, - ], - "total_electric_distance_community_average": [ - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - False, - ], - "total_electric_distance_community_high": [ - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - False, - ], - "total_electric_distance_community_low": [ - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - False, - ], - "total_electric_distance_user_average": [ - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - False, - ], - "total_electric_distance_user_total": [ - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - False, - ], - "total_saved_fuel": ["mdi:fuel", None, VOLUME_LITERS, False], -} - -ATTR_TO_HA_IMPERIAL = { - # "": [, , , ], - "mileage": ["mdi:speedometer", None, LENGTH_MILES, True], - "remaining_range_total": ["mdi:map-marker-distance", None, LENGTH_MILES, True], - "remaining_range_electric": ["mdi:map-marker-distance", None, LENGTH_MILES, True], - "remaining_range_fuel": ["mdi:map-marker-distance", None, LENGTH_MILES, True], - "max_range_electric": ["mdi:map-marker-distance", None, LENGTH_MILES, True], - "remaining_fuel": ["mdi:gas-station", None, VOLUME_GALLONS, True], - # LastTrip attributes - "average_combined_consumption": [ - "mdi:flash", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - True, - ], - "average_electric_consumption": [ - "mdi:power-plug-outline", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - True, - ], - "average_recuperation": [ - "mdi:recycle-variant", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - True, - ], - "electric_distance": ["mdi:map-marker-distance", None, LENGTH_MILES, True], - "saved_fuel": ["mdi:fuel", None, VOLUME_GALLONS, False], - "total_distance": ["mdi:map-marker-distance", None, LENGTH_MILES, True], - # AllTrips attributes - "average_combined_consumption_community_average": [ - "mdi:flash", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - False, - ], - "average_combined_consumption_community_high": [ - "mdi:flash", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - False, - ], - "average_combined_consumption_community_low": [ - "mdi:flash", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - False, - ], - "average_combined_consumption_user_average": [ - "mdi:flash", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - True, - ], - "average_electric_consumption_community_average": [ - "mdi:power-plug-outline", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - False, - ], - "average_electric_consumption_community_high": [ - "mdi:power-plug-outline", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - False, - ], - "average_electric_consumption_community_low": [ - "mdi:power-plug-outline", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - False, - ], - "average_electric_consumption_user_average": [ - "mdi:power-plug-outline", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - True, - ], - "average_recuperation_community_average": [ - "mdi:recycle-variant", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - False, - ], - "average_recuperation_community_high": [ - "mdi:recycle-variant", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - False, - ], - "average_recuperation_community_low": [ - "mdi:recycle-variant", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - False, - ], - "average_recuperation_user_average": [ - "mdi:recycle-variant", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - True, - ], - "chargecycle_range_community_average": [ - "mdi:map-marker-distance", - None, - LENGTH_MILES, - False, - ], - "chargecycle_range_community_high": [ - "mdi:map-marker-distance", - None, - LENGTH_MILES, - False, - ], - "chargecycle_range_community_low": [ - "mdi:map-marker-distance", - None, - LENGTH_MILES, - False, - ], - "chargecycle_range_user_average": [ - "mdi:map-marker-distance", - None, - LENGTH_MILES, - True, - ], - "chargecycle_range_user_current_charge_cycle": [ - "mdi:map-marker-distance", - None, - LENGTH_MILES, - True, - ], - "chargecycle_range_user_high": [ - "mdi:map-marker-distance", - None, - LENGTH_MILES, - True, - ], - "total_electric_distance_community_average": [ - "mdi:map-marker-distance", - None, - LENGTH_MILES, - False, - ], - "total_electric_distance_community_high": [ - "mdi:map-marker-distance", - None, - LENGTH_MILES, - False, - ], - "total_electric_distance_community_low": [ - "mdi:map-marker-distance", - None, - LENGTH_MILES, - False, - ], - "total_electric_distance_user_average": [ - "mdi:map-marker-distance", - None, - LENGTH_MILES, - False, - ], - "total_electric_distance_user_total": [ - "mdi:map-marker-distance", - None, - LENGTH_MILES, - False, - ], - "total_saved_fuel": ["mdi:fuel", None, VOLUME_GALLONS, False], -} - -ATTR_TO_HA_GENERIC = { - # "": [, , , ], - "charging_time_remaining": ["mdi:update", None, TIME_HOURS, True], - "charging_status": ["mdi:battery-charging", None, None, True], + ), # No icon as this is dealt with directly as a special case in icon() - "charging_level_hv": [None, None, PERCENTAGE, True], + "charging_level_hv": ( + None, + None, + PERCENTAGE, + PERCENTAGE, + True, + ), # LastTrip attributes - "date_utc": [None, DEVICE_CLASS_TIMESTAMP, None, True], - "duration": ["mdi:timer-outline", None, TIME_MINUTES, True], - "electric_distance_ratio": ["mdi:percent-outline", None, PERCENTAGE, False], + "date_utc": ( + None, + DEVICE_CLASS_TIMESTAMP, + None, + None, + True, + ), + "duration": ( + "mdi:timer-outline", + None, + TIME_MINUTES, + TIME_MINUTES, + True, + ), + "electric_distance_ratio": ( + "mdi:percent-outline", + None, + PERCENTAGE, + PERCENTAGE, + False, + ), # AllTrips attributes - "battery_size_max": ["mdi:battery-charging-high", None, ENERGY_WATT_HOUR, False], - "reset_date_utc": [None, DEVICE_CLASS_TIMESTAMP, None, False], - "saved_co2": ["mdi:tree-outline", None, MASS_KILOGRAMS, False], - "saved_co2_green_energy": ["mdi:tree-outline", None, MASS_KILOGRAMS, False], + "battery_size_max": ( + "mdi:battery-charging-high", + None, + ENERGY_WATT_HOUR, + ENERGY_WATT_HOUR, + False, + ), + "reset_date_utc": ( + None, + DEVICE_CLASS_TIMESTAMP, + None, + None, + False, + ), + "saved_co2": ( + "mdi:tree-outline", + None, + MASS_KILOGRAMS, + MASS_KILOGRAMS, + False, + ), + "saved_co2_green_energy": ( + "mdi:tree-outline", + None, + MASS_KILOGRAMS, + MASS_KILOGRAMS, + False, + ), + # --- Specific --- + "mileage": ( + "mdi:speedometer", + None, + LENGTH_KILOMETERS, + LENGTH_MILES, + True, + ), + "remaining_range_total": ( + "mdi:map-marker-distance", + None, + LENGTH_KILOMETERS, + LENGTH_MILES, + True, + ), + "remaining_range_electric": ( + "mdi:map-marker-distance", + None, + LENGTH_KILOMETERS, + LENGTH_MILES, + True, + ), + "remaining_range_fuel": ( + "mdi:map-marker-distance", + None, + LENGTH_KILOMETERS, + LENGTH_MILES, + True, + ), + "max_range_electric": ( + "mdi:map-marker-distance", + None, + LENGTH_KILOMETERS, + LENGTH_MILES, + True, + ), + "remaining_fuel": ( + "mdi:gas-station", + None, + VOLUME_LITERS, + VOLUME_GALLONS, + True, + ), + # LastTrip attributes + "average_combined_consumption": ( + "mdi:flash", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + True, + ), + "average_electric_consumption": ( + "mdi:power-plug-outline", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + True, + ), + "average_recuperation": ( + "mdi:recycle-variant", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + True, + ), + "electric_distance": ( + "mdi:map-marker-distance", + None, + LENGTH_KILOMETERS, + LENGTH_MILES, + True, + ), + "saved_fuel": ( + "mdi:fuel", + None, + VOLUME_LITERS, + VOLUME_GALLONS, + False, + ), + "total_distance": ( + "mdi:map-marker-distance", + None, + LENGTH_KILOMETERS, + LENGTH_MILES, + True, + ), + # AllTrips attributes + "average_combined_consumption_community_average": ( + "mdi:flash", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + False, + ), + "average_combined_consumption_community_high": ( + "mdi:flash", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + False, + ), + "average_combined_consumption_community_low": ( + "mdi:flash", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + False, + ), + "average_combined_consumption_user_average": ( + "mdi:flash", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + True, + ), + "average_electric_consumption_community_average": ( + "mdi:power-plug-outline", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + False, + ), + "average_electric_consumption_community_high": ( + "mdi:power-plug-outline", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + False, + ), + "average_electric_consumption_community_low": ( + "mdi:power-plug-outline", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + False, + ), + "average_electric_consumption_user_average": ( + "mdi:power-plug-outline", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + True, + ), + "average_recuperation_community_average": ( + "mdi:recycle-variant", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + False, + ), + "average_recuperation_community_high": ( + "mdi:recycle-variant", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + False, + ), + "average_recuperation_community_low": ( + "mdi:recycle-variant", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + False, + ), + "average_recuperation_user_average": ( + "mdi:recycle-variant", + None, + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + True, + ), + "chargecycle_range_community_average": ( + "mdi:map-marker-distance", + None, + LENGTH_KILOMETERS, + LENGTH_MILES, + False, + ), + "chargecycle_range_community_high": ( + "mdi:map-marker-distance", + None, + LENGTH_KILOMETERS, + LENGTH_MILES, + False, + ), + "chargecycle_range_community_low": ( + "mdi:map-marker-distance", + None, + LENGTH_KILOMETERS, + LENGTH_MILES, + False, + ), + "chargecycle_range_user_average": ( + "mdi:map-marker-distance", + None, + LENGTH_KILOMETERS, + LENGTH_MILES, + True, + ), + "chargecycle_range_user_current_charge_cycle": ( + "mdi:map-marker-distance", + None, + LENGTH_KILOMETERS, + LENGTH_MILES, + True, + ), + "chargecycle_range_user_high": ( + "mdi:map-marker-distance", + None, + LENGTH_KILOMETERS, + LENGTH_MILES, + True, + ), + "total_electric_distance_community_average": ( + "mdi:map-marker-distance", + None, + LENGTH_KILOMETERS, + LENGTH_MILES, + False, + ), + "total_electric_distance_community_high": ( + "mdi:map-marker-distance", + None, + LENGTH_KILOMETERS, + LENGTH_MILES, + False, + ), + "total_electric_distance_community_low": ( + "mdi:map-marker-distance", + None, + LENGTH_KILOMETERS, + LENGTH_MILES, + False, + ), + "total_electric_distance_user_average": ( + "mdi:map-marker-distance", + None, + LENGTH_KILOMETERS, + LENGTH_MILES, + False, + ), + "total_electric_distance_user_total": ( + "mdi:map-marker-distance", + None, + LENGTH_KILOMETERS, + LENGTH_MILES, + False, + ), + "total_saved_fuel": ( + "mdi:fuel", + None, + VOLUME_LITERS, + VOLUME_GALLONS, + False, + ), } -ATTR_TO_HA_METRIC.update(ATTR_TO_HA_GENERIC) -ATTR_TO_HA_IMPERIAL.update(ATTR_TO_HA_GENERIC) - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the BMW ConnectedDrive sensors from config entry.""" # pylint: disable=too-many-nested-blocks - if hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: - attribute_info = ATTR_TO_HA_IMPERIAL - else: - attribute_info = ATTR_TO_HA_METRIC - account = hass.data[BMW_DOMAIN][DATA_ENTRIES][config_entry.entry_id][CONF_ACCOUNT] entities = [] @@ -414,33 +375,33 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for attribute_name in vehicle.drive_train_attributes: if attribute_name in vehicle.available_attributes: device = BMWConnectedDriveSensor( - account, vehicle, attribute_name, attribute_info + hass, account, vehicle, attribute_name ) entities.append(device) if service == SERVICE_LAST_TRIP: for attribute_name in vehicle.state.last_trip.available_attributes: if attribute_name == "date": device = BMWConnectedDriveSensor( + hass, account, vehicle, "date_utc", - attribute_info, service, ) entities.append(device) else: device = BMWConnectedDriveSensor( - account, vehicle, attribute_name, attribute_info, service + hass, account, vehicle, attribute_name, service ) entities.append(device) if service == SERVICE_ALL_TRIPS: for attribute_name in vehicle.state.all_trips.available_attributes: if attribute_name == "reset_date": device = BMWConnectedDriveSensor( + hass, account, vehicle, "reset_date_utc", - attribute_info, service, ) entities.append(device) @@ -458,36 +419,36 @@ async def async_setup_entry(hass, config_entry, async_add_entities): "user_average", ): device = BMWConnectedDriveSensor( + hass, account, vehicle, f"{attribute_name}_{attr}", - attribute_info, service, ) entities.append(device) if attribute_name == "chargecycle_range": for attr in ("user_current_charge_cycle", "user_high"): device = BMWConnectedDriveSensor( + hass, account, vehicle, f"{attribute_name}_{attr}", - attribute_info, service, ) entities.append(device) if attribute_name == "total_electric_distance": for attr in ("user_total",): device = BMWConnectedDriveSensor( + hass, account, vehicle, f"{attribute_name}_{attr}", - attribute_info, service, ) entities.append(device) else: device = BMWConnectedDriveSensor( - account, vehicle, attribute_name, attribute_info, service + hass, account, vehicle, attribute_name, service ) entities.append(device) @@ -497,7 +458,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): """Representation of a BMW vehicle sensor.""" - def __init__(self, account, vehicle, attribute: str, attribute_info, service=None): + def __init__(self, hass, account, vehicle, attribute: str, service=None): """Initialize BMW vehicle sensor.""" super().__init__(account, vehicle) @@ -509,19 +470,16 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): else: self._attr_name = f"{vehicle.name} {attribute}" self._attr_unique_id = f"{vehicle.vin}-{attribute}" - self._attribute_info = attribute_info - self._attr_entity_registry_enabled_default = attribute_info.get( - attribute, [None, None, None, True] - )[3] - self._attr_icon = self._attribute_info.get( - self._attribute, [None, None, None, None] - )[0] - self._attr_device_class = attribute_info.get( - attribute, [None, None, None, None] - )[1] - self._attr_native_unit_of_measurement = attribute_info.get( - attribute, [None, None, None, None] - )[2] + self._attribute_info = SENSOR_TYPES.get( + attribute, (None, None, None, None, True) + ) + self._attr_entity_registry_enabled_default = self._attribute_info[4] + self._attr_icon = self._attribute_info[0] + self._attr_device_class = self._attribute_info[1] + if hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: + self._attr_native_unit_of_measurement = self._attribute_info[3] + else: + self._attr_native_unit_of_measurement = self._attribute_info[2] def update(self) -> None: """Read new state data from the library.""" From 892bf62dd579984e43439760bc3eb646cadb784b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 15 Oct 2021 19:11:06 +0200 Subject: [PATCH 0408/1038] Add PIR Detector (pir) device support to Tuya (#57784) --- .../components/tuya/binary_sensor.py | 43 ++++++++++++++++--- homeassistant/components/tuya/const.py | 5 ++- homeassistant/components/tuya/sensor.py | 18 ++++++++ 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index 0a6f0aed053..a00e4e19e1a 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -1,14 +1,18 @@ """Support for Tuya binary sensors.""" from __future__ import annotations +from dataclasses import dataclass + from tuya_iot import TuyaDevice, TuyaDeviceManager from homeassistant.components.binary_sensor import ( DEVICE_CLASS_DOOR, + DEVICE_CLASS_MOTION, BinarySensorEntity, BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -17,24 +21,46 @@ from . import HomeAssistantTuyaData from .base import TuyaEntity from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode + +@dataclass +class TuyaBinarySensorEntityDescription(BinarySensorEntityDescription): + """Describes a Tuya binary sensor.""" + + on_value: bool | float | int | str = True + + # All descriptions can be found here. Mostly the Boolean data types in the # default status set of each category (that don't have a set instruction) # end up being a binary sensor. # https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq -BINARY_SENSORS: dict[str, tuple[BinarySensorEntityDescription, ...]] = { +BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { # Door Window Sensor # https://developer.tuya.com/en/docs/iot/s?id=K9gf48hm02l8m "mcs": ( - BinarySensorEntityDescription( + TuyaBinarySensorEntityDescription( key=DPCode.DOORCONTACT_STATE, device_class=DEVICE_CLASS_DOOR, ), - BinarySensorEntityDescription( + TuyaBinarySensorEntityDescription( key=DPCode.TEMPER_ALARM, name="Tamper", entity_registry_enabled_default=False, ), ), + # PIR Detector + # https://developer.tuya.com/en/docs/iot/categorypir?id=Kaiuz3ss11b80 + "pir": ( + TuyaBinarySensorEntityDescription( + key=DPCode.PIR, + device_class=DEVICE_CLASS_MOTION, + on_value="pir", + ), + TuyaBinarySensorEntityDescription( + key=DPCode.TEMPER_ALARM, + name="Tamper", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + ), } @@ -74,11 +100,13 @@ async def async_setup_entry( class TuyaBinarySensorEntity(TuyaEntity, BinarySensorEntity): """Tuya Binary Sensor Entity.""" + entity_description: TuyaBinarySensorEntityDescription + def __init__( self, device: TuyaDevice, device_manager: TuyaDeviceManager, - description: BinarySensorEntityDescription, + description: TuyaBinarySensorEntityDescription, ) -> None: """Init Tuya binary sensor.""" super().__init__(device, device_manager) @@ -88,4 +116,9 @@ class TuyaBinarySensorEntity(TuyaEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return true if sensor is on.""" - return self.device.status.get(self.entity_description.key, False) + if self.entity_description.key not in self.device.status: + return False + return ( + self.device.status[self.entity_description.key] + == self.entity_description.on_value + ) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 0bc71a478b4..f25dacaab37 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -37,13 +37,13 @@ TUYA_SUPPORTED_PRODUCT_CATEGORIES = ( "fs", # Fan "fwl", # Ambient light "jsq", # Humidifier's light - "kfj", # Coffee Maker + "kfj", # Coffee maker "kg", # Switch "kj", # Air Purifier - "kfj", # Coffee maker "kt", # Air conditioner "mcs", # Door Window Sensor "pc", # Power Strip + "pir", # PIR Detector "qn", # Heater "wk", # Thermostat "xdd", # Ceiling Light @@ -96,6 +96,7 @@ class DPCode(str, Enum): LOCK = "lock" # Lock / Child lock MATERIAL = "material" # Material MODE = "mode" # Working mode / Mode + PIR = "pir" # Motion sensor POWDER_SET = "powder_set" # Powder PUMP_RESET = "pump_reset" # Water pump reset SHAKE = "shake" # Oscillating diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 8d36a9b1207..0d6d179ab76 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -17,6 +17,7 @@ from homeassistant.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, + ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, ) from homeassistant.core import HomeAssistant, callback @@ -74,6 +75,23 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { entity_registry_enabled_default=False, ), ), + # PIR Detector + # https://developer.tuya.com/en/docs/iot/categorypir?id=Kaiuz3ss11b80 + "pir": ( + SensorEntityDescription( + key=DPCode.BATTERY_PERCENTAGE, + name="Battery", + native_unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key=DPCode.BATTERY_STATE, + name="Battery State", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + ), } # Socket (duplicate of `kg`) From fcd0a877d6808628485869ebbe4d62b377de086b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 15 Oct 2021 10:23:36 -0700 Subject: [PATCH 0409/1038] Identify onetime listeners (#57751) --- homeassistant/core.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/core.py b/homeassistant/core.py index 4221c435a55..34b48e66953 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -834,6 +834,10 @@ class EventBus: self._async_remove_listener(event_type, filterable_job) self._hass.async_run_job(listener, event) + functools.update_wrapper( + _onetime_listener, listener, ("__name__", "__qualname__", "__module__"), [] + ) + filterable_job = (HassJob(_onetime_listener), None) return self._async_listen_filterable_job(event_type, filterable_job) From aeb00823aa0cacc124196765d1a38b40da7fa9f1 Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Sat, 16 Oct 2021 04:24:04 +1100 Subject: [PATCH 0410/1038] Log reason for DLNA-DMR device becoming unavailable (#57516) --- homeassistant/components/dlna_dmr/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 839b58b6b5a..e91ea1e830e 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -426,8 +426,8 @@ class DlnaDmrEntity(MediaPlayerEntity): try: do_ping = self.poll_availability or self.check_available await self._device.async_update(do_ping=do_ping) - except UpnpError: - _LOGGER.debug("Device unavailable") + except UpnpError as err: + _LOGGER.debug("Device unavailable: %r", err) await self._device_disconnect() return finally: From 31ccaac8652e6e2d3aacea747e5c2ab74f9509d1 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 15 Oct 2021 20:46:58 +0200 Subject: [PATCH 0411/1038] Add vlc telnet config flow (#57513) --- .coveragerc | 1 + CODEOWNERS | 2 +- .../components/vlc_telnet/__init__.py | 68 ++++- .../components/vlc_telnet/config_flow.py | 159 ++++++++++ homeassistant/components/vlc_telnet/const.py | 9 + .../components/vlc_telnet/manifest.json | 5 +- .../components/vlc_telnet/media_player.py | 243 +++++++++------- .../components/vlc_telnet/strings.json | 30 ++ .../vlc_telnet/translations/en.json | 30 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 6 +- requirements_test_all.txt | 3 + tests/components/vlc_telnet/__init__.py | 1 + .../components/vlc_telnet/test_config_flow.py | 272 ++++++++++++++++++ 14 files changed, 715 insertions(+), 115 deletions(-) create mode 100644 homeassistant/components/vlc_telnet/config_flow.py create mode 100644 homeassistant/components/vlc_telnet/const.py create mode 100644 homeassistant/components/vlc_telnet/strings.json create mode 100644 homeassistant/components/vlc_telnet/translations/en.json create mode 100644 tests/components/vlc_telnet/__init__.py create mode 100644 tests/components/vlc_telnet/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 91f4d43c702..d6fe547ac83 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1173,6 +1173,7 @@ omit = homeassistant/components/vilfo/const.py homeassistant/components/vivotek/camera.py homeassistant/components/vlc/media_player.py + homeassistant/components/vlc_telnet/__init__.py homeassistant/components/vlc_telnet/media_player.py homeassistant/components/volkszaehler/sensor.py homeassistant/components/volumio/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index bed454c62a7..28c037e6e2a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -571,7 +571,7 @@ homeassistant/components/vicare/* @oischinger homeassistant/components/vilfo/* @ManneW homeassistant/components/vivotek/* @HarlemSquirrel homeassistant/components/vizio/* @raman325 -homeassistant/components/vlc_telnet/* @rodripf @dmcc +homeassistant/components/vlc_telnet/* @rodripf @dmcc @MartinHjelmare homeassistant/components/volkszaehler/* @fabaff homeassistant/components/volumio/* @OnFreund homeassistant/components/wake_on_lan/* @ntilley905 diff --git a/homeassistant/components/vlc_telnet/__init__.py b/homeassistant/components/vlc_telnet/__init__.py index 91a3eb35444..68c1fbed004 100644 --- a/homeassistant/components/vlc_telnet/__init__.py +++ b/homeassistant/components/vlc_telnet/__init__.py @@ -1 +1,67 @@ -"""The vlc component.""" +"""The VLC media player Telnet integration.""" +from aiovlc.client import Client +from aiovlc.exceptions import AuthError, ConnectError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed + +from .const import DATA_AVAILABLE, DATA_VLC, DOMAIN, LOGGER + +PLATFORMS = ["media_player"] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up VLC media player Telnet from a config entry.""" + config = entry.data + + host = config[CONF_HOST] + port = config[CONF_PORT] + password = config[CONF_PASSWORD] + + vlc = Client(password=password, host=host, port=port) + + available = True + + try: + await vlc.connect() + except ConnectError as err: + LOGGER.warning("Failed to connect to VLC: %s. Trying again", err) + available = False + + if available: + try: + await vlc.login() + except AuthError as err: + await disconnect_vlc(vlc) + raise ConfigEntryAuthFailed() from err + + domain_data = hass.data.setdefault(DOMAIN, {}) + domain_data[entry.entry_id] = {DATA_VLC: vlc, DATA_AVAILABLE: available} + + hass.config_entries.async_setup_platforms(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: + entry_data = hass.data[DOMAIN].pop(entry.entry_id) + vlc = entry_data[DATA_VLC] + + await hass.async_add_executor_job(disconnect_vlc, vlc) + + return unload_ok + + +async def disconnect_vlc(vlc: Client) -> None: + """Disconnect from VLC.""" + LOGGER.debug("Disconnecting from VLC") + try: + await vlc.disconnect() + except ConnectError as err: + LOGGER.warning("Connection error: %s", err) diff --git a/homeassistant/components/vlc_telnet/config_flow.py b/homeassistant/components/vlc_telnet/config_flow.py new file mode 100644 index 00000000000..0044995c7db --- /dev/null +++ b/homeassistant/components/vlc_telnet/config_flow.py @@ -0,0 +1,159 @@ +"""Config flow for VLC media player Telnet integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from aiovlc.client import Client +from aiovlc.exceptions import AuthError, ConnectError +import voluptuous as vol + +from homeassistant import core, exceptions +from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT +from homeassistant.data_entry_flow import FlowResult + +from .const import DEFAULT_PORT, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +def user_form_schema(user_input: dict[str, Any] | None) -> vol.Schema: + """Return user form schema.""" + user_input = user_input or {} + return vol.Schema( + { + vol.Required(CONF_PASSWORD): str, + vol.Optional( + CONF_HOST, default=user_input.get(CONF_HOST, "localhost") + ): str, + vol.Optional( + CONF_PORT, default=user_input.get(CONF_PORT, DEFAULT_PORT) + ): int, + } + ) + + +STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) + + +async def vlc_connect(vlc: Client) -> None: + """Connect to VLC.""" + await vlc.connect() + await vlc.login() + await vlc.disconnect() + + +async def validate_input( + hass: core.HomeAssistant, data: dict[str, Any] +) -> dict[str, str]: + """Validate the user input allows us to connect.""" + vlc = Client( + password=data[CONF_PASSWORD], + host=data[CONF_HOST], + port=data[CONF_PORT], + ) + + try: + await vlc_connect(vlc) + except ConnectError as err: + raise CannotConnect from err + except AuthError as err: + raise InvalidAuth from err + + # CONF_NAME is only present in the imported YAML data. + return {"title": data.get(CONF_NAME) or data[CONF_HOST]} + + +class VLCTelnetConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for VLC media player Telnet.""" + + VERSION = 1 + entry: ConfigEntry | None = None + + 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=user_form_schema(user_input) + ) + + self._async_abort_entries_match( + {CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]} + ) + + errors = {} + + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=user_form_schema(user_input), errors=errors + ) + + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: + """Handle the import step.""" + return await self.async_step_user(user_input) + + async def async_step_reauth(self, data: dict[str, Any]) -> FlowResult: + """Handle reauth flow.""" + self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + assert self.entry + self.context["title_placeholders"] = {"host": self.entry.data[CONF_HOST]} + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle reauth confirm.""" + assert self.entry + errors = {} + + if user_input is not None: + try: + await validate_input(self.hass, {**self.entry.data, **user_input}) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + self.hass.config_entries.async_update_entry( + self.entry, + data={ + **self.entry.data, + CONF_PASSWORD: user_input[CONF_PASSWORD], + }, + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(self.entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="reauth_confirm", + description_placeholders={CONF_HOST: self.entry.data[CONF_HOST]}, + data_schema=STEP_REAUTH_DATA_SCHEMA, + errors=errors, + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/vlc_telnet/const.py b/homeassistant/components/vlc_telnet/const.py new file mode 100644 index 00000000000..432de5aa854 --- /dev/null +++ b/homeassistant/components/vlc_telnet/const.py @@ -0,0 +1,9 @@ +"""Integration shared constants.""" +import logging + +DATA_VLC = "vlc" +DATA_AVAILABLE = "available" +DEFAULT_NAME = "VLC-TELNET" +DEFAULT_PORT = 4212 +DOMAIN = "vlc_telnet" +LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/vlc_telnet/manifest.json b/homeassistant/components/vlc_telnet/manifest.json index d03e9163961..9c019abbf46 100644 --- a/homeassistant/components/vlc_telnet/manifest.json +++ b/homeassistant/components/vlc_telnet/manifest.json @@ -1,8 +1,9 @@ { "domain": "vlc_telnet", "name": "VLC media player Telnet", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/vlc_telnet", - "requirements": ["python-telnet-vlc==2.0.1"], - "codeowners": ["@rodripf", "@dmcc"], + "requirements": ["aiovlc==0.1.0"], + "codeowners": ["@rodripf", "@dmcc", "@MartinHjelmare"], "iot_class": "local_polling" } diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index 784df2cabcf..75b55b5f77b 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -1,13 +1,11 @@ """Provide functionality to interact with the vlc telnet interface.""" -import logging +from __future__ import annotations -from python_telnet_vlc import ( - CommandError, - ConnectionError as ConnErr, - LuaError, - ParseError, - VLCTelnet, -) +from datetime import datetime +from typing import Any + +from aiovlc.client import Client +from aiovlc.exceptions import AuthError, CommandError, ConnectError import voluptuous as vol from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity @@ -25,6 +23,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -33,17 +32,15 @@ from homeassistant.const import ( STATE_IDLE, STATE_PAUSED, STATE_PLAYING, - STATE_UNAVAILABLE, ) +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 import homeassistant.util.dt as dt_util -_LOGGER = logging.getLogger(__name__) +from .const import DATA_AVAILABLE, DATA_VLC, DEFAULT_NAME, DEFAULT_PORT, DOMAIN, LOGGER -DOMAIN = "vlc_telnet" - -DEFAULT_NAME = "VLC-TELNET" -DEFAULT_PORT = 4212 MAX_VOLUME = 500 SUPPORT_VLC = ( @@ -69,106 +66,129 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: """Set up the vlc platform.""" - add_entities( - [ - VlcDevice( - config.get(CONF_NAME), - config.get(CONF_HOST), - config.get(CONF_PORT), - config.get(CONF_PASSWORD), - ) - ], - True, + LOGGER.warning( + "Loading VLC media player Telnet integration via platform setup is deprecated; " + "Please remove it from your configuration" ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) + ) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the vlc platform.""" + # CONF_NAME is only present in imported YAML. + name = entry.data.get(CONF_NAME) or DEFAULT_NAME + vlc = hass.data[DOMAIN][entry.entry_id][DATA_VLC] + available = hass.data[DOMAIN][entry.entry_id][DATA_AVAILABLE] + + async_add_entities([VlcDevice(entry, vlc, name, available)], True) class VlcDevice(MediaPlayerEntity): """Representation of a vlc player.""" - def __init__(self, name, host, port, passwd): + def __init__( + self, config_entry: ConfigEntry, vlc: Client, name: str, available: bool + ) -> None: """Initialize the vlc device.""" + self._config_entry = config_entry self._name = name - self._volume = None - self._muted = None - self._state = STATE_UNAVAILABLE - self._media_position_updated_at = None - self._media_position = None - self._media_duration = None - self._host = host - self._port = port - self._password = passwd - self._vlc = None - self._available = True - self._volume_bkp = 0 - self._media_artist = "" - self._media_title = "" + self._volume: float | None = None + self._muted: bool | None = None + self._state: str | None = None + self._media_position_updated_at: datetime | None = None + self._media_position: int | None = None + self._media_duration: int | None = None + self._vlc = vlc + self._available = available + self._volume_bkp = 0.0 + self._media_artist: str | None = None + self._media_title: str | None = None + config_entry_id = config_entry.entry_id + self._attr_unique_id = config_entry_id + self._attr_device_info = { + "name": name, + "identifiers": {(DOMAIN, config_entry_id)}, + "manufacturer": "VideoLAN", + "entry_type": "service", + } - def update(self): + async def async_update(self) -> None: """Get the latest details from the device.""" - if self._vlc is None: + if not self._available: try: - self._vlc = VLCTelnet(self._host, self._password, self._port) - except (ConnErr, EOFError) as err: - if self._available: - _LOGGER.error("Connection error: %s", err) - self._available = False - self._vlc = None + await self._vlc.connect() + except ConnectError as err: + LOGGER.debug("Connection error: %s", err) + return + + try: + await self._vlc.login() + except AuthError: + LOGGER.debug("Failed to login to VLC") + self.hass.async_create_task( + self.hass.config_entries.async_reload(self._config_entry.entry_id) + ) return self._state = STATE_IDLE self._available = True + LOGGER.info("Connected to vlc host: %s", self._vlc.host) try: - status = self._vlc.status() - _LOGGER.debug("Status: %s", status) + status = await self._vlc.status() + LOGGER.debug("Status: %s", status) - if status: - if "volume" in status: - self._volume = status["volume"] / MAX_VOLUME - else: - self._volume = None - if "state" in status: - state = status["state"] - if state == "playing": - self._state = STATE_PLAYING - elif state == "paused": - self._state = STATE_PAUSED - else: - self._state = STATE_IDLE - else: - self._state = STATE_IDLE + self._volume = status.audio_volume / MAX_VOLUME + state = status.state + if state == "playing": + self._state = STATE_PLAYING + elif state == "paused": + self._state = STATE_PAUSED + else: + self._state = STATE_IDLE if self._state != STATE_IDLE: - self._media_duration = self._vlc.get_length() - vlc_position = self._vlc.get_time() + self._media_duration = (await self._vlc.get_length()).length + time_output = await self._vlc.get_time() + vlc_position = time_output.time # Check if current position is stale. if vlc_position != self._media_position: self._media_position_updated_at = dt_util.utcnow() self._media_position = vlc_position - info = self._vlc.info() - _LOGGER.debug("Info: %s", info) + info = await self._vlc.info() + data = info.data + LOGGER.debug("Info data: %s", data) - if info: - self._media_artist = info.get(0, {}).get("artist") - self._media_title = info.get(0, {}).get("title") + self._media_artist = data.get(0, {}).get("artist") + self._media_title = data.get(0, {}).get("title") - if not self._media_title: - # Fall back to filename. - data_info = info.get("data") - if data_info: - self._media_title = data_info["filename"] + if not self._media_title: + # Fall back to filename. + data_info = data.get("data") + if data_info: + self._media_title = data_info["filename"] - except (CommandError, LuaError, ParseError) as err: - _LOGGER.error("Command error: %s", err) - except (ConnErr, EOFError) as err: + except CommandError as err: + LOGGER.error("Command error: %s", err) + except ConnectError as err: if self._available: - _LOGGER.error("Connection error: %s", err) + LOGGER.error("Connection error: %s", err) self._available = False - self._vlc = None @property def name(self): @@ -186,7 +206,7 @@ class VlcDevice(MediaPlayerEntity): return self._available @property - def volume_level(self): + def volume_level(self) -> float | None: """Volume level of the media player (0..1).""" return self._volume @@ -230,72 +250,79 @@ class VlcDevice(MediaPlayerEntity): """Artist of current playing media, music track only.""" return self._media_artist - def media_seek(self, position): + async def async_media_seek(self, position: float) -> None: """Seek the media to a specific location.""" - self._vlc.seek(int(position)) + await self._vlc.seek(round(position)) - def mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Mute the volume.""" + assert self._volume is not None if mute: self._volume_bkp = self._volume - self.set_volume_level(0) + await self.async_set_volume_level(0) else: - self.set_volume_level(self._volume_bkp) + await self.async_set_volume_level(self._volume_bkp) self._muted = mute - def set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" - self._vlc.set_volume(volume * MAX_VOLUME) + await self._vlc.set_volume(round(volume * MAX_VOLUME)) self._volume = volume if self._muted and self._volume > 0: # This can happen if we were muted and then see a volume_up. self._muted = False - def media_play(self): + async def async_media_play(self) -> None: """Send play command.""" - self._vlc.play() + await self._vlc.play() self._state = STATE_PLAYING - def media_pause(self): + async def async_media_pause(self) -> None: """Send pause command.""" - current_state = self._vlc.status().get("state") + status = await self._vlc.status() + current_state = status.state if current_state != "paused": # Make sure we're not already paused since VLCTelnet.pause() toggles # pause. - self._vlc.pause() + await self._vlc.pause() + self._state = STATE_PAUSED - def media_stop(self): + async def async_media_stop(self) -> None: """Send stop command.""" - self._vlc.stop() + await self._vlc.stop() self._state = STATE_IDLE - def 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 from a URL or file.""" if media_type != MEDIA_TYPE_MUSIC: - _LOGGER.error( + LOGGER.error( "Invalid media type %s. Only %s is supported", media_type, MEDIA_TYPE_MUSIC, ) return - self._vlc.add(media_id) + + await self._vlc.add(media_id) self._state = STATE_PLAYING - def media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send previous track command.""" - self._vlc.prev() + await self._vlc.prev() - def media_next_track(self): + async def async_media_next_track(self) -> None: """Send next track command.""" - self._vlc.next() + await self._vlc.next() - def clear_playlist(self): + async def async_clear_playlist(self) -> None: """Clear players playlist.""" - self._vlc.clear() + await self._vlc.clear() - def set_shuffle(self, shuffle): + async def async_set_shuffle(self, shuffle: bool) -> None: """Enable/disable shuffle mode.""" - self._vlc.random(shuffle) + shuffle_command = "on" if shuffle else "off" + await self._vlc.random(shuffle_command) diff --git a/homeassistant/components/vlc_telnet/strings.json b/homeassistant/components/vlc_telnet/strings.json new file mode 100644 index 00000000000..dbdae9755ea --- /dev/null +++ b/homeassistant/components/vlc_telnet/strings.json @@ -0,0 +1,30 @@ +{ + "config": { + "flow_title": "{host}", + "step": { + "reauth_confirm": { + "description": "Please enter the correct password for host: {host}", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } + }, + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "password": "[%key:common::config_flow::data::password%]", + "name": "[%key:common::config_flow::data::name%]" + } + } + }, + "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_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + } +} diff --git a/homeassistant/components/vlc_telnet/translations/en.json b/homeassistant/components/vlc_telnet/translations/en.json new file mode 100644 index 00000000000..3f7cbadb4b7 --- /dev/null +++ b/homeassistant/components/vlc_telnet/translations/en.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "flow_title": "{host}", + "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "Please enter the correct password for host: {host}" + }, + "user": { + "data": { + "host": "Host", + "name": "Name", + "password": "Password", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index cf42c2bd24f..39719402239 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -309,6 +309,7 @@ FLOWS = [ "vesync", "vilfo", "vizio", + "vlc_telnet", "volumio", "wallbox", "watttime", diff --git a/requirements_all.txt b/requirements_all.txt index e855c20caac..f7d712ea07e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -254,6 +254,9 @@ aiotractive==0.5.2 # homeassistant.components.unifi aiounifi==27 +# homeassistant.components.vlc_telnet +aiovlc==0.1.0 + # homeassistant.components.watttime aiowatttime==0.1.1 @@ -1930,9 +1933,6 @@ python-tado==0.12.0 # homeassistant.components.telegram_bot python-telegram-bot==13.1 -# homeassistant.components.vlc_telnet -python-telnet-vlc==2.0.1 - # homeassistant.components.twitch python-twitch-client==0.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 64bb1109159..75b37fab7c0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -181,6 +181,9 @@ aiotractive==0.5.2 # homeassistant.components.unifi aiounifi==27 +# homeassistant.components.vlc_telnet +aiovlc==0.1.0 + # homeassistant.components.watttime aiowatttime==0.1.1 diff --git a/tests/components/vlc_telnet/__init__.py b/tests/components/vlc_telnet/__init__.py new file mode 100644 index 00000000000..8cc5b40b465 --- /dev/null +++ b/tests/components/vlc_telnet/__init__.py @@ -0,0 +1 @@ +"""Test the VLC media player Telnet integration.""" diff --git a/tests/components/vlc_telnet/test_config_flow.py b/tests/components/vlc_telnet/test_config_flow.py new file mode 100644 index 00000000000..7865648d565 --- /dev/null +++ b/tests/components/vlc_telnet/test_config_flow.py @@ -0,0 +1,272 @@ +"""Test the VLC media player Telnet config flow.""" +from __future__ import annotations + +from typing import Any +from unittest.mock import patch + +from aiovlc.exceptions import AuthError, ConnectError +import pytest + +from homeassistant import config_entries +from homeassistant.components.vlc_telnet.const import DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +# mypy: allow-untyped-calls + + +@pytest.mark.parametrize( + "input_data, entry_data", + [ + ( + { + "password": "test-password", + "host": "1.1.1.1", + "port": 8888, + }, + { + "password": "test-password", + "host": "1.1.1.1", + "port": 8888, + }, + ), + ( + { + "password": "test-password", + }, + { + "password": "test-password", + "host": "localhost", + "port": 4212, + }, + ), + ], +) +async def test_user_flow( + hass: HomeAssistant, input_data: dict[str, Any], entry_data: dict[str, Any] +) -> None: + """Test successful user flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch("homeassistant.components.vlc_telnet.config_flow.Client.connect"), patch( + "homeassistant.components.vlc_telnet.config_flow.Client.login" + ), patch( + "homeassistant.components.vlc_telnet.config_flow.Client.disconnect" + ), patch( + "homeassistant.components.vlc_telnet.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + input_data, + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == entry_data["host"] + assert result["data"] == entry_data + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_flow(hass: HomeAssistant) -> None: + """Test successful import flow.""" + with patch("homeassistant.components.vlc_telnet.config_flow.Client.connect"), patch( + "homeassistant.components.vlc_telnet.config_flow.Client.login" + ), patch( + "homeassistant.components.vlc_telnet.config_flow.Client.disconnect" + ), patch( + "homeassistant.components.vlc_telnet.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={ + "password": "test-password", + "host": "1.1.1.1", + "port": 8888, + "name": "custom name", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == "custom name" + assert result["data"] == { + "password": "test-password", + "host": "1.1.1.1", + "port": 8888, + "name": "custom name", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "source", [config_entries.SOURCE_USER, config_entries.SOURCE_IMPORT] +) +async def test_abort_already_configured(hass: HomeAssistant, source: str) -> None: + """Test we handle already configured host.""" + entry_data = { + "password": "test-password", + "host": "1.1.1.1", + "port": 8888, + "name": "custom name", + } + + entry = MockConfigEntry(domain=DOMAIN, data=entry_data) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": source}, + data=entry_data, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize( + "source", [config_entries.SOURCE_USER, config_entries.SOURCE_IMPORT] +) +@pytest.mark.parametrize( + "error, connect_side_effect, login_side_effect", + [ + ("invalid_auth", None, AuthError), + ("cannot_connect", ConnectError, None), + ("unknown", Exception, None), + ], +) +async def test_errors( + hass: HomeAssistant, + error: str, + connect_side_effect: Exception | None, + login_side_effect: Exception | None, + source: str, +) -> None: + """Test we handle form errors.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source} + ) + + with patch( + "homeassistant.components.vlc_telnet.config_flow.Client.connect", + side_effect=connect_side_effect, + ), patch( + "homeassistant.components.vlc_telnet.config_flow.Client.login", + side_effect=login_side_effect, + ), patch( + "homeassistant.components.vlc_telnet.config_flow.Client.disconnect" + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"password": "test-password"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": error} + + +async def test_reauth_flow(hass: HomeAssistant) -> None: + """Test successful reauth flow.""" + entry_data = { + "password": "old-password", + "host": "1.1.1.1", + "port": 8888, + "name": "custom name", + } + + entry = MockConfigEntry(domain=DOMAIN, data=entry_data) + 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, + "unique_id": entry.unique_id, + }, + data=entry_data, + ) + + with patch("homeassistant.components.vlc_telnet.config_flow.Client.connect"), patch( + "homeassistant.components.vlc_telnet.config_flow.Client.login" + ), patch( + "homeassistant.components.vlc_telnet.config_flow.Client.disconnect" + ), patch( + "homeassistant.components.vlc_telnet.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"password": "new-password"}, + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "reauth_successful" + assert len(mock_setup_entry.mock_calls) == 1 + assert dict(entry.data) == { + "password": "new-password", + "host": "1.1.1.1", + "port": 8888, + "name": "custom name", + } + + +@pytest.mark.parametrize( + "error, connect_side_effect, login_side_effect", + [ + ("invalid_auth", None, AuthError), + ("cannot_connect", ConnectError, None), + ("unknown", Exception, None), + ], +) +async def test_reauth_errors( + hass: HomeAssistant, + error: str, + connect_side_effect: Exception | None, + login_side_effect: Exception | None, +) -> None: + """Test we handle reauth errors.""" + entry_data = { + "password": "old-password", + "host": "1.1.1.1", + "port": 8888, + "name": "custom name", + } + + entry = MockConfigEntry(domain=DOMAIN, data=entry_data) + 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, + "unique_id": entry.unique_id, + }, + data=entry_data, + ) + + with patch( + "homeassistant.components.vlc_telnet.config_flow.Client.connect", + side_effect=connect_side_effect, + ), patch( + "homeassistant.components.vlc_telnet.config_flow.Client.login", + side_effect=login_side_effect, + ), patch( + "homeassistant.components.vlc_telnet.config_flow.Client.disconnect" + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"password": "test-password"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": error} From 9be3278ffaae62ba5c6492585fdb283043137bda Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 15 Oct 2021 21:32:12 +0200 Subject: [PATCH 0412/1038] Add Emergency Button (sos) device support to Tuya (#57794) --- homeassistant/components/tuya/binary_sensor.py | 14 ++++++++++++++ homeassistant/components/tuya/const.py | 3 +++ homeassistant/components/tuya/sensor.py | 17 +++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index a00e4e19e1a..665ee88e550 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -8,6 +8,7 @@ from tuya_iot import TuyaDevice, TuyaDeviceManager from homeassistant.components.binary_sensor import ( DEVICE_CLASS_DOOR, DEVICE_CLASS_MOTION, + DEVICE_CLASS_SAFETY, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -61,6 +62,19 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ), + # Emergency Button + # https://developer.tuya.com/en/docs/iot/categorysos?id=Kaiuz3oi6agjy + "sos": ( + TuyaBinarySensorEntityDescription( + key=DPCode.SOS_STATE, + device_class=DEVICE_CLASS_SAFETY, + ), + TuyaBinarySensorEntityDescription( + key=DPCode.TEMPER_ALARM, + name="Tamper", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + ), } diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index f25dacaab37..43ecefec13b 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -45,6 +45,7 @@ TUYA_SUPPORTED_PRODUCT_CATEGORIES = ( "pc", # Power Strip "pir", # PIR Detector "qn", # Heater + "sos", # SOS Button "wk", # Thermostat "xdd", # Ceiling Light "xxj", # Diffuser @@ -100,6 +101,8 @@ class DPCode(str, Enum): POWDER_SET = "powder_set" # Powder PUMP_RESET = "pump_reset" # Water pump reset SHAKE = "shake" # Oscillating + SOS = "sos" # Emergency State + SOS_STATE = "sos_state" # Emergency mode SPEED = "speed" # Speed level START = "start" # Start SWING = "swing" # Swing mode diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 0d6d179ab76..d90f5a8e72c 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -92,6 +92,23 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ), + # Emergency Button + # https://developer.tuya.com/en/docs/iot/categorysos?id=Kaiuz3oi6agjy + "sos": ( + SensorEntityDescription( + key=DPCode.BATTERY_PERCENTAGE, + name="Battery", + native_unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key=DPCode.BATTERY_STATE, + name="Battery State", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + ), } # Socket (duplicate of `kg`) From 12d1dfdaf9fcaab2f27d031bd858241eab252e96 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 15 Oct 2021 21:36:03 +0200 Subject: [PATCH 0413/1038] Use assignment expressions 10 (#57791) --- .../components/device_automation/__init__.py | 6 +-- .../device_automation/toggle_entity.py | 6 +-- .../components/emulated_hue/hue_api.py | 11 ++-- .../components/fan/reproduce_state.py | 4 +- .../components/google_assistant/helpers.py | 9 ++-- .../components/google_assistant/smart_home.py | 11 ++-- .../components/google_assistant/trait.py | 50 ++++++++----------- homeassistant/components/group/__init__.py | 19 ++----- homeassistant/components/group/cover.py | 12 ++--- homeassistant/components/group/util.py | 3 +- 10 files changed, 44 insertions(+), 87 deletions(-) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 567e579d8b8..e3aca4884b5 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -159,8 +159,7 @@ async def _async_get_device_automations( for device_id in match_device_ids: combined_results[device_id] = [] - device = device_registry.async_get(device_id) - if device is None: + if (device := device_registry.async_get(device_id)) is None: raise DeviceNotFound for entry_id in device.config_entries: if config_entry := hass.config_entries.async_get_entry(entry_id): @@ -221,8 +220,7 @@ async def _async_get_device_automation_capabilities(hass, automation_type, autom capabilities = capabilities.copy() - extra_fields = capabilities.get("extra_fields") - if extra_fields is None: + if (extra_fields := capabilities.get("extra_fields")) is None: capabilities["extra_fields"] = [] else: capabilities["extra_fields"] = voluptuous_serialize.convert( diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 5d08f8d9d31..99473777658 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -129,8 +129,7 @@ async def async_call_action_from_config( @callback def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: """Evaluate state based on configuration.""" - condition_type = config[CONF_TYPE] - if condition_type == CONF_IS_ON: + if config[CONF_TYPE] == CONF_IS_ON: stat = "on" else: stat = "off" @@ -152,8 +151,7 @@ async def async_attach_trigger( automation_info: AutomationTriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - trigger_type = config[CONF_TYPE] - if trigger_type == CONF_TURNED_ON: + if config[CONF_TYPE] == CONF_TURNED_ON: to_state = "on" else: to_state = "off" diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index a7106f5105f..3b5d1e7831e 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -294,9 +294,7 @@ class HueOneLightStateView(HomeAssistantView): ) return self.json_message("Entity not found", HTTPStatus.NOT_FOUND) - entity = hass.states.get(hass_entity_id) - - if entity is None: + if (entity := hass.states.get(hass_entity_id)) is None: _LOGGER.error("Entity not found: %s", hass_entity_id) return self.json_message("Entity not found", HTTPStatus.NOT_FOUND) @@ -333,9 +331,7 @@ class HueOneLightChangeView(HomeAssistantView): _LOGGER.error("Unknown entity number: %s", entity_number) return self.json_message("Entity not found", HTTPStatus.NOT_FOUND) - entity = hass.states.get(entity_id) - - if entity is None: + if (entity := hass.states.get(entity_id)) is None: _LOGGER.error("Entity not found: %s", entity_id) return self.json_message("Entity not found", HTTPStatus.NOT_FOUND) @@ -544,8 +540,7 @@ class HueOneLightChangeView(HomeAssistantView): ): domain = entity.domain # Convert 0-100 to a fan speed - brightness = parsed[STATE_BRIGHTNESS] - if brightness == 0: + if (brightness := parsed[STATE_BRIGHTNESS]) == 0: data[ATTR_SPEED] = SPEED_OFF elif 0 < brightness <= 33.3: data[ATTR_SPEED] = SPEED_LOW diff --git a/homeassistant/components/fan/reproduce_state.py b/homeassistant/components/fan/reproduce_state.py index 2d4244ec2dc..c18e8352b24 100644 --- a/homeassistant/components/fan/reproduce_state.py +++ b/homeassistant/components/fan/reproduce_state.py @@ -50,9 +50,7 @@ async def _async_reproduce_state( reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" - cur_state = hass.states.get(state.entity_id) - - if cur_state is None: + if (cur_state := hass.states.get(state.entity_id)) is None: _LOGGER.warning("Unable to find entity %s", state.entity_id) return diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 4e3ade38e39..534fdcfac1e 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -53,8 +53,7 @@ async def _get_entity_and_device( hass.helpers.entity_registry.async_get_registry(), ) - entity_entry = ent_reg.async_get(entity_id) - if not entity_entry: + if not (entity_entry := ent_reg.async_get(entity_id)): return None, None device_entry = dev_reg.devices.get(entity_entry.device_id) return entity_entry, device_entry @@ -500,8 +499,7 @@ class GoogleEntity: } # use aliases - aliases = entity_config.get(CONF_ALIASES) - if aliases: + if aliases := entity_config.get(CONF_ALIASES): device["name"]["nicknames"] = [name] + aliases if self.config.is_local_sdk_active and self.should_expose_local(): @@ -518,8 +516,7 @@ class GoogleEntity: for trt in traits: device["attributes"].update(trt.sync_attributes()) - room = entity_config.get(CONF_ROOM_HINT) - if room: + if room := entity_config.get(CONF_ROOM_HINT): device["roomHint"] = room else: area = await _get_area(self.hass, entity_entry, device_entry) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index dc55509b534..c9f6c20c7af 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -45,9 +45,7 @@ async def _process(hass, data, message): "payload": {"errorCode": ERR_PROTOCOL_ERROR}, } - handler = HANDLERS.get(inputs[0].get("intent")) - - if handler is None: + if (handler := HANDLERS.get(inputs[0].get("intent"))) is None: return { "requestId": data.request_id, "payload": {"errorCode": ERR_PROTOCOL_ERROR}, @@ -131,9 +129,8 @@ async def async_devices_query(hass, data, payload): devices = {} for device in payload_devices: devid = device["id"] - state = hass.states.get(devid) - if not state: + if not (state := hass.states.get(devid)): # If we can't find a state, the device is offline devices[devid] = {"online": False} continue @@ -199,9 +196,7 @@ async def handle_devices_execute(hass, data, payload): executions[entity_id].append(execution) continue - state = hass.states.get(entity_id) - - if state is None: + if (state := hass.states.get(entity_id)) is None: results[entity_id] = { "ids": [entity_id], "status": "ERROR", diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index fea2ea4a310..0301769aea7 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -954,16 +954,14 @@ class TemperatureSettingTrait(_Trait): 1, ) else: - target_temp = attrs.get(ATTR_TEMPERATURE) - if target_temp is not None: + if (target_temp := attrs.get(ATTR_TEMPERATURE)) is not None: target_temp = round( temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1 ) response["thermostatTemperatureSetpointHigh"] = target_temp response["thermostatTemperatureSetpointLow"] = target_temp else: - target_temp = attrs.get(ATTR_TEMPERATURE) - if target_temp is not None: + if (target_temp := attrs.get(ATTR_TEMPERATURE)) is not None: response["thermostatTemperatureSetpoint"] = round( temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1 ) @@ -1306,11 +1304,9 @@ class ArmDisArmTrait(_Trait): async def execute(self, command, data, params, challenge): """Execute an ArmDisarm command.""" if params["arm"] and not params.get("cancel"): - arm_level = params.get("armLevel") - # If no arm level given, we can only arm it if there is # only one supported arm type. We never default to triggered. - if not arm_level: + if not (arm_level := params.get("armLevel")): states = self._supported_states() if STATE_ALARM_TRIGGERED in states: @@ -1554,9 +1550,7 @@ class ModesTrait(_Trait): if self.state.domain != domain: continue - items = self.state.attributes.get(attr) - - if items is not None: + if (items := self.state.attributes.get(attr)) is not None: modes.append(self._generate(name, items)) # Shortcut since all domains are currently unique @@ -1668,19 +1662,19 @@ class ModesTrait(_Trait): ) return - if self.state.domain == media_player.DOMAIN: - sound_mode = settings.get("sound mode") - if sound_mode: - await self.hass.services.async_call( - media_player.DOMAIN, - media_player.SERVICE_SELECT_SOUND_MODE, - { - ATTR_ENTITY_ID: self.state.entity_id, - media_player.ATTR_SOUND_MODE: sound_mode, - }, - blocking=True, - context=data.context, - ) + if self.state.domain == media_player.DOMAIN and ( + sound_mode := settings.get("sound mode") + ): + await self.hass.services.async_call( + media_player.DOMAIN, + media_player.SERVICE_SELECT_SOUND_MODE, + { + ATTR_ENTITY_ID: self.state.entity_id, + media_player.ATTR_SOUND_MODE: sound_mode, + }, + blocking=True, + context=data.context, + ) _LOGGER.info( "Received an Options command for unrecognised domain %s", @@ -2042,9 +2036,7 @@ def _verify_pin_challenge(data, state, challenge): if not challenge: raise ChallengeNeeded(CHALLENGE_PIN_NEEDED) - pin = challenge.get("pin") - - if pin != data.config.secure_devices_pin: + if challenge.get("pin") != data.config.secure_devices_pin: raise ChallengeNeeded(CHALLENGE_FAILED_PIN_NEEDED) @@ -2320,8 +2312,7 @@ class SensorStateTrait(_Trait): def sync_attributes(self): """Return attributes for a sync request.""" device_class = self.state.attributes.get(ATTR_DEVICE_CLASS) - data = self.sensor_types.get(device_class) - if data is not None: + if (data := self.sensor_types.get(device_class)) is not None: return { "sensorStatesSupported": { "name": data[0], @@ -2332,8 +2323,7 @@ class SensorStateTrait(_Trait): def query_attributes(self): """Return the attributes of this trait for this entity.""" device_class = self.state.attributes.get(ATTR_DEVICE_CLASS) - data = self.sensor_types.get(device_class) - if data is not None: + if (data := self.sensor_types.get(device_class)) is not None: return { "currentSensorStateData": [ {"name": data[0], "rawValue": self.state.state} diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index dad8f943328..b06c25f48c9 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -121,9 +121,7 @@ def is_on(hass, entity_id): # Integration not setup yet, it cannot be on return False - state = hass.states.get(entity_id) - - if state is not None: + if (state := hass.states.get(entity_id)) is not None: return state.state in hass.data[REG_KEY].on_off_mapping return False @@ -213,9 +211,7 @@ def groups_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: async def async_setup(hass, config): """Set up all groups found defined in the configuration.""" - component = hass.data.get(DOMAIN) - - if component is None: + if (component := hass.data.get(DOMAIN)) is None: component = hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass) hass.data[REG_KEY] = GroupIntegrationRegistry() @@ -507,9 +503,7 @@ class Group(Entity): ) # If called before the platform async_setup is called (test cases) - component = hass.data.get(DOMAIN) - - if component is None: + if (component := hass.data.get(DOMAIN)) is None: component = hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass) await component.async_add_entities([group]) @@ -661,9 +655,8 @@ class Group(Entity): return self.async_set_context(event.context) - new_state = event.data.get("new_state") - if new_state is None: + if (new_state := event.data.get("new_state")) is None: # The state was removed from the state machine self._reset_tracked_state() @@ -677,9 +670,7 @@ class Group(Entity): self._on_states = set() for entity_id in self.trackable: - state = self.hass.states.get(entity_id) - - if state is not None: + if (state := self.hass.states.get(entity_id)) is not None: self._see_state(state) def _see_state(self, new_state): diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index 68ad61c33fc..8c4e260b8c1 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -112,8 +112,7 @@ class CoverGroup(GroupEntity, CoverEntity): async def _update_supported_features_event(self, event: Event) -> None: self.async_set_context(event.context) - entity = event.data.get("entity_id") - if entity is not None: + if (entity := event.data.get("entity_id")) is not None: await self.async_update_supported_features( entity, event.data.get("new_state") ) @@ -168,8 +167,7 @@ class CoverGroup(GroupEntity, CoverEntity): async def async_added_to_hass(self) -> None: """Register listeners.""" for entity_id in self._entities: - new_state = self.hass.states.get(entity_id) - if new_state is None: + if (new_state := self.hass.states.get(entity_id)) is None: continue await self.async_update_supported_features( entity_id, new_state, update_state=False @@ -264,8 +262,7 @@ class CoverGroup(GroupEntity, CoverEntity): self._attr_is_opening = False has_valid_state = False for entity_id in self._entities: - state = self.hass.states.get(entity_id) - if not state: + if not (state := self.hass.states.get(entity_id)): continue if state.state == STATE_OPEN: self._attr_is_closed = False @@ -322,8 +319,7 @@ class CoverGroup(GroupEntity, CoverEntity): if not self._attr_assumed_state: for entity_id in self._entities: - state = self.hass.states.get(entity_id) - if state is None: + if (state := self.hass.states.get(entity_id)) is None: continue if state and state.attributes.get(ATTR_ASSUMED_STATE): self._attr_assumed_state = True diff --git a/homeassistant/components/group/util.py b/homeassistant/components/group/util.py index 0944ceb6745..d1e40f616d4 100644 --- a/homeassistant/components/group/util.py +++ b/homeassistant/components/group/util.py @@ -11,8 +11,7 @@ from homeassistant.core import State def find_state_attributes(states: list[State], key: str) -> Iterator[Any]: """Find attributes with matching key from states.""" for state in states: - value = state.attributes.get(key) - if value is not None: + if (value := state.attributes.get(key)) is not None: yield value From b75f1b8951724a32e7e28cb046684466d331a6d2 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Fri, 15 Oct 2021 22:03:03 +0200 Subject: [PATCH 0414/1038] Fix broken upnp derived sensors reporting b/s instead of kb/s (#57681) --- homeassistant/components/upnp/__init__.py | 2 +- homeassistant/components/upnp/sensor.py | 5 +- tests/components/upnp/conftest.py | 30 +++++- tests/components/upnp/test_binary_sensor.py | 42 ++++++++ tests/components/upnp/test_config_flow.py | 3 +- tests/components/upnp/test_init.py | 5 - tests/components/upnp/test_sensor.py | 114 ++++++++++++++++++++ 7 files changed, 191 insertions(+), 10 deletions(-) create mode 100644 tests/components/upnp/test_binary_sensor.py create mode 100644 tests/components/upnp/test_sensor.py diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index d2d59d78c0e..ef3bad6da47 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -264,5 +264,5 @@ class UpnpEntity(CoordinatorEntity): def available(self) -> bool: """Return if entity is available.""" return super().available and ( - self.coordinator.data.get(self.entity_description.key) or False + self.coordinator.data.get(self.entity_description.key) is not None ) diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 334dc9e8c22..f7cc242f6f1 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -190,7 +190,10 @@ class DerivedUpnpSensor(UpnpSensor): # Calculate derivative. delta_value = current_value - self._last_value - if self.entity_description.native_unit_of_measurement == DATA_BYTES: + if ( + self.entity_description.native_unit_of_measurement + == DATA_RATE_KIBIBYTES_PER_SECOND + ): delta_value /= KIBIBYTE delta_time = current_timestamp - self._last_timestamp if delta_time.total_seconds() == 0: diff --git a/tests/components/upnp/conftest.py b/tests/components/upnp/conftest.py index 5af99e9ac2d..54a7fce44fb 100644 --- a/tests/components/upnp/conftest.py +++ b/tests/components/upnp/conftest.py @@ -9,6 +9,9 @@ from homeassistant.components import ssdp from homeassistant.components.upnp.const import ( BYTES_RECEIVED, BYTES_SENT, + CONFIG_ENTRY_ST, + CONFIG_ENTRY_UDN, + DOMAIN, PACKETS_RECEIVED, PACKETS_SENT, ROUTER_IP, @@ -19,6 +22,8 @@ from homeassistant.components.upnp.const import ( from homeassistant.core import HomeAssistant from homeassistant.util import dt +from tests.common import MockConfigEntry + TEST_UDN = "uuid:device" TEST_ST = "urn:schemas-upnp-org:device:InternetGatewayDevice:1" TEST_USN = f"{TEST_UDN}::{TEST_ST}" @@ -115,8 +120,8 @@ class MockDevice: self.status_times_polled += 1 return { WAN_STATUS: "Connected", - ROUTER_UPTIME: 0, - ROUTER_IP: "192.168.0.1", + ROUTER_UPTIME: 10, + ROUTER_IP: "8.9.10.11", } @@ -185,3 +190,24 @@ async def ssdp_no_discovery(): return_value=[], ) as mock_get_info: yield (mock_register, mock_get_info) + + +@pytest.fixture +async def setup_integration( + hass: HomeAssistant, mock_get_source_ip, ssdp_instant_discovery, mock_upnp_device +): + """Create an initialized integration.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONFIG_ENTRY_UDN: TEST_UDN, + CONFIG_ENTRY_ST: TEST_ST, + }, + ) + + # Load config_entry. + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + yield entry diff --git a/tests/components/upnp/test_binary_sensor.py b/tests/components/upnp/test_binary_sensor.py new file mode 100644 index 00000000000..46f0021a07b --- /dev/null +++ b/tests/components/upnp/test_binary_sensor.py @@ -0,0 +1,42 @@ +"""Tests for UPnP/IGD binary_sensor.""" + +from datetime import timedelta +from unittest.mock import AsyncMock + +from homeassistant.components.upnp.const import ( + DOMAIN, + ROUTER_IP, + ROUTER_UPTIME, + WAN_STATUS, +) +from homeassistant.core import HomeAssistant +import homeassistant.util.dt as dt_util + +from .conftest import MockDevice + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_upnp_binary_sensors( + hass: HomeAssistant, setup_integration: MockConfigEntry +): + """Test normal sensors.""" + mock_device: MockDevice = hass.data[DOMAIN][setup_integration.entry_id].device + + # First poll. + wan_status_state = hass.states.get("binary_sensor.mock_name_wan_status") + assert wan_status_state.state == "on" + + # Second poll. + mock_device.async_get_status = AsyncMock( + return_value={ + WAN_STATUS: "Disconnected", + ROUTER_UPTIME: 100, + ROUTER_IP: "", + } + ) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=31)) + await hass.async_block_till_done() + + wan_status_state = hass.states.get("binary_sensor.mock_name_wan_status") + assert wan_status_state.state == "off" diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index fa315804917..a704232ef84 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -25,6 +25,7 @@ from .conftest import ( TEST_ST, TEST_UDN, TEST_USN, + MockDevice, ) from tests.common import MockConfigEntry, async_fire_time_changed @@ -196,7 +197,7 @@ async def test_options_flow(hass: HomeAssistant): config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) is True await hass.async_block_till_done() - mock_device = hass.data[DOMAIN][config_entry.entry_id].device + mock_device: MockDevice = hass.data[DOMAIN][config_entry.entry_id].device # Reset. mock_device.traffic_times_polled = 0 diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index 6b3d2a5187f..7729068a2ed 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -9,7 +9,6 @@ from homeassistant.components.upnp.const import ( DOMAIN, ) from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from .conftest import TEST_ST, TEST_UDN @@ -28,10 +27,6 @@ async def test_async_setup_entry_default(hass: HomeAssistant): }, ) - # Initialisation of component, no device discovered. - await async_setup_component(hass, DOMAIN, {}) - await hass.async_block_till_done() - # Load config_entry. entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) is True diff --git a/tests/components/upnp/test_sensor.py b/tests/components/upnp/test_sensor.py new file mode 100644 index 00000000000..068b5260d45 --- /dev/null +++ b/tests/components/upnp/test_sensor.py @@ -0,0 +1,114 @@ +"""Tests for UPnP/IGD sensor.""" + +from datetime import timedelta +from unittest.mock import AsyncMock + +from homeassistant.components.upnp.const import ( + BYTES_RECEIVED, + BYTES_SENT, + DOMAIN, + PACKETS_RECEIVED, + PACKETS_SENT, + ROUTER_IP, + ROUTER_UPTIME, + TIMESTAMP, + UPDATE_INTERVAL, + WAN_STATUS, +) +from homeassistant.core import HomeAssistant +import homeassistant.util.dt as dt_util + +from .conftest import MockDevice + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_upnp_sensors(hass: HomeAssistant, setup_integration: MockConfigEntry): + """Test normal sensors.""" + mock_device: MockDevice = hass.data[DOMAIN][setup_integration.entry_id].device + + # First poll. + b_received_state = hass.states.get("sensor.mock_name_b_received") + b_sent_state = hass.states.get("sensor.mock_name_b_sent") + packets_received_state = hass.states.get("sensor.mock_name_packets_received") + packets_sent_state = hass.states.get("sensor.mock_name_packets_sent") + external_ip_state = hass.states.get("sensor.mock_name_external_ip") + wan_status_state = hass.states.get("sensor.mock_name_wan_status") + assert b_received_state.state == "0" + assert b_sent_state.state == "0" + assert packets_received_state.state == "0" + assert packets_sent_state.state == "0" + assert external_ip_state.state == "8.9.10.11" + assert wan_status_state.state == "Connected" + + # Second poll. + mock_device.async_get_traffic_data = AsyncMock( + return_value={ + TIMESTAMP: dt_util.utcnow() + UPDATE_INTERVAL, + BYTES_RECEIVED: 10240, + BYTES_SENT: 20480, + PACKETS_RECEIVED: 30, + PACKETS_SENT: 40, + } + ) + mock_device.async_get_status = AsyncMock( + return_value={ + WAN_STATUS: "Disconnected", + ROUTER_UPTIME: 100, + ROUTER_IP: "", + } + ) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=31)) + await hass.async_block_till_done() + + b_received_state = hass.states.get("sensor.mock_name_b_received") + b_sent_state = hass.states.get("sensor.mock_name_b_sent") + packets_received_state = hass.states.get("sensor.mock_name_packets_received") + packets_sent_state = hass.states.get("sensor.mock_name_packets_sent") + external_ip_state = hass.states.get("sensor.mock_name_external_ip") + wan_status_state = hass.states.get("sensor.mock_name_wan_status") + assert b_received_state.state == "10240" + assert b_sent_state.state == "20480" + assert packets_received_state.state == "30" + assert packets_sent_state.state == "40" + assert external_ip_state.state == "" + assert wan_status_state.state == "Disconnected" + + +async def test_derived_upnp_sensors( + hass: HomeAssistant, setup_integration: MockConfigEntry +): + """Test derived sensors.""" + mock_device: MockDevice = hass.data[DOMAIN][setup_integration.entry_id].device + + # First poll. + kib_s_received_state = hass.states.get("sensor.mock_name_kib_s_received") + kib_s_sent_state = hass.states.get("sensor.mock_name_kib_s_sent") + packets_s_received_state = hass.states.get("sensor.mock_name_packets_s_received") + packets_s_sent_state = hass.states.get("sensor.mock_name_packets_s_sent") + assert kib_s_received_state.state == "unknown" + assert kib_s_sent_state.state == "unknown" + assert packets_s_received_state.state == "unknown" + assert packets_s_sent_state.state == "unknown" + + # Second poll. + mock_device.async_get_traffic_data = AsyncMock( + return_value={ + TIMESTAMP: dt_util.utcnow() + UPDATE_INTERVAL, + BYTES_RECEIVED: int(10240 * UPDATE_INTERVAL.total_seconds()), + BYTES_SENT: int(20480 * UPDATE_INTERVAL.total_seconds()), + PACKETS_RECEIVED: int(30 * UPDATE_INTERVAL.total_seconds()), + PACKETS_SENT: int(40 * UPDATE_INTERVAL.total_seconds()), + } + ) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=31)) + await hass.async_block_till_done() + + kib_s_received_state = hass.states.get("sensor.mock_name_kib_s_received") + kib_s_sent_state = hass.states.get("sensor.mock_name_kib_s_sent") + packets_s_received_state = hass.states.get("sensor.mock_name_packets_s_received") + packets_s_sent_state = hass.states.get("sensor.mock_name_packets_s_sent") + assert kib_s_received_state.state == "10.0" + assert kib_s_sent_state.state == "20.0" + assert packets_s_received_state.state == "30.0" + assert packets_s_sent_state.state == "40.0" From 6e5d49144a768e09c5a38045901307901168a088 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 15 Oct 2021 22:28:14 +0200 Subject: [PATCH 0415/1038] Add siren platform to Tuya (#57780) --- .coveragerc | 1 + homeassistant/components/tuya/const.py | 7 ++ homeassistant/components/tuya/number.py | 10 +++ homeassistant/components/tuya/select.py | 15 ++++ homeassistant/components/tuya/siren.py | 92 +++++++++++++++++++++++++ homeassistant/components/tuya/switch.py | 10 +++ 6 files changed, 135 insertions(+) create mode 100644 homeassistant/components/tuya/siren.py diff --git a/.coveragerc b/.coveragerc index d6fe547ac83..35cdb0002f1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1120,6 +1120,7 @@ omit = homeassistant/components/tuya/scene.py homeassistant/components/tuya/select.py homeassistant/components/tuya/sensor.py + homeassistant/components/tuya/siren.py homeassistant/components/tuya/switch.py homeassistant/components/twentemilieu/const.py homeassistant/components/twentemilieu/sensor.py diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 43ecefec13b..48524f6b1f2 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -46,6 +46,7 @@ TUYA_SUPPORTED_PRODUCT_CATEGORIES = ( "pir", # PIR Detector "qn", # Heater "sos", # SOS Button + "sgbj", # Siren Alarm "wk", # Thermostat "xdd", # Ceiling Light "xxj", # Diffuser @@ -63,6 +64,7 @@ PLATFORMS = [ "scene", "select", "sensor", + "siren", "switch", ] @@ -73,9 +75,13 @@ class DPCode(str, Enum): https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq """ + ALARM_SWITCH = "alarm_switch" # Alarm switch + ALARM_TIME = "alarm_time" # Alarm time + ALARM_VOLUME = "alarm_volume" # Alarm volume ANION = "anion" # Ionizer unit BATTERY_PERCENTAGE = "battery_percentage" # Battery percentage BATTERY_STATE = "battery_state" # Battery state + BRIGHT_STATE = "Brightness" # Brightness BRIGHT_VALUE = "bright_value" # Brightness C_F = "c_f" # Temperature unit switching CHILD_LOCK = "child_lock" # Child lock @@ -98,6 +104,7 @@ class DPCode(str, Enum): MATERIAL = "material" # Material MODE = "mode" # Working mode / Mode PIR = "pir" # Motion sensor + MUFFLING = "muffling" # Muffling POWDER_SET = "powder_set" # Powder PUMP_RESET = "pump_reset" # Water pump reset SHAKE = "shake" # Oscillating diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index 91ad3f0e6ba..15c7bf3150d 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -8,6 +8,7 @@ from tuya_iot.device import TuyaDeviceStatusRange from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -47,6 +48,15 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { entity_registry_enabled_default=False, ), ), + # Siren Alarm + # https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu + "sgbj": ( + NumberEntityDescription( + key=DPCode.ALARM_TIME, + name="Time", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), } diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index 2b033757bc9..3c99e131284 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -8,6 +8,7 @@ from tuya_iot.device import TuyaDeviceStatusRange from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -42,6 +43,20 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { name="Mode", ), ), + # Siren Alarm + # https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu + "sgbj": ( + SelectEntityDescription( + key=DPCode.ALARM_VOLUME, + name="Volume", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + SelectEntityDescription( + key=DPCode.BRIGHT_STATE, + name="Brightness", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), } diff --git a/homeassistant/components/tuya/siren.py b/homeassistant/components/tuya/siren.py new file mode 100644 index 00000000000..42b1f6839f5 --- /dev/null +++ b/homeassistant/components/tuya/siren.py @@ -0,0 +1,92 @@ +"""Support for Tuya siren.""" +from __future__ import annotations + +from typing import Any + +from tuya_iot import TuyaDevice, TuyaDeviceManager + +from homeassistant.components.siren import SirenEntity, SirenEntityDescription +from homeassistant.components.siren.const import SUPPORT_TURN_OFF, SUPPORT_TURN_ON +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 . import HomeAssistantTuyaData +from .base import TuyaEntity +from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode + +# All descriptions can be found here: +# https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq +SIRENS: dict[str, tuple[SirenEntityDescription, ...]] = { + # Siren Alarm + # https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu + "sgbj": ( + SirenEntityDescription( + key=DPCode.ALARM_SWITCH, + name="Siren", + ), + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Tuya siren dynamically through Tuya discovery.""" + hass_data: HomeAssistantTuyaData = hass.data[DOMAIN][entry.entry_id] + + @callback + def async_discover_device(device_ids: list[str]) -> None: + """Discover and add a discovered Tuya siren.""" + entities: list[TuyaSirenEntity] = [] + for device_id in device_ids: + device = hass_data.device_manager.device_map[device_id] + if descriptions := SIRENS.get(device.category): + for description in descriptions: + if ( + description.key in device.function + or description.key in device.status + ): + entities.append( + TuyaSirenEntity( + device, hass_data.device_manager, description + ) + ) + + async_add_entities(entities) + + async_discover_device([*hass_data.device_manager.device_map]) + + entry.async_on_unload( + async_dispatcher_connect(hass, TUYA_DISCOVERY_NEW, async_discover_device) + ) + + +class TuyaSirenEntity(TuyaEntity, SirenEntity): + """Tuya Siren Entity.""" + + def __init__( + self, + device: TuyaDevice, + device_manager: TuyaDeviceManager, + description: SirenEntityDescription, + ) -> None: + """Init Tuya Siren.""" + super().__init__(device, device_manager) + self.entity_description = description + self._attr_unique_id = f"{super().unique_id}{description.key}" + self._attr_supported_features = SUPPORT_TURN_ON | SUPPORT_TURN_OFF + + @property + def is_on(self) -> bool: + """Return true if siren is on.""" + return self.device.status.get(self.entity_description.key, False) + + def turn_on(self, **kwargs: Any) -> None: + """Turn the siren on.""" + self._send_command([{"code": self.entity_description.key, "value": True}]) + + def turn_off(self, **kwargs: Any) -> None: + """Turn the siren off.""" + self._send_command([{"code": self.entity_description.key, "value": False}]) diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index e00fea62c7a..a828b6c6936 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -11,6 +11,7 @@ from homeassistant.components.switch import ( SwitchEntityDescription, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -237,6 +238,15 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { device_class=DEVICE_CLASS_OUTLET, ), ), + # Siren Alarm + # https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu + "sgbj": ( + SwitchEntityDescription( + key=DPCode.MUFFLING, + name="Muffling", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), # Diffuser "xxj": ( SwitchEntityDescription( From a7c7e58a5b1aa3b68111fe0e871d7392eed0cc76 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 15 Oct 2021 23:08:21 +0200 Subject: [PATCH 0416/1038] Add Luminance Sensor (ldcg) device support to Tuya (#57797) --- .../components/tuya/binary_sensor.py | 11 +++- homeassistant/components/tuya/const.py | 7 ++- homeassistant/components/tuya/sensor.py | 51 +++++++++++++++++++ 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index 665ee88e550..d2047f818ae 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -45,7 +45,16 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { TuyaBinarySensorEntityDescription( key=DPCode.TEMPER_ALARM, name="Tamper", - entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + ), + # Luminance Sensor + # https://developer.tuya.com/en/docs/iot/categoryldcg?id=Kaiuz3n7u69l8 + "ldcg": ( + TuyaBinarySensorEntityDescription( + key=DPCode.TEMPER_ALARM, + name="Tamper", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ), # PIR Detector diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 48524f6b1f2..b68d2573717 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -41,6 +41,7 @@ TUYA_SUPPORTED_PRODUCT_CATEGORIES = ( "kg", # Switch "kj", # Air Purifier "kt", # Air conditioner + "ldcg", # Luminance Sensor "mcs", # Door Window Sensor "pc", # Power Strip "pir", # PIR Detector @@ -81,10 +82,11 @@ class DPCode(str, Enum): ANION = "anion" # Ionizer unit BATTERY_PERCENTAGE = "battery_percentage" # Battery percentage BATTERY_STATE = "battery_state" # Battery state - BRIGHT_STATE = "Brightness" # Brightness + BRIGHT_STATE = "bright_state" # Brightness status BRIGHT_VALUE = "bright_value" # Brightness C_F = "c_f" # Temperature unit switching CHILD_LOCK = "child_lock" # Child lock + CO2_VALUE = "co2_value" # CO2 concentration COLOUR_DATA = "colour_data" # Colored light mode COLOUR_DATA_V2 = "colour_data_v2" # Colored light mode CONCENTRATION_SET = "concentration_set" # Concentration setting @@ -99,12 +101,13 @@ class DPCode(str, Enum): FILTER_RESET = "filter_reset" # Filter (cartridge) reset HUMIDITY_CURRENT = "humidity_current" # Current humidity HUMIDITY_SET = "humidity_set" # Humidity setting + HUMIDITY_VALUE = "humidity_value" # Humidity LIGHT = "light" # Light LOCK = "lock" # Lock / Child lock MATERIAL = "material" # Material MODE = "mode" # Working mode / Mode - PIR = "pir" # Motion sensor MUFFLING = "muffling" # Muffling + PIR = "pir" # Motion sensor POWDER_SET = "powder_set" # Powder PUMP_RESET = "pump_reset" # Water pump reset SHAKE = "shake" # Oscillating diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index d90f5a8e72c..b21d54bb999 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -14,8 +14,12 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + DEVICE_CLASS_CO2, DEVICE_CLASS_CURRENT, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, @@ -75,6 +79,53 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { entity_registry_enabled_default=False, ), ), + # Luminance Sensor + # https://developer.tuya.com/en/docs/iot/categoryldcg?id=Kaiuz3n7u69l8 + "ldcg": ( + SensorEntityDescription( + key=DPCode.BRIGHT_STATE, + name="Luminosity", + icon="mdi:brightness-6", + ), + SensorEntityDescription( + key=DPCode.BRIGHT_VALUE, + name="Luminosity", + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.TEMP_CURRENT, + name="Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.HUMIDITY_VALUE, + name="Humidity", + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.CO2_VALUE, + name="Carbon Dioxide (CO2)", + device_class=DEVICE_CLASS_CO2, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.BATTERY_PERCENTAGE, + name="Battery", + native_unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key=DPCode.BATTERY_STATE, + name="Battery State", + icon="mdi:battery", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + ), # PIR Detector # https://developer.tuya.com/en/docs/iot/categorypir?id=Kaiuz3ss11b80 "pir": ( From 0ad5ad5ca7fb5702161401c372fe09e550f151e5 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 15 Oct 2021 17:34:13 -0400 Subject: [PATCH 0417/1038] Don't use cast when possible for goalzero (#57742) * Don't use cast when possible for goalzero * tweak * tweak * tweak * Call first refresh on coordinator * don't use dict.get if not needed * tweak --- homeassistant/components/goalzero/__init__.py | 5 +++-- homeassistant/components/goalzero/binary_sensor.py | 4 ++-- homeassistant/components/goalzero/sensor.py | 5 +++-- homeassistant/components/goalzero/switch.py | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py index 53daa29de8a..08ad70c0a79 100644 --- a/homeassistant/components/goalzero/__init__.py +++ b/homeassistant/components/goalzero/__init__.py @@ -68,6 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_method=async_update_data, update_interval=MIN_TIME_BETWEEN_UPDATES, ) + await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { DATA_KEY_API: api, @@ -112,6 +113,6 @@ class YetiEntity(CoordinatorEntity): ATTR_IDENTIFIERS: {(DOMAIN, self._server_unique_id)}, ATTR_MANUFACTURER: "Goal Zero", ATTR_NAME: self._name, - ATTR_MODEL: self.api.sysdata.get(ATTR_MODEL), - ATTR_SW_VERSION: self.api.data.get("firmwareVersion"), + ATTR_MODEL: self.api.sysdata[ATTR_MODEL], + ATTR_SW_VERSION: self.api.data["firmwareVersion"], } diff --git a/homeassistant/components/goalzero/binary_sensor.py b/homeassistant/components/goalzero/binary_sensor.py index 0e61c7178bb..ddde8c80e96 100644 --- a/homeassistant/components/goalzero/binary_sensor.py +++ b/homeassistant/components/goalzero/binary_sensor.py @@ -82,5 +82,5 @@ class YetiBinarySensor(YetiEntity, BinarySensorEntity): @property def is_on(self) -> bool: - """Return if the service is on.""" - return cast(bool, self.api.data.get(self.entity_description.key) == 1) + """Return True if the service is on.""" + return cast(bool, self.api.data[self.entity_description.key] == 1) diff --git a/homeassistant/components/goalzero/sensor.py b/homeassistant/components/goalzero/sensor.py index bbf3fba753f..bd775ab82bb 100644 --- a/homeassistant/components/goalzero/sensor.py +++ b/homeassistant/components/goalzero/sensor.py @@ -31,6 +31,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import Yeti, YetiEntity @@ -170,6 +171,6 @@ class YetiSensor(YetiEntity, SensorEntity): self._attr_unique_id = f"{server_unique_id}/{description.key}" @property - def native_value(self) -> str: + def native_value(self) -> StateType: """Return the state.""" - return cast(str, self.api.data.get(self.entity_description.key)) + return cast(StateType, self.api.data[self.entity_description.key]) diff --git a/homeassistant/components/goalzero/switch.py b/homeassistant/components/goalzero/switch.py index 2932413465a..6c80a773a74 100644 --- a/homeassistant/components/goalzero/switch.py +++ b/homeassistant/components/goalzero/switch.py @@ -67,7 +67,7 @@ class YetiSwitch(YetiEntity, SwitchEntity): @property def is_on(self) -> bool: """Return state of the switch.""" - return cast(bool, self.api.data.get(self.entity_description.key)) + return cast(bool, self.api.data[self.entity_description.key] == 1) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" From 34984d78a1ea2efe1a2f43b50aa77943054e791c Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sat, 16 Oct 2021 01:06:36 +0200 Subject: [PATCH 0418/1038] Add float32 test to modbus (#57805) --- tests/components/modbus/test_sensor.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index 6bc4a352d93..4f631b279c1 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -541,6 +541,17 @@ async def test_config_wrong_struct_sensor(hass, error_message, mock_modbus, capl False, str(int(0x04030201)), ), + ( + { + CONF_COUNT: 2, + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, + CONF_DATA_TYPE: DataType.FLOAT32, + CONF_PRECISION: 2, + }, + [16286, 1617], + False, + "1.23", + ), ], ) async def test_all_sensor(hass, mock_do_cycle, expected): From 34fee4ba6071ba3e291ed2b3039ca40adcdd3a12 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 16 Oct 2021 00:12:06 +0000 Subject: [PATCH 0419/1038] [ci skip] Translation update --- .../cert_expiry/translations/cs.json | 6 ++-- .../co2signal/translations/zh-Hans.json | 27 +++++++++++++++-- .../components/coinbase/translations/cs.json | 7 +++-- .../components/dsmr/translations/cs.json | 17 +++++++++++ .../components/efergy/translations/cs.json | 14 +++++++++ .../components/efergy/translations/he.json | 20 +++++++++++++ .../emulated_roku/translations/cs.json | 5 +++- .../environment_canada/translations/cs.json | 17 +++++++++++ .../environment_canada/translations/he.json | 16 ++++++++++ .../environment_canada/translations/tr.json | 15 ++++++++++ .../fjaraskupan/translations/cs.json | 5 ++++ .../components/flux_led/translations/tr.json | 11 +++++++ .../geonetnz_quakes/translations/cs.json | 4 ++- .../components/hangouts/translations/cs.json | 2 ++ .../rainforest_eagle/translations/cs.json | 4 ++- .../components/smappee/translations/de.json | 2 +- .../components/soma/translations/ca.json | 8 ++--- .../components/soma/translations/de.json | 6 ++-- .../components/soma/translations/en.json | 6 ++-- .../components/soma/translations/et.json | 6 ++-- .../components/soma/translations/he.json | 8 +++++ .../components/soma/translations/ru.json | 4 +-- .../components/tradfri/translations/hu.json | 1 + .../components/tuya/translations/tr.json | 1 + .../twentemilieu/translations/cs.json | 1 + .../uptimerobot/translations/ca.json | 4 +-- .../uptimerobot/translations/de.json | 4 +-- .../uptimerobot/translations/en.json | 4 +-- .../uptimerobot/translations/et.json | 4 +-- .../uptimerobot/translations/ru.json | 4 +-- .../components/velbus/translations/cs.json | 3 +- .../vlc_telnet/translations/de.json | 30 +++++++++++++++++++ .../vlc_telnet/translations/et.json | 30 +++++++++++++++++++ .../vlc_telnet/translations/hu.json | 30 +++++++++++++++++++ .../vlc_telnet/translations/nl.json | 30 +++++++++++++++++++ .../vlc_telnet/translations/tr.json | 23 ++++++++++++++ .../components/watttime/translations/cs.json | 6 ++++ .../components/watttime/translations/de.json | 2 +- .../components/watttime/translations/he.json | 9 +++++- .../components/watttime/translations/tr.json | 11 +++++++ .../xiaomi_miio/translations/ca.json | 3 +- .../xiaomi_miio/translations/de.json | 3 +- .../xiaomi_miio/translations/en.json | 4 +-- .../xiaomi_miio/translations/et.json | 3 +- .../xiaomi_miio/translations/hu.json | 3 +- .../xiaomi_miio/translations/nl.json | 3 +- .../xiaomi_miio/translations/ru.json | 3 +- .../components/zha/translations/cs.json | 2 ++ .../components/zwave_js/translations/cs.json | 21 +++++++++++++ .../components/zwave_js/translations/hu.json | 2 +- 50 files changed, 407 insertions(+), 47 deletions(-) create mode 100644 homeassistant/components/efergy/translations/cs.json create mode 100644 homeassistant/components/efergy/translations/he.json create mode 100644 homeassistant/components/environment_canada/translations/cs.json create mode 100644 homeassistant/components/environment_canada/translations/he.json create mode 100644 homeassistant/components/environment_canada/translations/tr.json create mode 100644 homeassistant/components/flux_led/translations/tr.json create mode 100644 homeassistant/components/vlc_telnet/translations/de.json create mode 100644 homeassistant/components/vlc_telnet/translations/et.json create mode 100644 homeassistant/components/vlc_telnet/translations/hu.json create mode 100644 homeassistant/components/vlc_telnet/translations/nl.json create mode 100644 homeassistant/components/vlc_telnet/translations/tr.json create mode 100644 homeassistant/components/watttime/translations/tr.json diff --git a/homeassistant/components/cert_expiry/translations/cs.json b/homeassistant/components/cert_expiry/translations/cs.json index c4b61df7084..44adaa19710 100644 --- a/homeassistant/components/cert_expiry/translations/cs.json +++ b/homeassistant/components/cert_expiry/translations/cs.json @@ -6,7 +6,8 @@ }, "error": { "connection_refused": "P\u0159ipojen\u00ed bylo odm\u00edtnuto p\u0159i p\u0159ipojov\u00e1n\u00ed k hostiteli", - "connection_timeout": "\u010casov\u00fd limit p\u0159i p\u0159ipojen\u00ed k tomuto hostiteli vypr\u0161el" + "connection_timeout": "\u010casov\u00fd limit p\u0159i p\u0159ipojen\u00ed k tomuto hostiteli vypr\u0161el", + "resolve_failed": "Tohoto hostitele nelze vy\u0159e\u0161it" }, "step": { "user": { @@ -14,7 +15,8 @@ "host": "Hostitel", "name": "N\u00e1zev certifik\u00e1tu", "port": "Port" - } + }, + "title": "Definujte certifik\u00e1t, kter\u00fd chcete testovat" } } }, diff --git a/homeassistant/components/co2signal/translations/zh-Hans.json b/homeassistant/components/co2signal/translations/zh-Hans.json index af750541de5..b883b58c215 100644 --- a/homeassistant/components/co2signal/translations/zh-Hans.json +++ b/homeassistant/components/co2signal/translations/zh-Hans.json @@ -1,10 +1,33 @@ { "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86", + "api_ratelimit": "API \u8c03\u7528\u9891\u7387\u8d85\u9650", + "unknown": "\u975e\u9884\u671f\u7684\u9519\u8bef" + }, + "error": { + "api_ratelimit": "API \u8c03\u7528\u9891\u7387\u8d85\u9650", + "invalid_auth": "\u8eab\u4efd\u8ba4\u8bc1\u65e0\u6548", + "unknown": "\u975e\u9884\u671f\u7684\u9519\u8bef" + }, "step": { + "coordinates": { + "data": { + "latitude": "\u7eac\u5ea6", + "longitude": "\u7ecf\u5ea6" + } + }, + "country": { + "data": { + "country_code": "\u56fd\u5bb6/\u5730\u533a\u4ee3\u7801" + } + }, "user": { "data": { - "api_key": "\u8bbf\u95ee\u4ee4\u724c" - } + "api_key": "\u8bbf\u95ee token", + "location": "\u83b7\u53d6\u6570\u636e\u7684\u4f4d\u7f6e" + }, + "description": "\u8bf7\u8bbf\u95ee https://co2signal.com/ \u6765\u83b7\u53d6 token\u3002" } } } diff --git a/homeassistant/components/coinbase/translations/cs.json b/homeassistant/components/coinbase/translations/cs.json index c6f6a1f36f9..d25e431651d 100644 --- a/homeassistant/components/coinbase/translations/cs.json +++ b/homeassistant/components/coinbase/translations/cs.json @@ -11,8 +11,11 @@ "step": { "user": { "data": { - "api_key": "Kl\u00ed\u010d API" - } + "api_key": "Kl\u00ed\u010d API", + "api_token": "API Secret", + "exchange_rates": "Sm\u011bnn\u00e9 kurzy" + }, + "title": "Podrobnosti o API kl\u00ed\u010di Coinbase" } } }, diff --git a/homeassistant/components/dsmr/translations/cs.json b/homeassistant/components/dsmr/translations/cs.json index 9b38d280bdf..8078da1b1a2 100644 --- a/homeassistant/components/dsmr/translations/cs.json +++ b/homeassistant/components/dsmr/translations/cs.json @@ -2,6 +2,23 @@ "config": { "abort": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "step": { + "setup_serial": { + "data": { + "port": "Vyberte za\u0159\u00edzen\u00ed" + }, + "title": "Za\u0159\u00edzen\u00ed" + }, + "setup_serial_manual_path": { + "title": "Cesta" + }, + "user": { + "data": { + "type": "Typ p\u0159ipojen\u00ed" + }, + "title": "Vyberte typ p\u0159ipojen\u00ed" + } } }, "options": { diff --git a/homeassistant/components/efergy/translations/cs.json b/homeassistant/components/efergy/translations/cs.json new file mode 100644 index 00000000000..f7128887dac --- /dev/null +++ b/homeassistant/components/efergy/translations/cs.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "api_key": "API kl\u00ed\u010d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/he.json b/homeassistant/components/efergy/translations/he.json new file mode 100644 index 00000000000..4b0fd849742 --- /dev/null +++ b/homeassistant/components/efergy/translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "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", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/translations/cs.json b/homeassistant/components/emulated_roku/translations/cs.json index c84810814ed..0c44bb73ff0 100644 --- a/homeassistant/components/emulated_roku/translations/cs.json +++ b/homeassistant/components/emulated_roku/translations/cs.json @@ -6,9 +6,12 @@ "step": { "user": { "data": { + "advertise_port": "Port odesl\u00e1n\u00ed", "host_ip": "IP adresa hostitele", + "listen_port": "Port p\u0159\u00edjmu", "name": "Jm\u00e9no" - } + }, + "title": "Definice konfigurace serveru" } } }, diff --git a/homeassistant/components/environment_canada/translations/cs.json b/homeassistant/components/environment_canada/translations/cs.json new file mode 100644 index 00000000000..b8a770a8405 --- /dev/null +++ b/homeassistant/components/environment_canada/translations/cs.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "language": "Jazyk informac\u00ed o po\u010das\u00ed", + "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", + "station": "ID meteorologick\u00e9 stanice" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/he.json b/homeassistant/components/environment_canada/translations/he.json new file mode 100644 index 00000000000..9cb8e90c9f4 --- /dev/null +++ b/homeassistant/components/environment_canada/translations/he.json @@ -0,0 +1,16 @@ +{ + "config": { + "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" + }, + "step": { + "user": { + "data": { + "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/tr.json b/homeassistant/components/environment_canada/translations/tr.json new file mode 100644 index 00000000000..d7a3a7e9e78 --- /dev/null +++ b/homeassistant/components/environment_canada/translations/tr.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Ba\u011flant\u0131 ba\u015far\u0131s\u0131z" + }, + "step": { + "user": { + "data": { + "language": "Hava durumu bilgisi dili", + "station": "Hava istasyonu ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fjaraskupan/translations/cs.json b/homeassistant/components/fjaraskupan/translations/cs.json index 3f0012e00d2..8d729713ed2 100644 --- a/homeassistant/components/fjaraskupan/translations/cs.json +++ b/homeassistant/components/fjaraskupan/translations/cs.json @@ -2,6 +2,11 @@ "config": { "abort": { "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + }, + "step": { + "confirm": { + "description": "Chcete nastavit Fj\u00e4r\u00e5skupan?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/tr.json b/homeassistant/components/flux_led/translations/tr.json new file mode 100644 index 00000000000..6b603ef7232 --- /dev/null +++ b/homeassistant/components/flux_led/translations/tr.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Sunucu" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/translations/cs.json b/homeassistant/components/geonetnz_quakes/translations/cs.json index 0ddca983798..3613280d3e5 100644 --- a/homeassistant/components/geonetnz_quakes/translations/cs.json +++ b/homeassistant/components/geonetnz_quakes/translations/cs.json @@ -6,8 +6,10 @@ "step": { "user": { "data": { + "mmi": "MMI", "radius": "Polom\u011br" - } + }, + "title": "Vypl\u0148te \u00fadaje filtru." } } } diff --git a/homeassistant/components/hangouts/translations/cs.json b/homeassistant/components/hangouts/translations/cs.json index 8e721ed5ff1..11bef6d1d1a 100644 --- a/homeassistant/components/hangouts/translations/cs.json +++ b/homeassistant/components/hangouts/translations/cs.json @@ -14,6 +14,7 @@ "data": { "2fa": "Dvoufaktorov\u00fd ov\u011b\u0159ovac\u00ed k\u00f3d" }, + "description": "Pr\u00e1zdn\u00e9", "title": "Dvoufaktorov\u00e9 ov\u011b\u0159en\u00ed" }, "user": { @@ -22,6 +23,7 @@ "email": "E-mail", "password": "Heslo" }, + "description": "Pr\u00e1zdn\u00e9", "title": "P\u0159ihl\u00e1\u0161en\u00ed do slu\u017eby Google Hangouts" } } diff --git a/homeassistant/components/rainforest_eagle/translations/cs.json b/homeassistant/components/rainforest_eagle/translations/cs.json index aae081e61fe..8e6f606cab0 100644 --- a/homeassistant/components/rainforest_eagle/translations/cs.json +++ b/homeassistant/components/rainforest_eagle/translations/cs.json @@ -11,7 +11,9 @@ "step": { "user": { "data": { - "host": "Hostitel" + "cloud_id": "Cloud ID", + "host": "Hostitel", + "install_code": "Instala\u010dn\u00ed k\u00f3d" } } } diff --git a/homeassistant/components/smappee/translations/de.json b/homeassistant/components/smappee/translations/de.json index 121b74e9627..127c0f4d725 100644 --- a/homeassistant/components/smappee/translations/de.json +++ b/homeassistant/components/smappee/translations/de.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured_device": "Ger\u00e4t ist bereits konfiguriert", - "already_configured_local_device": "Lokale(s) Ger\u00e4t(e) ist/sind bereits konfiguriert. Bitte entferne diese zuerst, bevor du ein Cloud-Ger\u00e4t konfigurierst.", + "already_configured_local_device": "Lokale(s) Ger\u00e4t(e) ist / sind bereits konfiguriert. Bitte entferne diese zuerst, bevor du ein Cloud-Ger\u00e4t konfigurierst.", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", "cannot_connect": "Verbindung fehlgeschlagen", "invalid_mdns": "Nicht unterst\u00fctztes Ger\u00e4t f\u00fcr die Smappee-Integration.", diff --git a/homeassistant/components/soma/translations/ca.json b/homeassistant/components/soma/translations/ca.json index 3d79c8d744d..f01968ba1e9 100644 --- a/homeassistant/components/soma/translations/ca.json +++ b/homeassistant/components/soma/translations/ca.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "already_setup": "Nom\u00e9s pots configurar un \u00fanic compte amb Soma.", - "authorize_url_timeout": "S'ha acabat el temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", - "connection_error": "No s'ha pogut connectar amb SOMA Connect.", + "already_setup": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.", + "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", + "connection_error": "Ha fallat la connexi\u00f3", "missing_configuration": "El component Soma no est\u00e0 configurat. Mira'n la documentaci\u00f3.", "result_error": "SOMA Connect ha respost amb un estat d'error." }, "create_entry": { - "default": "Autenticaci\u00f3 exitosa amb Soma." + "default": "Autenticaci\u00f3 exitosa" }, "step": { "user": { diff --git a/homeassistant/components/soma/translations/de.json b/homeassistant/components/soma/translations/de.json index 17cc5c1899d..e326432e0fb 100644 --- a/homeassistant/components/soma/translations/de.json +++ b/homeassistant/components/soma/translations/de.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "already_setup": "Du kannst nur ein einziges Soma-Konto konfigurieren.", + "already_setup": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", - "connection_error": "Verbindung zu SOMA Connect fehlgeschlagen.", + "connection_error": "Verbindung fehlgeschlagen", "missing_configuration": "Die Soma-Komponente ist nicht konfiguriert. Bitte folge der Dokumentation.", "result_error": "SOMA Connect hat mit einem Fehlerstatus geantwortet." }, "create_entry": { - "default": "Erfolgreich bei Soma authentifiziert." + "default": "Erfolgreich authentifiziert" }, "step": { "user": { diff --git a/homeassistant/components/soma/translations/en.json b/homeassistant/components/soma/translations/en.json index fb5d17ac59d..d37e8687375 100644 --- a/homeassistant/components/soma/translations/en.json +++ b/homeassistant/components/soma/translations/en.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "already_setup": "You can only configure one Soma account.", + "already_setup": "Already configured. Only a single configuration possible.", "authorize_url_timeout": "Timeout generating authorize URL.", - "connection_error": "Failed to connect to SOMA Connect.", + "connection_error": "Failed to connect", "missing_configuration": "The Soma component is not configured. Please follow the documentation.", "result_error": "SOMA Connect responded with error status." }, "create_entry": { - "default": "Successfully authenticated with Soma." + "default": "Successfully authenticated" }, "step": { "user": { diff --git a/homeassistant/components/soma/translations/et.json b/homeassistant/components/soma/translations/et.json index 728e7ec0879..871ef86a8b4 100644 --- a/homeassistant/components/soma/translations/et.json +++ b/homeassistant/components/soma/translations/et.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "already_setup": "Saad h\u00e4\u00e4lestada ainult \u00fche Soma konto.", + "already_setup": "Seade on juba h\u00e4\u00e4lestatud. Ainult \u00fcks konto on lubatud.", "authorize_url_timeout": "Kinnitus-URLi loomise ajal\u00f5pp.", - "connection_error": "SOMA Connect seadmega \u00fchenduse loomine nurjus.", + "connection_error": "\u00dchendamine nurjus", "missing_configuration": "Soma komponent pole seadistatud. Palun loe dokumentatsiooni.", "result_error": "SOMA Connect vastas t\u00f5rkeolekuga." }, "create_entry": { - "default": "Edukalt Soma'ga autenditud." + "default": "Tuvastamine \u00f5nnestus" }, "step": { "user": { diff --git a/homeassistant/components/soma/translations/he.json b/homeassistant/components/soma/translations/he.json index cbb3cd76a9d..eaf18a6ed85 100644 --- a/homeassistant/components/soma/translations/he.json +++ b/homeassistant/components/soma/translations/he.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_setup": "\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.", + "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.", + "connection_error": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "create_entry": { + "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/soma/translations/ru.json b/homeassistant/components/soma/translations/ru.json index 49ac4e65768..add73375eee 100644 --- a/homeassistant/components/soma/translations/ru.json +++ b/homeassistant/components/soma/translations/ru.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_setup": "\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.", "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.", - "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a SOMA Connect.", + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Soma \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d. \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.", "result_error": "SOMA Connect \u043e\u0442\u0432\u0435\u0442\u0438\u043b \u0441\u043e \u0441\u0442\u0430\u0442\u0443\u0441\u043e\u043c \u043e\u0448\u0438\u0431\u043a\u0438." }, diff --git a/homeassistant/components/tradfri/translations/hu.json b/homeassistant/components/tradfri/translations/hu.json index e5f749a83df..b1cb0ffd5ad 100644 --- a/homeassistant/components/tradfri/translations/hu.json +++ b/homeassistant/components/tradfri/translations/hu.json @@ -5,6 +5,7 @@ "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van." }, "error": { + "cannot_authenticate": "Sikertelen azonos\u00edt\u00e1s. A Gateway egy m\u00e1sik eszk\u00f6zzel van p\u00e1ros\u00edtva, mint p\u00e9ld\u00e1ul a Homekittel?", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_key": "Nem siker\u00fclt regisztr\u00e1lni a megadott kulcs seg\u00edts\u00e9g\u00e9vel. Ha ez t\u00f6bbsz\u00f6r megt\u00f6rt\u00e9nik, pr\u00f3b\u00e1lja meg \u00fajraind\u00edtani a gatewayt.", "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a k\u00f3d \u00e9rv\u00e9nyes\u00edt\u00e9se sor\u00e1n." diff --git a/homeassistant/components/tuya/translations/tr.json b/homeassistant/components/tuya/translations/tr.json index 2edf3276b6c..02f3c7b1692 100644 --- a/homeassistant/components/tuya/translations/tr.json +++ b/homeassistant/components/tuya/translations/tr.json @@ -15,6 +15,7 @@ "country_code": "Hesap \u00fclke kodunuz (\u00f6r. ABD i\u00e7in 1 veya \u00c7in i\u00e7in 86)", "password": "Parola", "platform": "Hesab\u0131n\u0131z\u0131n kay\u0131tl\u0131 oldu\u011fu uygulama", + "region": "B\u00f6lge", "username": "Kullan\u0131c\u0131 Ad\u0131" }, "description": "Tuya kimlik bilgilerinizi girin.", diff --git a/homeassistant/components/twentemilieu/translations/cs.json b/homeassistant/components/twentemilieu/translations/cs.json index 708796d04c5..2eb6e267b2c 100644 --- a/homeassistant/components/twentemilieu/translations/cs.json +++ b/homeassistant/components/twentemilieu/translations/cs.json @@ -9,6 +9,7 @@ "step": { "user": { "data": { + "house_letter": "Roz\u0161\u00ed\u0159en\u00ed ozna\u010den\u00ed \u010d\u00edsla domu", "house_number": "\u010c\u00edslo domu", "post_code": "PS\u010c" }, diff --git a/homeassistant/components/uptimerobot/translations/ca.json b/homeassistant/components/uptimerobot/translations/ca.json index b845e271666..9ae97109fc1 100644 --- a/homeassistant/components/uptimerobot/translations/ca.json +++ b/homeassistant/components/uptimerobot/translations/ca.json @@ -17,14 +17,14 @@ "data": { "api_key": "Clau API" }, - "description": "Has de proporcionar una nova clau API de nom\u00e9s lectura d'Uptime Robot", + "description": "Has de proporcionar una nova clau API de nom\u00e9s lectura d'UptimeRobot", "title": "Reautenticaci\u00f3 de la integraci\u00f3" }, "user": { "data": { "api_key": "Clau API" }, - "description": "Has de proporcionar una clau API de nom\u00e9s lectura d'Uptime Robot" + "description": "Has de proporcionar una clau API de nom\u00e9s lectura d'UptimeRobot" } } } diff --git a/homeassistant/components/uptimerobot/translations/de.json b/homeassistant/components/uptimerobot/translations/de.json index a25f58dfe0c..3ac63f84de2 100644 --- a/homeassistant/components/uptimerobot/translations/de.json +++ b/homeassistant/components/uptimerobot/translations/de.json @@ -17,14 +17,14 @@ "data": { "api_key": "API-Schl\u00fcssel" }, - "description": "Du musst einen neuen schreibgesch\u00fctzten API-Schl\u00fcssel von Uptime Robot bereitstellen.", + "description": "Du musst einen neuen schreibgesch\u00fctzten API-Schl\u00fcssel von UptimeRobot bereitstellen", "title": "Integration erneut authentifizieren" }, "user": { "data": { "api_key": "API-Schl\u00fcssel" }, - "description": "Du musst einen schreibgesch\u00fctzten API-Schl\u00fcssel von Uptime Robot bereitstellen." + "description": "Du musst einen schreibgesch\u00fctzten API-Schl\u00fcssel von UptimeRobot bereitstellen" } } } diff --git a/homeassistant/components/uptimerobot/translations/en.json b/homeassistant/components/uptimerobot/translations/en.json index ae1a8cf5e45..a78af34102d 100644 --- a/homeassistant/components/uptimerobot/translations/en.json +++ b/homeassistant/components/uptimerobot/translations/en.json @@ -17,14 +17,14 @@ "data": { "api_key": "API Key" }, - "description": "You need to supply a new read-only API key from Uptime Robot", + "description": "You need to supply a new read-only API key from UptimeRobot", "title": "Reauthenticate Integration" }, "user": { "data": { "api_key": "API Key" }, - "description": "You need to supply a read-only API key from Uptime Robot" + "description": "You need to supply a read-only API key from UptimeRobot" } } } diff --git a/homeassistant/components/uptimerobot/translations/et.json b/homeassistant/components/uptimerobot/translations/et.json index c679ea3b19b..9b00c2a5b44 100644 --- a/homeassistant/components/uptimerobot/translations/et.json +++ b/homeassistant/components/uptimerobot/translations/et.json @@ -17,14 +17,14 @@ "data": { "api_key": "API v\u00f5ti" }, - "description": "Pead sisestama uue Uptime Roboti kirjutuskaitstud API-v\u00f5tme", + "description": "Pead sisestama uue UptimeRoboti kirjutuskaitstud API-v\u00f5tme", "title": "Taastuvasta sidumine" }, "user": { "data": { "api_key": "API v\u00f5ti" }, - "description": "Pead sisestama Uptime Roboti kirjutuskaitstud API-v\u00f5tme" + "description": "Pead sisestama UptimeRoboti kirjutuskaitstud API-v\u00f5tme" } } } diff --git a/homeassistant/components/uptimerobot/translations/ru.json b/homeassistant/components/uptimerobot/translations/ru.json index 88da4b3b768..fd26dbf969a 100644 --- a/homeassistant/components/uptimerobot/translations/ru.json +++ b/homeassistant/components/uptimerobot/translations/ru.json @@ -17,14 +17,14 @@ "data": { "api_key": "\u041a\u043b\u044e\u0447 API" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043d\u043e\u0432\u044b\u0439 \u043a\u043b\u044e\u0447 API Uptime Robot \u0441 \u043f\u0440\u0430\u0432\u0430\u043c\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0447\u0442\u0435\u043d\u0438\u044f", + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043d\u043e\u0432\u044b\u0439 \u043a\u043b\u044e\u0447 API UptimeRobot \u0441 \u043f\u0440\u0430\u0432\u0430\u043c\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0447\u0442\u0435\u043d\u0438\u044f", "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" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043b\u044e\u0447 API Uptime Robot \u0441 \u043f\u0440\u0430\u0432\u0430\u043c\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0447\u0442\u0435\u043d\u0438\u044f" + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043b\u044e\u0447 API UptimeRobot \u0441 \u043f\u0440\u0430\u0432\u0430\u043c\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0447\u0442\u0435\u043d\u0438\u044f" } } } diff --git a/homeassistant/components/velbus/translations/cs.json b/homeassistant/components/velbus/translations/cs.json index bdddbd45d47..850e4415405 100644 --- a/homeassistant/components/velbus/translations/cs.json +++ b/homeassistant/components/velbus/translations/cs.json @@ -10,7 +10,8 @@ "step": { "user": { "data": { - "name": "Jm\u00e9no tohoto p\u0159ipojen\u00ed velbus" + "name": "Jm\u00e9no tohoto p\u0159ipojen\u00ed velbus", + "port": "P\u0159ipojovac\u00ed \u0159et\u011bzec" } } } diff --git a/homeassistant/components/vlc_telnet/translations/de.json b/homeassistant/components/vlc_telnet/translations/de.json new file mode 100644 index 00000000000..89d763fb458 --- /dev/null +++ b/homeassistant/components/vlc_telnet/translations/de.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Service ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "flow_title": "{host}", + "step": { + "reauth_confirm": { + "data": { + "password": "Passwort" + }, + "description": "Bitte gib das richtige Passwort f\u00fcr den Host ein: {host}" + }, + "user": { + "data": { + "host": "Host", + "name": "Name", + "password": "Passwort", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vlc_telnet/translations/et.json b/homeassistant/components/vlc_telnet/translations/et.json new file mode 100644 index 00000000000..54c28d2f084 --- /dev/null +++ b/homeassistant/components/vlc_telnet/translations/et.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "flow_title": "{host}", + "step": { + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "Sisesta hosti {host} jaoks \u00f5ige parool:" + }, + "user": { + "data": { + "host": "Host", + "name": "Kasutajanimi", + "password": "Salas\u00f5na", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vlc_telnet/translations/hu.json b/homeassistant/components/vlc_telnet/translations/hu.json new file mode 100644 index 00000000000..639dc690c13 --- /dev/null +++ b/homeassistant/components/vlc_telnet/translations/hu.json @@ -0,0 +1,30 @@ +{ + "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_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "flow_title": "{host}", + "step": { + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "K\u00e9rj\u00fck, adja meg a helyes jelsz\u00f3t: {host}" + }, + "user": { + "data": { + "host": "C\u00edm", + "name": "N\u00e9v", + "password": "Jelsz\u00f3", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vlc_telnet/translations/nl.json b/homeassistant/components/vlc_telnet/translations/nl.json new file mode 100644 index 00000000000..ad9fc759014 --- /dev/null +++ b/homeassistant/components/vlc_telnet/translations/nl.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Deze service is al geconfigureerd", + "reauth_successful": "Het opnieuw verifi\u00ebren is geslaagd" + }, + "error": { + "cannot_connect": "Verbinding tot stand brengen is mislukt", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "flow_title": "{host}", + "step": { + "reauth_confirm": { + "data": { + "password": "Wachtwoord" + }, + "description": "Voer het juiste wachtwoord voor de host in: {host}" + }, + "user": { + "data": { + "host": "Host", + "name": "Naam", + "password": "Wachtwoord", + "port": "Poort" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vlc_telnet/translations/tr.json b/homeassistant/components/vlc_telnet/translations/tr.json new file mode 100644 index 00000000000..72f3c337205 --- /dev/null +++ b/homeassistant/components/vlc_telnet/translations/tr.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "cannot_connect": "Ba\u011flant\u0131 ba\u015far\u0131s\u0131z" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "\u015eifre" + }, + "description": "L\u00fctfen sunucunun do\u011fru \u015fifresini giriniz: {host}" + }, + "user": { + "data": { + "host": "Sunucu", + "name": "\u0130sim", + "password": "\u015eifre", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/cs.json b/homeassistant/components/watttime/translations/cs.json index b4df21bd3a1..d67789246ba 100644 --- a/homeassistant/components/watttime/translations/cs.json +++ b/homeassistant/components/watttime/translations/cs.json @@ -19,6 +19,12 @@ "location_type": "Um\u00edst\u011bn\u00ed" } }, + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "description": "Zadejte pros\u00edm znovu heslo pro {username}:" + }, "user": { "data": { "password": "Heslo", diff --git a/homeassistant/components/watttime/translations/de.json b/homeassistant/components/watttime/translations/de.json index 65552140eca..c1a660e6ffe 100644 --- a/homeassistant/components/watttime/translations/de.json +++ b/homeassistant/components/watttime/translations/de.json @@ -27,7 +27,7 @@ "data": { "password": "Passwort" }, - "description": "Bitte gib das Passwort f\u00fcr {ame} erneut ein:", + "description": "Bitte gib das Passwort f\u00fcr {username} erneut ein:", "title": "Integration erneut authentifizieren" }, "user": { diff --git a/homeassistant/components/watttime/translations/he.json b/homeassistant/components/watttime/translations/he.json index bbc82f5fa86..3649a9f0cad 100644 --- a/homeassistant/components/watttime/translations/he.json +++ b/homeassistant/components/watttime/translations/he.json @@ -1,7 +1,8 @@ { "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": { "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", @@ -19,6 +20,12 @@ "location_type": "\u05de\u05d9\u05e7\u05d5\u05dd" } }, + "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/watttime/translations/tr.json b/homeassistant/components/watttime/translations/tr.json new file mode 100644 index 00000000000..866fc513d4a --- /dev/null +++ b/homeassistant/components/watttime/translations/tr.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u015eifre" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/ca.json b/homeassistant/components/xiaomi_miio/translations/ca.json index 7e9d7d5c7eb..cb36fbbb2e0 100644 --- a/homeassistant/components/xiaomi_miio/translations/ca.json +++ b/homeassistant/components/xiaomi_miio/translations/ca.json @@ -13,7 +13,8 @@ "cloud_login_error": "No s'ha pogut iniciar sessi\u00f3 a Xiaomi Miio Cloud, comprova les credencials.", "cloud_no_devices": "No s'han trobat dispositius en aquest compte al n\u00favol de Xiaomi Miio.", "no_device_selected": "No hi ha cap dispositiu seleccionat, selecciona'n un.", - "unknown_device": "No es reconeix el model del dispositiu, no es pot configurar el dispositiu mitjan\u00e7ant el flux de configuraci\u00f3." + "unknown_device": "No es reconeix el model del dispositiu, no es pot configurar el dispositiu mitjan\u00e7ant el flux de configuraci\u00f3.", + "wrong_token": "Error de verificaci\u00f3, token erroni" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/xiaomi_miio/translations/de.json b/homeassistant/components/xiaomi_miio/translations/de.json index 24e639e3a23..291323f31bd 100644 --- a/homeassistant/components/xiaomi_miio/translations/de.json +++ b/homeassistant/components/xiaomi_miio/translations/de.json @@ -13,7 +13,8 @@ "cloud_login_error": "Die Anmeldung bei Xiaomi Miio Cloud ist fehlgeschlagen, \u00fcberpr\u00fcfe die Anmeldedaten.", "cloud_no_devices": "Keine Ger\u00e4te in diesem Xiaomi Miio Cloud-Konto gefunden.", "no_device_selected": "Kein Ger\u00e4t ausgew\u00e4hlt, bitte w\u00e4hle ein Ger\u00e4t aus.", - "unknown_device": "Das Ger\u00e4temodell ist nicht bekannt und das Ger\u00e4t kann nicht mithilfe des Assistenten eingerichtet werden." + "unknown_device": "Das Ger\u00e4temodell ist nicht bekannt und das Ger\u00e4t kann nicht mithilfe des Assistenten eingerichtet werden.", + "wrong_token": "Pr\u00fcfsummenfehler, falscher Token" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/xiaomi_miio/translations/en.json b/homeassistant/components/xiaomi_miio/translations/en.json index 9ad0063df58..8bd35f3e7d9 100644 --- a/homeassistant/components/xiaomi_miio/translations/en.json +++ b/homeassistant/components/xiaomi_miio/translations/en.json @@ -9,12 +9,12 @@ }, "error": { "cannot_connect": "Failed to connect", - "wrong_token": "Checksum error, wrong token", "cloud_credentials_incomplete": "Cloud credentials incomplete, please fill in username, password and country", "cloud_login_error": "Could not login to Xiaomi Miio Cloud, check the credentials.", "cloud_no_devices": "No devices found in this Xiaomi Miio cloud account.", "no_device_selected": "No device selected, please select one device.", - "unknown_device": "The device model is not known, not able to setup the device using config flow." + "unknown_device": "The device model is not known, not able to setup the device using config flow.", + "wrong_token": "Checksum error, wrong token" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/xiaomi_miio/translations/et.json b/homeassistant/components/xiaomi_miio/translations/et.json index 4eb326d7f08..6fd3c4257ef 100644 --- a/homeassistant/components/xiaomi_miio/translations/et.json +++ b/homeassistant/components/xiaomi_miio/translations/et.json @@ -13,7 +13,8 @@ "cloud_login_error": "Xiaomi Miio Cloudi ei saanud sisse logida, kontrolli mandaati.", "cloud_no_devices": "Xiaomi Miio pilvekontolt ei leitud \u00fchtegi seadet.", "no_device_selected": "Seadmeid pole valitud, vali \u00fcks seade.", - "unknown_device": "Seadme mudel pole teada, seadet ei saa seadistamisvoo abil seadistada." + "unknown_device": "Seadme mudel pole teada, seadet ei saa seadistamisvoo abil seadistada.", + "wrong_token": "Kontrollsumma t\u00f5rge, vigane r\u00e4si" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/xiaomi_miio/translations/hu.json b/homeassistant/components/xiaomi_miio/translations/hu.json index 6d53aad6f56..89515a62beb 100644 --- a/homeassistant/components/xiaomi_miio/translations/hu.json +++ b/homeassistant/components/xiaomi_miio/translations/hu.json @@ -13,7 +13,8 @@ "cloud_login_error": "Nem siker\u00fclt bejelentkezni a Xioami Miio Cloud szolg\u00e1ltat\u00e1sba, ellen\u0151rizze a hiteles\u00edt\u0151 adatokat.", "cloud_no_devices": "Nincs eszk\u00f6z ebben a Xiaomi Miio felh\u0151fi\u00f3kban.", "no_device_selected": "Nincs kiv\u00e1lasztva eszk\u00f6z, k\u00e9rj\u00fck, v\u00e1lasszon egyet.", - "unknown_device": "Az eszk\u00f6z modell nem ismert, nem tudja be\u00e1ll\u00edtani az eszk\u00f6zt a konfigur\u00e1ci\u00f3s folyamat seg\u00edts\u00e9g\u00e9vel." + "unknown_device": "Az eszk\u00f6z modell nem ismert, nem tudja be\u00e1ll\u00edtani az eszk\u00f6zt a konfigur\u00e1ci\u00f3s folyamat seg\u00edts\u00e9g\u00e9vel.", + "wrong_token": "Ellen\u0151rz\u0151\u00f6sszeg-hiba, hib\u00e1s token" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/xiaomi_miio/translations/nl.json b/homeassistant/components/xiaomi_miio/translations/nl.json index 08c64808a3b..8ef2ef70fae 100644 --- a/homeassistant/components/xiaomi_miio/translations/nl.json +++ b/homeassistant/components/xiaomi_miio/translations/nl.json @@ -13,7 +13,8 @@ "cloud_login_error": "Kan niet inloggen op Xioami Miio Cloud, controleer de inloggegevens.", "cloud_no_devices": "Geen apparaten gevonden in dit Xiaomi Miio-cloudaccount.", "no_device_selected": "Geen apparaat geselecteerd, selecteer 1 apparaat alstublieft", - "unknown_device": "Het apparaatmodel is niet bekend, niet in staat om het apparaat in te stellen met config flow." + "unknown_device": "Het apparaatmodel is niet bekend, niet in staat om het apparaat in te stellen met config flow.", + "wrong_token": "Checksum-fout, verkeerd token" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/xiaomi_miio/translations/ru.json b/homeassistant/components/xiaomi_miio/translations/ru.json index 017660e51c6..101f442d656 100644 --- a/homeassistant/components/xiaomi_miio/translations/ru.json +++ b/homeassistant/components/xiaomi_miio/translations/ru.json @@ -13,7 +13,8 @@ "cloud_login_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0432\u043e\u0439\u0442\u0438 \u0432 Xiaomi Miio Cloud, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", "cloud_no_devices": "\u0412 \u044d\u0442\u043e\u0439 \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Xiaomi Miio \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b.", "no_device_selected": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0434\u043d\u043e \u0438\u0437 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432.", - "unknown_device": "\u041c\u043e\u0434\u0435\u043b\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430, \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043c\u0430\u0441\u0442\u0435\u0440\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." + "unknown_device": "\u041c\u043e\u0434\u0435\u043b\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430, \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043c\u0430\u0441\u0442\u0435\u0440\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.", + "wrong_token": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044c\u043d\u043e\u0439 \u0441\u0443\u043c\u043c\u044b, \u043d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d." }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/zha/translations/cs.json b/homeassistant/components/zha/translations/cs.json index 2422941312b..6a7d4aa17ea 100644 --- a/homeassistant/components/zha/translations/cs.json +++ b/homeassistant/components/zha/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "not_zha_device": "Toto za\u0159\u00edzen\u00ed nen\u00ed za\u0159\u00edzen\u00ed zha", "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." }, "error": { @@ -48,6 +49,7 @@ "turn_on": "Zapnout" }, "trigger_type": { + "device_dropped": "Za\u0159\u00edzen\u00ed vypado", "device_offline": "Za\u0159\u00edzen\u00ed je offline", "device_rotated": "Za\u0159\u00edzen\u00ed oto\u010deno \"{subtype}\"", "device_shaken": "Za\u0159\u00edzen\u00ed se zat\u0159\u00e1slo", diff --git a/homeassistant/components/zwave_js/translations/cs.json b/homeassistant/components/zwave_js/translations/cs.json index 013488d113f..5c16938b23b 100644 --- a/homeassistant/components/zwave_js/translations/cs.json +++ b/homeassistant/components/zwave_js/translations/cs.json @@ -37,5 +37,26 @@ "event.value_notification.scene_activation": "Aktivace sc\u00e9ny na {subtype}", "state.node_status": "Stav uzlu zm\u011bn\u011bn" } + }, + "options": { + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "configure_addon": { + "data": { + "log_level": "\u00darove\u0148 protokolu", + "network_key": "S\u00ed\u0165ov\u00fd kl\u00ed\u010d" + } + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "title": "Vyberte metodu p\u0159ipojen\u00ed" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/hu.json b/homeassistant/components/zwave_js/translations/hu.json index da3bed46f19..16b219d1b7c 100644 --- a/homeassistant/components/zwave_js/translations/hu.json +++ b/homeassistant/components/zwave_js/translations/hu.json @@ -64,7 +64,7 @@ "device_automation": { "action_type": { "clear_lock_usercode": "{entity_name} felhaszn\u00e1l\u00f3i k\u00f3dj\u00e1nak t\u00f6rl\u00e9se", - "ping": "Eszk\u00f6z pinget\u00e9se", + "ping": "Eszk\u00f6z pingel\u00e9se", "refresh_value": "{entity_name} \u00e9rt\u00e9keinek friss\u00edt\u00e9se", "reset_meter": "{subtype} m\u00e9r\u00e9sek alaphelyzetbe \u00e1ll\u00edt\u00e1sa", "set_config_parameter": "{subtype} konfigur\u00e1ci\u00f3s param\u00e9ter \u00e9rt\u00e9k\u00e9nek be\u00e1ll\u00edt\u00e1sa", From f13eeee969d776e5650f7f27537e65d9ba8e764b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 15 Oct 2021 21:33:26 -0600 Subject: [PATCH 0420/1038] Add configuration URL to RainMachine (#57732) --- homeassistant/components/rainmachine/__init__.py | 2 ++ homeassistant/components/rainmachine/binary_sensor.py | 2 +- homeassistant/components/rainmachine/sensor.py | 2 +- homeassistant/components/rainmachine/switch.py | 8 ++++---- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 72951223d09..0fee1259349 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -323,6 +323,7 @@ class RainMachineEntity(CoordinatorEntity): def __init__( self, + entry: ConfigEntry, coordinator: DataUpdateCoordinator, controller: Controller, description: EntityDescription, @@ -332,6 +333,7 @@ class RainMachineEntity(CoordinatorEntity): self._attr_device_info = { "identifiers": {(DOMAIN, 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), "manufacturer": "RainMachine", diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 7e886dbad90..78f3863dd16 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -131,7 +131,7 @@ async def async_setup_entry( async_add_entities( [ - async_get_sensor(description.api_category)(controller, description) + async_get_sensor(description.api_category)(entry, controller, description) for description in BINARY_SENSOR_DESCRIPTIONS ] ) diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 57b51472a6f..4eec124f936 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -114,7 +114,7 @@ async def async_setup_entry( async_add_entities( [ - async_get_sensor(description.api_category)(controller, description) + async_get_sensor(description.api_category)(entry, controller, description) for description in SENSOR_DESCRIPTIONS ] ) diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index bf84bc1f360..e693664bab4 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -157,9 +157,9 @@ async def async_setup_entry( entities: list[RainMachineProgram | RainMachineZone] = [ RainMachineProgram( + entry, programs_coordinator, controller, - entry, RainMachineSwitchDescription( key=f"RainMachineProgram_{uid}", name=program["name"], uid=uid ), @@ -169,9 +169,9 @@ async def async_setup_entry( entities.extend( [ RainMachineZone( + entry, zones_coordinator, controller, - entry, RainMachineSwitchDescription( key=f"RainMachineZone_{uid}", name=zone["name"], uid=uid ), @@ -191,13 +191,13 @@ class RainMachineSwitch(RainMachineEntity, SwitchEntity): def __init__( self, + entry: ConfigEntry, coordinator: DataUpdateCoordinator, controller: Controller, - entry: ConfigEntry, description: RainMachineSwitchDescription, ) -> None: """Initialize a generic RainMachine switch.""" - super().__init__(coordinator, controller, description) + super().__init__(entry, coordinator, controller, description) self._attr_is_on = False self._data = coordinator.data[self.entity_description.uid] From 1cf76349429531a01625db756582bf820b0e1fe3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 15 Oct 2021 18:53:05 -1000 Subject: [PATCH 0421/1038] Discover plex via zeroconf (#57825) --- homeassistant/components/plex/manifest.json | 1 + homeassistant/generated/zeroconf.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 27461d0d8ad..2f73d8e84aa 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -8,6 +8,7 @@ "plexauth==0.0.6", "plexwebsocket==0.0.13" ], + "zeroconf": ["_plexmediasvr._tcp.local."], "dependencies": ["http"], "codeowners": ["@jjlawren"], "iot_class": "local_push" diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index da7c08df675..6a1a7f03be1 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -189,6 +189,11 @@ ZEROCONF = { "domain": "nut" } ], + "_plexmediasvr._tcp.local.": [ + { + "domain": "plex" + } + ], "_plugwise._tcp.local.": [ { "domain": "plugwise" From 8405331204a6a513bf9685a41c73a34b02357544 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 15 Oct 2021 18:53:27 -1000 Subject: [PATCH 0422/1038] Add configuration_url to Sense (#57814) --- homeassistant/components/sense/__init__.py | 3 ++ homeassistant/components/sense/sensor.py | 46 ++++++++-------------- 2 files changed, 19 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index d7953a5a5fe..5c8028f2525 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -92,6 +92,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_method=gateway.update_trend_data, update_interval=timedelta(seconds=300), ) + # Start out as unavailable so we do not report 0 data + # until the update happens + trends_coordinator.last_update_success = False # This can take longer than 60s and we already know # sense is online since get_discovered_device_data was diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py index ce22551eff2..1e3cb3d8aca 100644 --- a/homeassistant/components/sense/sensor.py +++ b/homeassistant/components/sense/sensor.py @@ -15,6 +15,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ACTIVE_NAME, @@ -85,11 +86,10 @@ def sense_to_mdi(sense_icon): async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Sense sensor.""" - data = hass.data[DOMAIN][config_entry.entry_id][SENSE_DATA] - sense_devices_data = hass.data[DOMAIN][config_entry.entry_id][SENSE_DEVICES_DATA] - trends_coordinator = hass.data[DOMAIN][config_entry.entry_id][ - SENSE_TRENDS_COORDINATOR - ] + base_data = hass.data[DOMAIN][config_entry.entry_id] + data = base_data[SENSE_DATA] + sense_devices_data = base_data[SENSE_DEVICES_DATA] + trends_coordinator = base_data[SENSE_TRENDS_COORDINATOR] # Request only in case it takes longer # than 60s @@ -141,6 +141,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): variant_name, trends_coordinator, unique_id, + sense_monitor_id, ) ) @@ -245,7 +246,7 @@ class SenseVoltageSensor(SensorEntity): self.async_write_ha_state() -class SenseTrendsSensor(SensorEntity): +class SenseTrendsSensor(CoordinatorEntity, SensorEntity): """Implementation of a Sense energy sensor.""" _attr_device_class = DEVICE_CLASS_ENERGY @@ -264,49 +265,34 @@ class SenseTrendsSensor(SensorEntity): variant_name, trends_coordinator, unique_id, + sense_monitor_id, ): """Initialize the Sense sensor.""" + super().__init__(trends_coordinator) self._attr_name = f"{name} {variant_name}" self._attr_unique_id = unique_id self._data = data self._sensor_type = sensor_type - self._coordinator = trends_coordinator self._variant_id = variant_id self._had_any_update = False - if variant_id in [PRODUCTION_PCT_ID, SOLAR_POWERED_ID]: self._attr_native_unit_of_measurement = PERCENTAGE self._attr_entity_registry_enabled_default = False self._attr_state_class = None self._attr_device_class = None + self._attr_device_info = { + "name": f"Sense {sense_monitor_id}", + "identifiers": {(DOMAIN, sense_monitor_id)}, + "model": "Sense", + "manufacturer": "Sense Labs, Inc.", + "configuration_url": "https://home.sense.com", + } @property def native_value(self): """Return the state of the sensor.""" return round(self._data.get_trend(self._sensor_type, self._variant_id), 1) - @property - def available(self): - """Return if entity is available.""" - return self._had_any_update and self._coordinator.last_update_success - - @callback - def _async_update(self): - """Track if we had an update so we do not report zero data.""" - self._had_any_update = True - self.async_write_ha_state() - - async def async_update(self): - """Update the entity. - - Only used by the generic entity update service. - """ - await self._coordinator.async_request_refresh() - - async def async_added_to_hass(self): - """When entity is added to hass.""" - self.async_on_remove(self._coordinator.async_add_listener(self._async_update)) - class SenseEnergyDevice(SensorEntity): """Implementation of a Sense energy device.""" From 76b6fce19cee17c27d18d1cb038696ff09698d16 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 15 Oct 2021 18:54:00 -1000 Subject: [PATCH 0423/1038] Avoid exposing effects to flux_led lights that do not support them (#57810) --- homeassistant/components/flux_led/light.py | 11 +++++++---- tests/components/flux_led/test_light.py | 5 ++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 885145c4b5c..4298a4f11e6 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -92,7 +92,7 @@ from .entity import FluxEntity _LOGGER = logging.getLogger(__name__) -SUPPORT_FLUX_LED: Final = SUPPORT_EFFECT | SUPPORT_TRANSITION +SUPPORT_FLUX_LED: Final = SUPPORT_TRANSITION FLUX_COLOR_MODE_TO_HASS: Final = { @@ -103,6 +103,7 @@ FLUX_COLOR_MODE_TO_HASS: Final = { FLUX_COLOR_MODE_DIM: COLOR_MODE_BRIGHTNESS, } +EFFECT_SUPPORT_MODES = {COLOR_MODE_RGB, COLOR_MODE_RGBW, COLOR_MODE_RGBWW} # Constant color temp values for 2 flux_led special modes # Warm-white and Cool-white modes @@ -299,9 +300,11 @@ class FluxLight(FluxEntity, CoordinatorEntity, LightEntity): FLUX_COLOR_MODE_TO_HASS.get(mode, COLOR_MODE_ONOFF) for mode in self._device.color_modes } - self._attr_effect_list = FLUX_EFFECT_LIST - if custom_effect_colors: - self._attr_effect_list = [*FLUX_EFFECT_LIST, EFFECT_CUSTOM] + if self._attr_supported_color_modes.intersection(EFFECT_SUPPORT_MODES): + self._attr_supported_features |= SUPPORT_EFFECT + self._attr_effect_list = FLUX_EFFECT_LIST + if custom_effect_colors: + self._attr_effect_list = [*FLUX_EFFECT_LIST, EFFECT_CUSTOM] self._custom_effect_colors = custom_effect_colors self._custom_effect_speed_pct = custom_effect_speed_pct self._custom_effect_transition = custom_effect_transition diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index 2a1e004556a..afca4956055 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -638,6 +638,7 @@ async def test_white_light(hass: HomeAssistant) -> None: assert attributes[ATTR_BRIGHTNESS] == 128 assert attributes[ATTR_COLOR_MODE] == "brightness" assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["brightness"] + assert ATTR_EFFECT_LIST not in attributes # single channel does not support effects await hass.services.async_call( LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True @@ -897,7 +898,9 @@ async def test_addressable_light(hass: HomeAssistant) -> None: assert state.state == STATE_ON attributes = state.attributes assert attributes[ATTR_COLOR_MODE] == "onoff" - assert attributes[ATTR_EFFECT_LIST] == FLUX_EFFECT_LIST + assert ( + ATTR_EFFECT_LIST not in attributes + ) # no support for effects with addressable yet assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["onoff"] await hass.services.async_call( From 71e2fb62afba3d142fd18bc11216cd08c005e907 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 16 Oct 2021 12:20:02 +0200 Subject: [PATCH 0424/1038] Add myself as codeowner for Tuya (#57837) --- CODEOWNERS | 2 +- homeassistant/components/tuya/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 28c037e6e2a..5687b8100da 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -544,7 +544,7 @@ homeassistant/components/trafikverket_train/* @endor-force homeassistant/components/trafikverket_weatherstation/* @endor-force homeassistant/components/transmission/* @engrbm87 @JPHutchins homeassistant/components/tts/* @pvizeli -homeassistant/components/tuya/* @Tuya @zlinoliver @METISU +homeassistant/components/tuya/* @Tuya @zlinoliver @METISU @frenck homeassistant/components/twentemilieu/* @frenck homeassistant/components/twinkly/* @dr1rrb homeassistant/components/ubus/* @noltari diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index 20df33f4573..4b957663947 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -3,7 +3,7 @@ "name": "Tuya", "documentation": "https://github.com/tuya/tuya-home-assistant", "requirements": ["tuya-iot-py-sdk==0.5.0"], - "codeowners": ["@Tuya", "@zlinoliver", "@METISU"], + "codeowners": ["@Tuya", "@zlinoliver", "@METISU", "@frenck"], "config_flow": true, "iot_class": "cloud_push" } From 5e2a9aa7815612779c1b69ccde509276f8c07a59 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 16 Oct 2021 14:39:13 +0200 Subject: [PATCH 0425/1038] Fix vlc_telnet disconnect on unload (#57836) --- homeassistant/components/vlc_telnet/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/vlc_telnet/__init__.py b/homeassistant/components/vlc_telnet/__init__.py index 68c1fbed004..72bbf57ff94 100644 --- a/homeassistant/components/vlc_telnet/__init__.py +++ b/homeassistant/components/vlc_telnet/__init__.py @@ -53,7 +53,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry_data = hass.data[DOMAIN].pop(entry.entry_id) vlc = entry_data[DATA_VLC] - await hass.async_add_executor_job(disconnect_vlc, vlc) + await disconnect_vlc(vlc) return unload_ok From a71c5b760f4761eb43bf5b455e4dbc34bc47add7 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 16 Oct 2021 09:37:36 -0600 Subject: [PATCH 0426/1038] Make sure Tile data storage conforms to standards (#57818) --- homeassistant/components/tile/__init__.py | 20 ++++++++++--------- .../components/tile/device_tracker.py | 4 ++-- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/tile/__init__.py b/homeassistant/components/tile/__init__.py index 5b52e637c64..65b86cd1c6d 100644 --- a/homeassistant/components/tile/__init__.py +++ b/homeassistant/components/tile/__init__.py @@ -30,9 +30,8 @@ CONF_SHOW_INACTIVE = "show_inactive" async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Tile as config entry.""" - hass.data.setdefault(DOMAIN, {DATA_COORDINATOR: {}, DATA_TILE: {}}) - hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = {} - hass.data[DOMAIN][DATA_TILE][entry.entry_id] = {} + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = {} @callback def async_migrate_callback(entity_entry: RegistryEntry) -> dict | None: @@ -68,7 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data[CONF_PASSWORD], session=websession, ) - hass.data[DOMAIN][DATA_TILE][entry.entry_id] = await client.async_get_tiles() + tiles = await client.async_get_tiles() except InvalidAuthError: LOGGER.error("Invalid credentials provided") return False @@ -85,11 +84,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except TileError as err: raise UpdateFailed(f"Error while retrieving data: {err}") from err + coordinators = {} coordinator_init_tasks = [] - for tile_uuid, tile in hass.data[DOMAIN][DATA_TILE][entry.entry_id].items(): - coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ - tile_uuid - ] = DataUpdateCoordinator( + + for tile_uuid, tile in tiles.items(): + coordinator = coordinators[tile_uuid] = DataUpdateCoordinator( hass, LOGGER, name=tile.name, @@ -99,6 +98,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator_init_tasks.append(coordinator.async_refresh()) await gather_with_concurrency(DEFAULT_INIT_TASK_LIMIT, *coordinator_init_tasks) + hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] = coordinators + hass.data[DOMAIN][entry.entry_id][DATA_TILE] = tiles hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -109,5 +110,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a Tile config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index 36cd16de23a..1276dc3f5fd 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -44,10 +44,10 @@ async def async_setup_entry( [ TileDeviceTracker( entry, - hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][tile_uuid], + hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR][tile_uuid], tile, ) - for tile_uuid, tile in hass.data[DOMAIN][DATA_TILE][entry.entry_id].items() + for tile_uuid, tile in hass.data[DOMAIN][entry.entry_id][DATA_TILE].items() ] ) From 845652da15c00638ee003dcf0e54f7ed09bf373c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 16 Oct 2021 09:37:47 -0600 Subject: [PATCH 0427/1038] Make sure ReCollect Waste data storage conforms to standards (#57817) --- homeassistant/components/recollect_waste/__init__.py | 8 ++++---- homeassistant/components/recollect_waste/sensor.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recollect_waste/__init__.py b/homeassistant/components/recollect_waste/__init__.py index f6a5398d901..c32b4119c67 100644 --- a/homeassistant/components/recollect_waste/__init__.py +++ b/homeassistant/components/recollect_waste/__init__.py @@ -21,7 +21,8 @@ PLATFORMS = ["sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up RainMachine as config entry.""" - hass.data.setdefault(DOMAIN, {DATA_COORDINATOR: {}}) + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = {} session = aiohttp_client.async_get_clientsession(hass) client = Client( @@ -48,8 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await coordinator.async_config_entry_first_refresh() - - hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = coordinator + hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] = coordinator hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -67,6 +67,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload an RainMachine config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index a7e50d33ff6..06a96a3bb74 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -57,7 +57,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up ReCollect Waste sensors based on a config entry.""" - coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] + coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] async_add_entities([ReCollectWasteSensor(coordinator, entry)]) From 4b474f47f2ab162730a1d108711b2202ac0b94e0 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 16 Oct 2021 14:33:17 -0400 Subject: [PATCH 0428/1038] Add configuration URL to Efergy (#57839) Co-authored-by: Franck Nijhof --- homeassistant/components/efergy/__init__.py | 31 +++++++-------------- tests/components/efergy/test_init.py | 1 + 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/efergy/__init__.py b/homeassistant/components/efergy/__init__.py index 74bcf6ff7b0..dd6c6001259 100644 --- a/homeassistant/components/efergy/__init__.py +++ b/homeassistant/components/efergy/__init__.py @@ -5,15 +5,7 @@ from pyefergy import Efergy, exceptions from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ATTRIBUTION, - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, - ATTR_SW_VERSION, - CONF_API_KEY, -) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -69,15 +61,12 @@ class EfergyEntity(Entity): self.api = api self._server_unique_id = server_unique_id self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} - - @property - def device_info(self) -> DeviceInfo: - """Return the device information of the entity.""" - return { - "connections": {(dr.CONNECTION_NETWORK_MAC, self.api.info["mac"])}, - ATTR_IDENTIFIERS: {(DOMAIN, self._server_unique_id)}, - ATTR_MANUFACTURER: DEFAULT_NAME, - ATTR_NAME: DEFAULT_NAME, - ATTR_MODEL: self.api.info["type"], - ATTR_SW_VERSION: self.api.info["version"], - } + self._attr_device_info = DeviceInfo( + configuration_url="https://engage.efergy.com/user/login", + connections={(dr.CONNECTION_NETWORK_MAC, self.api.info["mac"])}, + identifiers={(DOMAIN, self._server_unique_id)}, + manufacturer=DEFAULT_NAME, + name=DEFAULT_NAME, + model=self.api.info["type"], + sw_version=self.api.info["version"], + ) diff --git a/tests/components/efergy/test_init.py b/tests/components/efergy/test_init.py index f32551a4e9b..07c80e7bb04 100644 --- a/tests/components/efergy/test_init.py +++ b/tests/components/efergy/test_init.py @@ -53,6 +53,7 @@ async def test_device_info(hass: HomeAssistant, aioclient_mock: AiohttpClientMoc device = device_registry.async_get_device({(DOMAIN, entry.entry_id)}) + assert device.configuration_url == "https://engage.efergy.com/user/login" assert device.connections == {("mac", "ff:ff:ff:ff:ff:ff")} assert device.identifiers == {(DOMAIN, entry.entry_id)} assert device.manufacturer == DEFAULT_NAME From a9f940d8a20dc6ce9a7cae12a602e7d622040345 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 16 Oct 2021 21:34:04 +0200 Subject: [PATCH 0429/1038] Add support for device configuration URL to Nettigo Air Monitor integration (#57695) --- homeassistant/components/nam/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/nam/__init__.py b/homeassistant/components/nam/__init__.py index 52e506fc4dd..8df6a43ba30 100644 --- a/homeassistant/components/nam/__init__.py +++ b/homeassistant/components/nam/__init__.py @@ -120,4 +120,5 @@ class NAMDataUpdateCoordinator(DataUpdateCoordinator): "name": DEFAULT_NAME, "sw_version": self.nam.software_version, "manufacturer": MANUFACTURER, + "configuration_url": f"http://{self.host}/", } From 623d0ae932ecb959ed91c12ccf40a68a38e591e2 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sat, 16 Oct 2021 22:19:14 +0200 Subject: [PATCH 0430/1038] Bump pytradfri to v.7.1.0 (#57861) --- homeassistant/components/tradfri/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index d26093c32ed..0393cda0c5b 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -3,7 +3,7 @@ "name": "IKEA TR\u00c5DFRI", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tradfri", - "requirements": ["pytradfri[async]==7.0.7"], + "requirements": ["pytradfri[async]==7.1.0"], "homekit": { "models": ["TRADFRI"] }, diff --git a/requirements_all.txt b/requirements_all.txt index f7d712ea07e..9a804111b91 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1961,7 +1961,7 @@ pytouchline==0.7 pytraccar==0.9.0 # homeassistant.components.tradfri -pytradfri[async]==7.0.7 +pytradfri[async]==7.1.0 # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 75b37fab7c0..456fdc40d05 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1142,7 +1142,7 @@ pytile==5.2.3 pytraccar==0.9.0 # homeassistant.components.tradfri -pytradfri[async]==7.0.7 +pytradfri[async]==7.1.0 # homeassistant.components.usb pyudev==0.22.0 From 74d72957b2b6703f7611618e881191b2381ed726 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 16 Oct 2021 22:26:00 +0200 Subject: [PATCH 0431/1038] Add support for 'freeze' mode in Tuya thermostats (wk) (#57851) --- homeassistant/components/tuya/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index 3d678b84fd0..1ba061463f5 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -37,6 +37,7 @@ from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode TUYA_HVAC_TO_HA = { "auto": HVAC_MODE_HEAT_COOL, "cold": HVAC_MODE_COOL, + "freeze": HVAC_MODE_COOL, "heat": HVAC_MODE_HEAT, "hot": HVAC_MODE_HEAT, "manual": HVAC_MODE_HEAT_COOL, From aa3e17cae948f9213d74b83ee80bc732ad37a2b7 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 16 Oct 2021 17:17:25 -0400 Subject: [PATCH 0432/1038] Add new mac vendor to sense DHCP (#57858) --- homeassistant/components/sense/manifest.json | 4 ++++ homeassistant/generated/dhcp.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index 16cecd1cd97..940902851e2 100644 --- a/homeassistant/components/sense/manifest.json +++ b/homeassistant/components/sense/manifest.json @@ -13,6 +13,10 @@ { "hostname": "sense-*", "macaddress": "DCEFCA*" + }, + { + "hostname": "sense-*", + "macaddress": "A4D578*" } ], "iot_class": "cloud_polling" diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index eb22c45cef7..1f3b3ec648d 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -257,6 +257,11 @@ DHCP = [ "hostname": "sense-*", "macaddress": "DCEFCA*" }, + { + "domain": "sense", + "hostname": "sense-*", + "macaddress": "A4D578*" + }, { "domain": "smartthings", "hostname": "st*", From 441b0b2fb7ddb7c82a0ec3a2e873f9c64a552fa3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 16 Oct 2021 23:30:52 +0200 Subject: [PATCH 0433/1038] Add tamper device class for binary sensor (#57632) --- homeassistant/components/binary_sensor/__init__.py | 4 ++++ .../components/binary_sensor/device_condition.py | 9 +++++++++ homeassistant/components/binary_sensor/device_trigger.py | 6 ++++++ homeassistant/components/binary_sensor/strings.json | 4 ++++ .../components/binary_sensor/translations/en.json | 4 ++++ 5 files changed, 27 insertions(+) diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index 87d574fc4b0..d478780f9ac 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -92,6 +92,9 @@ DEVICE_CLASS_SMOKE = "smoke" # On means sound detected, Off means no sound (clear) DEVICE_CLASS_SOUND = "sound" +# On means tampering detected, Off means no tampering (clear) +DEVICE_CLASS_TAMPER = "tamper" + # On means update available, Off means up-to-date DEVICE_CLASS_UPDATE = "update" @@ -124,6 +127,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, + DEVICE_CLASS_TAMPER, DEVICE_CLASS_UPDATE, DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index a6b9d3ffb8b..ad305beb11b 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -37,6 +37,7 @@ from . import ( DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, + DEVICE_CLASS_TAMPER, DEVICE_CLASS_UPDATE, DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, @@ -85,6 +86,8 @@ CONF_IS_SMOKE = "is_smoke" CONF_IS_NO_SMOKE = "is_no_smoke" CONF_IS_SOUND = "is_sound" CONF_IS_NO_SOUND = "is_no_sound" +CONF_IS_TAMPERED = "is_tampered" +CONF_IS_NOT_TAMPERED = "is_not_tampered" CONF_IS_UPDATE = "is_update" CONF_IS_NO_UPDATE = "is_no_update" CONF_IS_VIBRATION = "is_vibration" @@ -112,6 +115,7 @@ IS_ON = [ CONF_IS_PROBLEM, CONF_IS_SMOKE, CONF_IS_SOUND, + CONF_IS_TAMPERED, CONF_IS_UPDATE, CONF_IS_UNSAFE, CONF_IS_VIBRATION, @@ -132,6 +136,7 @@ IS_OFF = [ CONF_IS_NOT_PLUGGED_IN, CONF_IS_NOT_POWERED, CONF_IS_NOT_PRESENT, + CONF_IS_NOT_TAMPERED, CONF_IS_NOT_UNSAFE, CONF_IS_NO_GAS, CONF_IS_NO_LIGHT, @@ -194,6 +199,10 @@ ENTITY_CONDITIONS = { DEVICE_CLASS_SAFETY: [{CONF_TYPE: CONF_IS_UNSAFE}, {CONF_TYPE: CONF_IS_NOT_UNSAFE}], DEVICE_CLASS_SMOKE: [{CONF_TYPE: CONF_IS_SMOKE}, {CONF_TYPE: CONF_IS_NO_SMOKE}], DEVICE_CLASS_SOUND: [{CONF_TYPE: CONF_IS_SOUND}, {CONF_TYPE: CONF_IS_NO_SOUND}], + DEVICE_CLASS_TAMPER: [ + {CONF_TYPE: CONF_IS_TAMPERED}, + {CONF_TYPE: CONF_IS_NOT_TAMPERED}, + ], DEVICE_CLASS_UPDATE: [{CONF_TYPE: CONF_IS_UPDATE}, {CONF_TYPE: CONF_IS_NO_UPDATE}], DEVICE_CLASS_VIBRATION: [ {CONF_TYPE: CONF_IS_VIBRATION}, diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index a0966b5a018..f3eb1851247 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -35,6 +35,7 @@ from . import ( DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, + DEVICE_CLASS_TAMPER, DEVICE_CLASS_UPDATE, DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, @@ -83,6 +84,8 @@ CONF_SMOKE = "smoke" CONF_NO_SMOKE = "no_smoke" CONF_SOUND = "sound" CONF_NO_SOUND = "no_sound" +CONF_TAMPERED = "tampered" +CONF_NOT_TAMPERED = "not_tampered" CONF_UPDATE = "update" CONF_NO_UPDATE = "no_update" CONF_VIBRATION = "vibration" @@ -113,6 +116,7 @@ TURNED_ON = [ CONF_UNSAFE, CONF_UPDATE, CONF_VIBRATION, + CONF_TAMPERED, CONF_TURNED_ON, ] @@ -129,6 +133,7 @@ TURNED_OFF = [ CONF_NOT_PLUGGED_IN, CONF_NOT_POWERED, CONF_NOT_PRESENT, + CONF_NOT_TAMPERED, CONF_NOT_UNSAFE, CONF_NO_GAS, CONF_NO_LIGHT, @@ -174,6 +179,7 @@ ENTITY_TRIGGERS = { DEVICE_CLASS_SMOKE: [{CONF_TYPE: CONF_SMOKE}, {CONF_TYPE: CONF_NO_SMOKE}], DEVICE_CLASS_SOUND: [{CONF_TYPE: CONF_SOUND}, {CONF_TYPE: CONF_NO_SOUND}], DEVICE_CLASS_UPDATE: [{CONF_TYPE: CONF_UPDATE}, {CONF_TYPE: CONF_NO_UPDATE}], + DEVICE_CLASS_TAMPER: [{CONF_TYPE: CONF_TAMPERED}, {CONF_TYPE: CONF_NOT_TAMPERED}], DEVICE_CLASS_VIBRATION: [ {CONF_TYPE: CONF_VIBRATION}, {CONF_TYPE: CONF_NO_VIBRATION}, diff --git a/homeassistant/components/binary_sensor/strings.json b/homeassistant/components/binary_sensor/strings.json index 62b6ec20323..70991f58759 100644 --- a/homeassistant/components/binary_sensor/strings.json +++ b/homeassistant/components/binary_sensor/strings.json @@ -38,6 +38,8 @@ "is_no_smoke": "{entity_name} is not detecting smoke", "is_sound": "{entity_name} is detecting sound", "is_no_sound": "{entity_name} is not detecting sound", + "is_tampered": "{entity_name} is detecting tampering", + "is_not_tampered": "{entity_name} is not detecting tampering", "is_update": "{entity_name} has an update available", "is_no_update": "{entity_name} is up-to-date", "is_vibration": "{entity_name} is detecting vibration", @@ -84,6 +86,8 @@ "no_smoke": "{entity_name} stopped detecting smoke", "sound": "{entity_name} started detecting sound", "no_sound": "{entity_name} stopped detecting sound", + "is_tampered": "{entity_name} started detecting tampering", + "is_not_tampered": "{entity_name} stopped detecting tampering", "update": "{entity_name} got an update available", "no_update": "{entity_name} became up-to-date", "vibration": "{entity_name} started detecting vibration", diff --git a/homeassistant/components/binary_sensor/translations/en.json b/homeassistant/components/binary_sensor/translations/en.json index 047820498da..be831864f38 100644 --- a/homeassistant/components/binary_sensor/translations/en.json +++ b/homeassistant/components/binary_sensor/translations/en.json @@ -31,6 +31,7 @@ "is_not_plugged_in": "{entity_name} is unplugged", "is_not_powered": "{entity_name} is not powered", "is_not_present": "{entity_name} is not present", + "is_not_tampered": "{entity_name} is not detecting tampering", "is_not_unsafe": "{entity_name} is safe", "is_occupied": "{entity_name} is occupied", "is_off": "{entity_name} is off", @@ -42,6 +43,7 @@ "is_problem": "{entity_name} is detecting problem", "is_smoke": "{entity_name} is detecting smoke", "is_sound": "{entity_name} is detecting sound", + "is_tampered": "{entity_name} is detecting tampering", "is_unsafe": "{entity_name} is unsafe", "is_update": "{entity_name} has an update available", "is_vibration": "{entity_name} is detecting vibration" @@ -52,6 +54,8 @@ "connected": "{entity_name} connected", "gas": "{entity_name} started detecting gas", "hot": "{entity_name} became hot", + "is_not_tampered": "{entity_name} stopped detecting tampering", + "is_tampered": "{entity_name} started detecting tampering", "light": "{entity_name} started detecting light", "locked": "{entity_name} locked", "moist": "{entity_name} became moist", From 4300f1de46133c7e7ea4f1d471223f644126b9cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sat, 16 Oct 2021 23:51:10 +0200 Subject: [PATCH 0434/1038] Use DeviceInfo class (#57868) --- homeassistant/components/mill/climate.py | 13 +++++++------ homeassistant/components/mill/sensor.py | 13 +++++++------ homeassistant/components/opengarage/cover.py | 11 ++++++----- homeassistant/components/surepetcare/entity.py | 13 +++++++------ homeassistant/components/tibber/sensor.py | 11 ++++++----- homeassistant/components/tractive/entity.py | 16 ++++++++-------- 6 files changed, 41 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index 199bdf393a1..3ab9c64942c 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -14,6 +14,7 @@ from homeassistant.components.climate.const import ( from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS from homeassistant.core import callback from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -83,12 +84,12 @@ class MillHeater(CoordinatorEntity, ClimateEntity): self._id = heater.device_id self._attr_unique_id = heater.device_id self._attr_name = heater.name - self._attr_device_info = { - "identifiers": {(DOMAIN, heater.device_id)}, - "name": self.name, - "manufacturer": MANUFACTURER, - "model": f"generation {1 if heater.is_gen1 else 2}", - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, heater.device_id)}, + name=self.name, + manufacturer=MANUFACTURER, + model=f"generation {1 if heater.is_gen1 else 2}", + ) if heater.is_gen1: self._attr_hvac_modes = [HVAC_MODE_HEAT] else: diff --git a/homeassistant/components/mill/sensor.py b/homeassistant/components/mill/sensor.py index 11b006e4b6e..caec13d7dee 100644 --- a/homeassistant/components/mill/sensor.py +++ b/homeassistant/components/mill/sensor.py @@ -7,6 +7,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import ENERGY_KILO_WATT_HOUR from homeassistant.core import callback +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONSUMPTION_TODAY, CONSUMPTION_YEAR, DOMAIN, MANUFACTURER @@ -41,12 +42,12 @@ class MillHeaterEnergySensor(CoordinatorEntity, SensorEntity): self._attr_name = f"{heater.name} {sensor_type.replace('_', ' ')}" self._attr_unique_id = f"{heater.device_id}_{sensor_type}" - self._attr_device_info = { - "identifiers": {(DOMAIN, heater.device_id)}, - "name": self.name, - "manufacturer": MANUFACTURER, - "model": f"generation {1 if heater.is_gen1 else 2}", - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, heater.device_id)}, + name=self.name, + manufacturer=MANUFACTURER, + model=f"generation {1 if heater.is_gen1 else 2}", + ) self._update_attr(heater) @callback diff --git a/homeassistant/components/opengarage/cover.py b/homeassistant/components/opengarage/cover.py index 12a1103f7df..ad3b2a4f74f 100644 --- a/homeassistant/components/opengarage/cover.py +++ b/homeassistant/components/opengarage/cover.py @@ -24,6 +24,7 @@ from homeassistant.const import ( STATE_OPENING, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from .const import ( ATTR_DISTANCE_SENSOR, @@ -181,9 +182,9 @@ class OpenGarageCover(CoverEntity): @property def device_info(self): """Return the device_info of the device.""" - device_info = { - "identifiers": {(DOMAIN, self._device_id)}, - "name": self.name, - "manufacturer": "Open Garage", - } + device_info = DeviceInfo( + identifiers={(DOMAIN, self._device_id)}, + name=self.name, + manufacturer="Open Garage", + ) return device_info diff --git a/homeassistant/components/surepetcare/entity.py b/homeassistant/components/surepetcare/entity.py index f7797b4c166..8cdf0a74189 100644 --- a/homeassistant/components/surepetcare/entity.py +++ b/homeassistant/components/surepetcare/entity.py @@ -6,6 +6,7 @@ from abc import abstractmethod from surepy.entities import SurepyEntity from homeassistant.core import callback +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import SurePetcareDataCoordinator @@ -33,12 +34,12 @@ class SurePetcareEntity(CoordinatorEntity): self._device_name = surepy_entity.type.name.capitalize().replace("_", " ") self._device_id = f"{surepy_entity.household_id}-{surepetcare_id}" - self._attr_device_info = { - "identifiers": {(DOMAIN, self._device_id)}, - "name": self._device_name, - "manufacturer": "Sure Petcare", - "model": surepy_entity.type.name.capitalize().replace("_", " "), - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._device_id)}, + name=self._device_name, + manufacturer="Sure Petcare", + model=surepy_entity.type.name.capitalize().replace("_", " "), + ) self._update_attr(coordinator.data[surepetcare_id]) @abstractmethod diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index d376bf0a7d5..c7184d38792 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -34,6 +34,7 @@ from homeassistant.core import callback from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import update_coordinator from homeassistant.helpers.device_registry import async_get as async_get_dev_reg +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_registry import async_get as async_get_entity_reg from homeassistant.util import Throttle, dt as dt_util @@ -264,11 +265,11 @@ class TibberSensor(SensorEntity): @property def device_info(self): """Return the device_info of the device.""" - device_info = { - "identifiers": {(TIBBER_DOMAIN, self._tibber_home.home_id)}, - "name": self._device_name, - "manufacturer": MANUFACTURER, - } + device_info = DeviceInfo( + identifiers={(TIBBER_DOMAIN, self._tibber_home.home_id)}, + name=self._device_name, + manufacturer=MANUFACTURER, + ) if self._model is not None: device_info["model"] = self._model return device_info diff --git a/homeassistant/components/tractive/entity.py b/homeassistant/components/tractive/entity.py index ffc84fc9788..abdcbb4586c 100644 --- a/homeassistant/components/tractive/entity.py +++ b/homeassistant/components/tractive/entity.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN @@ -15,13 +15,13 @@ class TractiveEntity(Entity): self, user_id: str, trackable: dict[str, Any], tracker_details: dict[str, Any] ) -> None: """Initialize tracker entity.""" - self._attr_device_info = { - "identifiers": {(DOMAIN, tracker_details["_id"])}, - "name": f"Tractive ({tracker_details['_id']})", - "manufacturer": "Tractive GmbH", - "sw_version": tracker_details["fw_version"], - "model": tracker_details["model_number"], - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, tracker_details["_id"])}, + name=f"Tractive ({tracker_details['_id']})", + manufacturer="Tractive GmbH", + sw_version=tracker_details["fw_version"], + model=tracker_details["model_number"], + ) self._user_id = user_id self._tracker_id = tracker_details["_id"] self._trackable = trackable From bdf96943ae9fd2390a36921641ccac6603eb7ea8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 16 Oct 2021 11:51:37 -1000 Subject: [PATCH 0435/1038] Restore dhcp discovery support to tuya (#57826) Co-authored-by: Franck Nijhof --- homeassistant/components/tuya/manifest.json | 15 ++++++- homeassistant/generated/dhcp.py | 44 +++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index 4b957663947..8412c40dfc0 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -5,5 +5,18 @@ "requirements": ["tuya-iot-py-sdk==0.5.0"], "codeowners": ["@Tuya", "@zlinoliver", "@METISU", "@frenck"], "config_flow": true, - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "dhcp": [ + {"macaddress": "105A17*"}, + {"macaddress": "10D561*"}, + {"macaddress": "1869D8*"}, + {"macaddress": "381F8D*"}, + {"macaddress": "508A06*"}, + {"macaddress": "68572D*"}, + {"macaddress": "708976*"}, + {"macaddress": "7CF666*"}, + {"macaddress": "84E342*"}, + {"macaddress": "D4A651*"}, + {"macaddress": "D81F12*"} + ] } diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 1f3b3ec648d..a20dee6b2f5 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -431,6 +431,50 @@ DHCP = [ "hostname": "lb*", "macaddress": "B09575*" }, + { + "domain": "tuya", + "macaddress": "105A17*" + }, + { + "domain": "tuya", + "macaddress": "10D561*" + }, + { + "domain": "tuya", + "macaddress": "1869D8*" + }, + { + "domain": "tuya", + "macaddress": "381F8D*" + }, + { + "domain": "tuya", + "macaddress": "508A06*" + }, + { + "domain": "tuya", + "macaddress": "68572D*" + }, + { + "domain": "tuya", + "macaddress": "708976*" + }, + { + "domain": "tuya", + "macaddress": "7CF666*" + }, + { + "domain": "tuya", + "macaddress": "84E342*" + }, + { + "domain": "tuya", + "macaddress": "D4A651*" + }, + { + "domain": "tuya", + "macaddress": "D81F12*" + }, { "domain": "verisure", "macaddress": "0023C1*" From 0dcb8ca270ace103af1c6289bb089da33f57c6ca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 16 Oct 2021 11:52:10 -1000 Subject: [PATCH 0436/1038] Avoid probing brother devices that have an existing config entry (#57829) --- homeassistant/components/brother/config_flow.py | 3 +++ tests/components/brother/test_config_flow.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/homeassistant/components/brother/config_flow.py b/homeassistant/components/brother/config_flow.py index 08daf0155a1..73196302207 100644 --- a/homeassistant/components/brother/config_flow.py +++ b/homeassistant/components/brother/config_flow.py @@ -86,6 +86,9 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # Hostname is format: brother.local. self.host = discovery_info["hostname"].rstrip(".") + # Do not probe the device if the host is already configured + self._async_abort_entries_match({CONF_HOST: self.host}) + snmp_engine = get_snmp_engine(self.hass) self.brother = Brother(self.host, snmp_engine=snmp_engine) diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py index c518076615a..3a828c97488 100644 --- a/tests/components/brother/test_config_flow.py +++ b/tests/components/brother/test_config_flow.py @@ -170,6 +170,23 @@ async def test_zeroconf_device_exists_abort(hass): assert result["reason"] == "already_configured" +async def test_zeroconf_no_probe_existing_device(hass): + """Test we do not probe the device is the host is already configured.""" + entry = MockConfigEntry(domain=DOMAIN, unique_id="0123456789", data=CONFIG) + entry.add_to_hass(hass) + with patch("brother.Brother._get_data") as mock_get_data: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data={"hostname": "localhost", "name": "Brother Printer"}, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert len(mock_get_data.mock_calls) == 0 + + async def test_zeroconf_confirm_create_entry(hass): """Test zeroconf confirmation and create config entry.""" with patch( From c09c8f424f4da24144eccca22dd98dc7f139d285 Mon Sep 17 00:00:00 2001 From: b-pass Date: Sat, 16 Oct 2021 17:52:30 -0400 Subject: [PATCH 0437/1038] Set state class in JuiceNet component (#57870) --- homeassistant/components/juicenet/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/juicenet/sensor.py b/homeassistant/components/juicenet/sensor.py index 4eaaba41b55..0b39bcd3507 100644 --- a/homeassistant/components/juicenet/sensor.py +++ b/homeassistant/components/juicenet/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, SensorEntityDescription, ) @@ -66,6 +67,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Energy added", native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), ) From 2fa08ae6abd920bb6b2b7008d537cfeafaff6d74 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 16 Oct 2021 23:53:57 +0200 Subject: [PATCH 0438/1038] Add hassio discovery to VLC telnet (#57815) --- .../components/vlc_telnet/config_flow.py | 38 +++++ .../components/vlc_telnet/strings.json | 8 +- .../vlc_telnet/translations/en.json | 8 +- .../components/vlc_telnet/test_config_flow.py | 152 +++++++++++++++--- 4 files changed, 178 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/vlc_telnet/config_flow.py b/homeassistant/components/vlc_telnet/config_flow.py index 0044995c7db..e86ab635517 100644 --- a/homeassistant/components/vlc_telnet/config_flow.py +++ b/homeassistant/components/vlc_telnet/config_flow.py @@ -70,6 +70,7 @@ class VLCTelnetConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 entry: ConfigEntry | None = None + hassio_discovery: dict[str, Any] | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -150,6 +151,43 @@ class VLCTelnetConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) + async def async_step_hassio(self, discovery_info: dict[str, Any]) -> FlowResult: + """Handle the discovery step via hassio.""" + await self.async_set_unique_id("hassio") + self._abort_if_unique_id_configured(discovery_info) + + self.hassio_discovery = discovery_info + self.context["title_placeholders"] = {"host": discovery_info[CONF_HOST]} + return await self.async_step_hassio_confirm() + + async def async_step_hassio_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm Supervisor discovery.""" + assert self.hassio_discovery + if user_input is None: + return self.async_show_form( + step_id="hassio_confirm", + data_schema=vol.Schema({}), + description_placeholders={"addon": self.hassio_discovery["addon"]}, + ) + + self.hassio_discovery.pop("addon") + + try: + info = await validate_input(self.hass, self.hassio_discovery) + except CannotConnect: + return self.async_abort(reason="cannot_connect") + except InvalidAuth: + return self.async_abort(reason="invalid_auth") + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + return self.async_abort(reason="unknown") + else: + return self.async_create_entry( + title=info["title"], data=self.hassio_discovery + ) + class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/vlc_telnet/strings.json b/homeassistant/components/vlc_telnet/strings.json index dbdae9755ea..3a22bd06602 100644 --- a/homeassistant/components/vlc_telnet/strings.json +++ b/homeassistant/components/vlc_telnet/strings.json @@ -15,11 +15,17 @@ "password": "[%key:common::config_flow::data::password%]", "name": "[%key:common::config_flow::data::name%]" } + }, + "hassio_confirm": { + "description": "Do you want to connect to add-on {addon}?" } }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "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%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", diff --git a/homeassistant/components/vlc_telnet/translations/en.json b/homeassistant/components/vlc_telnet/translations/en.json index 3f7cbadb4b7..7802768a2c6 100644 --- a/homeassistant/components/vlc_telnet/translations/en.json +++ b/homeassistant/components/vlc_telnet/translations/en.json @@ -2,7 +2,10 @@ "config": { "abort": { "already_configured": "Service is already configured", - "reauth_successful": "Re-authentication was successful" + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "reauth_successful": "Re-authentication was successful", + "unknown": "Unexpected error" }, "error": { "cannot_connect": "Failed to connect", @@ -11,6 +14,9 @@ }, "flow_title": "{host}", "step": { + "hassio_confirm": { + "description": "Do you want to connect to add-on {addon}?" + }, "reauth_confirm": { "data": { "password": "Password" diff --git a/tests/components/vlc_telnet/test_config_flow.py b/tests/components/vlc_telnet/test_config_flow.py index 7865648d565..5ef8b6c0400 100644 --- a/tests/components/vlc_telnet/test_config_flow.py +++ b/tests/components/vlc_telnet/test_config_flow.py @@ -10,6 +10,11 @@ import pytest from homeassistant import config_entries from homeassistant.components.vlc_telnet.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) from tests.common import MockConfigEntry @@ -50,7 +55,7 @@ async def test_user_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] == RESULT_TYPE_FORM assert result["errors"] is None with patch("homeassistant.components.vlc_telnet.config_flow.Client.connect"), patch( @@ -67,7 +72,7 @@ async def test_user_flow( ) await hass.async_block_till_done() - assert result["type"] == "create_entry" + assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["title"] == entry_data["host"] assert result["data"] == entry_data assert len(mock_setup_entry.mock_calls) == 1 @@ -75,6 +80,12 @@ async def test_user_flow( async def test_import_flow(hass: HomeAssistant) -> None: """Test successful import flow.""" + test_data = { + "password": "test-password", + "host": "1.1.1.1", + "port": 8888, + "name": "custom name", + } with patch("homeassistant.components.vlc_telnet.config_flow.Client.connect"), patch( "homeassistant.components.vlc_telnet.config_flow.Client.login" ), patch( @@ -86,23 +97,13 @@ async def test_import_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data={ - "password": "test-password", - "host": "1.1.1.1", - "port": 8888, - "name": "custom name", - }, + data=test_data, ) await hass.async_block_till_done() - assert result["type"] == "create_entry" - assert result["title"] == "custom name" - assert result["data"] == { - "password": "test-password", - "host": "1.1.1.1", - "port": 8888, - "name": "custom name", - } + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == test_data["name"] + assert result["data"] == test_data assert len(mock_setup_entry.mock_calls) == 1 @@ -127,7 +128,7 @@ async def test_abort_already_configured(hass: HomeAssistant, source: str) -> Non data=entry_data, ) - assert result["type"] == "abort" + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" @@ -168,7 +169,7 @@ async def test_errors( {"password": "test-password"}, ) - assert result2["type"] == "form" + assert result2["type"] == RESULT_TYPE_FORM assert result2["errors"] == {"base": error} @@ -208,15 +209,10 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == "abort" + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 - assert dict(entry.data) == { - "password": "new-password", - "host": "1.1.1.1", - "port": 8888, - "name": "custom name", - } + assert dict(entry.data) == {**entry_data, "password": "new-password"} @pytest.mark.parametrize( @@ -268,5 +264,109 @@ async def test_reauth_errors( {"password": "test-password"}, ) - assert result2["type"] == "form" + assert result2["type"] == RESULT_TYPE_FORM assert result2["errors"] == {"base": error} + + +async def test_hassio_flow(hass: HomeAssistant) -> None: + """Test successful hassio flow.""" + with patch("homeassistant.components.vlc_telnet.config_flow.Client.connect"), patch( + "homeassistant.components.vlc_telnet.config_flow.Client.login" + ), patch( + "homeassistant.components.vlc_telnet.config_flow.Client.disconnect" + ), patch( + "homeassistant.components.vlc_telnet.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + test_data = { + "password": "test-password", + "host": "1.1.1.1", + "port": 8888, + "name": "custom name", + "addon": "vlc", + } + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=test_data, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_FORM + + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == test_data["name"] + assert result2["data"] == test_data + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_hassio_already_configured(hass: HomeAssistant) -> None: + """Test successful hassio flow.""" + + entry_data = { + "password": "test-password", + "host": "1.1.1.1", + "port": 8888, + "name": "custom name", + "addon": "vlc", + } + + entry = MockConfigEntry(domain=DOMAIN, data=entry_data, unique_id="hassio") + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=entry_data, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + + +@pytest.mark.parametrize( + "error, connect_side_effect, login_side_effect", + [ + ("invalid_auth", None, AuthError), + ("cannot_connect", ConnectError, None), + ("unknown", Exception, None), + ], +) +async def test_hassio_errors( + hass: HomeAssistant, + error: str, + connect_side_effect: Exception | None, + login_side_effect: Exception | None, +) -> None: + """Test we handle hassio errors.""" + with patch( + "homeassistant.components.vlc_telnet.config_flow.Client.connect", + side_effect=connect_side_effect, + ), patch( + "homeassistant.components.vlc_telnet.config_flow.Client.login", + side_effect=login_side_effect, + ), patch( + "homeassistant.components.vlc_telnet.config_flow.Client.disconnect" + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data={ + "password": "test-password", + "host": "1.1.1.1", + "port": 8888, + "name": "custom name", + "addon": "vlc", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_FORM + + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == error From d64f210b672178e574dcc46ff62df1e0b944223e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 16 Oct 2021 23:57:30 +0200 Subject: [PATCH 0439/1038] Add camera platform to Tuya (#57865) --- .coveragerc | 1 + homeassistant/components/tuya/camera.py | 130 ++++++++++++++++++++ homeassistant/components/tuya/const.py | 6 +- homeassistant/components/tuya/manifest.json | 1 + 4 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/tuya/camera.py diff --git a/.coveragerc b/.coveragerc index 35cdb0002f1..5198d8c34b2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1112,6 +1112,7 @@ omit = homeassistant/components/tuya/__init__.py homeassistant/components/tuya/base.py homeassistant/components/tuya/binary_sensor.py + homeassistant/components/tuya/camera.py homeassistant/components/tuya/climate.py homeassistant/components/tuya/const.py homeassistant/components/tuya/fan.py diff --git a/homeassistant/components/tuya/camera.py b/homeassistant/components/tuya/camera.py new file mode 100644 index 00000000000..78c38725d7e --- /dev/null +++ b/homeassistant/components/tuya/camera.py @@ -0,0 +1,130 @@ +"""Support for Tuya cameras.""" +from __future__ import annotations + +from tuya_iot import TuyaDevice, TuyaDeviceManager + +from homeassistant.components import ffmpeg +from homeassistant.components.camera import SUPPORT_STREAM, Camera as CameraEntity +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 . import HomeAssistantTuyaData +from .base import TuyaEntity +from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode + +# All descriptions can be found here: +# https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq +CAMERAS: tuple[str, ...] = ( + # Smart Camera (including doorbells) + # https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu + "sp", +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Tuya cameras dynamically through Tuya discovery.""" + hass_data: HomeAssistantTuyaData = hass.data[DOMAIN][entry.entry_id] + + @callback + def async_discover_device(device_ids: list[str]) -> None: + """Discover and add a discovered Tuya camera.""" + entities: list[TuyaCameraEntity] = [] + for device_id in device_ids: + device = hass_data.device_manager.device_map[device_id] + if device.category in CAMERAS: + entities.append(TuyaCameraEntity(device, hass_data.device_manager)) + + async_add_entities(entities) + + async_discover_device([*hass_data.device_manager.device_map]) + + entry.async_on_unload( + async_dispatcher_connect(hass, TUYA_DISCOVERY_NEW, async_discover_device) + ) + + +class TuyaCameraEntity(TuyaEntity, CameraEntity): + """Tuya Camera Entity.""" + + def __init__( + self, + device: TuyaDevice, + device_manager: TuyaDeviceManager, + ) -> None: + """Init Tuya Camera.""" + super().__init__(device, device_manager) + CameraEntity.__init__(self) + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return SUPPORT_STREAM + + @property + def is_recording(self) -> bool: + """Return true if the device is recording.""" + return self.device.status.get(DPCode.RECORD_SWITCH, False) + + @property + def brand(self) -> str | None: + """Return the camera brand.""" + return "Tuya" + + @property + def motion_detection_enabled(self) -> bool: + """Return the camera motion detection status.""" + return self.device.status.get(DPCode.MOTION_SWITCH, False) + + async def stream_source(self) -> str | None: + """Return the source of the stream.""" + + def _stream_source() -> str | None: + # This method can be replaced by the following snippet, once + # upstream changes have been merged. + # + # return self.device_manager.get_device_stream_allocate( + # self.device.id, stream_type="rtsp" + # ) + # + # https://github.com/tuya/tuya-iot-python-sdk/pull/28 + + response = self.device_manager.api.post( + f"/v1.0/devices/{self.device.id}/stream/actions/allocate", + {"type": "rtsp"}, + ) + if response["success"]: + return response["result"]["url"] + return None + + return await self.hass.async_add_executor_job(_stream_source) + + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ) -> bytes | None: + """Return a still image response from the camera.""" + stream_source = await self.stream_source() + if not stream_source: + return None + return await ffmpeg.async_get_image( + self.hass, + stream_source, + width=width, + height=height, + ) + + @property + def model(self) -> str | None: + """Return the camera model.""" + return self.device.product_name + + def enable_motion_detection(self) -> None: + """Enable motion detection in the camera.""" + self._send_command([{"code": DPCode.MOTION_SWITCH, "value": True}]) + + def disable_motion_detection(self) -> None: + """Disable motion detection in camera.""" + self._send_command([{"code": DPCode.MOTION_SWITCH, "value": False}]) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index b68d2573717..7dc1e529a47 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -46,8 +46,9 @@ TUYA_SUPPORTED_PRODUCT_CATEGORIES = ( "pc", # Power Strip "pir", # PIR Detector "qn", # Heater - "sos", # SOS Button "sgbj", # Siren Alarm + "sos", # SOS Button + "sp", # Smart Camera "wk", # Thermostat "xdd", # Ceiling Light "xxj", # Diffuser @@ -58,6 +59,7 @@ SMARTLIFE_APP = "smartlife" PLATFORMS = [ "binary_sensor", + "camera", "climate", "fan", "light", @@ -106,10 +108,12 @@ class DPCode(str, Enum): LOCK = "lock" # Lock / Child lock MATERIAL = "material" # Material MODE = "mode" # Working mode / Mode + MOTION_SWITCH = "motion_switch" # Motion switch MUFFLING = "muffling" # Muffling PIR = "pir" # Motion sensor POWDER_SET = "powder_set" # Powder PUMP_RESET = "pump_reset" # Water pump reset + RECORD_SWITCH = "record_switch" # Recording switch SHAKE = "shake" # Oscillating SOS = "sos" # Emergency State SOS_STATE = "sos_state" # Emergency mode diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index 8412c40dfc0..5e59f2a3e8e 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -3,6 +3,7 @@ "name": "Tuya", "documentation": "https://github.com/tuya/tuya-home-assistant", "requirements": ["tuya-iot-py-sdk==0.5.0"], + "dependencies": ["ffmpeg"], "codeowners": ["@Tuya", "@zlinoliver", "@METISU", "@frenck"], "config_flow": true, "iot_class": "cloud_push", From 6a80559fa8729b07705b3a00c6c9aea5c1a81549 Mon Sep 17 00:00:00 2001 From: avee87 <6134677+avee87@users.noreply.github.com> Date: Sat, 16 Oct 2021 23:57:47 +0100 Subject: [PATCH 0440/1038] Use separate weather condition for clear night in MetOffice forecasts (#55135) --- homeassistant/components/metoffice/const.py | 4 +++- tests/components/metoffice/test_weather.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/metoffice/const.py b/homeassistant/components/metoffice/const.py index 0b275f301cd..e413b102898 100644 --- a/homeassistant/components/metoffice/const.py +++ b/homeassistant/components/metoffice/const.py @@ -2,6 +2,7 @@ from datetime import timedelta from homeassistant.components.weather import ( + ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, ATTR_CONDITION_EXCEPTIONAL, ATTR_CONDITION_FOG, @@ -37,6 +38,7 @@ MODE_DAILY = "daily" MODE_DAILY_LABEL = "Daily" CONDITION_CLASSES = { + ATTR_CONDITION_CLEAR_NIGHT: ["0"], ATTR_CONDITION_CLOUDY: ["7", "8"], ATTR_CONDITION_FOG: ["5", "6"], ATTR_CONDITION_HAIL: ["19", "20", "21"], @@ -47,7 +49,7 @@ CONDITION_CLASSES = { ATTR_CONDITION_RAINY: ["9", "10", "11", "12"], ATTR_CONDITION_SNOWY: ["22", "23", "24", "25", "26", "27"], ATTR_CONDITION_SNOWY_RAINY: ["16", "17", "18"], - ATTR_CONDITION_SUNNY: ["0", "1"], + ATTR_CONDITION_SUNNY: ["1"], ATTR_CONDITION_WINDY: [], ATTR_CONDITION_WINDY_VARIANT: [], ATTR_CONDITION_EXCEPTIONAL: [], diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index 21b2196804c..76e01b638c3 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -231,7 +231,7 @@ async def test_two_weather_sites_running(hass, requests_mock, legacy_patchable_t weather.attributes.get("forecast")[18]["datetime"] == "2020-04-27T21:00:00+00:00" ) - assert weather.attributes.get("forecast")[18]["condition"] == "sunny" + assert weather.attributes.get("forecast")[18]["condition"] == "clear-night" assert weather.attributes.get("forecast")[18]["temperature"] == 9 assert weather.attributes.get("forecast")[18]["wind_speed"] == 4 assert weather.attributes.get("forecast")[18]["wind_bearing"] == "NW" From c76e15149ce128bde1a5891b47e96b14c300c062 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 17 Oct 2021 00:12:27 +0000 Subject: [PATCH 0441/1038] [ci skip] Translation update --- .../accuweather/translations/bg.json | 7 ++++ .../components/adguard/translations/bg.json | 4 ++ .../advantage_air/translations/bg.json | 12 ++++++ .../components/agent_dvr/translations/bg.json | 11 +++++ .../components/airthings/translations/bg.json | 19 +++++++++ .../components/airthings/translations/cs.json | 19 +++++++++ .../components/airthings/translations/pl.json | 4 ++ .../components/airtouch4/translations/bg.json | 17 ++++++++ .../components/airvisual/translations/bg.json | 11 +++++ .../alarm_control_panel/translations/tr.json | 33 +++++++++++---- .../alarmdecoder/translations/bg.json | 11 +++++ .../alarmdecoder/translations/tr.json | 4 +- .../components/almond/translations/bg.json | 3 +- .../amberelectric/translations/bg.json | 12 ++++++ .../ambiclimate/translations/bg.json | 3 +- .../components/arcam_fmj/translations/bg.json | 11 +++++ .../components/atag/translations/bg.json | 11 +++++ .../azure_devops/translations/bg.json | 8 ++++ .../binary_sensor/translations/bg.json | 8 ++++ .../binary_sensor/translations/ca.json | 4 ++ .../binary_sensor/translations/de.json | 4 ++ .../components/blebox/translations/bg.json | 11 +++++ .../components/blebox/translations/tr.json | 3 +- .../components/blink/translations/bg.json | 7 ++++ .../components/blink/translations/tr.json | 3 ++ .../components/braviatv/translations/bg.json | 11 +++++ .../components/broadlink/translations/bg.json | 11 +++++ .../components/brother/translations/bg.json | 7 ++++ .../components/bsblan/translations/bg.json | 11 +++++ .../components/canary/translations/bg.json | 7 ++++ .../components/cloud/translations/bg.json | 7 ++++ .../cloudflare/translations/bg.json | 23 +++++++++++ .../crownstone/translations/bg.json | 19 +++++++++ .../crownstone/translations/cs.json | 16 ++++++++ .../crownstone/translations/pl.json | 4 ++ .../components/daikin/translations/bg.json | 6 ++- .../components/daikin/translations/pl.json | 1 + .../components/deconz/translations/bg.json | 5 +++ .../components/dlna_dmr/translations/bg.json | 25 +++++++++++ .../components/dlna_dmr/translations/cs.json | 18 ++++++++ .../components/dlna_dmr/translations/pl.json | 15 ++++++- .../components/dlna_dmr/translations/tr.json | 16 ++++++++ .../components/dsmr/translations/bg.json | 11 +++++ .../components/efergy/translations/bg.json | 20 +++++++++ .../components/efergy/translations/cs.json | 9 +++- .../components/efergy/translations/pl.json | 20 +++++++++ .../components/efergy/translations/tr.json | 19 +++++++++ .../components/elgato/translations/bg.json | 11 +++++ .../emulated_roku/translations/bg.json | 3 ++ .../components/energy/translations/bg.json | 3 ++ .../environment_canada/translations/bg.json | 19 +++++++++ .../environment_canada/translations/cs.json | 1 + .../environment_canada/translations/pl.json | 18 ++++++++ .../environment_canada/translations/tr.json | 9 +++- .../components/esphome/translations/bg.json | 14 ++++++- .../components/esphome/translations/hu.json | 10 ++--- .../components/esphome/translations/pl.json | 3 +- .../faa_delays/translations/bg.json | 4 ++ .../fjaraskupan/translations/bg.json | 8 ++++ .../fjaraskupan/translations/cs.json | 3 +- .../components/flipr/translations/bg.json | 20 +++++++++ .../components/flo/translations/bg.json | 7 ++++ .../components/flux_led/translations/bg.json | 35 ++++++++++++++++ .../components/flux_led/translations/cs.json | 19 +++++++++ .../components/flux_led/translations/pl.json | 17 +++++++- .../components/flux_led/translations/tr.json | 7 +++- .../forecast_solar/translations/bg.json | 11 +++++ .../forked_daapd/translations/tr.json | 4 +- .../components/freebox/translations/bg.json | 11 +++++ .../freedompro/translations/bg.json | 11 +++++ .../components/fritz/translations/bg.json | 16 ++++++++ .../geonetnz_volcano/translations/bg.json | 3 ++ .../components/goalzero/translations/bg.json | 7 ++++ .../components/group/translations/hr.json | 2 +- .../components/guardian/translations/bg.json | 14 +++++++ .../homeassistant/translations/bg.json | 8 ++++ .../components/homekit/translations/bg.json | 16 ++++++++ .../components/hyperion/translations/bg.json | 11 +++++ .../components/ialarm/translations/bg.json | 11 +++++ .../components/ialarm/translations/tr.json | 12 ++++++ .../components/insteon/translations/bg.json | 34 +++++++++++++++ .../components/iotawatt/translations/bg.json | 22 ++++++++++ .../components/ipp/translations/bg.json | 11 +++++ .../components/isy994/translations/tr.json | 4 +- .../components/kodi/translations/bg.json | 22 ++++++++++ .../components/kodi/translations/hu.json | 2 +- .../components/konnected/translations/bg.json | 11 +++++ .../components/konnected/translations/tr.json | 6 +++ .../components/life360/translations/bg.json | 4 ++ .../components/luftdaten/translations/bg.json | 2 + .../lutron_caseta/translations/tr.json | 4 ++ .../components/met/translations/bg.json | 3 ++ .../components/mikrotik/translations/bg.json | 11 +++++ .../components/mill/translations/bg.json | 7 ++++ .../modem_callerid/translations/bg.json | 22 ++++++++++ .../modem_callerid/translations/pl.json | 4 ++ .../components/monoprice/translations/bg.json | 11 +++++ .../components/mqtt/translations/bg.json | 10 +++++ .../components/mysensors/translations/bg.json | 6 +++ .../components/nam/translations/bg.json | 7 ++++ .../components/nanoleaf/translations/bg.json | 22 ++++++++++ .../components/nest/translations/bg.json | 3 ++ .../components/netgear/translations/bg.json | 23 +++++++++++ .../components/netgear/translations/cs.json | 3 ++ .../components/netgear/translations/pl.json | 5 +++ .../nightscout/translations/bg.json | 7 ++++ .../components/notion/translations/bg.json | 13 +++++- .../components/notion/translations/cs.json | 12 +++++- .../components/notion/translations/pl.json | 6 ++- .../components/nut/translations/bg.json | 11 +++++ .../components/nzbget/translations/bg.json | 14 +++++++ .../components/omnilogic/translations/bg.json | 7 ++++ .../components/onewire/translations/bg.json | 23 +++++++++++ .../components/onvif/translations/bg.json | 20 +++++++++ .../opengarage/translations/bg.json | 21 ++++++++++ .../opengarage/translations/cs.json | 21 ++++++++++ .../opengarage/translations/pl.json | 21 ++++++++++ .../opengarage/translations/tr.json | 19 +++++++++ .../opentherm_gw/translations/bg.json | 1 + .../openweathermap/translations/bg.json | 7 ++++ .../ovo_energy/translations/bg.json | 7 ++++ .../p1_monitor/translations/bg.json | 16 ++++++++ .../philips_js/translations/bg.json | 7 ++++ .../components/pi_hole/translations/bg.json | 11 +++++ .../components/pi_hole/translations/tr.json | 1 + .../components/plex/translations/bg.json | 5 +++ .../components/plugwise/translations/bg.json | 12 ++++++ .../progettihwsw/translations/bg.json | 14 +++++++ .../components/prosegur/translations/bg.json | 28 +++++++++++++ .../rainforest_eagle/translations/bg.json | 20 +++++++++ .../components/renault/translations/bg.json | 26 ++++++++++++ .../components/rfxtrx/translations/bg.json | 17 ++++++++ .../components/risco/translations/bg.json | 7 ++++ .../components/risco/translations/tr.json | 17 ++++++++ .../components/roku/translations/hu.json | 2 +- .../ruckus_unleashed/translations/bg.json | 11 +++++ .../screenlogic/translations/bg.json | 11 +++++ .../components/sharkiq/translations/bg.json | 11 +++++ .../components/shelly/translations/bg.json | 5 +++ .../components/sia/translations/bg.json | 11 +++++ .../simplisafe/translations/bg.json | 3 +- .../smart_meter_texas/translations/bg.json | 7 ++++ .../components/soma/translations/cs.json | 8 ++-- .../components/soma/translations/nl.json | 6 +-- .../components/sonarr/translations/bg.json | 11 +++++ .../squeezebox/translations/bg.json | 11 +++++ .../stookalert/translations/bg.json | 7 ++++ .../stookalert/translations/cs.json | 7 ++++ .../stookalert/translations/pl.json | 7 ++++ .../stookalert/translations/tr.json | 7 ++++ .../components/sun/translations/tr.json | 4 +- .../surepetcare/translations/bg.json | 20 +++++++++ .../surepetcare/translations/pl.json | 7 +++- .../components/switchbot/translations/bg.json | 23 +++++++++++ .../components/switchbot/translations/cs.json | 9 +++- .../components/switchbot/translations/pl.json | 2 + .../synology_dsm/translations/bg.json | 25 +++++++++++ .../synology_dsm/translations/cs.json | 6 +++ .../synology_dsm/translations/pl.json | 3 +- .../system_bridge/translations/bg.json | 11 +++++ .../components/tasmota/translations/bg.json | 15 +++++++ .../components/tile/translations/bg.json | 7 ++++ .../components/tplink/translations/bg.json | 19 +++++++++ .../components/tplink/translations/cs.json | 14 +++++++ .../components/tplink/translations/pl.json | 9 ++++ .../components/tractive/translations/bg.json | 20 +++++++++ .../components/tuya/translations/bg.json | 41 +++++++++++++++++++ .../components/tuya/translations/cs.json | 5 ++- .../components/tuya/translations/tr.json | 9 ++++ .../components/upb/translations/tr.json | 6 +++ .../components/upcloud/translations/bg.json | 16 ++++++++ .../components/upnp/translations/bg.json | 7 ++++ .../uptimerobot/translations/bg.json | 27 ++++++++++++ .../components/velbus/translations/bg.json | 3 ++ .../components/vizio/translations/tr.json | 3 +- .../vlc_telnet/translations/bg.json | 36 ++++++++++++++++ .../vlc_telnet/translations/ca.json | 36 ++++++++++++++++ .../vlc_telnet/translations/cs.json | 29 +++++++++++++ .../vlc_telnet/translations/de.json | 8 +++- .../vlc_telnet/translations/he.json | 29 +++++++++++++ .../vlc_telnet/translations/pl.json | 30 ++++++++++++++ .../vlc_telnet/translations/ru.json | 30 ++++++++++++++ .../vlc_telnet/translations/tr.json | 6 ++- .../components/volumio/translations/bg.json | 14 +++++++ .../components/volumio/translations/hu.json | 2 +- .../water_heater/translations/bg.json | 8 ++++ .../components/watttime/translations/bg.json | 40 ++++++++++++++++++ .../components/watttime/translations/cs.json | 6 ++- .../components/watttime/translations/pl.json | 11 +++++ .../components/whirlpool/translations/bg.json | 17 ++++++++ .../components/whirlpool/translations/pl.json | 1 + .../components/wiffi/translations/bg.json | 11 +++++ .../components/wled/translations/bg.json | 6 +++ .../xiaomi_miio/translations/bg.json | 3 ++ .../xiaomi_miio/translations/select.bg.json | 7 ++++ .../yale_smart_alarm/translations/bg.json | 26 ++++++++++++ .../yale_smart_alarm/translations/tr.json | 22 ++++++++++ .../components/yeelight/translations/bg.json | 7 ++++ .../components/youless/translations/bg.json | 15 +++++++ .../components/zha/translations/bg.json | 9 ++++ .../zoneminder/translations/bg.json | 7 ++++ .../components/zwave_js/translations/bg.json | 21 ++++++++++ .../components/zwave_js/translations/tr.json | 5 +++ 203 files changed, 2338 insertions(+), 59 deletions(-) create mode 100644 homeassistant/components/accuweather/translations/bg.json create mode 100644 homeassistant/components/advantage_air/translations/bg.json create mode 100644 homeassistant/components/agent_dvr/translations/bg.json create mode 100644 homeassistant/components/airthings/translations/bg.json create mode 100644 homeassistant/components/airthings/translations/cs.json create mode 100644 homeassistant/components/airtouch4/translations/bg.json create mode 100644 homeassistant/components/alarmdecoder/translations/bg.json create mode 100644 homeassistant/components/amberelectric/translations/bg.json create mode 100644 homeassistant/components/arcam_fmj/translations/bg.json create mode 100644 homeassistant/components/atag/translations/bg.json create mode 100644 homeassistant/components/azure_devops/translations/bg.json create mode 100644 homeassistant/components/blebox/translations/bg.json create mode 100644 homeassistant/components/blink/translations/bg.json create mode 100644 homeassistant/components/braviatv/translations/bg.json create mode 100644 homeassistant/components/broadlink/translations/bg.json create mode 100644 homeassistant/components/brother/translations/bg.json create mode 100644 homeassistant/components/bsblan/translations/bg.json create mode 100644 homeassistant/components/canary/translations/bg.json create mode 100644 homeassistant/components/cloud/translations/bg.json create mode 100644 homeassistant/components/cloudflare/translations/bg.json create mode 100644 homeassistant/components/crownstone/translations/bg.json create mode 100644 homeassistant/components/dlna_dmr/translations/bg.json create mode 100644 homeassistant/components/dlna_dmr/translations/cs.json create mode 100644 homeassistant/components/dlna_dmr/translations/tr.json create mode 100644 homeassistant/components/dsmr/translations/bg.json create mode 100644 homeassistant/components/efergy/translations/bg.json create mode 100644 homeassistant/components/efergy/translations/pl.json create mode 100644 homeassistant/components/efergy/translations/tr.json create mode 100644 homeassistant/components/elgato/translations/bg.json create mode 100644 homeassistant/components/energy/translations/bg.json create mode 100644 homeassistant/components/environment_canada/translations/bg.json create mode 100644 homeassistant/components/environment_canada/translations/pl.json create mode 100644 homeassistant/components/fjaraskupan/translations/bg.json create mode 100644 homeassistant/components/flipr/translations/bg.json create mode 100644 homeassistant/components/flo/translations/bg.json create mode 100644 homeassistant/components/flux_led/translations/bg.json create mode 100644 homeassistant/components/flux_led/translations/cs.json create mode 100644 homeassistant/components/forecast_solar/translations/bg.json create mode 100644 homeassistant/components/freebox/translations/bg.json create mode 100644 homeassistant/components/freedompro/translations/bg.json create mode 100644 homeassistant/components/fritz/translations/bg.json create mode 100644 homeassistant/components/goalzero/translations/bg.json create mode 100644 homeassistant/components/guardian/translations/bg.json create mode 100644 homeassistant/components/homeassistant/translations/bg.json create mode 100644 homeassistant/components/homekit/translations/bg.json create mode 100644 homeassistant/components/hyperion/translations/bg.json create mode 100644 homeassistant/components/ialarm/translations/bg.json create mode 100644 homeassistant/components/ialarm/translations/tr.json create mode 100644 homeassistant/components/insteon/translations/bg.json create mode 100644 homeassistant/components/iotawatt/translations/bg.json create mode 100644 homeassistant/components/ipp/translations/bg.json create mode 100644 homeassistant/components/kodi/translations/bg.json create mode 100644 homeassistant/components/konnected/translations/bg.json create mode 100644 homeassistant/components/mikrotik/translations/bg.json create mode 100644 homeassistant/components/mill/translations/bg.json create mode 100644 homeassistant/components/modem_callerid/translations/bg.json create mode 100644 homeassistant/components/monoprice/translations/bg.json create mode 100644 homeassistant/components/nam/translations/bg.json create mode 100644 homeassistant/components/nanoleaf/translations/bg.json create mode 100644 homeassistant/components/netgear/translations/bg.json create mode 100644 homeassistant/components/nightscout/translations/bg.json create mode 100644 homeassistant/components/nut/translations/bg.json create mode 100644 homeassistant/components/nzbget/translations/bg.json create mode 100644 homeassistant/components/omnilogic/translations/bg.json create mode 100644 homeassistant/components/onewire/translations/bg.json create mode 100644 homeassistant/components/onvif/translations/bg.json create mode 100644 homeassistant/components/opengarage/translations/bg.json create mode 100644 homeassistant/components/opengarage/translations/cs.json create mode 100644 homeassistant/components/opengarage/translations/pl.json create mode 100644 homeassistant/components/opengarage/translations/tr.json create mode 100644 homeassistant/components/openweathermap/translations/bg.json create mode 100644 homeassistant/components/ovo_energy/translations/bg.json create mode 100644 homeassistant/components/p1_monitor/translations/bg.json create mode 100644 homeassistant/components/pi_hole/translations/bg.json create mode 100644 homeassistant/components/plugwise/translations/bg.json create mode 100644 homeassistant/components/progettihwsw/translations/bg.json create mode 100644 homeassistant/components/prosegur/translations/bg.json create mode 100644 homeassistant/components/rainforest_eagle/translations/bg.json create mode 100644 homeassistant/components/renault/translations/bg.json create mode 100644 homeassistant/components/rfxtrx/translations/bg.json create mode 100644 homeassistant/components/risco/translations/bg.json create mode 100644 homeassistant/components/ruckus_unleashed/translations/bg.json create mode 100644 homeassistant/components/screenlogic/translations/bg.json create mode 100644 homeassistant/components/sharkiq/translations/bg.json create mode 100644 homeassistant/components/sia/translations/bg.json create mode 100644 homeassistant/components/smart_meter_texas/translations/bg.json create mode 100644 homeassistant/components/sonarr/translations/bg.json create mode 100644 homeassistant/components/squeezebox/translations/bg.json create mode 100644 homeassistant/components/stookalert/translations/bg.json create mode 100644 homeassistant/components/stookalert/translations/cs.json create mode 100644 homeassistant/components/stookalert/translations/pl.json create mode 100644 homeassistant/components/stookalert/translations/tr.json create mode 100644 homeassistant/components/surepetcare/translations/bg.json create mode 100644 homeassistant/components/switchbot/translations/bg.json create mode 100644 homeassistant/components/synology_dsm/translations/bg.json create mode 100644 homeassistant/components/system_bridge/translations/bg.json create mode 100644 homeassistant/components/tasmota/translations/bg.json create mode 100644 homeassistant/components/tile/translations/bg.json create mode 100644 homeassistant/components/tractive/translations/bg.json create mode 100644 homeassistant/components/tuya/translations/bg.json create mode 100644 homeassistant/components/upcloud/translations/bg.json create mode 100644 homeassistant/components/uptimerobot/translations/bg.json create mode 100644 homeassistant/components/vlc_telnet/translations/bg.json create mode 100644 homeassistant/components/vlc_telnet/translations/ca.json create mode 100644 homeassistant/components/vlc_telnet/translations/cs.json create mode 100644 homeassistant/components/vlc_telnet/translations/he.json create mode 100644 homeassistant/components/vlc_telnet/translations/pl.json create mode 100644 homeassistant/components/vlc_telnet/translations/ru.json create mode 100644 homeassistant/components/volumio/translations/bg.json create mode 100644 homeassistant/components/water_heater/translations/bg.json create mode 100644 homeassistant/components/watttime/translations/bg.json create mode 100644 homeassistant/components/whirlpool/translations/bg.json create mode 100644 homeassistant/components/wiffi/translations/bg.json create mode 100644 homeassistant/components/xiaomi_miio/translations/select.bg.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/bg.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/tr.json create mode 100644 homeassistant/components/yeelight/translations/bg.json create mode 100644 homeassistant/components/youless/translations/bg.json create mode 100644 homeassistant/components/zoneminder/translations/bg.json diff --git a/homeassistant/components/accuweather/translations/bg.json b/homeassistant/components/accuweather/translations/bg.json new file mode 100644 index 00000000000..2ac8a444100 --- /dev/null +++ b/homeassistant/components/accuweather/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/translations/bg.json b/homeassistant/components/adguard/translations/bg.json index 97d8547861f..00ad33588a4 100644 --- a/homeassistant/components/adguard/translations/bg.json +++ b/homeassistant/components/adguard/translations/bg.json @@ -3,6 +3,9 @@ "abort": { "existing_instance_updated": "\u0410\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430\u0442\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "hassio_confirm": { "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 Home Assistant \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0437\u0432\u0430 \u0441 AdGuard Home, \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0435\u043d \u043e\u0442 Supervisor \u0434\u043e\u0431\u0430\u0432\u043a\u0430\u0442\u0430: {addon} ?", @@ -11,6 +14,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", "ssl": "AdGuard Home \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 SSL \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435", "verify_ssl": "AdGuard Home \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u043d\u0430\u0434\u0435\u0436\u0434\u0435\u043d \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442" diff --git a/homeassistant/components/advantage_air/translations/bg.json b/homeassistant/components/advantage_air/translations/bg.json new file mode 100644 index 00000000000..426266d26be --- /dev/null +++ b/homeassistant/components/advantage_air/translations/bg.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "ip_address": "IP \u0430\u0434\u0440\u0435\u0441", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/bg.json b/homeassistant/components/agent_dvr/translations/bg.json new file mode 100644 index 00000000000..4983c9a14b2 --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings/translations/bg.json b/homeassistant/components/airthings/translations/bg.json new file mode 100644 index 00000000000..df9d136dfe8 --- /dev/null +++ b/homeassistant/components/airthings/translations/bg.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + }, + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "id": "ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings/translations/cs.json b/homeassistant/components/airthings/translations/cs.json new file mode 100644 index 00000000000..740a9675b96 --- /dev/null +++ b/homeassistant/components/airthings/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "id": "ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings/translations/pl.json b/homeassistant/components/airthings/translations/pl.json index c3895e64423..671360f0ecd 100644 --- a/homeassistant/components/airthings/translations/pl.json +++ b/homeassistant/components/airthings/translations/pl.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane" + }, "error": { "cannot_connect": "Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107", + "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { diff --git a/homeassistant/components/airtouch4/translations/bg.json b/homeassistant/components/airtouch4/translations/bg.json new file mode 100644 index 00000000000..4c9b4c409d0 --- /dev/null +++ b/homeassistant/components/airtouch4/translations/bg.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/bg.json b/homeassistant/components/airvisual/translations/bg.json index 7e463418576..065d5854420 100644 --- a/homeassistant/components/airvisual/translations/bg.json +++ b/homeassistant/components/airvisual/translations/bg.json @@ -1,11 +1,22 @@ { "config": { + "abort": { + "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\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "geography_by_name": { "data": { "city": "\u0413\u0440\u0430\u0434", "country": "\u0421\u0442\u0440\u0430\u043d\u0430" } + }, + "reauth_confirm": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + } } } } diff --git a/homeassistant/components/alarm_control_panel/translations/tr.json b/homeassistant/components/alarm_control_panel/translations/tr.json index cc509430436..c7a8235d5c9 100644 --- a/homeassistant/components/alarm_control_panel/translations/tr.json +++ b/homeassistant/components/alarm_control_panel/translations/tr.json @@ -1,23 +1,40 @@ { "device_automation": { + "action_type": { + "arm_away": "D\u0131\u015farda", + "arm_home": "Evde", + "arm_night": "Gece", + "disarm": "Devre d\u0131\u015f\u0131 {entity_name}", + "trigger": "Tetikle {entity_name}" + }, + "condition_type": { + "is_armed_away": "{entity_name} D\u0131\u015farda Modu Aktif", + "is_armed_home": "{entity_name} Evde Modu Aktif", + "is_armed_night": "{entity_name} Gece Modu Aktif", + "is_disarmed": "{entity_name} Devre D\u0131\u015f\u0131", + "is_triggered": "{entity_name} tetiklendi" + }, "trigger_type": { - "disarmed": "{entity_name} b\u0131rak\u0131ld\u0131", + "armed_away": "{entity_name} D\u0131\u015farda Modu Aktif", + "armed_home": "{entity_name} Evde Modu Aktif", + "armed_night": "{entity_name} Gece Modu Aktif", + "disarmed": "{entity_name} Devre D\u0131\u015f\u0131", "triggered": "{entity_name} tetiklendi" } }, "state": { "_": { - "armed": "Etkin", - "armed_away": "Etkin d\u0131\u015far\u0131da", - "armed_custom_bypass": "Alarm etkin \u00f6zel baypas", - "armed_home": "Etkin evde", - "armed_night": "Etkin gece", + "armed": "Aktif", + "armed_away": "D\u0131\u015farda Aktif", + "armed_custom_bypass": "\u00d6zel Mod Aktif", + "armed_home": "Evde Aktif", + "armed_night": "Gece Aktif", "arming": "Alarm etkinle\u015fiyor", - "disarmed": "Etkisiz", + "disarmed": "Devre D\u0131\u015f\u0131", "disarming": "Alarm devre d\u0131\u015f\u0131", "pending": "Beklemede", "triggered": "Tetiklendi" } }, - "title": "Alarm kontrol paneli" + "title": "Alarm Kontrol Paneli" } \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/translations/bg.json b/homeassistant/components/alarmdecoder/translations/bg.json new file mode 100644 index 00000000000..3d12f7d19a5 --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "protocol": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/translations/tr.json b/homeassistant/components/alarmdecoder/translations/tr.json index 276b733b31f..2334f9fb99f 100644 --- a/homeassistant/components/alarmdecoder/translations/tr.json +++ b/homeassistant/components/alarmdecoder/translations/tr.json @@ -34,7 +34,9 @@ "data": { "zone_name": "B\u00f6lge Ad\u0131", "zone_relayaddr": "R\u00f6le Adresi", - "zone_relaychan": "R\u00f6le Kanal\u0131" + "zone_relaychan": "R\u00f6le Kanal\u0131", + "zone_rfid": "RF Id", + "zone_type": "B\u00f6lge Tipi" } }, "zone_select": { diff --git a/homeassistant/components/almond/translations/bg.json b/homeassistant/components/almond/translations/bg.json index bb0c874517b..81e1094b1ab 100644 --- a/homeassistant/components/almond/translations/bg.json +++ b/homeassistant/components/almond/translations/bg.json @@ -2,7 +2,8 @@ "config": { "abort": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 Almond \u0441\u044a\u0440\u0432\u044a\u0440\u0430.", - "missing_configuration": "\u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430 \u043a\u0430\u043a \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Almond." + "missing_configuration": "\u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430 \u043a\u0430\u043a \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Almond.", + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." }, "step": { "pick_implementation": { diff --git a/homeassistant/components/amberelectric/translations/bg.json b/homeassistant/components/amberelectric/translations/bg.json new file mode 100644 index 00000000000..1e3ce9e6025 --- /dev/null +++ b/homeassistant/components/amberelectric/translations/bg.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "site": { + "title": "Amber Electric" + }, + "user": { + "title": "Amber Electric" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/translations/bg.json b/homeassistant/components/ambiclimate/translations/bg.json index 627dd472018..35a413e3627 100644 --- a/homeassistant/components/ambiclimate/translations/bg.json +++ b/homeassistant/components/ambiclimate/translations/bg.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "access_token": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043a\u043e\u0434 \u0437\u0430 \u0434\u043e\u0441\u0442\u044a\u043f." + "access_token": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043a\u043e\u0434 \u0437\u0430 \u0434\u043e\u0441\u0442\u044a\u043f.", + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" }, "create_entry": { "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u043d\u0435 \u0441 Ambiclimate." diff --git a/homeassistant/components/arcam_fmj/translations/bg.json b/homeassistant/components/arcam_fmj/translations/bg.json new file mode 100644 index 00000000000..4983c9a14b2 --- /dev/null +++ b/homeassistant/components/arcam_fmj/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/atag/translations/bg.json b/homeassistant/components/atag/translations/bg.json new file mode 100644 index 00000000000..4983c9a14b2 --- /dev/null +++ b/homeassistant/components/atag/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_devops/translations/bg.json b/homeassistant/components/azure_devops/translations/bg.json new file mode 100644 index 00000000000..d9f03d82592 --- /dev/null +++ b/homeassistant/components/azure_devops/translations/bg.json @@ -0,0 +1,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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/bg.json b/homeassistant/components/binary_sensor/translations/bg.json index 2d969af731e..b1b3d766dc4 100644 --- a/homeassistant/components/binary_sensor/translations/bg.json +++ b/homeassistant/components/binary_sensor/translations/bg.json @@ -17,6 +17,7 @@ "is_no_problem": "{entity_name} \u043d\u0435 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c", "is_no_smoke": "{entity_name} \u043d\u0435 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0438\u043c", "is_no_sound": "{entity_name} \u043d\u0435 \u0437\u0430\u0441\u0438\u0447\u0430 \u0437\u0432\u0443\u043a", + "is_no_update": "{entity_name} \u0435 \u0430\u043a\u0442\u0443\u0430\u043b\u0435\u043d", "is_no_vibration": "{entity_name} \u043d\u0435 \u0437\u0430\u0441\u0438\u0447\u0430 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u0438", "is_not_bat_low": "{entity_name} \u0431\u0430\u0442\u0435\u0440\u0438\u044f\u0442\u0430 \u0435 \u0437\u0430\u0440\u0435\u0434\u0435\u043d\u0430", "is_not_cold": "{entity_name} \u043d\u0435 \u0435 \u0441\u0442\u0443\u0434\u0435\u043d", @@ -42,6 +43,7 @@ "is_smoke": "{entity_name} \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0438\u043c", "is_sound": "{entity_name} \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0437\u0432\u0443\u043a", "is_unsafe": "{entity_name} \u043d\u0435 \u0435 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u0435\u043d", + "is_update": "{entity_name} \u0438\u043c\u0430 \u043d\u0430\u043b\u0438\u0447\u043d\u0430 \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f", "is_vibration": "{entity_name} \u0437\u0430\u0441\u0438\u0447\u0430 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u0438" }, "trigger_type": { @@ -61,6 +63,7 @@ "no_problem": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c", "no_smoke": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0438\u043c", "no_sound": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0437\u0432\u0443\u043a", + "no_update": "{entity_name} \u0435 \u0430\u043a\u0442\u0443\u0430\u043b\u0435\u043d", "no_vibration": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u0437\u0430\u0441\u0438\u0447\u0430 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u0438", "not_bat_low": "{entity_name} \u0431\u0430\u0442\u0435\u0440\u0438\u044f\u0442\u0430 \u043d\u0435 \u0435 \u0438\u0437\u0442\u043e\u0449\u0435\u043d\u0430", "not_cold": "{entity_name} \u0441\u0435 \u0441\u0442\u043e\u043f\u043b\u0438", @@ -86,6 +89,7 @@ "turned_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", "turned_on": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d", "unsafe": "{entity_name} \u0441\u0442\u0430\u043d\u0430 \u043e\u043f\u0430\u0441\u0435\u043d", + "update": "{entity_name} \u0438\u043c\u0430 \u043d\u0430\u043b\u0438\u0447\u043d\u0430 \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f", "vibration": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u0437\u0430\u0441\u0438\u0447\u0430 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u0438" } }, @@ -162,6 +166,10 @@ "off": "\u0427\u0438\u0441\u0442\u043e", "on": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d" }, + "update": { + "off": "\u0410\u043a\u0442\u0443\u0430\u043b\u0435\u043d", + "on": "\u041d\u0430\u043b\u0438\u0447\u043d\u0430 \u0435 \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f" + }, "vibration": { "off": "\u0427\u0438\u0441\u0442\u043e", "on": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d\u0430" diff --git a/homeassistant/components/binary_sensor/translations/ca.json b/homeassistant/components/binary_sensor/translations/ca.json index 089f72f51d5..f2889315fe5 100644 --- a/homeassistant/components/binary_sensor/translations/ca.json +++ b/homeassistant/components/binary_sensor/translations/ca.json @@ -31,6 +31,7 @@ "is_not_plugged_in": "{entity_name} est\u00e0 desendollat", "is_not_powered": "{entity_name} no est\u00e0 alimentat", "is_not_present": "{entity_name} no est\u00e0 present", + "is_not_tampered": "{entity_name} no detecta manipulaci\u00f3", "is_not_unsafe": "{entity_name} \u00e9s segur", "is_occupied": "{entity_name} est\u00e0 ocupat", "is_off": "{entity_name} est\u00e0 apagat", @@ -42,6 +43,7 @@ "is_problem": "{entity_name} est\u00e0 detectant un problema", "is_smoke": "{entity_name} est\u00e0 detectant fum", "is_sound": "{entity_name} est\u00e0 detectant so", + "is_tampered": "{entity_name} detecta manipulaci\u00f3", "is_unsafe": "{entity_name} \u00e9s insegur", "is_update": "{entity_name} t\u00e9 una actualitzaci\u00f3 disponible", "is_vibration": "{entity_name} est\u00e0 detectant vibraci\u00f3" @@ -52,6 +54,8 @@ "connected": "{entity_name} est\u00e0 connectat", "gas": "{entity_name} ha comen\u00e7at a detectar gas", "hot": "{entity_name} es torna calent", + "is_not_tampered": "{entity_name} ha deixat de detectar manipulaci\u00f3", + "is_tampered": "{entity_name} ha comen\u00e7at a detectar manipulaci\u00f3", "light": "{entity_name} ha comen\u00e7at a detectar llum", "locked": "{entity_name} est\u00e0 bloquejat", "moist": "{entity_name} es torna humit", diff --git a/homeassistant/components/binary_sensor/translations/de.json b/homeassistant/components/binary_sensor/translations/de.json index 21d1eff1ebf..e2b4c1962eb 100644 --- a/homeassistant/components/binary_sensor/translations/de.json +++ b/homeassistant/components/binary_sensor/translations/de.json @@ -31,6 +31,7 @@ "is_not_plugged_in": "{entity_name} ist nicht angeschlossen", "is_not_powered": "{entity_name} wird nicht mit Strom versorgt", "is_not_present": "{entity_name} ist nicht vorhanden", + "is_not_tampered": "{entity_name} erkennt keine Manipulationen", "is_not_unsafe": "{entity_name} ist sicher", "is_occupied": "{entity_name} ist besch\u00e4ftigt / besetzt", "is_off": "{entity_name} ist ausgeschaltet", @@ -42,6 +43,7 @@ "is_problem": "{entity_name} hat ein Problem festgestellt", "is_smoke": "{entity_name} hat Rauch detektiert", "is_sound": "{entity_name} hat Ger\u00e4usche detektiert", + "is_tampered": "{Einheit_Name} erkennt Manipulationen", "is_unsafe": "{entity_name} ist unsicher", "is_update": "{entity_name} hat ein Update verf\u00fcgbar", "is_vibration": "{entity_name} erkennt Vibrationen." @@ -52,6 +54,8 @@ "connected": "{entity_name} verbunden", "gas": "{entity_name} hat Gas detektiert", "hot": "{entity_name} wurde hei\u00df", + "is_not_tampered": "{entity_name} hat aufgeh\u00f6rt, Manipulationen zu erkennen", + "is_tampered": "{entity_name} hat begonnen, Manipulationen zu erkennen", "light": "{entity_name} hat Licht detektiert", "locked": "{entity_name} gesperrt", "moist": "{entity_name} wurde feucht", diff --git a/homeassistant/components/blebox/translations/bg.json b/homeassistant/components/blebox/translations/bg.json new file mode 100644 index 00000000000..4983c9a14b2 --- /dev/null +++ b/homeassistant/components/blebox/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blebox/translations/tr.json b/homeassistant/components/blebox/translations/tr.json index 31df3fb5e30..6acd2cf7d43 100644 --- a/homeassistant/components/blebox/translations/tr.json +++ b/homeassistant/components/blebox/translations/tr.json @@ -6,7 +6,8 @@ }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", - "unknown": "Beklenmeyen hata" + "unknown": "Beklenmeyen hata", + "unsupported_version": "BleBox cihaz\u0131n\u0131n g\u00fcncel olmayan bellenimi var. L\u00fctfen \u00f6nce y\u00fckseltin." }, "step": { "user": { diff --git a/homeassistant/components/blink/translations/bg.json b/homeassistant/components/blink/translations/bg.json new file mode 100644 index 00000000000..2ac8a444100 --- /dev/null +++ b/homeassistant/components/blink/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/translations/tr.json b/homeassistant/components/blink/translations/tr.json index 8193ff9d8be..cef806cb309 100644 --- a/homeassistant/components/blink/translations/tr.json +++ b/homeassistant/components/blink/translations/tr.json @@ -11,6 +11,9 @@ }, "step": { "2fa": { + "data": { + "2fa": "\u0130ki ad\u0131ml\u0131 kimlik do\u011frulama kodu" + }, "description": "E-postan\u0131za g\u00f6nderilen PIN kodunu girin" }, "user": { diff --git a/homeassistant/components/braviatv/translations/bg.json b/homeassistant/components/braviatv/translations/bg.json new file mode 100644 index 00000000000..5ee1a63993d --- /dev/null +++ b/homeassistant/components/braviatv/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "authorize": { + "data": { + "pin": "\u041f\u0418\u041d \u043a\u043e\u0434" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/bg.json b/homeassistant/components/broadlink/translations/bg.json new file mode 100644 index 00000000000..7534f7228f9 --- /dev/null +++ b/homeassistant/components/broadlink/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "not_supported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/translations/bg.json b/homeassistant/components/brother/translations/bg.json new file mode 100644 index 00000000000..2ac8a444100 --- /dev/null +++ b/homeassistant/components/brother/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/bg.json b/homeassistant/components/bsblan/translations/bg.json new file mode 100644 index 00000000000..4983c9a14b2 --- /dev/null +++ b/homeassistant/components/bsblan/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/canary/translations/bg.json b/homeassistant/components/canary/translations/bg.json new file mode 100644 index 00000000000..2ac8a444100 --- /dev/null +++ b/homeassistant/components/canary/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloud/translations/bg.json b/homeassistant/components/cloud/translations/bg.json new file mode 100644 index 00000000000..957c2a49d5c --- /dev/null +++ b/homeassistant/components/cloud/translations/bg.json @@ -0,0 +1,7 @@ +{ + "system_health": { + "info": { + "remote_server": "\u041e\u0442\u0434\u0430\u043b\u0435\u0447\u0435\u043d \u0441\u044a\u0440\u0432\u044a\u0440" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloudflare/translations/bg.json b/homeassistant/components/cloudflare/translations/bg.json new file mode 100644 index 00000000000..4716bbfb615 --- /dev/null +++ b/homeassistant/components/cloudflare/translations/bg.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "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_zone": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430 \u0437\u043e\u043d\u0430" + }, + "flow_title": "{name}", + "step": { + "user": { + "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043a\u044a\u043c Cloudflare" + }, + "zone": { + "data": { + "zone": "\u0417\u043e\u043d\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/bg.json b/homeassistant/components/crownstone/translations/bg.json new file mode 100644 index 00000000000..2c567e2a1e8 --- /dev/null +++ b/homeassistant/components/crownstone/translations/bg.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u0440\u043e\u0444\u0438\u043b\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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/cs.json b/homeassistant/components/crownstone/translations/cs.json index a7aaa1746f9..f1e209b21d8 100644 --- a/homeassistant/components/crownstone/translations/cs.json +++ b/homeassistant/components/crownstone/translations/cs.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, "step": { "usb_manual_config": { "data": { @@ -16,11 +22,21 @@ }, "options": { "step": { + "usb_config": { + "data": { + "usb_path": "Cesta k USB za\u0159\u00edzen\u00ed" + } + }, "usb_config_option": { "data": { "usb_path": "Cesta k USB za\u0159\u00edzen\u00ed" } }, + "usb_manual_config": { + "data": { + "usb_manual_path": "Cesta k USB za\u0159\u00edzen\u00ed" + } + }, "usb_manual_config_option": { "data": { "usb_manual_path": "Cesta k USB za\u0159\u00edzen\u00ed" diff --git a/homeassistant/components/crownstone/translations/pl.json b/homeassistant/components/crownstone/translations/pl.json index 948d71471e3..e201e7da0c7 100644 --- a/homeassistant/components/crownstone/translations/pl.json +++ b/homeassistant/components/crownstone/translations/pl.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane" + }, "error": { + "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { diff --git a/homeassistant/components/daikin/translations/bg.json b/homeassistant/components/daikin/translations/bg.json index a1f8209b2bf..a9b3e51b37d 100644 --- a/homeassistant/components/daikin/translations/bg.json +++ b/homeassistant/components/daikin/translations/bg.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "error": { + "api_password": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435, \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0439\u0442\u0435 API \u043a\u043b\u044e\u0447 \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u0430." }, "step": { "user": { diff --git a/homeassistant/components/daikin/translations/pl.json b/homeassistant/components/daikin/translations/pl.json index d11ffd4dd3a..c376bb0eb0e 100644 --- a/homeassistant/components/daikin/translations/pl.json +++ b/homeassistant/components/daikin/translations/pl.json @@ -5,6 +5,7 @@ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "error": { + "api_password": "Niepoprawne uwierzytelnienie, u\u017cyj klucza API albo has\u0142a.", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" diff --git a/homeassistant/components/deconz/translations/bg.json b/homeassistant/components/deconz/translations/bg.json index 24e36ecbe55..ba2b02e9232 100644 --- a/homeassistant/components/deconz/translations/bg.json +++ b/homeassistant/components/deconz/translations/bg.json @@ -19,6 +19,11 @@ "link": { "description": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438 deCONZ \u0448\u043b\u044e\u0437\u0430 \u0437\u0430 \u0434\u0430 \u0441\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430 \u0441 Home Assistant.\n\n1. \u041e\u0442\u0438\u0434\u0435\u0442\u0435 \u043d\u0430 deCONZ Settings -> Gateway -> Advanced\n2. \u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0431\u0443\u0442\u043e\u043d\u0430 \"Authenticate app\"", "title": "\u0412\u0440\u044a\u0437\u043a\u0430 \u0441 deCONZ" + }, + "manual_input": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } } } }, diff --git a/homeassistant/components/dlna_dmr/translations/bg.json b/homeassistant/components/dlna_dmr/translations/bg.json new file mode 100644 index 00000000000..9f4eca6ca56 --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/bg.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "could_not_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 DLNA \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", + "non_unique_id": "\u041d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0441\u0430 \u043d\u044f\u043a\u043e\u043b\u043a\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u0435\u0434\u0438\u043d \u0438 \u0441\u044a\u0449 \u0443\u043d\u0438\u043a\u0430\u043b\u0435\u043d \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440" + }, + "error": { + "could_not_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 DLNA \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + }, + "options": { + "error": { + "invalid_url": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d URL" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/cs.json b/homeassistant/components/dlna_dmr/translations/cs.json new file mode 100644 index 00000000000..85c9a831dda --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Chcete za\u010d\u00edt nastavovat?" + }, + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/pl.json b/homeassistant/components/dlna_dmr/translations/pl.json index e8940bef26a..b7687d3831a 100644 --- a/homeassistant/components/dlna_dmr/translations/pl.json +++ b/homeassistant/components/dlna_dmr/translations/pl.json @@ -1,5 +1,18 @@ { "config": { - "flow_title": "{name}" + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" + }, + "user": { + "data": { + "url": "URL" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/tr.json b/homeassistant/components/dlna_dmr/translations/tr.json new file mode 100644 index 00000000000..64e3f950b25 --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + }, + "options": { + "error": { + "invalid_url": "Ge\u00e7ersiz URL" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/bg.json b/homeassistant/components/dsmr/translations/bg.json new file mode 100644 index 00000000000..b3341fccde8 --- /dev/null +++ b/homeassistant/components/dsmr/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "setup_network": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/bg.json b/homeassistant/components/efergy/translations/bg.json new file mode 100644 index 00000000000..14d4c77c8f9 --- /dev/null +++ b/homeassistant/components/efergy/translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "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\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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/cs.json b/homeassistant/components/efergy/translations/cs.json index f7128887dac..b2fe2ea015f 100644 --- a/homeassistant/components/efergy/translations/cs.json +++ b/homeassistant/components/efergy/translations/cs.json @@ -1,13 +1,20 @@ { "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { "user": { "data": { "api_key": "API kl\u00ed\u010d" - } + }, + "title": "Efergy" } } } diff --git a/homeassistant/components/efergy/translations/pl.json b/homeassistant/components/efergy/translations/pl.json new file mode 100644 index 00000000000..9ef0e4a5a43 --- /dev/null +++ b/homeassistant/components/efergy/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/tr.json b/homeassistant/components/efergy/translations/tr.json new file mode 100644 index 00000000000..212abb7cb64 --- /dev/null +++ b/homeassistant/components/efergy/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flant\u0131 ba\u015far\u0131s\u0131z", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/translations/bg.json b/homeassistant/components/elgato/translations/bg.json new file mode 100644 index 00000000000..4983c9a14b2 --- /dev/null +++ b/homeassistant/components/elgato/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/translations/bg.json b/homeassistant/components/emulated_roku/translations/bg.json index a1a0fd75c60..9bdff95e9b3 100644 --- a/homeassistant/components/emulated_roku/translations/bg.json +++ b/homeassistant/components/emulated_roku/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\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/energy/translations/bg.json b/homeassistant/components/energy/translations/bg.json new file mode 100644 index 00000000000..cada66c2ac2 --- /dev/null +++ b/homeassistant/components/energy/translations/bg.json @@ -0,0 +1,3 @@ +{ + "title": "\u0415\u043d\u0435\u0440\u0433\u0438\u044f" +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/bg.json b/homeassistant/components/environment_canada/translations/bg.json new file mode 100644 index 00000000000..28c4730e5cd --- /dev/null +++ b/homeassistant/components/environment_canada/translations/bg.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "too_many_attempts": "\u0412\u0440\u044a\u0437\u043a\u0438\u0442\u0435 \u0441 Environment Canada \u0441\u0430 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438; \u041e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u0441\u043b\u0435\u0434 60 \u0441\u0435\u043a\u0443\u043d\u0434\u0438", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "language": "\u0415\u0437\u0438\u043a \u043d\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f\u0442\u0430 \u0437\u0430 \u0432\u0440\u0435\u043c\u0435\u0442\u043e", + "latitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0448\u0438\u0440\u0438\u043d\u0430", + "longitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0434\u044a\u043b\u0436\u0438\u043d\u0430", + "station": "ID \u043d\u0430 \u043c\u0435\u0442\u0435\u043e\u0440\u043e\u043b\u043e\u0433\u0438\u0447\u043d\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/cs.json b/homeassistant/components/environment_canada/translations/cs.json index b8a770a8405..4eb6ccd754c 100644 --- a/homeassistant/components/environment_canada/translations/cs.json +++ b/homeassistant/components/environment_canada/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { diff --git a/homeassistant/components/environment_canada/translations/pl.json b/homeassistant/components/environment_canada/translations/pl.json new file mode 100644 index 00000000000..b840de8a10e --- /dev/null +++ b/homeassistant/components/environment_canada/translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "language": "J\u0119zyk informacji pogodowych", + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "station": "Identyfikator stacji pogodowej" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/tr.json b/homeassistant/components/environment_canada/translations/tr.json index d7a3a7e9e78..afd8eb43d46 100644 --- a/homeassistant/components/environment_canada/translations/tr.json +++ b/homeassistant/components/environment_canada/translations/tr.json @@ -1,14 +1,19 @@ { "config": { "error": { - "cannot_connect": "Ba\u011flant\u0131 ba\u015far\u0131s\u0131z" + "bad_station_id": "\u0130stasyon Kimli\u011fi ge\u00e7ersiz, eksik veya istasyon kimli\u011fi veritaban\u0131nda bulunamad\u0131", + "cannot_connect": "Ba\u011flant\u0131 ba\u015far\u0131s\u0131z", + "unknown": "Beklenmeyen hata" }, "step": { "user": { "data": { "language": "Hava durumu bilgisi dili", + "latitude": "Enlem", + "longitude": "Boylam", "station": "Hava istasyonu ID" - } + }, + "description": "Bir istasyon kimli\u011fi veya enlem/boylam belirtilmelidir. Kullan\u0131lan varsay\u0131lan enlem/boylam, Home Assistant kurulumunuzda yap\u0131land\u0131r\u0131lan de\u011ferlerdir. Koordinatlar belirtilirse, koordinatlara en yak\u0131n meteoroloji istasyonu kullan\u0131lacakt\u0131r. Bir istasyon kodu kullan\u0131l\u0131yorsa, \u015fu bi\u00e7imde olmal\u0131d\u0131r: PP/kod, burada PP iki harfli ildir ve kod istasyon kimli\u011fidir. \u0130stasyon kimliklerinin listesi burada bulunabilir: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. Hava durumu bilgileri \u0130ngilizce veya Frans\u0131zca olarak al\u0131nabilir." } } } diff --git a/homeassistant/components/esphome/translations/bg.json b/homeassistant/components/esphome/translations/bg.json index 1a92f62cbb6..699a993403f 100644 --- a/homeassistant/components/esphome/translations/bg.json +++ b/homeassistant/components/esphome/translations/bg.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "ESP \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + "already_configured": "ESP \u0432\u0435\u0447\u0435 \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": { "connection_error": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 ESP. \u041c\u043e\u043b\u044f, \u0443\u0432\u0435\u0440\u0435\u0442\u0435 \u0441\u0435, \u0447\u0435 \u0432\u0430\u0448\u0438\u044f\u0442 YAML \u0444\u0430\u0439\u043b \u0441\u044a\u0434\u044a\u0440\u0436\u0430 \u0440\u0435\u0434 \"api:\".", @@ -19,6 +20,17 @@ "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u0435 ESPHome \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e ` {name} ` \u043a\u044a\u043c Home Assistant?", "title": "\u041e\u0442\u043a\u0440\u0438\u0442\u043e \u0435 ESPHome \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" }, + "encryption_key": { + "data": { + "noise_psk": "\u041a\u043b\u044e\u0447 \u0437\u0430 \u043a\u0440\u0438\u043f\u0442\u0438\u0440\u0430\u043d\u0435" + }, + "description": "\u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043a\u043b\u044e\u0447\u0430 \u0437\u0430 \u043a\u0440\u0438\u043f\u0442\u0438\u0440\u0430\u043d\u0435, \u043a\u043e\u0439\u0442\u043e \u0441\u0442\u0435 \u0437\u0430\u0434\u0430\u043b\u0438 \u0432\u044a\u0432 \u0432\u0430\u0448\u0430\u0442\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0437\u0430 {name}." + }, + "reauth_confirm": { + "data": { + "noise_psk": "\u041a\u043b\u044e\u0447 \u0437\u0430 \u043a\u0440\u0438\u043f\u0442\u0438\u0440\u0430\u043d\u0435" + } + }, "user": { "data": { "host": "\u0410\u0434\u0440\u0435\u0441", diff --git a/homeassistant/components/esphome/translations/hu.json b/homeassistant/components/esphome/translations/hu.json index e65577f055e..a35248864ff 100644 --- a/homeassistant/components/esphome/translations/hu.json +++ b/homeassistant/components/esphome/translations/hu.json @@ -6,7 +6,7 @@ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" }, "error": { - "connection_error": "Nem lehet csatlakozni az ESP-hez. K\u00e9rem, gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy a YAML f\u00e1jl tartalmaz egy \"api:\" sort.", + "connection_error": "Nem lehet csatlakozni az ESP-hez. K\u00e9rem, gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy a YAML konfigur\u00e1ci\u00f3 tartalmaz egy \"api:\" sort.", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "invalid_psk": "Az adat\u00e1tviteli titkos\u00edt\u00e1si kulcs \u00e9rv\u00e9nytelen. K\u00e9rj\u00fck, gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy megegyezik a konfigur\u00e1ci\u00f3ban l\u00e9v\u0151vel.", "resolve_error": "Az ESP c\u00edme nem oldhat\u00f3 fel. Ha a hiba tov\u00e1bbra is fenn\u00e1ll, k\u00e9rem, \u00e1ll\u00edtson be egy statikus IP-c\u00edmet: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" @@ -17,23 +17,23 @@ "data": { "password": "Jelsz\u00f3" }, - "description": "K\u00e9rem, adja meg a konfigur\u00e1ci\u00f3ban {name} n\u00e9vhez be\u00e1ll\u00edtott jelsz\u00f3t." + "description": "K\u00e9rem, adja meg {name} konfigur\u00e1ci\u00f3j\u00e1ban be\u00e1ll\u00edtott API jelsz\u00f3t." }, "discovery_confirm": { "description": "Szeretn\u00e9 hozz\u00e1adni `{name}` ESPHome csom\u00f3pontot Home Assistanthoz?", - "title": "Felfedezett ESPHome csom\u00f3pont" + "title": "ESPHome csom\u00f3pont felfedezve" }, "encryption_key": { "data": { "noise_psk": "Titkos\u00edt\u00e1si kulcs" }, - "description": "K\u00e9rj\u00fck, adja meg a {name} konfigur\u00e1ci\u00f3j\u00e1ban be\u00e1ll\u00edtott titkos\u00edt\u00e1si kulcsot." + "description": "K\u00e9rem, adja meg {name} konfigur\u00e1ci\u00f3j\u00e1ban be\u00e1ll\u00edtott titkos\u00edt\u00e1si kulcsot." }, "reauth_confirm": { "data": { "noise_psk": "Titkos\u00edt\u00e1si kulcs" }, - "description": "{name} ESPHome eszk\u00f6z enged\u00e9lyezte az adat\u00e1tviteli titkos\u00edt\u00e1st vagy megv\u00e1ltoztatta a titkos\u00edt\u00e1si kulcsot. K\u00e9rj\u00fck, adja meg az aktu\u00e1lis kulcsot." + "description": "{name} ESPHome eszk\u00f6z aktiv\u00e1lta az adat\u00e1tviteli titkos\u00edt\u00e1st vagy megv\u00e1ltoztatta a titkos\u00edt\u00e1si kulcsot. K\u00e9rj\u00fck, adja meg az aktu\u00e1lis kulcsot." }, "user": { "data": { diff --git a/homeassistant/components/esphome/translations/pl.json b/homeassistant/components/esphome/translations/pl.json index 7619f29ad64..2fa5f37ff18 100644 --- a/homeassistant/components/esphome/translations/pl.json +++ b/homeassistant/components/esphome/translations/pl.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "already_in_progress": "Konfiguracja jest ju\u017c w toku" + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z ESP. Upewnij si\u0119, \u017ce Tw\u00f3j plik YAML zawiera lini\u0119 'api:'.", diff --git a/homeassistant/components/faa_delays/translations/bg.json b/homeassistant/components/faa_delays/translations/bg.json index 0995436221b..93fa3f04d6c 100644 --- a/homeassistant/components/faa_delays/translations/bg.json +++ b/homeassistant/components/faa_delays/translations/bg.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "\u0422\u043e\u0432\u0430 \u043b\u0435\u0442\u0438\u0449\u0435 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e." + }, "error": { + "invalid_airport": "\u041a\u043e\u0434\u044a\u0442 \u043d\u0430 \u043b\u0435\u0442\u0438\u0449\u0435\u0442\u043e \u043d\u0435 \u0435 \u0432\u0430\u043b\u0438\u0434\u0435\u043d", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { diff --git a/homeassistant/components/fjaraskupan/translations/bg.json b/homeassistant/components/fjaraskupan/translations/bg.json new file mode 100644 index 00000000000..4db9b1af40e --- /dev/null +++ b/homeassistant/components/fjaraskupan/translations/bg.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fjaraskupan/translations/cs.json b/homeassistant/components/fjaraskupan/translations/cs.json index 8d729713ed2..5f890becd56 100644 --- a/homeassistant/components/fjaraskupan/translations/cs.json +++ b/homeassistant/components/fjaraskupan/translations/cs.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", + "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." }, "step": { "confirm": { diff --git a/homeassistant/components/flipr/translations/bg.json b/homeassistant/components/flipr/translations/bg.json new file mode 100644 index 00000000000..51ee3653e15 --- /dev/null +++ b/homeassistant/components/flipr/translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flo/translations/bg.json b/homeassistant/components/flo/translations/bg.json new file mode 100644 index 00000000000..2ac8a444100 --- /dev/null +++ b/homeassistant/components/flo/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/bg.json b/homeassistant/components/flux_led/translations/bg.json new file mode 100644 index 00000000000..462548016e5 --- /dev/null +++ b/homeassistant/components/flux_led/translations/bg.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "no_devices_found": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "flow_title": "{model} {id} ({ipaddr})", + "step": { + "discovery_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {model} {id} ({ipaddr})?" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u0410\u043a\u043e \u043e\u0441\u0442\u0430\u0432\u0438\u0442\u0435 \u0445\u043e\u0441\u0442\u0430 \u043f\u0440\u0430\u0437\u0435\u043d, \u043e\u0442\u043a\u0440\u0438\u0432\u0430\u043d\u0435\u0442\u043e \u0449\u0435 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u0437\u0430 \u043d\u0430\u043c\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "custom_effect_colors": "\u041f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d \u0435\u0444\u0435\u043a\u0442: \u0421\u043f\u0438\u0441\u044a\u043a \u043e\u0442 1 \u0434\u043e 16 [R,G,B] \u0446\u0432\u044f\u0442\u0430. \u041f\u0440\u0438\u043c\u0435\u0440: [255,0,255],[60,128,0]", + "custom_effect_speed_pct": "\u041f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d \u0435\u0444\u0435\u043a\u0442: \u0421\u043a\u043e\u0440\u043e\u0441\u0442 \u0432 \u043f\u0440\u043e\u0446\u0435\u043d\u0442\u0438 \u0437\u0430 \u0435\u0444\u0435\u043a\u0442\u0430, \u043a\u043e\u0439\u0442\u043e \u043f\u0440\u0435\u0432\u043a\u043b\u044e\u0447\u0432\u0430 \u0446\u0432\u0435\u0442\u043e\u0432\u0435\u0442\u0435.", + "custom_effect_transition": "\u041f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d \u0435\u0444\u0435\u043a\u0442: \u0422\u0438\u043f \u043f\u0440\u0435\u0445\u043e\u0434 \u043c\u0435\u0436\u0434\u0443 \u0446\u0432\u0435\u0442\u043e\u0432\u0435\u0442\u0435.", + "mode": "\u0418\u0437\u0431\u0440\u0430\u043d\u0438\u044f\u0442 \u0440\u0435\u0436\u0438\u043c \u043d\u0430 \u044f\u0440\u043a\u043e\u0441\u0442." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/cs.json b/homeassistant/components/flux_led/translations/cs.json new file mode 100644 index 00000000000..542a503a360 --- /dev/null +++ b/homeassistant/components/flux_led/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/pl.json b/homeassistant/components/flux_led/translations/pl.json index 1e321012040..2e749564860 100644 --- a/homeassistant/components/flux_led/translations/pl.json +++ b/homeassistant/components/flux_led/translations/pl.json @@ -1,5 +1,20 @@ { "config": { - "flow_title": "{model} {id} ({ipaddr})" + "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" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "flow_title": "{model} {id} ({ipaddr})", + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/tr.json b/homeassistant/components/flux_led/translations/tr.json index 6b603ef7232..3be9b8e3c26 100644 --- a/homeassistant/components/flux_led/translations/tr.json +++ b/homeassistant/components/flux_led/translations/tr.json @@ -1,9 +1,14 @@ { "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" + }, "step": { "user": { "data": { - "host": "Sunucu" + "host": "Ana Bilgisayar" } } } diff --git a/homeassistant/components/forecast_solar/translations/bg.json b/homeassistant/components/forecast_solar/translations/bg.json new file mode 100644 index 00000000000..35cfa0ad1d7 --- /dev/null +++ b/homeassistant/components/forecast_solar/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u0418\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/tr.json b/homeassistant/components/forked_daapd/translations/tr.json index cf354c5c87f..1ca2f9d4715 100644 --- a/homeassistant/components/forked_daapd/translations/tr.json +++ b/homeassistant/components/forked_daapd/translations/tr.json @@ -11,9 +11,9 @@ "user": { "data": { "host": "Ana Bilgisayar", - "name": "Kolay ad", + "name": "Kolay Ad\u0131", "password": "API parolas\u0131 (parola yoksa bo\u015f b\u0131rak\u0131n)", - "port": "API ba\u011flant\u0131 noktas\u0131" + "port": "API Port" } } } diff --git a/homeassistant/components/freebox/translations/bg.json b/homeassistant/components/freebox/translations/bg.json new file mode 100644 index 00000000000..4983c9a14b2 --- /dev/null +++ b/homeassistant/components/freebox/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freedompro/translations/bg.json b/homeassistant/components/freedompro/translations/bg.json new file mode 100644 index 00000000000..fdbdc5b1cdf --- /dev/null +++ b/homeassistant/components/freedompro/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/bg.json b/homeassistant/components/fritz/translations/bg.json new file mode 100644 index 00000000000..5502d41d8a2 --- /dev/null +++ b/homeassistant/components/fritz/translations/bg.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "start_config": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + }, + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/translations/bg.json b/homeassistant/components/geonetnz_volcano/translations/bg.json index 042696219fc..17f38fa0971 100644 --- a/homeassistant/components/geonetnz_volcano/translations/bg.json +++ b/homeassistant/components/geonetnz_volcano/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/goalzero/translations/bg.json b/homeassistant/components/goalzero/translations/bg.json new file mode 100644 index 00000000000..2ac8a444100 --- /dev/null +++ b/homeassistant/components/goalzero/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/group/translations/hr.json b/homeassistant/components/group/translations/hr.json index 85abe33638b..fbf123b0e88 100644 --- a/homeassistant/components/group/translations/hr.json +++ b/homeassistant/components/group/translations/hr.json @@ -5,7 +5,7 @@ "home": "Doma", "locked": "Zaklju\u010dano", "not_home": "Odsutan", - "off": "Uklju\u010deno", + "off": "Isklju\u010deno", "ok": "U redu", "on": "Uklju\u010deno", "open": "Otvoreno", diff --git a/homeassistant/components/guardian/translations/bg.json b/homeassistant/components/guardian/translations/bg.json new file mode 100644 index 00000000000..9c063cbbd0d --- /dev/null +++ b/homeassistant/components/guardian/translations/bg.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/bg.json b/homeassistant/components/homeassistant/translations/bg.json new file mode 100644 index 00000000000..7467c64f64a --- /dev/null +++ b/homeassistant/components/homeassistant/translations/bg.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "user": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b", + "version": "\u0412\u0435\u0440\u0441\u0438\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/bg.json b/homeassistant/components/homekit/translations/bg.json new file mode 100644 index 00000000000..4e5677f124a --- /dev/null +++ b/homeassistant/components/homekit/translations/bg.json @@ -0,0 +1,16 @@ +{ + "options": { + "step": { + "include_exclude": { + "data": { + "mode": "\u0420\u0435\u0436\u0438\u043c" + } + }, + "init": { + "data": { + "mode": "\u0420\u0435\u0436\u0438\u043c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/bg.json b/homeassistant/components/hyperion/translations/bg.json new file mode 100644 index 00000000000..4983c9a14b2 --- /dev/null +++ b/homeassistant/components/hyperion/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/bg.json b/homeassistant/components/ialarm/translations/bg.json new file mode 100644 index 00000000000..4983c9a14b2 --- /dev/null +++ b/homeassistant/components/ialarm/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/tr.json b/homeassistant/components/ialarm/translations/tr.json new file mode 100644 index 00000000000..21a477c75a7 --- /dev/null +++ b/homeassistant/components/ialarm/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/bg.json b/homeassistant/components/insteon/translations/bg.json new file mode 100644 index 00000000000..923715c0559 --- /dev/null +++ b/homeassistant/components/insteon/translations/bg.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "hubv1": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + }, + "hubv2": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + }, + "options": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "change_hub_config": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iotawatt/translations/bg.json b/homeassistant/components/iotawatt/translations/bg.json new file mode 100644 index 00000000000..f4f0d0c32c1 --- /dev/null +++ b/homeassistant/components/iotawatt/translations/bg.json @@ -0,0 +1,22 @@ +{ + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "auth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/bg.json b/homeassistant/components/ipp/translations/bg.json new file mode 100644 index 00000000000..4983c9a14b2 --- /dev/null +++ b/homeassistant/components/ipp/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/tr.json b/homeassistant/components/isy994/translations/tr.json index d1423202fe0..d2d58a89f31 100644 --- a/homeassistant/components/isy994/translations/tr.json +++ b/homeassistant/components/isy994/translations/tr.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_host": "Ana bilgisayar giri\u015fi tam URL bi\u00e7iminde de\u011fildi, \u00f6r. http://192.168.10.100:80", "unknown": "Beklenmeyen hata" }, "step": { @@ -23,7 +24,8 @@ "init": { "data": { "variable_sensor_string": "De\u011fi\u015fken Sens\u00f6r Dizesi" - } + }, + "description": "ISY Entegrasyonu i\u00e7in se\u00e7enekleri ayarlay\u0131n:\n \u2022 D\u00fc\u011f\u00fcm Sens\u00f6r\u00fc Dizisi: Ad\u0131nda 'D\u00fc\u011f\u00fcm Sens\u00f6r\u00fc Dizisi' i\u00e7eren herhangi bir cihaz veya klas\u00f6r, bir sens\u00f6r veya ikili sens\u00f6r olarak ele al\u0131nacakt\u0131r.\n \u2022 Ignore String: Ad\u0131nda 'Ignore String' olan herhangi bir cihaz yoksay\u0131lacakt\u0131r.\n \u2022 De\u011fi\u015fken Sens\u00f6r Dizisi: 'De\u011fi\u015fken Sens\u00f6r Dizisi' i\u00e7eren herhangi bir de\u011fi\u015fken sens\u00f6r olarak eklenecektir.\n \u2022 I\u015f\u0131k Parlakl\u0131\u011f\u0131n\u0131 Geri Y\u00fckle: Etkinle\u015ftirilirse, bir \u0131\u015f\u0131k a\u00e7\u0131ld\u0131\u011f\u0131nda cihaz\u0131n yerle\u015fik On-Level yerine \u00f6nceki parlakl\u0131k geri y\u00fcklenir." } } } diff --git a/homeassistant/components/kodi/translations/bg.json b/homeassistant/components/kodi/translations/bg.json new file mode 100644 index 00000000000..0eb3c94dd0d --- /dev/null +++ b/homeassistant/components/kodi/translations/bg.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + }, + "ws_port": { + "data": { + "ws_port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/hu.json b/homeassistant/components/kodi/translations/hu.json index e561bd5d6a4..c1713436979 100644 --- a/homeassistant/components/kodi/translations/hu.json +++ b/homeassistant/components/kodi/translations/hu.json @@ -23,7 +23,7 @@ }, "discovery_confirm": { "description": "Szeretn\u00e9 hozz\u00e1adni a Kodi (`{name}`)-t Home Assistanthoz?", - "title": "Felfedezett Kodi" + "title": "Kodi felfedezve" }, "user": { "data": { diff --git a/homeassistant/components/konnected/translations/bg.json b/homeassistant/components/konnected/translations/bg.json new file mode 100644 index 00000000000..4983c9a14b2 --- /dev/null +++ b/homeassistant/components/konnected/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/tr.json b/homeassistant/components/konnected/translations/tr.json index a0e759903bd..f86f09eeea7 100644 --- a/homeassistant/components/konnected/translations/tr.json +++ b/homeassistant/components/konnected/translations/tr.json @@ -9,6 +9,12 @@ "cannot_connect": "Ba\u011flanma hatas\u0131" }, "step": { + "confirm": { + "description": "Model: {model}\nID: {id}\nSunucu: {host}\nPort: {port}\n\nIO ve panel davran\u0131\u015f\u0131n\u0131 Ba\u011fl\u0131 Alarm Paneli ayarlar\u0131nda yap\u0131land\u0131rabilirsiniz." + }, + "import_confirm": { + "description": "configuration.yaml'de {id} kimli\u011fine sahip Ba\u011fl\u0131 bir Alarm Paneli ke\u015ffedildi. Bu ak\u0131\u015f, onu bir yap\u0131land\u0131rma giri\u015fine aktarman\u0131za olanak tan\u0131r." + }, "user": { "data": { "host": "\u0130p Adresi", diff --git a/homeassistant/components/life360/translations/bg.json b/homeassistant/components/life360/translations/bg.json index fe116225550..5436cfcf718 100644 --- a/homeassistant/components/life360/translations/bg.json +++ b/homeassistant/components/life360/translations/bg.json @@ -1,9 +1,13 @@ { "config": { + "abort": { + "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" + }, "create_entry": { "default": "\u0417\u0430 \u0434\u0430 \u0437\u0430\u0434\u0430\u0434\u0435\u0442\u0435 \u0440\u0430\u0437\u0448\u0438\u0440\u0435\u043d\u0438 \u043e\u043f\u0446\u0438\u0438, \u0432\u0438\u0436\u0442\u0435 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f \u043d\u0430 Life360]({docs_url})." }, "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", "invalid_username": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" }, "step": { diff --git a/homeassistant/components/luftdaten/translations/bg.json b/homeassistant/components/luftdaten/translations/bg.json index 39787684871..784b010219d 100644 --- a/homeassistant/components/luftdaten/translations/bg.json +++ b/homeassistant/components/luftdaten/translations/bg.json @@ -1,6 +1,8 @@ { "config": { "error": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_sensor": "\u0421\u0435\u043d\u0437\u043e\u0440\u044a\u0442 \u043d\u0435 \u0435 \u043d\u0430\u043b\u0438\u0447\u0435\u043d \u0438\u043b\u0438 \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d" }, "step": { diff --git a/homeassistant/components/lutron_caseta/translations/tr.json b/homeassistant/components/lutron_caseta/translations/tr.json index fdc5e71a7ac..06b46972264 100644 --- a/homeassistant/components/lutron_caseta/translations/tr.json +++ b/homeassistant/components/lutron_caseta/translations/tr.json @@ -10,6 +10,10 @@ }, "flow_title": "Lutron Cas\u00e9ta {name} ( {host} )", "step": { + "import_failed": { + "description": "Configuration.yaml'den i\u00e7e aktar\u0131lan k\u00f6pr\u00fc (ana bilgisayar: {host}) kurulamad\u0131.", + "title": "Cas\u00e9ta k\u00f6pr\u00fc yap\u0131land\u0131rmas\u0131 i\u00e7e aktar\u0131lamad\u0131." + }, "link": { "description": "{name} ( {host} ) ile e\u015fle\u015ftirmek i\u00e7in, bu formu g\u00f6nderdikten sonra k\u00f6pr\u00fcn\u00fcn arkas\u0131ndaki siyah d\u00fc\u011fmeye bas\u0131n.", "title": "K\u00f6pr\u00fc ile e\u015fle\u015ftirin" diff --git a/homeassistant/components/met/translations/bg.json b/homeassistant/components/met/translations/bg.json index f6a10ce42ec..ee2071403c4 100644 --- a/homeassistant/components/met/translations/bg.json +++ b/homeassistant/components/met/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/mikrotik/translations/bg.json b/homeassistant/components/mikrotik/translations/bg.json new file mode 100644 index 00000000000..4983c9a14b2 --- /dev/null +++ b/homeassistant/components/mikrotik/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mill/translations/bg.json b/homeassistant/components/mill/translations/bg.json new file mode 100644 index 00000000000..2ac8a444100 --- /dev/null +++ b/homeassistant/components/mill/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/bg.json b/homeassistant/components/modem_callerid/translations/bg.json new file mode 100644 index 00000000000..7383941d1ca --- /dev/null +++ b/homeassistant/components/modem_callerid/translations/bg.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "usb_confirm": { + "title": "\u0422\u0435\u043b\u0435\u0444\u043e\u043d\u0435\u043d \u043c\u043e\u0434\u0435\u043c" + }, + "user": { + "data": { + "name": "\u0418\u043c\u0435", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "\u0422\u0435\u043b\u0435\u0444\u043e\u043d\u0435\u043d \u043c\u043e\u0434\u0435\u043c" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/pl.json b/homeassistant/components/modem_callerid/translations/pl.json index a07df641d7a..12638ae272b 100644 --- a/homeassistant/components/modem_callerid/translations/pl.json +++ b/homeassistant/components/modem_callerid/translations/pl.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku" + }, "error": { "cannot_connect": "Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107" }, diff --git a/homeassistant/components/monoprice/translations/bg.json b/homeassistant/components/monoprice/translations/bg.json new file mode 100644 index 00000000000..4983c9a14b2 --- /dev/null +++ b/homeassistant/components/monoprice/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/bg.json b/homeassistant/components/mqtt/translations/bg.json index 96343a7f87a..4790a02329d 100644 --- a/homeassistant/components/mqtt/translations/bg.json +++ b/homeassistant/components/mqtt/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", "single_instance_allowed": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 MQTT." }, "error": { @@ -25,5 +26,14 @@ "title": "MQTT \u0431\u0440\u043e\u043a\u0435\u0440 \u0447\u0440\u0435\u0437 Supervisor \u0434\u043e\u0431\u0430\u0432\u043a\u0430" } } + }, + "options": { + "step": { + "broker": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/bg.json b/homeassistant/components/mysensors/translations/bg.json index 854e88b38b9..69f11b05ce4 100644 --- a/homeassistant/components/mysensors/translations/bg.json +++ b/homeassistant/components/mysensors/translations/bg.json @@ -1,9 +1,15 @@ { "config": { "abort": { + "invalid_port": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u043d\u043e\u043c\u0435\u0440 \u043d\u0430 \u043f\u043e\u0440\u0442", + "invalid_serial": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u0441\u0435\u0440\u0438\u0435\u043d \u043f\u043e\u0440\u0442", + "port_out_of_range": "\u041d\u043e\u043c\u0435\u0440\u044a\u0442 \u043d\u0430 \u043f\u043e\u0440\u0442\u0430 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043d\u0430\u0439-\u043c\u0430\u043b\u043a\u043e 1 \u0438 \u043d\u0430\u0439-\u043c\u043d\u043e\u0433\u043e 65535", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "error": { + "invalid_port": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u043d\u043e\u043c\u0435\u0440 \u043d\u0430 \u043f\u043e\u0440\u0442", + "invalid_serial": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u0441\u0435\u0440\u0438\u0435\u043d \u043f\u043e\u0440\u0442", + "port_out_of_range": "\u041d\u043e\u043c\u0435\u0440\u044a\u0442 \u043d\u0430 \u043f\u043e\u0440\u0442\u0430 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043d\u0430\u0439-\u043c\u0430\u043b\u043a\u043e 1 \u0438 \u043d\u0430\u0439-\u043c\u043d\u043e\u0433\u043e 65535", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { diff --git a/homeassistant/components/nam/translations/bg.json b/homeassistant/components/nam/translations/bg.json new file mode 100644 index 00000000000..c902368616e --- /dev/null +++ b/homeassistant/components/nam/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "device_unsupported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/bg.json b/homeassistant/components/nanoleaf/translations/bg.json new file mode 100644 index 00000000000..467fcb0d9bb --- /dev/null +++ b/homeassistant/components/nanoleaf/translations/bg.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/bg.json b/homeassistant/components/nest/translations/bg.json index e53ab436a77..2509668a859 100644 --- a/homeassistant/components/nest/translations/bg.json +++ b/homeassistant/components/nest/translations/bg.json @@ -3,6 +3,9 @@ "abort": { "authorize_url_timeout": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0434\u0440\u0435\u0441 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0432 \u0441\u0440\u043e\u043a." }, + "create_entry": { + "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u0435\u043d" + }, "error": { "internal_error": "\u0412\u044a\u0437\u043d\u0438\u043a\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0432\u0430\u043b\u0438\u0434\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043a\u043e\u0434\u0430", "timeout": "\u0412\u0440\u0435\u043c\u0435\u0442\u043e \u0437\u0430 \u043f\u043e\u0442\u0432\u044a\u0440\u0436\u0434\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u043a\u043e\u0434\u0430 \u0438\u0437\u0442\u0435\u0447\u0435", diff --git a/homeassistant/components/netgear/translations/bg.json b/homeassistant/components/netgear/translations/bg.json new file mode 100644 index 00000000000..d90e23ebf92 --- /dev/null +++ b/homeassistant/components/netgear/translations/bg.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442 (\u043f\u043e \u0438\u0437\u0431\u043e\u0440)" + }, + "title": "Netgear" + } + } + }, + "options": { + "step": { + "init": { + "title": "Netgear" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netgear/translations/cs.json b/homeassistant/components/netgear/translations/cs.json index 786cd2229ab..6d942ff2ff4 100644 --- a/homeassistant/components/netgear/translations/cs.json +++ b/homeassistant/components/netgear/translations/cs.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/netgear/translations/pl.json b/homeassistant/components/netgear/translations/pl.json index 488f0580ca2..a9c0c4af199 100644 --- a/homeassistant/components/netgear/translations/pl.json +++ b/homeassistant/components/netgear/translations/pl.json @@ -1,10 +1,15 @@ { "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, "step": { "user": { "data": { + "host": "Nazwa hosta lub adres IP (Opcjonalne)", "password": "Has\u0142o", "port": "Port (Opcjonalnie)", + "ssl": "Certyfikat SSL", "username": "Nazwa u\u017cytkownika (Opcjonalnie)" }, "title": "Netgear" diff --git a/homeassistant/components/nightscout/translations/bg.json b/homeassistant/components/nightscout/translations/bg.json new file mode 100644 index 00000000000..2ac8a444100 --- /dev/null +++ b/homeassistant/components/nightscout/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/translations/bg.json b/homeassistant/components/notion/translations/bg.json index f320ae6c5a3..ef4a919401e 100644 --- a/homeassistant/components/notion/translations/bg.json +++ b/homeassistant/components/notion/translations/bg.json @@ -1,9 +1,20 @@ { "config": { + "abort": { + "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": { - "no_devices": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043f\u0440\u043e\u0444\u0438\u043b\u0430" + "no_devices": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043f\u0440\u043e\u0444\u0438\u043b\u0430", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "description": "\u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0437\u0430 {username}.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", diff --git a/homeassistant/components/notion/translations/cs.json b/homeassistant/components/notion/translations/cs.json index 7d5eebeb564..53cae8ab73b 100644 --- a/homeassistant/components/notion/translations/cs.json +++ b/homeassistant/components/notion/translations/cs.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "\u00da\u010det je ji\u017e nastaven" + "already_configured": "\u00da\u010det je ji\u017e nastaven", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", - "no_devices": "V \u00fa\u010dtu nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + "no_devices": "V \u00fa\u010dtu nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "title": "Znovu ov\u011b\u0159it integraci" + }, "user": { "data": { "password": "Heslo", diff --git a/homeassistant/components/notion/translations/pl.json b/homeassistant/components/notion/translations/pl.json index cc74db0f75e..edd7b608b3d 100644 --- a/homeassistant/components/notion/translations/pl.json +++ b/homeassistant/components/notion/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": { "invalid_auth": "Niepoprawne uwierzytelnienie", @@ -13,7 +14,8 @@ "data": { "password": "Has\u0142o" }, - "description": "Wprowad\u017a ponownie has\u0142o dla u\u017cytkownika {username}." + "description": "Wprowad\u017a ponownie has\u0142o dla u\u017cytkownika {username}.", + "title": "Ponownie uwierzytelnij integracj\u0119" }, "user": { "data": { diff --git a/homeassistant/components/nut/translations/bg.json b/homeassistant/components/nut/translations/bg.json new file mode 100644 index 00000000000..4983c9a14b2 --- /dev/null +++ b/homeassistant/components/nut/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nzbget/translations/bg.json b/homeassistant/components/nzbget/translations/bg.json new file mode 100644 index 00000000000..a610a1f2a64 --- /dev/null +++ b/homeassistant/components/nzbget/translations/bg.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/omnilogic/translations/bg.json b/homeassistant/components/omnilogic/translations/bg.json new file mode 100644 index 00000000000..2ac8a444100 --- /dev/null +++ b/homeassistant/components/omnilogic/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/bg.json b/homeassistant/components/onewire/translations/bg.json new file mode 100644 index 00000000000..353a6523eed --- /dev/null +++ b/homeassistant/components/onewire/translations/bg.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "owserver": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + } + }, + "user": { + "data": { + "type": "\u0412\u0438\u0434 \u043d\u0430 \u0432\u0440\u044a\u0437\u043a\u0430\u0442\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/bg.json b/homeassistant/components/onvif/translations/bg.json new file mode 100644 index 00000000000..0cebe1fb7e9 --- /dev/null +++ b/homeassistant/components/onvif/translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "configure": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u0418\u043c\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + }, + "manual_input": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opengarage/translations/bg.json b/homeassistant/components/opengarage/translations/bg.json new file mode 100644 index 00000000000..660511fd675 --- /dev/null +++ b/homeassistant/components/opengarage/translations/bg.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "device_key": "\u041a\u043b\u044e\u0447 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e", + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opengarage/translations/cs.json b/homeassistant/components/opengarage/translations/cs.json new file mode 100644 index 00000000000..19a9536c194 --- /dev/null +++ b/homeassistant/components/opengarage/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "port": "Port", + "verify_ssl": "Ov\u011b\u0159it certifik\u00e1t SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opengarage/translations/pl.json b/homeassistant/components/opengarage/translations/pl.json new file mode 100644 index 00000000000..d0905f8ed3d --- /dev/null +++ b/homeassistant/components/opengarage/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "port": "Port", + "verify_ssl": "Weryfikacja certyfikatu SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opengarage/translations/tr.json b/homeassistant/components/opengarage/translations/tr.json new file mode 100644 index 00000000000..cd800abed1d --- /dev/null +++ b/homeassistant/components/opengarage/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flant\u0131 ba\u015far\u0131s\u0131z", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "device_key": "Cihaz Anahtar\u0131", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/bg.json b/homeassistant/components/opentherm_gw/translations/bg.json index 3b120cebde9..a7c30b4a45a 100644 --- a/homeassistant/components/opentherm_gw/translations/bg.json +++ b/homeassistant/components/opentherm_gw/translations/bg.json @@ -2,6 +2,7 @@ "config": { "error": { "already_configured": "\u0428\u043b\u044e\u0437\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "id_exists": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440a \u043d\u0430 \u0448\u043b\u044e\u0437\u0430 \u0432\u0435\u0447\u0435 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430" }, "step": { diff --git a/homeassistant/components/openweathermap/translations/bg.json b/homeassistant/components/openweathermap/translations/bg.json new file mode 100644 index 00000000000..2ac8a444100 --- /dev/null +++ b/homeassistant/components/openweathermap/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/bg.json b/homeassistant/components/ovo_energy/translations/bg.json new file mode 100644 index 00000000000..946b62a8690 --- /dev/null +++ b/homeassistant/components/ovo_energy/translations/bg.json @@ -0,0 +1,7 @@ +{ + "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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/p1_monitor/translations/bg.json b/homeassistant/components/p1_monitor/translations/bg.json new file mode 100644 index 00000000000..5c4f6a7bdc3 --- /dev/null +++ b/homeassistant/components/p1_monitor/translations/bg.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u0418\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/bg.json b/homeassistant/components/philips_js/translations/bg.json index eb502f5a135..2182a101454 100644 --- a/homeassistant/components/philips_js/translations/bg.json +++ b/homeassistant/components/philips_js/translations/bg.json @@ -3,6 +3,13 @@ "error": { "invalid_pin": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u041f\u0418\u041d", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "pair": { + "data": { + "pin": "\u041f\u0418\u041d \u043a\u043e\u0434" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/bg.json b/homeassistant/components/pi_hole/translations/bg.json new file mode 100644 index 00000000000..4983c9a14b2 --- /dev/null +++ b/homeassistant/components/pi_hole/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/tr.json b/homeassistant/components/pi_hole/translations/tr.json index a14e020d360..e2fff8d904b 100644 --- a/homeassistant/components/pi_hole/translations/tr.json +++ b/homeassistant/components/pi_hole/translations/tr.json @@ -17,6 +17,7 @@ "api_key": "API Anahtar\u0131", "host": "Ana Bilgisayar", "location": "Konum", + "name": "\u0130sim", "port": "Port", "statistics_only": "Yaln\u0131zca \u0130statistikler" } diff --git a/homeassistant/components/plex/translations/bg.json b/homeassistant/components/plex/translations/bg.json index dfc12080ec5..c575c261fbd 100644 --- a/homeassistant/components/plex/translations/bg.json +++ b/homeassistant/components/plex/translations/bg.json @@ -13,6 +13,11 @@ "not_found": "Plex \u0441\u044a\u0440\u0432\u044a\u0440\u044a\u0442 \u043d\u0435 \u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d" }, "step": { + "manual_setup": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + }, "select_server": { "data": { "server": "\u0421\u044a\u0440\u0432\u044a\u0440" diff --git a/homeassistant/components/plugwise/translations/bg.json b/homeassistant/components/plugwise/translations/bg.json new file mode 100644 index 00000000000..cf043c65495 --- /dev/null +++ b/homeassistant/components/plugwise/translations/bg.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user_gateway": { + "data": { + "host": "IP \u0430\u0434\u0440\u0435\u0441", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/bg.json b/homeassistant/components/progettihwsw/translations/bg.json new file mode 100644 index 00000000000..a610a1f2a64 --- /dev/null +++ b/homeassistant/components/progettihwsw/translations/bg.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/bg.json b/homeassistant/components/prosegur/translations/bg.json new file mode 100644 index 00000000000..c00de2c8049 --- /dev/null +++ b/homeassistant/components/prosegur/translations/bg.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "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\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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + }, + "user": { + "data": { + "country": "\u0414\u044a\u0440\u0436\u0430\u0432\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainforest_eagle/translations/bg.json b/homeassistant/components/rainforest_eagle/translations/bg.json new file mode 100644 index 00000000000..5fff22d7a70 --- /dev/null +++ b/homeassistant/components/rainforest_eagle/translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "install_code": "\u0418\u043d\u0441\u0442\u0430\u043b\u0430\u0446\u0438\u043e\u043d\u0435\u043d \u043a\u043e\u0434" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/bg.json b/homeassistant/components/renault/translations/bg.json new file mode 100644 index 00000000000..f074b9653c5 --- /dev/null +++ b/homeassistant/components/renault/translations/bg.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u0430 \u0432\u0435\u0447\u0435 \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": { + "invalid_credentials": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "description": "\u041c\u043e\u043b\u044f, \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0441\u0438 \u0437\u0430 {username}", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/bg.json b/homeassistant/components/rfxtrx/translations/bg.json new file mode 100644 index 00000000000..f7406217eed --- /dev/null +++ b/homeassistant/components/rfxtrx/translations/bg.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "setup_network": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/bg.json b/homeassistant/components/risco/translations/bg.json new file mode 100644 index 00000000000..2ac8a444100 --- /dev/null +++ b/homeassistant/components/risco/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/tr.json b/homeassistant/components/risco/translations/tr.json index 02a3b505f84..280de17f77d 100644 --- a/homeassistant/components/risco/translations/tr.json +++ b/homeassistant/components/risco/translations/tr.json @@ -19,8 +19,25 @@ }, "options": { "step": { + "ha_to_risco": { + "data": { + "armed_away": "D\u0131\u015farda Modu Aktif", + "armed_custom_bypass": "\u00d6zel Mod Aktif", + "armed_home": "Evde Modu Aktif", + "armed_night": "Gece Modu Aktif" + }, + "description": "Home Assistant alarm\u0131n\u0131 kurarken Risco alarm\u0131n\u0131z\u0131 hangi duruma ayarlayaca\u011f\u0131n\u0131z\u0131 se\u00e7in" + }, "init": { "title": "Se\u00e7enekleri yap\u0131land\u0131r\u0131n" + }, + "risco_to_ha": { + "data": { + "A": "Grup A", + "B": "Grup B", + "C": "Grup C", + "D": "Grup D" + } } } } diff --git a/homeassistant/components/roku/translations/hu.json b/homeassistant/components/roku/translations/hu.json index 101931e0d21..a5d243dada4 100644 --- a/homeassistant/components/roku/translations/hu.json +++ b/homeassistant/components/roku/translations/hu.json @@ -16,7 +16,7 @@ "other": "Egy\u00e9b" }, "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?", - "title": "Roku" + "title": "Roku felfedezve" }, "ssdp_confirm": { "data": { diff --git a/homeassistant/components/ruckus_unleashed/translations/bg.json b/homeassistant/components/ruckus_unleashed/translations/bg.json new file mode 100644 index 00000000000..ffb69776060 --- /dev/null +++ b/homeassistant/components/ruckus_unleashed/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/bg.json b/homeassistant/components/screenlogic/translations/bg.json new file mode 100644 index 00000000000..1c611d756fd --- /dev/null +++ b/homeassistant/components/screenlogic/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "gateway_entry": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/bg.json b/homeassistant/components/sharkiq/translations/bg.json new file mode 100644 index 00000000000..7b79ce3a72c --- /dev/null +++ b/homeassistant/components/sharkiq/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/bg.json b/homeassistant/components/shelly/translations/bg.json index c856929a5e1..6b8902ebfcc 100644 --- a/homeassistant/components/shelly/translations/bg.json +++ b/homeassistant/components/shelly/translations/bg.json @@ -1,4 +1,9 @@ { + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } + }, "device_automation": { "trigger_subtype": { "button": "\u0411\u0443\u0442\u043e\u043d", diff --git a/homeassistant/components/sia/translations/bg.json b/homeassistant/components/sia/translations/bg.json new file mode 100644 index 00000000000..4983c9a14b2 --- /dev/null +++ b/homeassistant/components/sia/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/bg.json b/homeassistant/components/simplisafe/translations/bg.json index bdac125ee72..8d93bacf69b 100644 --- a/homeassistant/components/simplisafe/translations/bg.json +++ b/homeassistant/components/simplisafe/translations/bg.json @@ -1,7 +1,8 @@ { "config": { "error": { - "identifier_exists": "\u041f\u0440\u043e\u0444\u0438\u043b\u044a\u0442 \u0435 \u0432\u0435\u0447\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d" + "identifier_exists": "\u041f\u0440\u043e\u0444\u0438\u043b\u044a\u0442 \u0435 \u0432\u0435\u0447\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d", + "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": { diff --git a/homeassistant/components/smart_meter_texas/translations/bg.json b/homeassistant/components/smart_meter_texas/translations/bg.json new file mode 100644 index 00000000000..2ac8a444100 --- /dev/null +++ b/homeassistant/components/smart_meter_texas/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/translations/cs.json b/homeassistant/components/soma/translations/cs.json index ba1261c1100..b518b762086 100644 --- a/homeassistant/components/soma/translations/cs.json +++ b/homeassistant/components/soma/translations/cs.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "already_setup": "M\u016f\u017eete nastavit pouze jeden \u00fa\u010det Soma.", - "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el.", - "connection_error": "P\u0159ipojen\u00ed k za\u0159\u00edzen\u00ed SOMA Connect se nezda\u0159ilo.", + "already_setup": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace.", + "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", + "connection_error": "Nepoda\u0159ilo se p\u0159ipojit", "missing_configuration": "Integrace Soma nen\u00ed nastavena. Postupujte podle dokumentace.", "result_error": "SOMA Connect odpov\u011bd\u011blo chybov\u00fdm stavem." }, "create_entry": { - "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno pomoc\u00ed Soma." + "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno" }, "step": { "user": { diff --git a/homeassistant/components/soma/translations/nl.json b/homeassistant/components/soma/translations/nl.json index fad7efdf398..32db40348ba 100644 --- a/homeassistant/components/soma/translations/nl.json +++ b/homeassistant/components/soma/translations/nl.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "already_setup": "U kunt slechts \u00e9\u00e9n Soma-account configureren.", + "already_setup": "Al geconfigureerd. Slechts een enkele configuratie mogelijk.", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "connection_error": "Kan geen verbinding maken met SOMA Connect.", + "connection_error": "Kan geen verbinding maken", "missing_configuration": "De Soma-component is niet geconfigureerd. Gelieve de documentatie te volgen.", "result_error": "SOMA Connect reageerde met een foutstatus." }, "create_entry": { - "default": "Succesvol geverifieerd met Soma." + "default": "Succesvol geauthenticeerd" }, "step": { "user": { diff --git a/homeassistant/components/sonarr/translations/bg.json b/homeassistant/components/sonarr/translations/bg.json new file mode 100644 index 00000000000..4983c9a14b2 --- /dev/null +++ b/homeassistant/components/sonarr/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/bg.json b/homeassistant/components/squeezebox/translations/bg.json new file mode 100644 index 00000000000..a9446bb8a17 --- /dev/null +++ b/homeassistant/components/squeezebox/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "edit": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookalert/translations/bg.json b/homeassistant/components/stookalert/translations/bg.json new file mode 100644 index 00000000000..80a7cc489a9 --- /dev/null +++ b/homeassistant/components/stookalert/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookalert/translations/cs.json b/homeassistant/components/stookalert/translations/cs.json new file mode 100644 index 00000000000..8440070c91a --- /dev/null +++ b/homeassistant/components/stookalert/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba je ji\u017e nastavena" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookalert/translations/pl.json b/homeassistant/components/stookalert/translations/pl.json new file mode 100644 index 00000000000..ef80051717e --- /dev/null +++ b/homeassistant/components/stookalert/translations/pl.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookalert/translations/tr.json b/homeassistant/components/stookalert/translations/tr.json new file mode 100644 index 00000000000..717f6d72b94 --- /dev/null +++ b/homeassistant/components/stookalert/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sun/translations/tr.json b/homeassistant/components/sun/translations/tr.json index 9f3cb5d412f..50634454b98 100644 --- a/homeassistant/components/sun/translations/tr.json +++ b/homeassistant/components/sun/translations/tr.json @@ -1,8 +1,8 @@ { "state": { "_": { - "above_horizon": "Ufkun \u00fczerinde", - "below_horizon": "Ufkun alt\u0131nda" + "above_horizon": "G\u00fcnd\u00fcz", + "below_horizon": "Gece" } }, "title": "G\u00fcne\u015f" diff --git a/homeassistant/components/surepetcare/translations/bg.json b/homeassistant/components/surepetcare/translations/bg.json new file mode 100644 index 00000000000..cc84865ed77 --- /dev/null +++ b/homeassistant/components/surepetcare/translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + }, + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/surepetcare/translations/pl.json b/homeassistant/components/surepetcare/translations/pl.json index 002e46163db..b44e3977ba5 100644 --- a/homeassistant/components/surepetcare/translations/pl.json +++ b/homeassistant/components/surepetcare/translations/pl.json @@ -1,7 +1,12 @@ { "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane" + }, "error": { - "cannot_connect": "Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107" + "cannot_connect": "Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "user": { diff --git a/homeassistant/components/switchbot/translations/bg.json b/homeassistant/components/switchbot/translations/bg.json new file mode 100644 index 00000000000..5a5b74b2318 --- /dev/null +++ b/homeassistant/components/switchbot/translations/bg.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "no_unconfigured_devices": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u043d\u0435\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "mac": "MAC \u0430\u0434\u0440\u0435\u0441 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e", + "name": "\u0418\u043c\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/cs.json b/homeassistant/components/switchbot/translations/cs.json index 7a44ab78d3b..6f16b6faac5 100644 --- a/homeassistant/components/switchbot/translations/cs.json +++ b/homeassistant/components/switchbot/translations/cs.json @@ -1,11 +1,18 @@ { "config": { "abort": { - "already_configured_device": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + "already_configured_device": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "flow_title": "{name}", "step": { "user": { "data": { + "name": "Jm\u00e9no", "password": "Heslo" } } diff --git a/homeassistant/components/switchbot/translations/pl.json b/homeassistant/components/switchbot/translations/pl.json index c65ebf08924..473535519af 100644 --- a/homeassistant/components/switchbot/translations/pl.json +++ b/homeassistant/components/switchbot/translations/pl.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "already_configured_device": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { diff --git a/homeassistant/components/synology_dsm/translations/bg.json b/homeassistant/components/synology_dsm/translations/bg.json new file mode 100644 index 00000000000..7b4805e3905 --- /dev/null +++ b/homeassistant/components/synology_dsm/translations/bg.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "reconfigure_successful": "\u041f\u0440\u0435\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, + "step": { + "link": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + }, + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/cs.json b/homeassistant/components/synology_dsm/translations/cs.json index 561c6c97e0b..ae77a93a790 100644 --- a/homeassistant/components/synology_dsm/translations/cs.json +++ b/homeassistant/components/synology_dsm/translations/cs.json @@ -36,6 +36,12 @@ }, "title": "Synology DSM Znovu ov\u011b\u0159it integraci" }, + "reauth_confirm": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, "user": { "data": { "host": "Hostitel", diff --git a/homeassistant/components/synology_dsm/translations/pl.json b/homeassistant/components/synology_dsm/translations/pl.json index 375d96a14e2..06a21cfdf18 100644 --- a/homeassistant/components/synology_dsm/translations/pl.json +++ b/homeassistant/components/synology_dsm/translations/pl.json @@ -43,7 +43,8 @@ "data": { "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika" - } + }, + "title": "Ponownie uwierzytelnij integracj\u0119 z Synology DSM" }, "user": { "data": { diff --git a/homeassistant/components/system_bridge/translations/bg.json b/homeassistant/components/system_bridge/translations/bg.json new file mode 100644 index 00000000000..4983c9a14b2 --- /dev/null +++ b/homeassistant/components/system_bridge/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/bg.json b/homeassistant/components/tasmota/translations/bg.json new file mode 100644 index 00000000000..a2321080a6a --- /dev/null +++ b/homeassistant/components/tasmota/translations/bg.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, + "step": { + "config": { + "title": "Tasmota" + }, + "confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Tasmota?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tile/translations/bg.json b/homeassistant/components/tile/translations/bg.json new file mode 100644 index 00000000000..946b62a8690 --- /dev/null +++ b/homeassistant/components/tile/translations/bg.json @@ -0,0 +1,7 @@ +{ + "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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/translations/bg.json b/homeassistant/components/tplink/translations/bg.json index cdceae66cbf..33ae523d8cf 100644 --- a/homeassistant/components/tplink/translations/bg.json +++ b/homeassistant/components/tplink/translations/bg.json @@ -1,12 +1,31 @@ { "config": { "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "no_devices_found": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 TP-Link \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430.", "single_instance_allowed": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "flow_title": "{name} {model} ({host})", "step": { "confirm": { "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 TP-Link \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430?" + }, + "discovery_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name} {model} ({host})?" + }, + "pick_device": { + "data": { + "device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + } + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u0410\u043a\u043e \u043e\u0441\u0442\u0430\u0432\u0438\u0442\u0435 \u0445\u043e\u0441\u0442\u0430 \u043f\u0440\u0430\u0437\u0435\u043d, \u043e\u0442\u043a\u0440\u0438\u0432\u0430\u043d\u0435\u0442\u043e \u0449\u0435 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u0437\u0430 \u043d\u0430\u043c\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430." } } } diff --git a/homeassistant/components/tplink/translations/cs.json b/homeassistant/components/tplink/translations/cs.json index 36d303b0f01..5ee79c8e6f6 100644 --- a/homeassistant/components/tplink/translations/cs.json +++ b/homeassistant/components/tplink/translations/cs.json @@ -1,12 +1,26 @@ { "config": { "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, "step": { "confirm": { "description": "Chcete nastavit inteligentn\u00ed za\u0159\u00edzen\u00ed TP-Link?" + }, + "pick_device": { + "data": { + "device": "Za\u0159\u00edzen\u00ed" + } + }, + "user": { + "data": { + "host": "Hostitel" + } } } } diff --git a/homeassistant/components/tplink/translations/pl.json b/homeassistant/components/tplink/translations/pl.json index e0e4e817f04..da91b12ea7c 100644 --- a/homeassistant/components/tplink/translations/pl.json +++ b/homeassistant/components/tplink/translations/pl.json @@ -1,9 +1,13 @@ { "config": { "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, "step": { "confirm": { "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" @@ -12,6 +16,11 @@ "data": { "device": "Urz\u0105dzenie" } + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + } } } } diff --git a/homeassistant/components/tractive/translations/bg.json b/homeassistant/components/tractive/translations/bg.json new file mode 100644 index 00000000000..bd02d32720a --- /dev/null +++ b/homeassistant/components/tractive/translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "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": { + "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": { + "data": { + "email": "Email", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/bg.json b/homeassistant/components/tuya/translations/bg.json new file mode 100644 index 00000000000..4b2bf8f53fe --- /dev/null +++ b/homeassistant/components/tuya/translations/bg.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "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" + }, + "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", + "login_error": "\u0413\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0432\u043b\u0438\u0437\u0430\u043d\u0435 ({code}): {msg}" + }, + "step": { + "login": { + "data": { + "country_code": "\u041a\u043e\u0434 \u043d\u0430 \u0434\u044a\u0440\u0436\u0430\u0432\u0430\u0442\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "tuya_app_type": "\u041c\u043e\u0431\u0438\u043b\u043d\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435", + "username": "\u0410\u043a\u0430\u0443\u043d\u0442" + }, + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438\u0442\u0435 \u0441\u0438 \u0434\u0430\u043d\u043d\u0438 \u0437\u0430 Tuya", + "title": "Tuya" + }, + "user": { + "data": { + "region": "\u0420\u0435\u0433\u0438\u043e\u043d" + } + } + } + }, + "options": { + "error": { + "dev_not_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u043e" + }, + "step": { + "device": { + "data": { + "unit_of_measurement": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u043d\u0430 \u0435\u0434\u0438\u043d\u0438\u0446\u0430, \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0430 \u043e\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/cs.json b/homeassistant/components/tuya/translations/cs.json index 1dda4ea6df7..7f23e409165 100644 --- a/homeassistant/components/tuya/translations/cs.json +++ b/homeassistant/components/tuya/translations/cs.json @@ -12,10 +12,11 @@ "step": { "user": { "data": { - "country_code": "K\u00f3d zem\u011b va\u0161eho \u00fa\u010dtu (nap\u0159. 1 pro USA nebo 86 pro \u010c\u00ednu)", + "country_code": "Zem\u011b", "password": "Heslo", "platform": "Aplikace, ve kter\u00e9 m\u00e1te zaregistrovan\u00fd \u00fa\u010det", - "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + "region": "Region", + "username": "\u00da\u010det" }, "description": "Zadejte sv\u00e9 p\u0159ihla\u0161ovac\u00ed \u00fadaje k Tuya.", "title": "Tuya" diff --git a/homeassistant/components/tuya/translations/tr.json b/homeassistant/components/tuya/translations/tr.json index 02f3c7b1692..37eae2e8ae0 100644 --- a/homeassistant/components/tuya/translations/tr.json +++ b/homeassistant/components/tuya/translations/tr.json @@ -10,6 +10,15 @@ }, "flow_title": "Tuya yap\u0131land\u0131rmas\u0131", "step": { + "login": { + "data": { + "country_code": "\u00dclke Kodu", + "password": "\u015eifre", + "tuya_app_type": "Tuya uygulama tipi", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "title": "Tuya" + }, "user": { "data": { "country_code": "Hesap \u00fclke kodunuz (\u00f6r. ABD i\u00e7in 1 veya \u00c7in i\u00e7in 86)", diff --git a/homeassistant/components/upb/translations/tr.json b/homeassistant/components/upb/translations/tr.json index 818531fcaa0..b70e56b88da 100644 --- a/homeassistant/components/upb/translations/tr.json +++ b/homeassistant/components/upb/translations/tr.json @@ -5,7 +5,13 @@ }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_upb_file": "Eksik veya ge\u00e7ersiz UPB UPS d\u0131\u015fa aktarma dosyas\u0131n\u0131 ba\u015flat\u0131n, dosyan\u0131n ad\u0131n\u0131 ve yolunu kontrol edin.", "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "description": "Bir Evrensel Elektrik Hatt\u0131 Veriyolu Elektrik Hatt\u0131 Aray\u00fcz Mod\u00fcl\u00fc (UPB PIM) ba\u011flay\u0131n. Adres dizesi 'tcp' i\u00e7in 'adres[:port]' bi\u00e7iminde olmal\u0131d\u0131r. Ba\u011flant\u0131 noktas\u0131 iste\u011fe ba\u011fl\u0131d\u0131r ve varsay\u0131lan olarak 2101'dir. \u00d6rnek: '192.168.1.42'. Seri protokol i\u00e7in adres 'tty[:baud]' bi\u00e7iminde olmal\u0131d\u0131r. Baud iste\u011fe ba\u011fl\u0131d\u0131r ve varsay\u0131lan olarak 4800'd\u00fcr. \u00d6rnek: '/dev/ttyS1'." + } } } } \ No newline at end of file diff --git a/homeassistant/components/upcloud/translations/bg.json b/homeassistant/components/upcloud/translations/bg.json new file mode 100644 index 00000000000..ba7cd86486f --- /dev/null +++ b/homeassistant/components/upcloud/translations/bg.json @@ -0,0 +1,16 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/bg.json b/homeassistant/components/upnp/translations/bg.json index 6d0427f9164..cf55d95f9ab 100644 --- a/homeassistant/components/upnp/translations/bg.json +++ b/homeassistant/components/upnp/translations/bg.json @@ -7,6 +7,13 @@ "error": { "one": "\u0433\u0440\u0435\u0448\u043a\u0430", "other": "\u0433\u0440\u0435\u0448\u043a\u0438" + }, + "step": { + "user": { + "data": { + "unique_id": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/bg.json b/homeassistant/components/uptimerobot/translations/bg.json new file mode 100644 index 00000000000..d75409d8ee1 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/bg.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u0430 \u0432\u0435\u0447\u0435 \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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_api_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + }, + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/translations/bg.json b/homeassistant/components/velbus/translations/bg.json index f354b271215..62b75d4bd81 100644 --- a/homeassistant/components/velbus/translations/bg.json +++ b/homeassistant/components/velbus/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/vizio/translations/tr.json b/homeassistant/components/vizio/translations/tr.json index 4b923cfb4b3..b976471b56a 100644 --- a/homeassistant/components/vizio/translations/tr.json +++ b/homeassistant/components/vizio/translations/tr.json @@ -5,7 +5,8 @@ "cannot_connect": "Ba\u011flanma hatas\u0131" }, "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "complete_pairing_failed": "E\u015fle\u015ftirme tamamlanamad\u0131. Yeniden g\u00f6ndermeden \u00f6nce, verdi\u011finiz PIN'in do\u011fru oldu\u011fundan ve TV'ye g\u00fc\u00e7 verildi\u011finden ve a\u011fa ba\u011fl\u0131 oldu\u011fundan emin olun." }, "step": { "user": { diff --git a/homeassistant/components/vlc_telnet/translations/bg.json b/homeassistant/components/vlc_telnet/translations/bg.json new file mode 100644 index 00000000000..41e4a0484f6 --- /dev/null +++ b/homeassistant/components/vlc_telnet/translations/bg.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", + "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", + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "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\u0443\u0441\u043f\u0435\u0448\u043d\u0430 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "flow_title": "{host}", + "step": { + "hassio_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0436\u0435\u0442\u0435 \u0441 \u0434\u043e\u0431\u0430\u0432\u043a\u0430\u0442\u0430 {addon}?" + }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "description": "\u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u043d\u0430\u0442\u0430 \u043f\u0430\u0440\u043e\u043b\u0430 \u0437\u0430 \u0445\u043e\u0441\u0442\u0430: {host}" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u0418\u043c\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vlc_telnet/translations/ca.json b/homeassistant/components/vlc_telnet/translations/ca.json new file mode 100644 index 00000000000..e1f5fad1b31 --- /dev/null +++ b/homeassistant/components/vlc_telnet/translations/ca.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat", + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "unknown": "Error inesperat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "flow_title": "{host}", + "step": { + "hassio_confirm": { + "description": "Vols connectar-te al complement {addon}?" + }, + "reauth_confirm": { + "data": { + "password": "Contrasenya" + }, + "description": "Introdueix la contrasenya correcta de l'amfitri\u00f3: {host}" + }, + "user": { + "data": { + "host": "Amfitri\u00f3", + "name": "Nom", + "password": "Contrasenya", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vlc_telnet/translations/cs.json b/homeassistant/components/vlc_telnet/translations/cs.json new file mode 100644 index 00000000000..a06a73ff569 --- /dev/null +++ b/homeassistant/components/vlc_telnet/translations/cs.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba je ji\u017e nastavena", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "flow_title": "{host}", + "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + } + }, + "user": { + "data": { + "host": "Hostitel", + "name": "Jm\u00e9no", + "password": "Heslo", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vlc_telnet/translations/de.json b/homeassistant/components/vlc_telnet/translations/de.json index 89d763fb458..8174c4355a6 100644 --- a/homeassistant/components/vlc_telnet/translations/de.json +++ b/homeassistant/components/vlc_telnet/translations/de.json @@ -2,7 +2,10 @@ "config": { "abort": { "already_configured": "Service ist bereits konfiguriert", - "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "unknown": "Unerwarteter Fehler" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", @@ -11,6 +14,9 @@ }, "flow_title": "{host}", "step": { + "hassio_confirm": { + "description": "M\u00f6chtest du eine Verbindung zum Add-on {addon} herstellen?" + }, "reauth_confirm": { "data": { "password": "Passwort" diff --git a/homeassistant/components/vlc_telnet/translations/he.json b/homeassistant/components/vlc_telnet/translations/he.json new file mode 100644 index 00000000000..2d7d7e546ad --- /dev/null +++ b/homeassistant/components/vlc_telnet/translations/he.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", + "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" + }, + "flow_title": "{host}", + "step": { + "reauth_confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + }, + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "name": "\u05e9\u05dd", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "port": "\u05e4\u05ea\u05d7\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vlc_telnet/translations/pl.json b/homeassistant/components/vlc_telnet/translations/pl.json new file mode 100644 index 00000000000..8ddfa57f6d5 --- /dev/null +++ b/homeassistant/components/vlc_telnet/translations/pl.json @@ -0,0 +1,30 @@ +{ + "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_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "flow_title": "{host}", + "step": { + "reauth_confirm": { + "data": { + "password": "Has\u0142o" + }, + "description": "Prosz\u0119 wpisa\u0107 prawid\u0142owe has\u0142o dla hosta: {host}." + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "name": "Nazwa", + "password": "Has\u0142o", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vlc_telnet/translations/ru.json b/homeassistant/components/vlc_telnet/translations/ru.json new file mode 100644 index 00000000000..b83181482ac --- /dev/null +++ b/homeassistant/components/vlc_telnet/translations/ru.json @@ -0,0 +1,30 @@ +{ + "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_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "{host}", + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0445\u043e\u0441\u0442\u0430: {host}" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vlc_telnet/translations/tr.json b/homeassistant/components/vlc_telnet/translations/tr.json index 72f3c337205..40379347fd4 100644 --- a/homeassistant/components/vlc_telnet/translations/tr.json +++ b/homeassistant/components/vlc_telnet/translations/tr.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Kimlik do\u011frulama yeniden ba\u015far\u0131l\u0131 oldu" + }, "error": { "cannot_connect": "Ba\u011flant\u0131 ba\u015far\u0131s\u0131z" }, @@ -12,7 +16,7 @@ }, "user": { "data": { - "host": "Sunucu", + "host": "Ana Bilgisayar", "name": "\u0130sim", "password": "\u015eifre", "port": "Port" diff --git a/homeassistant/components/volumio/translations/bg.json b/homeassistant/components/volumio/translations/bg.json new file mode 100644 index 00000000000..a610a1f2a64 --- /dev/null +++ b/homeassistant/components/volumio/translations/bg.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volumio/translations/hu.json b/homeassistant/components/volumio/translations/hu.json index b504275e03f..c80bd0e7525 100644 --- a/homeassistant/components/volumio/translations/hu.json +++ b/homeassistant/components/volumio/translations/hu.json @@ -11,7 +11,7 @@ "step": { "discovery_confirm": { "description": "Szeretn\u00e9 hozz\u00e1adni a Volumio (`{name}`)-t a Home Assistanthoz?", - "title": "Felfedezett Volumio" + "title": "Volumio felfedezve" }, "user": { "data": { diff --git a/homeassistant/components/water_heater/translations/bg.json b/homeassistant/components/water_heater/translations/bg.json new file mode 100644 index 00000000000..b751234eaea --- /dev/null +++ b/homeassistant/components/water_heater/translations/bg.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/bg.json b/homeassistant/components/watttime/translations/bg.json new file mode 100644 index 00000000000..0ce2b541513 --- /dev/null +++ b/homeassistant/components/watttime/translations/bg.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "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": { + "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", + "unknown_coordinates": "\u041d\u044f\u043c\u0430 \u0434\u0430\u043d\u043d\u0438 \u0437\u0430 \u0433\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0448\u0438\u0440\u0438\u043d\u0430/\u0434\u044a\u043b\u0436\u0438\u043d\u0430" + }, + "step": { + "coordinates": { + "data": { + "latitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0448\u0438\u0440\u0438\u043d\u0430", + "longitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0434\u044a\u043b\u0436\u0438\u043d\u0430" + } + }, + "location": { + "data": { + "location_type": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435" + } + }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "description": "\u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0437\u0430 {username}:", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + }, + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0432\u0430\u0448\u0435\u0442\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/cs.json b/homeassistant/components/watttime/translations/cs.json index d67789246ba..64cb45a167e 100644 --- a/homeassistant/components/watttime/translations/cs.json +++ b/homeassistant/components/watttime/translations/cs.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", @@ -23,7 +24,8 @@ "data": { "password": "Heslo" }, - "description": "Zadejte pros\u00edm znovu heslo pro {username}:" + "description": "Zadejte pros\u00edm znovu heslo pro {username}:", + "title": "Znovu ov\u011b\u0159it integraci" }, "user": { "data": { diff --git a/homeassistant/components/watttime/translations/pl.json b/homeassistant/components/watttime/translations/pl.json index f9c3ac32ac5..b135f54a6c0 100644 --- a/homeassistant/components/watttime/translations/pl.json +++ b/homeassistant/components/watttime/translations/pl.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, "error": { + "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d", "unknown_coordinates": "Brak danych dla szeroko\u015bci/d\u0142ugo\u015bci geograficznej" }, @@ -18,6 +23,12 @@ }, "description": "Wybierz lokalizacj\u0119 do monitorowania:" }, + "reauth_confirm": { + "data": { + "password": "Has\u0142o" + }, + "title": "Ponownie uwierzytelnij integracj\u0119" + }, "user": { "data": { "password": "Has\u0142o", diff --git a/homeassistant/components/whirlpool/translations/bg.json b/homeassistant/components/whirlpool/translations/bg.json new file mode 100644 index 00000000000..9cbfcd4ede8 --- /dev/null +++ b/homeassistant/components/whirlpool/translations/bg.json @@ -0,0 +1,17 @@ +{ + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/pl.json b/homeassistant/components/whirlpool/translations/pl.json index e6575c9f9a7..f66fe9bdc33 100644 --- a/homeassistant/components/whirlpool/translations/pl.json +++ b/homeassistant/components/whirlpool/translations/pl.json @@ -2,6 +2,7 @@ "config": { "error": { "cannot_connect": "Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107", + "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { diff --git a/homeassistant/components/wiffi/translations/bg.json b/homeassistant/components/wiffi/translations/bg.json new file mode 100644 index 00000000000..4983c9a14b2 --- /dev/null +++ b/homeassistant/components/wiffi/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/translations/bg.json b/homeassistant/components/wled/translations/bg.json index beb1bc0d6e6..77f63ca8684 100644 --- a/homeassistant/components/wled/translations/bg.json +++ b/homeassistant/components/wled/translations/bg.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/bg.json b/homeassistant/components/xiaomi_miio/translations/bg.json index bd5387fe8f9..ee40fdacacf 100644 --- a/homeassistant/components/xiaomi_miio/translations/bg.json +++ b/homeassistant/components/xiaomi_miio/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "device": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/select.bg.json b/homeassistant/components/xiaomi_miio/translations/select.bg.json new file mode 100644 index 00000000000..fa3683359ec --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/select.bg.json @@ -0,0 +1,7 @@ +{ + "state": { + "xiaomi_miio__led_brightness": { + "off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/bg.json b/homeassistant/components/yale_smart_alarm/translations/bg.json new file mode 100644 index 00000000000..b4e9a017081 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/bg.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u0440\u043e\u0444\u0438\u043b\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" + }, + "step": { + "reauth_confirm": { + "data": { + "name": "\u0418\u043c\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + }, + "user": { + "data": { + "name": "\u0418\u043c\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/tr.json b/homeassistant/components/yale_smart_alarm/translations/tr.json new file mode 100644 index 00000000000..e1780e06fce --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/tr.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "area_id": "Alan Kodu", + "name": "\u0130sim", + "password": "\u015eifre", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + }, + "user": { + "data": { + "area_id": "Alan Kodu", + "name": "\u0130sim", + "password": "\u015eifre", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/bg.json b/homeassistant/components/yeelight/translations/bg.json new file mode 100644 index 00000000000..2ac8a444100 --- /dev/null +++ b/homeassistant/components/yeelight/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/bg.json b/homeassistant/components/youless/translations/bg.json new file mode 100644 index 00000000000..acbebfb4d36 --- /dev/null +++ b/homeassistant/components/youless/translations/bg.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u0418\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/bg.json b/homeassistant/components/zha/translations/bg.json index c0bd15fd462..eeb1df867ad 100644 --- a/homeassistant/components/zha/translations/bg.json +++ b/homeassistant/components/zha/translations/bg.json @@ -7,6 +7,15 @@ "cannot_connect": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 ZHA \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" }, "step": { + "confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name}?" + }, + "port_config": { + "data": { + "baudrate": "\u0441\u043a\u043e\u0440\u043e\u0441\u0442 \u043d\u0430 \u043f\u043e\u0440\u0442\u0430" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + }, "user": { "title": "ZHA" } diff --git a/homeassistant/components/zoneminder/translations/bg.json b/homeassistant/components/zoneminder/translations/bg.json new file mode 100644 index 00000000000..946b62a8690 --- /dev/null +++ b/homeassistant/components/zoneminder/translations/bg.json @@ -0,0 +1,7 @@ +{ + "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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/bg.json b/homeassistant/components/zwave_js/translations/bg.json index cc046c009d8..222c9fd34a1 100644 --- a/homeassistant/components/zwave_js/translations/bg.json +++ b/homeassistant/components/zwave_js/translations/bg.json @@ -3,12 +3,33 @@ "error": { "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, + "flow_title": "{name}", "step": { + "configure_addon": { + "data": { + "s0_legacy_key": "S0 \u043a\u043b\u044e\u0447 (\u043d\u0430\u0441\u043b\u0435\u0434\u0435\u043d)", + "s2_access_control_key": "S2 \u043a\u043b\u044e\u0447 \u0437\u0430 \u043a\u043e\u043d\u0442\u0440\u043e\u043b \u043d\u0430 \u0434\u043e\u0441\u0442\u044a\u043f\u0430", + "s2_authenticated_key": "S2 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u0435\u043d \u043a\u043b\u044e\u0447", + "s2_unauthenticated_key": "S2 \u043d\u0435\u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u0435\u043d \u043a\u043b\u044e\u0447" + } + }, "manual": { "data": { "url": "URL" } } } + }, + "options": { + "step": { + "configure_addon": { + "data": { + "s0_legacy_key": "S0 \u043a\u043b\u044e\u0447 (\u043d\u0430\u0441\u043b\u0435\u0434\u0435\u043d)", + "s2_access_control_key": "S2 \u043a\u043b\u044e\u0447 \u0437\u0430 \u043a\u043e\u043d\u0442\u0440\u043e\u043b \u043d\u0430 \u0434\u043e\u0441\u0442\u044a\u043f\u0430", + "s2_authenticated_key": "S2 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u0435\u043d \u043a\u043b\u044e\u0447", + "s2_unauthenticated_key": "S2 \u043d\u0435\u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u0435\u043d \u043a\u043b\u044e\u0447" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/tr.json b/homeassistant/components/zwave_js/translations/tr.json index 10c9c54a98b..5fe4f92b857 100644 --- a/homeassistant/components/zwave_js/translations/tr.json +++ b/homeassistant/components/zwave_js/translations/tr.json @@ -46,5 +46,10 @@ } } }, + "device_automation": { + "action_type": { + "ping": "ping" + } + }, "title": "Z-Wave JS" } \ No newline at end of file From 276345e20a8c6a68d8be2062f2a7d6b19b2ea021 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 17 Oct 2021 03:13:12 +0200 Subject: [PATCH 0442/1038] Decrease `timeout` and `update_interval` in Xiaomi Miio integration (#57339) * Decrease timeout and update_interval * Improve UPDATE_INTERVAL const * Update values after testing --- homeassistant/components/xiaomi_miio/__init__.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 4788c3220bf..de5baf69683 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -73,6 +73,9 @@ from .gateway import ConnectXiaomiGateway _LOGGER = logging.getLogger(__name__) +POLLING_TIMEOUT_SEC = 10 +UPDATE_INTERVAL = timedelta(seconds=15) + GATEWAY_PLATFORMS = ["alarm_control_panel", "light", "sensor", "switch"] SWITCH_PLATFORMS = ["switch"] FAN_PLATFORMS = ["binary_sensor", "fan", "number", "select", "sensor", "switch"] @@ -151,7 +154,7 @@ def _async_update_data_default(hass, device): async def _async_fetch_data(): """Fetch data from the device.""" - async with async_timeout.timeout(10): + async with async_timeout.timeout(POLLING_TIMEOUT_SEC): state = await hass.async_add_executor_job(device.status) _LOGGER.debug("Got new state: %s", state) return state @@ -239,7 +242,7 @@ def _async_update_data_vacuum(hass, device: Vacuum): """Fetch data from the device using async_add_executor_job.""" async def execute_update(): - async with async_timeout.timeout(10): + async with async_timeout.timeout(POLLING_TIMEOUT_SEC): state = await hass.async_add_executor_job(update) _LOGGER.debug("Got new vacuum state: %s", state) return state @@ -336,7 +339,7 @@ async def async_create_miio_device_and_coordinator( name=name, update_method=update_method(hass, device), # Polling interval. Will only be polled if there are subscribers. - update_interval=timedelta(seconds=60), + update_interval=UPDATE_INTERVAL, ) hass.data[DOMAIN][entry.entry_id] = { KEY_DEVICE: device, @@ -409,7 +412,7 @@ async def async_setup_gateway_entry( name=name, update_method=async_update_data, # Polling interval. Will only be polled if there are subscribers. - update_interval=timedelta(seconds=10), + update_interval=UPDATE_INTERVAL, ) hass.data[DOMAIN][entry.entry_id] = { From 5461fa9a2dd49fee9404579b3343192c10059a94 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 16 Oct 2021 15:57:11 -1000 Subject: [PATCH 0443/1038] Bump bond-api to 0.1.14 (#57874) --- homeassistant/components/bond/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bond/manifest.json b/homeassistant/components/bond/manifest.json index 7d1486b2e8f..6f11b8c66e3 100644 --- a/homeassistant/components/bond/manifest.json +++ b/homeassistant/components/bond/manifest.json @@ -3,7 +3,7 @@ "name": "Bond", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bond", - "requirements": ["bond-api==0.1.13"], + "requirements": ["bond-api==0.1.14"], "zeroconf": ["_bond._tcp.local."], "codeowners": ["@prystupa", "@joshs85"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 9a804111b91..eb3a5af35e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ blockchain==1.4.4 # bme680==1.0.5 # homeassistant.components.bond -bond-api==0.1.13 +bond-api==0.1.14 # homeassistant.components.bosch_shc boschshcpy==0.2.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 456fdc40d05..e70b6128387 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -257,7 +257,7 @@ blebox_uniapi==1.3.3 blinkpy==0.17.0 # homeassistant.components.bond -bond-api==0.1.13 +bond-api==0.1.14 # homeassistant.components.bosch_shc boschshcpy==0.2.19 From d46ae04ec75bad975d0805175a20309de4101939 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 16 Oct 2021 18:43:41 -1000 Subject: [PATCH 0444/1038] Add additional models to flux_led DHCP discovery (#57881) --- homeassistant/components/flux_led/manifest.json | 12 ++++++++++++ homeassistant/generated/dhcp.py | 16 ++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 1c5a6d16100..fd194ac90b9 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -27,9 +27,21 @@ "macaddress": "8CCE4E*", "hostname": "lwip*" }, + { + "hostname": "zengge_06_*" + }, + { + "hostname": "zengge_07_*" + }, + { + "hostname": "zengge_33_*" + }, { "hostname": "zengge_35_*" }, + { + "hostname": "zengge_41_*" + }, { "hostname": "zengge_0e_*" }, diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index a20dee6b2f5..ecfbfe657df 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -96,10 +96,26 @@ DHCP = [ "macaddress": "8CCE4E*", "hostname": "lwip*" }, + { + "domain": "flux_led", + "hostname": "zengge_06_*" + }, + { + "domain": "flux_led", + "hostname": "zengge_07_*" + }, + { + "domain": "flux_led", + "hostname": "zengge_33_*" + }, { "domain": "flux_led", "hostname": "zengge_35_*" }, + { + "domain": "flux_led", + "hostname": "zengge_41_*" + }, { "domain": "flux_led", "hostname": "zengge_0e_*" From 6fb0609f0c5ab9b84228db350b8a353b395b6123 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 17 Oct 2021 07:39:25 +0200 Subject: [PATCH 0445/1038] Add use time sensor for air purifiers (#57775) --- homeassistant/components/xiaomi_miio/sensor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index c41b6a1d00d..e1e2d91ad1a 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -307,6 +307,7 @@ PURIFIER_MIIO_SENSORS = ( ATTR_MOTOR_SPEED, ATTR_PM25, ATTR_TEMPERATURE, + ATTR_USE_TIME, ) PURIFIER_MIOT_SENSORS = ( ATTR_FILTER_LIFE_REMAINING, @@ -316,6 +317,7 @@ PURIFIER_MIOT_SENSORS = ( ATTR_PM25, ATTR_PURIFY_VOLUME, ATTR_TEMPERATURE, + ATTR_USE_TIME, ) PURIFIER_3C_SENSORS = ( ATTR_FILTER_LIFE_REMAINING, @@ -331,6 +333,7 @@ PURIFIER_V2_SENSORS = ( ATTR_PM25, ATTR_PURIFY_VOLUME, ATTR_TEMPERATURE, + ATTR_USE_TIME, ) PURIFIER_V3_SENSORS = ( ATTR_FILTER_LIFE_REMAINING, @@ -340,6 +343,7 @@ PURIFIER_V3_SENSORS = ( ATTR_MOTOR_SPEED, ATTR_PM25, ATTR_PURIFY_VOLUME, + ATTR_USE_TIME, ) PURIFIER_PRO_SENSORS = ( ATTR_FILTER_LIFE_REMAINING, @@ -351,6 +355,7 @@ PURIFIER_PRO_SENSORS = ( ATTR_PM25, ATTR_PURIFY_VOLUME, ATTR_TEMPERATURE, + ATTR_USE_TIME, ) PURIFIER_PRO_V7_SENSORS = ( ATTR_FILTER_LIFE_REMAINING, @@ -361,6 +366,7 @@ PURIFIER_PRO_V7_SENSORS = ( ATTR_MOTOR_SPEED, ATTR_PM25, ATTR_TEMPERATURE, + ATTR_USE_TIME, ) AIRFRESH_SENSORS = ( ATTR_CARBON_DIOXIDE, @@ -370,6 +376,7 @@ AIRFRESH_SENSORS = ( ATTR_ILLUMINANCE_LUX, ATTR_PM25, ATTR_TEMPERATURE, + ATTR_USE_TIME, ) FAN_V2_V3_SENSORS = ( ATTR_BATTERY, From 3da3d26573147ade2a4db537e7e417cf02340a1c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 16 Oct 2021 23:58:39 -0600 Subject: [PATCH 0446/1038] Make sure AirVisual data storage conforms to standards (#57806) --- .../components/airvisual/__init__.py | 21 +++++++++---------- homeassistant/components/airvisual/sensor.py | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 39df25c0f4a..72b063c9394 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -104,12 +104,12 @@ def async_get_cloud_coordinators_by_api_key( hass: HomeAssistant, api_key: str ) -> list[DataUpdateCoordinator]: """Get all DataUpdateCoordinator objects related to a particular API key.""" - coordinators = [] - for entry_id, coordinator in hass.data[DOMAIN][DATA_COORDINATOR].items(): - entry = hass.config_entries.async_get_entry(entry_id) - if entry and entry.data.get(CONF_API_KEY) == api_key: - coordinators.append(coordinator) - return coordinators + return [ + attrs[DATA_COORDINATOR] + for entry_id, attrs in hass.data[DOMAIN].items() + if (entry := hass.config_entries.async_get_entry(entry_id)) + and entry.data.get(CONF_API_KEY) == api_key + ] @callback @@ -190,7 +190,8 @@ def _standardize_node_pro_config_entry(hass: HomeAssistant, entry: ConfigEntry) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up AirVisual as config entry.""" - hass.data.setdefault(DOMAIN, {DATA_COORDINATOR: {}}) + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = {} if CONF_API_KEY in entry.data: _standardize_geography_config_entry(hass, entry) @@ -270,8 +271,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await coordinator.async_config_entry_first_refresh() - - hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = coordinator + hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] = coordinator # Reassess the interval between 2 server requests if CONF_API_KEY in entry.data: @@ -329,8 +329,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) - + hass.data[DOMAIN].pop(entry.entry_id) if CONF_API_KEY in entry.data: # Re-calculate the update interval period for any remaining consumers of # this API key: diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index 26f89d06dde..70d9800e736 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -192,7 +192,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up AirVisual sensors based on a config entry.""" - coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] + coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] sensors: list[AirVisualGeographySensor | AirVisualNodeProSensor] if entry.data[CONF_INTEGRATION_TYPE] in ( From fd49da37b8afd8446c202fe34085177e006c5da7 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 16 Oct 2021 23:59:19 -0600 Subject: [PATCH 0447/1038] Make sure OpenUV data storage conforms to standards (#57813) --- homeassistant/components/openuv/__init__.py | 8 ++++---- homeassistant/components/openuv/binary_sensor.py | 2 +- homeassistant/components/openuv/const.py | 1 - homeassistant/components/openuv/sensor.py | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index 5b92dbcc39e..e38d95a6101 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -31,7 +31,6 @@ from .const import ( CONF_FROM_WINDOW, CONF_TO_WINDOW, DATA_CLIENT, - DATA_LISTENER, DATA_PROTECTION_WINDOW, DATA_UV, DEFAULT_FROM_WINDOW, @@ -52,7 +51,8 @@ PLATFORMS = ["binary_sensor", "sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up OpenUV as config entry.""" - hass.data.setdefault(DOMAIN, {DATA_CLIENT: {}, DATA_LISTENER: {}}) + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = {} _verify_domain_control = verify_domain_control(hass, DOMAIN) @@ -69,11 +69,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ), ) await openuv.async_update() - hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] = openuv except OpenUvError as err: LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady from err + hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] = openuv hass.config_entries.async_setup_platforms(entry, PLATFORMS) @_verify_domain_control @@ -111,7 +111,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload an OpenUV config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - hass.data[DOMAIN][DATA_CLIENT].pop(entry.entry_id) + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/openuv/binary_sensor.py b/homeassistant/components/openuv/binary_sensor.py index a632d212abd..4d10aa53a39 100644 --- a/homeassistant/components/openuv/binary_sensor.py +++ b/homeassistant/components/openuv/binary_sensor.py @@ -33,7 +33,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up an OpenUV sensor based on a config entry.""" - openuv = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] + openuv = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] async_add_entities( [OpenUvBinarySensor(openuv, BINARY_SENSOR_DESCRIPTION_PROTECTION_WINDOW)] ) diff --git a/homeassistant/components/openuv/const.py b/homeassistant/components/openuv/const.py index 3b117fe37aa..975511c7297 100644 --- a/homeassistant/components/openuv/const.py +++ b/homeassistant/components/openuv/const.py @@ -8,7 +8,6 @@ CONF_FROM_WINDOW = "from_window" CONF_TO_WINDOW = "to_window" DATA_CLIENT = "data_client" -DATA_LISTENER = "data_listener" DATA_PROTECTION_WINDOW = "protection_window" DATA_UV = "uv" diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py index 7f091bc1a79..2eac6ba41b2 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -122,7 +122,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up a OpenUV sensor based on a config entry.""" - openuv = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] + openuv = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] async_add_entities( [OpenUvSensor(openuv, description) for description in SENSOR_DESCRIPTIONS] ) From b6ed8ca206f26f7d684265997c7170373912067d Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 16 Oct 2021 23:59:36 -0600 Subject: [PATCH 0448/1038] Make sure Notion data storage conforms to standards (#57812) --- homeassistant/components/notion/__init__.py | 10 +++++----- homeassistant/components/notion/binary_sensor.py | 2 +- homeassistant/components/notion/sensor.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 2fb9339955a..a65e8ab338c 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -39,7 +39,8 @@ CONFIG_SCHEMA = cv.deprecated(DOMAIN) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Notion as a config entry.""" - hass.data.setdefault(DOMAIN, {DATA_COORDINATOR: {}}) + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = {} if not entry.unique_id: hass.config_entries.async_update_entry( @@ -89,9 +90,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return data - coordinator = hass.data[DOMAIN][DATA_COORDINATOR][ - entry.entry_id - ] = DataUpdateCoordinator( + coordinator = DataUpdateCoordinator( hass, LOGGER, name=entry.data[CONF_USERNAME], @@ -100,6 +99,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await coordinator.async_config_entry_first_refresh() + hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] = coordinator hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -110,7 +110,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a Notion config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py index bfd90010a94..c064e492b16 100644 --- a/homeassistant/components/notion/binary_sensor.py +++ b/homeassistant/components/notion/binary_sensor.py @@ -119,7 +119,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Notion sensors based on a config entry.""" - coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] + coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] async_add_entities( [ diff --git a/homeassistant/components/notion/sensor.py b/homeassistant/components/notion/sensor.py index 204074ed884..efb33944990 100644 --- a/homeassistant/components/notion/sensor.py +++ b/homeassistant/components/notion/sensor.py @@ -27,7 +27,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Notion sensors based on a config entry.""" - coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] + coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] async_add_entities( [ From 48d4cdf88286f82e66506257483f6bab753c0d89 Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Sun, 17 Oct 2021 09:43:18 +0200 Subject: [PATCH 0449/1038] Update xknx to 0.18.11 and fix flaky test (#57877) * Update xknx to 0.18.11 * review: join the queue before actually asserting --- homeassistant/components/knx/__init__.py | 1 - homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/knx/conftest.py | 5 +++++ tests/components/knx/test_binary_sensor.py | 19 ++++++++++--------- 6 files changed, 18 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index e52a79cf25a..54d4b4b7237 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -290,7 +290,6 @@ class KNXModule: """Start XKNX object. Connect to tunneling or Routing device.""" await self.xknx.start() self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop) - self.connected = True async def stop(self, event: Event) -> None: """Stop XKNX object. Disconnect from tunneling or Routing device.""" diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index aafa6630560..6c0b1811a6b 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -2,7 +2,7 @@ "domain": "knx", "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.18.10"], + "requirements": ["xknx==0.18.11"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "silver", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index eb3a5af35e5..a482ddc619f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2427,7 +2427,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.18.10 +xknx==0.18.11 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e70b6128387..fe4b1c52f27 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1398,7 +1398,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.18.10 +xknx==0.18.11 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index 5f60f5603aa..47e7b94c32d 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -6,6 +6,7 @@ from unittest.mock import DEFAULT, AsyncMock, Mock, patch import pytest from xknx import XKNX +from xknx.core import XknxConnectionState from xknx.dpt import DPTArray, DPTBinary from xknx.telegram import Telegram, TelegramDirection from xknx.telegram.address import GroupAddress, IndividualAddress @@ -53,6 +54,9 @@ class KNXTestKit: side_effect=fish_xknx, ): 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() ######################## @@ -94,6 +98,7 @@ class KNXTestKit: apci_type: type[APCI], ) -> None: """Assert outgoing telegram. One by one in timely order.""" + await self.xknx.telegrams.join() await self.hass.async_block_till_done() try: telegram = self._outgoing_telegrams.get_nowait() diff --git a/tests/components/knx/test_binary_sensor.py b/tests/components/knx/test_binary_sensor.py index 9f9785150c0..1cb9f14e61f 100644 --- a/tests/components/knx/test_binary_sensor.py +++ b/tests/components/knx/test_binary_sensor.py @@ -1,5 +1,4 @@ """Test KNX binary sensor.""" -import asyncio from datetime import timedelta from homeassistant.components.knx.const import CONF_STATE_ADDRESS, CONF_SYNC_STATE @@ -120,6 +119,7 @@ async def test_binary_sensor_counter(hass: HomeAssistant, knx: KNXTestKit): """Test KNX binary_sensor with context timeout.""" async_fire_time_changed(hass, dt.utcnow()) events = async_capture_events(hass, "state_changed") + context_timeout = 1 await knx.setup_integration( { @@ -127,7 +127,7 @@ async def test_binary_sensor_counter(hass: HomeAssistant, knx: KNXTestKit): { CONF_NAME: "test", CONF_STATE_ADDRESS: "2/2/2", - BinarySensorSchema.CONF_CONTEXT_TIMEOUT: 0.001, + BinarySensorSchema.CONF_CONTEXT_TIMEOUT: context_timeout, CONF_SYNC_STATE: False, }, ] @@ -145,9 +145,9 @@ async def test_binary_sensor_counter(hass: HomeAssistant, knx: KNXTestKit): state = hass.states.get("binary_sensor.test") assert state.state is STATE_OFF assert state.attributes.get("counter") == 0 - async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=0.001)) + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=context_timeout)) await hass.async_block_till_done() - await asyncio.sleep(0.002) + await knx.xknx.task_registry.block_till_done() # state changed twice after context timeout - once to ON with counter 1 and once to counter 0 state = hass.states.get("binary_sensor.test") assert state.state is STATE_ON @@ -169,18 +169,19 @@ async def test_binary_sensor_counter(hass: HomeAssistant, knx: KNXTestKit): state = hass.states.get("binary_sensor.test") assert state.state is STATE_ON assert state.attributes.get("counter") == 0 - await hass.async_block_till_done() - async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=context_timeout)) + await knx.xknx.task_registry.block_till_done() await hass.async_block_till_done() state = hass.states.get("binary_sensor.test") assert state.state is STATE_ON assert state.attributes.get("counter") == 0 - await hass.async_block_till_done() - await hass.async_block_till_done() - assert len(events) == 1 + assert len(events) == 2 event = events.pop(0).data assert event.get("new_state").attributes.get("counter") == 2 assert event.get("old_state").attributes.get("counter") == 0 + event = events.pop(0).data + assert event.get("new_state").attributes.get("counter") == 0 + assert event.get("old_state").attributes.get("counter") == 2 async def test_binary_sensor_reset(hass: HomeAssistant, knx: KNXTestKit): From a1e9a06675541d9a813f7e293d68faa4282b1ebc Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 17 Oct 2021 09:45:11 +0200 Subject: [PATCH 0450/1038] Add sensor category for rssi and battery (#57848) --- homeassistant/components/rfxtrx/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index c72d2e288e1..68c29e20328 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -28,6 +28,7 @@ from homeassistant.const import ( ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, + ENTITY_CATEGORY_DIAGNOSTIC, LENGTH_MILLIMETERS, PERCENTAGE, POWER_WATT, @@ -86,6 +87,7 @@ SENSOR_TYPES = ( state_class=STATE_CLASS_MEASUREMENT, native_unit_of_measurement=PERCENTAGE, convert=_battery_convert, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), RfxtrxSensorEntityDescription( key="Current", @@ -129,6 +131,7 @@ SENSOR_TYPES = ( state_class=STATE_CLASS_MEASUREMENT, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, convert=_rssi_convert, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), RfxtrxSensorEntityDescription( key="Temperature", From c9f55c01afd39ce83059d243d5c53e5612d7b8b9 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 17 Oct 2021 10:11:28 +0200 Subject: [PATCH 0451/1038] Bump pymodbus to 2.5.3. (#57887) --- homeassistant/components/modbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index ceade8c6455..ccf2bf81384 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -2,7 +2,7 @@ "domain": "modbus", "name": "Modbus", "documentation": "https://www.home-assistant.io/integrations/modbus", - "requirements": ["pymodbus==2.5.3rc1"], + "requirements": ["pymodbus==2.5.3"], "codeowners": ["@adamchengtkc", "@janiversen", "@vzahradnik"], "quality_scale": "gold", "iot_class": "local_polling" diff --git a/requirements_all.txt b/requirements_all.txt index a482ddc619f..dd86b6d97ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1634,7 +1634,7 @@ pymitv==1.4.3 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==2.5.3rc1 +pymodbus==2.5.3 # homeassistant.components.monoprice pymonoprice==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fe4b1c52f27..dee1f74f72f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -974,7 +974,7 @@ pymfy==0.11.0 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==2.5.3rc1 +pymodbus==2.5.3 # homeassistant.components.monoprice pymonoprice==0.3 From 4b55893781ca9957aa595041ee21eaefe46b69f9 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Sun, 17 Oct 2021 10:50:23 +0200 Subject: [PATCH 0452/1038] Bump pypoint (#57888) --- homeassistant/components/point/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/point/manifest.json b/homeassistant/components/point/manifest.json index 13a1ac5ce23..df90a230fab 100644 --- a/homeassistant/components/point/manifest.json +++ b/homeassistant/components/point/manifest.json @@ -3,7 +3,7 @@ "name": "Minut Point", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/point", - "requirements": ["pypoint==2.2.0"], + "requirements": ["pypoint==2.2.1"], "dependencies": ["webhook", "http"], "codeowners": ["@fredrike"], "quality_scale": "gold", diff --git a/requirements_all.txt b/requirements_all.txt index dd86b6d97ec..5d78dc85a89 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1717,7 +1717,7 @@ pypjlink2==1.2.1 pyplaato==0.0.15 # homeassistant.components.point -pypoint==2.2.0 +pypoint==2.2.1 # homeassistant.components.profiler pyprof2calltree==1.4.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dee1f74f72f..fe436c9d569 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1030,7 +1030,7 @@ pypck==0.7.10 pyplaato==0.0.15 # homeassistant.components.point -pypoint==2.2.0 +pypoint==2.2.1 # homeassistant.components.profiler pyprof2calltree==1.4.5 From f4918b2d9ab3f92622d79f1234de9ad794c0285e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 17 Oct 2021 10:50:48 +0200 Subject: [PATCH 0453/1038] Fix Tuya documentation URL (#57889) --- homeassistant/components/tuya/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index 5e59f2a3e8e..28b5633a633 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -1,7 +1,7 @@ { "domain": "tuya", "name": "Tuya", - "documentation": "https://github.com/tuya/tuya-home-assistant", + "documentation": "https://www.home-assistant.io/integrations/tuya", "requirements": ["tuya-iot-py-sdk==0.5.0"], "dependencies": ["ffmpeg"], "codeowners": ["@Tuya", "@zlinoliver", "@METISU", "@frenck"], From 378c48da15720da937a19b072dc53685dd271e6f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 16 Oct 2021 23:11:35 -1000 Subject: [PATCH 0454/1038] Improve lutron caseta error reporting when bridge is offline (#57832) --- homeassistant/components/lutron_caseta/__init__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 144a9a74c55..44e6e8761b1 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -1,5 +1,6 @@ """Component for interacting with a Lutron Caseta system.""" import asyncio +import contextlib import logging import ssl @@ -106,16 +107,17 @@ async def async_setup_entry(hass, config_entry): return False timed_out = True - try: + with contextlib.suppress(asyncio.TimeoutError): async with async_timeout.timeout(BRIDGE_TIMEOUT): await bridge.connect() timed_out = False - except asyncio.TimeoutError: - _LOGGER.error("Timeout while trying to connect to bridge at %s", host) if timed_out or not bridge.is_connected(): await bridge.close() - raise ConfigEntryNotReady + if timed_out: + raise ConfigEntryNotReady(f"Timed out while trying to connect to {host}") + if not bridge.is_connected(): + raise ConfigEntryNotReady(f"Cannot connect to {host}") _LOGGER.debug("Connected to Lutron Caseta bridge via LEAP at %s", host) From 0b932b53c9299b6b09206a7239dd936e423e103c Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 17 Oct 2021 12:12:01 +0200 Subject: [PATCH 0455/1038] Do not probe `nam` device if the host is already configured (#57843) --- homeassistant/components/nam/config_flow.py | 3 +++ tests/components/nam/test_config_flow.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/homeassistant/components/nam/config_flow.py b/homeassistant/components/nam/config_flow.py index a44f3f2ba6a..458895e69c5 100644 --- a/homeassistant/components/nam/config_flow.py +++ b/homeassistant/components/nam/config_flow.py @@ -74,6 +74,9 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle zeroconf discovery.""" self.host = discovery_info[CONF_HOST] + # Do not probe the device if the host is already configured + self._async_abort_entries_match({CONF_HOST: self.host}) + try: mac = await self._async_get_mac(cast(str, self.host)) except (ApiError, ClientConnectorError, asyncio.TimeoutError): diff --git a/tests/components/nam/test_config_flow.py b/tests/components/nam/test_config_flow.py index 99a252ada0a..b0ecb20da11 100644 --- a/tests/components/nam/test_config_flow.py +++ b/tests/components/nam/test_config_flow.py @@ -150,6 +150,23 @@ async def test_zeroconf(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_zeroconf_host_already_configured(hass): + """Test that errors are shown when host is already configured.""" + entry = MockConfigEntry( + domain=DOMAIN, unique_id="aa:bb:cc:dd:ee:ff", data=VALID_CONFIG + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DISCOVERY_INFO, + context={"source": SOURCE_ZEROCONF}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + @pytest.mark.parametrize( "error", [ From 85c6942f55e20860363447ebd6afeb86669bbbf2 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 17 Oct 2021 12:12:35 +0200 Subject: [PATCH 0456/1038] Bump `brother` library to version 1.1.0 (#57892) --- .../components/brother/config_flow.py | 7 +++++-- homeassistant/components/brother/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/brother/test_config_flow.py | 18 ++++++++++++++++++ 5 files changed, 26 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/brother/config_flow.py b/homeassistant/components/brother/config_flow.py index 73196302207..20e5938884f 100644 --- a/homeassistant/components/brother/config_flow.py +++ b/homeassistant/components/brother/config_flow.py @@ -90,11 +90,14 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._async_abort_entries_match({CONF_HOST: self.host}) snmp_engine = get_snmp_engine(self.hass) + model = discovery_info.get("properties", {}).get("product") - self.brother = Brother(self.host, snmp_engine=snmp_engine) try: + self.brother = Brother(self.host, snmp_engine=snmp_engine, model=model) await self.brother.async_update() - except (ConnectionError, SnmpError, UnsupportedModel): + except UnsupportedModel: + return self.async_abort(reason="unsupported_model") + except (ConnectionError, SnmpError): return self.async_abort(reason="cannot_connect") # Check if already configured diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index 0365918a78b..77a84c70de8 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -3,7 +3,7 @@ "name": "Brother Printer", "documentation": "https://www.home-assistant.io/integrations/brother", "codeowners": ["@bieniu"], - "requirements": ["brother==1.0.2"], + "requirements": ["brother==1.1.0"], "zeroconf": [ { "type": "_printer._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 5d78dc85a89..91c941b8e62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -431,7 +431,7 @@ bravia-tv==1.0.11 broadlink==0.17.0 # homeassistant.components.brother -brother==1.0.2 +brother==1.1.0 # homeassistant.components.brottsplatskartan brottsplatskartan==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fe436c9d569..9c4ebcca511 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -269,7 +269,7 @@ bravia-tv==1.0.11 broadlink==0.17.0 # homeassistant.components.brother -brother==1.0.2 +brother==1.1.0 # homeassistant.components.bsblan bsblan==0.4.0 diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py index 3a828c97488..b7e4b31cfab 100644 --- a/tests/components/brother/test_config_flow.py +++ b/tests/components/brother/test_config_flow.py @@ -150,6 +150,24 @@ async def test_zeroconf_snmp_error(hass): assert result["reason"] == "cannot_connect" +async def test_zeroconf_unsupported_model(hass): + """Test unsupported printer model error.""" + with patch("brother.Brother._get_data") as mock_get_data: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data={ + "hostname": "example.local.", + "name": "Brother Printer", + "properties": {"product": "MFC-8660DN"}, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "unsupported_model" + assert len(mock_get_data.mock_calls) == 0 + + async def test_zeroconf_device_exists_abort(hass): """Test we abort zeroconf flow if Brother printer already configured.""" with patch( From f5e2960a92a4fd916bed9cb2784e24a1c63ebdd2 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 17 Oct 2021 08:53:18 -0400 Subject: [PATCH 0457/1038] Fix mode_callerid attributes (#57774) * Allow mode_callerid to display number only * alphabetize * tweak * only include attr if there is data --- .../components/modem_callerid/sensor.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/modem_callerid/sensor.py b/homeassistant/components/modem_callerid/sensor.py index 6c08ea8d6cf..469e8ecb994 100644 --- a/homeassistant/components/modem_callerid/sensor.py +++ b/homeassistant/components/modem_callerid/sensor.py @@ -106,18 +106,13 @@ class ModemCalleridSensor(SensorEntity): @callback def _async_incoming_call(self, new_state) -> None: """Handle new states.""" - if new_state == PhoneModem.STATE_RING: - if self.native_value == PhoneModem.STATE_IDLE: - self._attr_extra_state_attributes = { - CID.CID_NUMBER: "", - CID.CID_NAME: "", - } - elif new_state == PhoneModem.STATE_CALLERID: - self._attr_extra_state_attributes = { - CID.CID_NUMBER: self.api.cid_number, - CID.CID_NAME: self.api.cid_name, - } - self._attr_extra_state_attributes[CID.CID_TIME] = self.api.cid_time + self._attr_extra_state_attributes = {} + if self.api.cid_name: + self._attr_extra_state_attributes[CID.CID_NAME] = self.api.cid_name + if self.api.cid_number: + self._attr_extra_state_attributes[CID.CID_NUMBER] = self.api.cid_number + if self.api.cid_time: + self._attr_extra_state_attributes[CID.CID_TIME] = self.api.cid_time self._attr_native_value = self.api.state self.async_write_ha_state() From 58f13e4e3482f9ddc5614aeaf253fbcc7ac487d6 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 17 Oct 2021 15:53:49 +0200 Subject: [PATCH 0458/1038] push motionblinds to 0.5.7 (#57902) --- 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 6ad29e4257e..346729925e9 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.5.6"], + "requirements": ["motionblinds==0.5.7"], "codeowners": ["@starkillerOG"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 91c941b8e62..2b868e0cc34 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1011,7 +1011,7 @@ minio==4.0.9 mitemp_bt==0.0.3 # homeassistant.components.motion_blinds -motionblinds==0.5.6 +motionblinds==0.5.7 # homeassistant.components.motioneye motioneye-client==0.3.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9c4ebcca511..88738728123 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -600,7 +600,7 @@ millheater==0.6.2 minio==4.0.9 # homeassistant.components.motion_blinds -motionblinds==0.5.6 +motionblinds==0.5.7 # homeassistant.components.motioneye motioneye-client==0.3.11 From 2edad82078335760261b6d8bcc4e43e2b0c08ade Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 Oct 2021 05:44:48 -1000 Subject: [PATCH 0459/1038] Fix order of arguments in rainmachine sensors (#57895) --- homeassistant/components/rainmachine/binary_sensor.py | 5 ++++- homeassistant/components/rainmachine/sensor.py | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 78f3863dd16..bfd36ecf550 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -115,23 +115,26 @@ async def async_setup_entry( if api_category == DATA_PROVISION_SETTINGS: return partial( ProvisionSettingsBinarySensor, + entry, coordinators[DATA_PROVISION_SETTINGS], ) if api_category == DATA_RESTRICTIONS_CURRENT: return partial( CurrentRestrictionsBinarySensor, + entry, coordinators[DATA_RESTRICTIONS_CURRENT], ) return partial( UniversalRestrictionsBinarySensor, + entry, coordinators[DATA_RESTRICTIONS_UNIVERSAL], ) async_add_entities( [ - async_get_sensor(description.api_category)(entry, controller, description) + async_get_sensor(description.api_category)(controller, description) for description in BINARY_SENSOR_DESCRIPTIONS ] ) diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 4eec124f936..beb893ddb74 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -104,17 +104,19 @@ async def async_setup_entry( if api_category == DATA_PROVISION_SETTINGS: return partial( ProvisionSettingsSensor, + entry, coordinators[DATA_PROVISION_SETTINGS], ) return partial( UniversalRestrictionsSensor, + entry, coordinators[DATA_RESTRICTIONS_UNIVERSAL], ) async_add_entities( [ - async_get_sensor(description.api_category)(entry, controller, description) + async_get_sensor(description.api_category)(controller, description) for description in SENSOR_DESCRIPTIONS ] ) From 93ba96680852d0362f85adfca9919380710b274a Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 17 Oct 2021 17:45:32 +0200 Subject: [PATCH 0460/1038] Add the correct device class to deCONZ Tamper entity (#57834) --- homeassistant/components/deconz/binary_sensor.py | 4 ++-- tests/components/deconz/test_binary_sensor.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index d79e8708d4b..05c8a134074 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -15,8 +15,8 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, DEVICE_CLASS_OPENING, - DEVICE_CLASS_PROBLEM, DEVICE_CLASS_SMOKE, + DEVICE_CLASS_TAMPER, DEVICE_CLASS_VIBRATION, DOMAIN, BinarySensorEntity, @@ -169,7 +169,7 @@ class DeconzTampering(DeconzDevice, BinarySensorEntity): TYPE = DOMAIN - _attr_device_class = DEVICE_CLASS_PROBLEM + _attr_device_class = DEVICE_CLASS_TAMPER def __init__(self, device, gateway): """Initialize deCONZ binary sensor.""" diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 7f986ce4b81..96437800773 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -4,7 +4,7 @@ from unittest.mock import patch from homeassistant.components.binary_sensor import ( DEVICE_CLASS_MOTION, - DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_TAMPER, DEVICE_CLASS_VIBRATION, ) from homeassistant.components.deconz.const import ( @@ -136,7 +136,7 @@ async def test_tampering_sensor(hass, aioclient_mock, mock_deconz_websocket): assert len(hass.states.async_all()) == 3 presence_tamper = hass.states.get("binary_sensor.presence_sensor_tampered") assert presence_tamper.state == STATE_OFF - assert presence_tamper.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_PROBLEM + assert presence_tamper.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TAMPER event_changed_sensor = { "t": "event", From 0fc2946f8830f94167ecb891227124f2f01e841f Mon Sep 17 00:00:00 2001 From: Johannes la Poutre Date: Sun, 17 Oct 2021 18:49:18 +0200 Subject: [PATCH 0461/1038] Fix device class for energy plugwise sensors (#57803) Co-authored-by: Franck Nijhof --- homeassistant/components/plugwise/sensor.py | 23 +++++++++++---------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index 854c2e6676c..888f5bac5c2 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -5,6 +5,7 @@ import logging from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( DEVICE_CLASS_BATTERY, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, DEVICE_CLASS_PRESSURE, @@ -68,32 +69,32 @@ ENERGY_SENSOR_MAP = { "electricity_consumed_interval": [ "Consumed Power Interval", ENERGY_WATT_HOUR, - DEVICE_CLASS_POWER, + DEVICE_CLASS_ENERGY, ], "electricity_consumed_peak_interval": [ "Consumed Power Interval", ENERGY_WATT_HOUR, - DEVICE_CLASS_POWER, + DEVICE_CLASS_ENERGY, ], "electricity_consumed_off_peak_interval": [ "Consumed Power Interval (off peak)", ENERGY_WATT_HOUR, - DEVICE_CLASS_POWER, + DEVICE_CLASS_ENERGY, ], "electricity_produced_interval": [ "Produced Power Interval", ENERGY_WATT_HOUR, - DEVICE_CLASS_POWER, + DEVICE_CLASS_ENERGY, ], "electricity_produced_peak_interval": [ "Produced Power Interval", ENERGY_WATT_HOUR, - DEVICE_CLASS_POWER, + DEVICE_CLASS_ENERGY, ], "electricity_produced_off_peak_interval": [ "Produced Power Interval (off peak)", ENERGY_WATT_HOUR, - DEVICE_CLASS_POWER, + DEVICE_CLASS_ENERGY, ], "electricity_consumed_off_peak_point": [ "Current Consumed Power (off peak)", @@ -108,12 +109,12 @@ ENERGY_SENSOR_MAP = { "electricity_consumed_off_peak_cumulative": [ "Cumulative Consumed Power (off peak)", ENERGY_KILO_WATT_HOUR, - DEVICE_CLASS_POWER, + DEVICE_CLASS_ENERGY, ], "electricity_consumed_peak_cumulative": [ "Cumulative Consumed Power", ENERGY_KILO_WATT_HOUR, - DEVICE_CLASS_POWER, + DEVICE_CLASS_ENERGY, ], "electricity_produced_off_peak_point": [ "Current Produced Power (off peak)", @@ -128,12 +129,12 @@ ENERGY_SENSOR_MAP = { "electricity_produced_off_peak_cumulative": [ "Cumulative Produced Power (off peak)", ENERGY_KILO_WATT_HOUR, - DEVICE_CLASS_POWER, + DEVICE_CLASS_ENERGY, ], "electricity_produced_peak_cumulative": [ "Cumulative Produced Power", ENERGY_KILO_WATT_HOUR, - DEVICE_CLASS_POWER, + DEVICE_CLASS_ENERGY, ], "gas_consumed_interval": [ "Current Consumed Gas Interval", @@ -145,7 +146,7 @@ ENERGY_SENSOR_MAP = { "net_electricity_cumulative": [ "Cumulative net Power", ENERGY_KILO_WATT_HOUR, - DEVICE_CLASS_POWER, + DEVICE_CLASS_ENERGY, ], } From 4aadb848e18e8e69f01d31a163f76de2e87025d7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 17 Oct 2021 19:24:49 +0200 Subject: [PATCH 0462/1038] Add unit/device_class validation and normalization to Tuya (#57913) Co-authored-by: Martin Hjelmare --- homeassistant/components/tuya/const.py | 267 +++++++++++++++++++++++- homeassistant/components/tuya/sensor.py | 45 +++- 2 files changed, 309 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 7dc1e529a47..3a9dd15ee55 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -1,9 +1,68 @@ """Constants for the Tuya integration.""" -from dataclasses import dataclass +from __future__ import annotations + +from dataclasses import dataclass, field from enum import Enum +from typing import Callable from tuya_iot import TuyaCloudOpenAPIEndpoint +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_BILLION, + CONCENTRATION_PARTS_PER_MILLION, + DEVICE_CLASS_AQI, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_CO, + DEVICE_CLASS_CO2, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_DATE, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_GAS, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_MONETARY, + DEVICE_CLASS_NITROGEN_DIOXIDE, + DEVICE_CLASS_NITROGEN_MONOXIDE, + DEVICE_CLASS_NITROUS_OXIDE, + DEVICE_CLASS_OZONE, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, + DEVICE_CLASS_POWER, + DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_SULPHUR_DIOXIDE, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + DEVICE_CLASS_VOLTAGE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_CURRENT_MILLIAMPERE, + ELECTRIC_POTENTIAL_MILLIVOLT, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, + ENERGY_WATT_HOUR, + LIGHT_LUX, + PERCENTAGE, + POWER_KILO_WATT, + POWER_WATT, + PRESSURE_BAR, + PRESSURE_HPA, + PRESSURE_INHG, + PRESSURE_MBAR, + PRESSURE_PA, + PRESSURE_PSI, + SIGNAL_STRENGTH_DECIBELS, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + VOLUME_CUBIC_FEET, + VOLUME_CUBIC_METERS, +) + DOMAIN = "tuya" CONF_AUTH_TYPE = "auth_type" @@ -155,6 +214,212 @@ class DPCode(str, Enum): WORK_MODE = "work_mode" # Working mode +@dataclass +class UnitOfMeasurement: + """Describes a unit of measurement.""" + + unit: str + device_classes: set[str] + + aliases: set[str] = field(default_factory=set) + conversion_unit: str | None = None + conversion_fn: Callable[[float], float] | None = None + + +# A tuple of available units of measurements we can work with. +# Tuya's devices aren't consistent in UOM use, thus this provides +# a list of aliases for units and possible conversions we can do +# to make them compatible with our model. +UNITS = ( + UnitOfMeasurement( + unit="", + aliases={" "}, + device_classes={ + DEVICE_CLASS_AQI, + DEVICE_CLASS_DATE, + DEVICE_CLASS_MONETARY, + DEVICE_CLASS_TIMESTAMP, + }, + ), + UnitOfMeasurement( + unit=PERCENTAGE, + aliases={"pct", "percent"}, + device_classes={ + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_POWER_FACTOR, + }, + ), + UnitOfMeasurement( + unit=CONCENTRATION_PARTS_PER_MILLION, + device_classes={ + DEVICE_CLASS_CO, + DEVICE_CLASS_CO2, + }, + ), + UnitOfMeasurement( + unit=CONCENTRATION_PARTS_PER_BILLION, + device_classes={ + DEVICE_CLASS_CO, + DEVICE_CLASS_CO2, + }, + conversion_unit=CONCENTRATION_PARTS_PER_MILLION, + conversion_fn=lambda x: x / 1000, + ), + UnitOfMeasurement( + unit=ELECTRIC_CURRENT_AMPERE, + aliases={"a", "ampere"}, + device_classes={DEVICE_CLASS_CURRENT}, + ), + UnitOfMeasurement( + unit=ELECTRIC_CURRENT_MILLIAMPERE, + aliases={"ma", "milliampere"}, + device_classes={DEVICE_CLASS_CURRENT}, + conversion_unit=ELECTRIC_CURRENT_AMPERE, + conversion_fn=lambda x: x / 1000, + ), + UnitOfMeasurement( + unit=ENERGY_WATT_HOUR, + aliases={"wh", "watthour"}, + device_classes={DEVICE_CLASS_ENERGY}, + ), + UnitOfMeasurement( + unit=ENERGY_KILO_WATT_HOUR, + aliases={"kwh", "kilowatt-hour"}, + device_classes={DEVICE_CLASS_ENERGY}, + ), + UnitOfMeasurement( + unit=VOLUME_CUBIC_FEET, + aliases={"ft3"}, + device_classes={DEVICE_CLASS_GAS}, + ), + UnitOfMeasurement( + unit=VOLUME_CUBIC_METERS, + aliases={"m3"}, + device_classes={DEVICE_CLASS_GAS}, + ), + UnitOfMeasurement( + unit=LIGHT_LUX, + aliases={"lux"}, + device_classes={DEVICE_CLASS_ILLUMINANCE}, + ), + UnitOfMeasurement( + unit="lm", + aliases={"lum", "lumen"}, + device_classes={DEVICE_CLASS_ILLUMINANCE}, + ), + UnitOfMeasurement( + unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + aliases={"ug/m3", "µg/m3", "ug/m³"}, + device_classes={ + DEVICE_CLASS_NITROGEN_DIOXIDE, + DEVICE_CLASS_NITROGEN_MONOXIDE, + DEVICE_CLASS_NITROUS_OXIDE, + DEVICE_CLASS_OZONE, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM25, + DEVICE_CLASS_PM10, + DEVICE_CLASS_SULPHUR_DIOXIDE, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + }, + ), + UnitOfMeasurement( + unit=CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, + aliases={"mg/m3"}, + device_classes={ + DEVICE_CLASS_NITROGEN_DIOXIDE, + DEVICE_CLASS_NITROGEN_MONOXIDE, + DEVICE_CLASS_NITROUS_OXIDE, + DEVICE_CLASS_OZONE, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM25, + DEVICE_CLASS_PM10, + DEVICE_CLASS_SULPHUR_DIOXIDE, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + }, + conversion_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + conversion_fn=lambda x: x * 1000, + ), + UnitOfMeasurement( + unit=POWER_WATT, + aliases={"watt"}, + device_classes={DEVICE_CLASS_POWER}, + ), + UnitOfMeasurement( + unit=POWER_KILO_WATT, + aliases={"kilowatt"}, + device_classes={DEVICE_CLASS_POWER}, + ), + UnitOfMeasurement( + unit=PRESSURE_BAR, + device_classes={DEVICE_CLASS_PRESSURE}, + ), + UnitOfMeasurement( + unit=PRESSURE_MBAR, + aliases={"millibar"}, + device_classes={DEVICE_CLASS_PRESSURE}, + ), + UnitOfMeasurement( + unit=PRESSURE_HPA, + aliases={"hpa", "hectopascal"}, + device_classes={DEVICE_CLASS_PRESSURE}, + ), + UnitOfMeasurement( + unit=PRESSURE_INHG, + aliases={"inhg"}, + device_classes={DEVICE_CLASS_PRESSURE}, + ), + UnitOfMeasurement( + unit=PRESSURE_PSI, + device_classes={DEVICE_CLASS_PRESSURE}, + ), + UnitOfMeasurement( + unit=PRESSURE_PA, + device_classes={DEVICE_CLASS_PRESSURE}, + ), + UnitOfMeasurement( + unit=SIGNAL_STRENGTH_DECIBELS, + aliases={"db"}, + device_classes={DEVICE_CLASS_SIGNAL_STRENGTH}, + ), + UnitOfMeasurement( + unit=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + aliases={"dbm"}, + device_classes={DEVICE_CLASS_SIGNAL_STRENGTH}, + ), + UnitOfMeasurement( + unit=TEMP_CELSIUS, + aliases={"°c", "c", "celsius"}, + device_classes={DEVICE_CLASS_TEMPERATURE}, + ), + UnitOfMeasurement( + unit=TEMP_FAHRENHEIT, + aliases={"°f", "f", "fahrenheit"}, + device_classes={DEVICE_CLASS_TEMPERATURE}, + ), + UnitOfMeasurement( + unit=ELECTRIC_POTENTIAL_VOLT, + aliases={"volt"}, + device_classes={DEVICE_CLASS_VOLTAGE}, + ), + UnitOfMeasurement( + unit=ELECTRIC_POTENTIAL_MILLIVOLT, + aliases={"mv", "millivolt"}, + device_classes={DEVICE_CLASS_VOLTAGE}, + conversion_unit=ELECTRIC_POTENTIAL_VOLT, + conversion_fn=lambda x: x / 1000, + ), +) + + +DEVICE_CLASS_UNITS: dict[str, dict[str, UnitOfMeasurement]] = {} +for uom in UNITS: + for device_class in uom.device_classes: + DEVICE_CLASS_UNITS.setdefault(device_class, {})[uom.unit] = uom + for unit_alias in uom.aliases: + DEVICE_CLASS_UNITS[device_class][unit_alias] = uom + + @dataclass class Country: """Describe a supported country.""" diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index b21d54bb999..3d3647b0cc4 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -31,7 +31,13 @@ from homeassistant.helpers.typing import StateType from . import HomeAssistantTuyaData from .base import EnumTypeData, IntegerTypeData, TuyaEntity -from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode +from .const import ( + DEVICE_CLASS_UNITS, + DOMAIN, + TUYA_DISCOVERY_NEW, + DPCode, + UnitOfMeasurement, +) # All descriptions can be found here. Mostly the Integer data types in the # default status set of each category (that don't have a set instruction) @@ -209,6 +215,7 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity): _status_range: TuyaDeviceStatusRange | None = None _type_data: IntegerTypeData | EnumTypeData | None = None + _uom: UnitOfMeasurement | None = None def __init__( self, @@ -235,6 +242,37 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity): elif self._status_range.type == "Enum": self._type_data = EnumTypeData.from_json(self._status_range.values) + # Logic to ensure the set device class and API received Unit Of Measurement + # match Home Assistants requirements. + if ( + self.device_class is not None + and description.native_unit_of_measurement is None + ): + # We cannot have a device class, if the UOM isn't set or the + # device class cannot be found in the validation mapping. + if ( + self.unit_of_measurement is None + or self.device_class not in DEVICE_CLASS_UNITS + ): + self._attr_device_class = None + return + + uoms = DEVICE_CLASS_UNITS[self.device_class] + self._uom = uoms.get(self.unit_of_measurement) or uoms.get( + self.unit_of_measurement.lower() + ) + + # Unknown unit of measurement, device class should not be used. + if self._uom is None: + self._attr_device_class = None + return + + # Found unit of measurement, use the standardized Unit + # Use the target conversion unit (if set) + self._attr_native_unit_of_measurement = ( + self._uom.conversion_unit or self._uom.unit + ) + @property def native_value(self) -> StateType: """Return the value reported by the sensor.""" @@ -253,7 +291,10 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity): # Scale integer/float value if isinstance(self._type_data, IntegerTypeData): - return self._type_data.scale_value(value) + scaled_value = self._type_data.scale_value(value) + if self._uom and self._uom.conversion_fn is not None: + return self._uom.conversion_fn(scaled_value) + return scaled_value # Unexpected enum value if ( From dd2d708cb9b31d14acf43b5678f79d96579939fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sun, 17 Oct 2021 19:25:49 +0200 Subject: [PATCH 0463/1038] Add category diagnostic to Tibber signal sensor (#57840) --- homeassistant/components/tibber/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index c7184d38792..120432e0608 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -25,6 +25,7 @@ from homeassistant.const import ( ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, + ENTITY_CATEGORY_DIAGNOSTIC, EVENT_HOMEASSISTANT_STOP, PERCENTAGE, POWER_WATT, @@ -172,6 +173,7 @@ RT_SENSORS: tuple[SensorEntityDescription, ...] = ( device_class=DEVICE_CLASS_SIGNAL_STRENGTH, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key="accumulatedReward", From 9c3aa8156d94691555c30684fa4fc2b88e0b683a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sun, 17 Oct 2021 19:26:29 +0200 Subject: [PATCH 0464/1038] Add category diagnostic to Surepetcare battery sensor (#57852) --- homeassistant/components/surepetcare/sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/surepetcare/sensor.py b/homeassistant/components/surepetcare/sensor.py index c0441d0a7fd..cc4fe01fffa 100644 --- a/homeassistant/components/surepetcare/sensor.py +++ b/homeassistant/components/surepetcare/sensor.py @@ -12,6 +12,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_VOLTAGE, DEVICE_CLASS_BATTERY, + ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, VOLUME_MILLILITERS, ) @@ -49,9 +50,10 @@ async def async_setup_entry( class SureBattery(SurePetcareEntity, SensorEntity): - """A sensor implementation for Sure Petcare Entities.""" + """A sensor implementation for Sure Petcare batteries.""" _attr_device_class = DEVICE_CLASS_BATTERY + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC _attr_native_unit_of_measurement = PERCENTAGE def __init__( From 64145d6ccf425e9c8c405a8332a1f5fb331d8894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sun, 17 Oct 2021 19:27:03 +0200 Subject: [PATCH 0465/1038] Add category diagnostic to Switchbot battery and signal sensor (#57854) --- homeassistant/components/switchbot/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index 78b078c26b4..0a0c4b265ec 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -9,6 +9,7 @@ from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_SIGNAL_STRENGTH, + ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ) @@ -27,11 +28,13 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=DEVICE_CLASS_SIGNAL_STRENGTH, entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "battery": SensorEntityDescription( key="battery", native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "lightLevel": SensorEntityDescription( key="lightLevel", From 9b693f7f2b652a376121e2d8289d017d138040d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sun, 17 Oct 2021 19:30:13 +0200 Subject: [PATCH 0466/1038] Airthings entity category diagnostic (#57850) --- homeassistant/components/airthings/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/airthings/sensor.py b/homeassistant/components/airthings/sensor.py index 4aab2307d9a..cc54430c417 100644 --- a/homeassistant/components/airthings/sensor.py +++ b/homeassistant/components/airthings/sensor.py @@ -22,6 +22,7 @@ from homeassistant.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, PRESSURE_MBAR, SIGNAL_STRENGTH_DECIBELS, @@ -64,6 +65,7 @@ SENSORS: dict[str, SensorEntityDescription] = { key="battery", device_class=DEVICE_CLASS_BATTERY, native_unit_of_measurement=PERCENTAGE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, name="Battery", ), "co2": SensorEntityDescription( @@ -96,6 +98,7 @@ SENSORS: dict[str, SensorEntityDescription] = { device_class=DEVICE_CLASS_SIGNAL_STRENGTH, name="RSSI", entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "pm1": SensorEntityDescription( key="pm1", From bcd431e848d5e5c547dad1c5013bf258819e8521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sun, 17 Oct 2021 19:40:47 +0200 Subject: [PATCH 0467/1038] Add device info to Adax (#57907) --- homeassistant/components/adax/climate.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/adax/climate.py b/homeassistant/components/adax/climate.py index 674329aaf6b..6de31e289b6 100644 --- a/homeassistant/components/adax/climate.py +++ b/homeassistant/components/adax/climate.py @@ -20,9 +20,10 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ACCOUNT_ID +from .const import ACCOUNT_ID, DOMAIN async def async_setup_entry( @@ -59,6 +60,11 @@ class AdaxDevice(ClimateEntity): self._adax_data_handler = adax_data_handler self._attr_unique_id = f"{heater_data['homeId']}_{heater_data['id']}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, heater_data["id"])}, + name=self.name, + manufacturer="Adax", + ) @property def name(self) -> str: From fe0291012c6f624a90e984d3f9b6019aab3d17f3 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Sun, 17 Oct 2021 19:43:15 +0200 Subject: [PATCH 0468/1038] Use attr_device_info and add init tests for nut (#57725) --- homeassistant/components/nut/__init__.py | 37 +++++++++--- homeassistant/components/nut/const.py | 3 - homeassistant/components/nut/sensor.py | 51 +++++------------ tests/components/nut/test_init.py | 72 ++++++++++++++++++++++++ 4 files changed, 116 insertions(+), 47 deletions(-) create mode 100644 tests/components/nut/test_init.py diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index 6c8b5c69e80..81dd5f91f59 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -7,6 +7,9 @@ from pynut2.nut2 import PyNUTClient, PyNUTError from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_SW_VERSION, CONF_ALIAS, CONF_HOST, CONF_PASSWORD, @@ -24,9 +27,6 @@ from .const import ( DOMAIN, PLATFORMS, PYNUT_DATA, - PYNUT_FIRMWARE, - PYNUT_MANUFACTURER, - PYNUT_MODEL, PYNUT_UNIQUE_ID, UNDO_UPDATE_LISTENER, ) @@ -78,7 +78,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: undo_listener = entry.add_update_listener(_async_update_listener) unique_id = _unique_id_from_status(status) - if unique_id is None: unique_id = entry.entry_id @@ -87,9 +86,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: COORDINATOR: coordinator, PYNUT_DATA: data, PYNUT_UNIQUE_ID: unique_id, - PYNUT_MANUFACTURER: _manufacturer_from_status(status), - PYNUT_MODEL: _model_from_status(status), - PYNUT_FIRMWARE: _firmware_from_status(status), UNDO_UPDATE_LISTENER: undo_listener, } @@ -185,6 +181,7 @@ class PyNUTData: self._client = PyNUTClient(self._host, port, username, password, 5, False) self.ups_list = None self._status = None + self._device_info = None @property def status(self): @@ -196,6 +193,11 @@ class PyNUTData: """Return the name of the ups.""" return self._alias + @property + def device_info(self): + """Return the device info for the ups.""" + return self._device_info or {} + def _get_alias(self): """Get the ups alias from NUT.""" try: @@ -211,6 +213,23 @@ class PyNUTData: self.ups_list = ups_list return list(ups_list)[0] + def _get_device_info(self): + """Get the ups device info from NUT.""" + if not self._status: + return None + + manufacturer = _manufacturer_from_status(self._status) + model = _model_from_status(self._status) + firmware = _firmware_from_status(self._status) + device_info = {} + if model: + device_info[ATTR_MODEL] = model + if manufacturer: + device_info[ATTR_MANUFACTURER] = manufacturer + if firmware: + device_info[ATTR_SW_VERSION] = firmware + return device_info + def _get_status(self): """Get the ups status from NUT.""" if self._alias is None: @@ -222,6 +241,8 @@ class PyNUTData: _LOGGER.debug("Error getting NUT vars for host %s: %s", self._host, err) return None - def update(self, **kwargs): + def update(self): """Fetch the latest status from NUT.""" self._status = self._get_status() + if self._device_info is None: + self._device_info = self._get_device_info() diff --git a/homeassistant/components/nut/const.py b/homeassistant/components/nut/const.py index 0261c0209be..74d9614c29b 100644 --- a/homeassistant/components/nut/const.py +++ b/homeassistant/components/nut/const.py @@ -44,9 +44,6 @@ DEFAULT_SCAN_INTERVAL = 60 PYNUT_DATA = "data" PYNUT_UNIQUE_ID = "unique_id" -PYNUT_MANUFACTURER = "manufacturer" -PYNUT_MODEL = "model" -PYNUT_FIRMWARE = "firmware" SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { "ups.status.display": SensorEntityDescription( diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index 48253674be8..c533a095270 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -5,7 +5,12 @@ import logging from homeassistant.components.nut import PyNUTData from homeassistant.components.sensor import SensorEntity, SensorEntityDescription -from homeassistant.const import CONF_RESOURCES, STATE_UNKNOWN +from homeassistant.const import ( + ATTR_IDENTIFIERS, + ATTR_NAME, + CONF_RESOURCES, + STATE_UNKNOWN, +) from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -17,9 +22,6 @@ from .const import ( KEY_STATUS, KEY_STATUS_DISPLAY, PYNUT_DATA, - PYNUT_FIRMWARE, - PYNUT_MANUFACTURER, - PYNUT_MODEL, PYNUT_UNIQUE_ID, SENSOR_TYPES, STATE_TYPES, @@ -32,12 +34,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the NUT sensors.""" pynut_data = hass.data[DOMAIN][config_entry.entry_id] - unique_id = pynut_data[PYNUT_UNIQUE_ID] - manufacturer = pynut_data[PYNUT_MANUFACTURER] - model = pynut_data[PYNUT_MODEL] - firmware = pynut_data[PYNUT_FIRMWARE] coordinator = pynut_data[COORDINATOR] data = pynut_data[PYNUT_DATA] + unique_id = pynut_data[PYNUT_UNIQUE_ID] status = coordinator.data enabled_resources = [ @@ -52,12 +51,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [ NUTSensor( coordinator, - data, SENSOR_TYPES[sensor_type], + data, unique_id, - manufacturer, - model, - firmware, sensor_type in enabled_resources, ) for sensor_type in resources @@ -72,41 +68,24 @@ class NUTSensor(CoordinatorEntity, SensorEntity): def __init__( self, coordinator: DataUpdateCoordinator, - data: PyNUTData, sensor_description: SensorEntityDescription, + data: PyNUTData, unique_id: str, - manufacturer: str | None, - model: str | None, - firmware: str | None, enabled_default: bool, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) self.entity_description = sensor_description - self._manufacturer = manufacturer - self._firmware = firmware - self._model = model - self._device_name = data.name.title() - self._unique_id = unique_id + device_name = data.name.title() self._attr_entity_registry_enabled_default = enabled_default - self._attr_name = f"{self._device_name} {sensor_description.name}" + self._attr_name = f"{device_name} {sensor_description.name}" self._attr_unique_id = f"{unique_id}_{sensor_description.key}" - - @property - def device_info(self): - """Device info for the ups.""" - device_info = { - "identifiers": {(DOMAIN, self._unique_id)}, - "name": self._device_name, + self._attr_device_info = { + ATTR_IDENTIFIERS: {(DOMAIN, unique_id)}, + ATTR_NAME: device_name, } - if self._model: - device_info["model"] = self._model - if self._manufacturer: - device_info["manufacturer"] = self._manufacturer - if self._firmware: - device_info["sw_version"] = self._firmware - return device_info + self._attr_device_info.update(data.device_info) @property def native_value(self): diff --git a/tests/components/nut/test_init.py b/tests/components/nut/test_init.py new file mode 100644 index 00000000000..b9b5441a86c --- /dev/null +++ b/tests/components/nut/test_init.py @@ -0,0 +1,72 @@ +"""Test init of Nut integration.""" +from unittest.mock import patch + +from homeassistant.components.nut.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_RESOURCES, STATE_UNAVAILABLE + +from .util import _get_mock_pynutclient + +from tests.common import MockConfigEntry + + +async def test_async_setup_entry(hass): + """Test a successful setup entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "mock", + CONF_PORT: "mock", + CONF_RESOURCES: ["ups.status"], + }, + ) + entry.add_to_hass(hass) + + mock_pynut = _get_mock_pynutclient( + list_ups={"ups1": "UPS 1"}, list_vars={"ups.status": "OL"} + ) + + with patch( + "homeassistant.components.nut.PyNUTClient", + return_value=mock_pynut, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state is ConfigEntryState.LOADED + + state = hass.states.get("sensor.ups1_status_data") + assert state is not None + assert state.state != STATE_UNAVAILABLE + assert state.state == "OL" + + 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.get(DOMAIN) + + +async def test_config_not_ready(hass): + """Test for setup failure if connection to broker is missing.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "mock", + CONF_PORT: "mock", + CONF_RESOURCES: ["ups.status"], + }, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.nut.PyNUTClient.list_ups", + return_value=["ups1"], + ), patch( + "homeassistant.components.nut.PyNUTClient.list_vars", + side_effect=ConnectionResetError, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state is ConfigEntryState.SETUP_RETRY From 95b07c138c7bd3d7844cdee9d9cd7a650888b63c Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 17 Oct 2021 10:45:31 -0700 Subject: [PATCH 0469/1038] Set `nest` camera always on STATE_STREAMING (#57882) --- homeassistant/components/nest/camera_sdm.py | 1 + tests/components/nest/camera_sdm_test.py | 18 +++++++++--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index 9c6ac7070e4..b610d328b42 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -76,6 +76,7 @@ class NestCamera(Camera): self._event_id: str | None = None self._event_image_bytes: bytes | None = None self._event_image_cleanup_unsub: Callable[[], None] | None = None + self.is_streaming = CameraLiveStreamTrait.NAME in self._device.traits @property def should_poll(self) -> bool: diff --git a/tests/components/nest/camera_sdm_test.py b/tests/components/nest/camera_sdm_test.py index df36ae762df..a6c7ad64605 100644 --- a/tests/components/nest/camera_sdm_test.py +++ b/tests/components/nest/camera_sdm_test.py @@ -14,7 +14,7 @@ from google_nest_sdm.event import EventMessage import pytest from homeassistant.components import camera -from homeassistant.components.camera import STATE_IDLE +from homeassistant.components.camera import STATE_IDLE, STATE_STREAMING from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -168,7 +168,7 @@ async def test_camera_device(hass): assert len(hass.states.async_all()) == 1 camera = hass.states.get("camera.my_camera") assert camera is not None - assert camera.state == STATE_IDLE + assert camera.state == STATE_STREAMING registry = er.async_get(hass) entry = registry.async_get("camera.my_camera") @@ -191,7 +191,7 @@ async def test_camera_stream(hass, auth): assert len(hass.states.async_all()) == 1 cam = hass.states.get("camera.my_camera") assert cam is not None - assert cam.state == STATE_IDLE + assert cam.state == STATE_STREAMING stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") assert stream_source == "rtsp://some/url?auth=g.0.streamingToken" @@ -253,7 +253,7 @@ async def test_refresh_expired_stream_token(hass, auth): assert len(hass.states.async_all()) == 1 cam = hass.states.get("camera.my_camera") assert cam is not None - assert cam.state == STATE_IDLE + assert cam.state == STATE_STREAMING # Request a stream for the camera entity to exercise nest cam + camera interaction # and shutdown on url expiration @@ -318,7 +318,7 @@ async def test_stream_response_already_expired(hass, auth): assert len(hass.states.async_all()) == 1 cam = hass.states.get("camera.my_camera") assert cam is not None - assert cam.state == STATE_IDLE + assert cam.state == STATE_STREAMING # The stream is expired, but we return it anyway stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") @@ -342,7 +342,7 @@ async def test_camera_removed(hass, auth): assert len(hass.states.async_all()) == 1 cam = hass.states.get("camera.my_camera") assert cam is not None - assert cam.state == STATE_IDLE + assert cam.state == STATE_STREAMING # Start a stream, exercising cleanup on remove auth.responses = [ @@ -386,7 +386,7 @@ async def test_refresh_expired_stream_failure(hass, auth): assert len(hass.states.async_all()) == 1 cam = hass.states.get("camera.my_camera") assert cam is not None - assert cam.state == STATE_IDLE + assert cam.state == STATE_STREAMING # Request an HLS stream with patch("homeassistant.components.camera.create_stream") as create_stream: @@ -639,7 +639,7 @@ async def test_camera_web_rtc(hass, auth, hass_ws_client): assert len(hass.states.async_all()) == 1 cam = hass.states.get("camera.my_camera") assert cam is not None - assert cam.state == STATE_IDLE + assert cam.state == STATE_STREAMING client = await hass_ws_client(hass) await client.send_json( @@ -669,7 +669,7 @@ async def test_camera_web_rtc_unsupported(hass, auth, hass_ws_client): assert len(hass.states.async_all()) == 1 cam = hass.states.get("camera.my_camera") assert cam is not None - assert cam.state == STATE_IDLE + assert cam.state == STATE_STREAMING client = await hass_ws_client(hass) await client.send_json( From 3d33cad655beae4908881a6b4b4fc9bd0ff07572 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 17 Oct 2021 10:46:18 -0700 Subject: [PATCH 0470/1038] Improve nest error handling for websocket streams (#57885) --- homeassistant/components/nest/camera_sdm.py | 12 ++- tests/components/nest/camera_sdm_test.py | 100 ++++++++++++++++++++ 2 files changed, 109 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index b610d328b42..5620653819f 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -24,7 +24,7 @@ from homeassistant.components.camera.const import STREAM_TYPE_HLS, STREAM_TYPE_W from homeassistant.components.ffmpeg import async_get_image from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady +from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time @@ -136,7 +136,10 @@ class NestCamera(Camera): trait = self._device.traits[CameraLiveStreamTrait.NAME] if not self._stream: _LOGGER.debug("Fetching stream url") - self._stream = await trait.generate_rtsp_stream() + try: + self._stream = await trait.generate_rtsp_stream() + except GoogleNestException as err: + raise HomeAssistantError(f"Nest API error: {err}") from err self._schedule_stream_refresh() assert self._stream if self._stream.expires_at < utcnow(): @@ -271,5 +274,8 @@ class NestCamera(Camera): async def async_handle_web_rtc_offer(self, offer_sdp: str) -> str: """Return the source of the stream.""" trait: CameraLiveStreamTrait = self._device.traits[CameraLiveStreamTrait.NAME] - stream = await trait.generate_web_rtc_stream(offer_sdp) + try: + stream = await trait.generate_web_rtc_stream(offer_sdp) + except GoogleNestException as err: + raise HomeAssistantError(f"Nest API error: {err}") from err return stream.answer_sdp diff --git a/tests/components/nest/camera_sdm_test.py b/tests/components/nest/camera_sdm_test.py index a6c7ad64605..08797fb3c6a 100644 --- a/tests/components/nest/camera_sdm_test.py +++ b/tests/components/nest/camera_sdm_test.py @@ -200,6 +200,61 @@ async def test_camera_stream(hass, auth): assert image.content == IMAGE_BYTES_FROM_STREAM +async def test_camera_ws_stream(hass, auth, hass_ws_client): + """Test a basic camera that supports web rtc.""" + auth.responses = [make_stream_url_response()] + await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) + + assert len(hass.states.async_all()) == 1 + cam = hass.states.get("camera.my_camera") + assert cam is not None + assert cam.state == STATE_IDLE + + with patch("homeassistant.components.camera.create_stream") as mock_stream: + mock_stream().endpoint_url.return_value = "http://home.assistant/playlist.m3u8" + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 2, + "type": "camera/stream", + "entity_id": "camera.my_camera", + } + ) + msg = await client.receive_json() + + assert msg["id"] == 2 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + assert msg["result"]["url"] == "http://home.assistant/playlist.m3u8" + + +async def test_camera_ws_stream_failure(hass, auth, hass_ws_client): + """Test a basic camera that supports web rtc.""" + auth.responses = [aiohttp.web.Response(status=400)] + await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) + + assert len(hass.states.async_all()) == 1 + cam = hass.states.get("camera.my_camera") + assert cam is not None + assert cam.state == STATE_IDLE + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 3, + "type": "camera/stream", + "entity_id": "camera.my_camera", + } + ) + + msg = await client.receive_json() + assert msg["id"] == 3 + assert msg["type"] == TYPE_RESULT + assert not msg["success"] + assert msg["error"]["code"] == "start_stream_failed" + assert msg["error"]["message"].startswith("Nest API error") + + async def test_camera_stream_missing_trait(hass, auth): """Test fetching a video stream when not supported by the API.""" traits = { @@ -686,3 +741,48 @@ async def test_camera_web_rtc_unsupported(hass, auth, hass_ws_client): assert msg["type"] == TYPE_RESULT assert not msg["success"] assert msg["error"]["code"] == "web_rtc_offer_failed" + assert msg["error"]["message"].startswith("Camera does not support WebRTC") + + +async def test_camera_web_rtc_offer_failure(hass, auth, hass_ws_client): + """Test a basic camera that supports web rtc.""" + auth.responses = [ + aiohttp.web.Response(status=400), + ] + device_traits = { + "sdm.devices.traits.Info": { + "customName": "My Camera", + }, + "sdm.devices.traits.CameraLiveStream": { + "maxVideoResolution": { + "width": 640, + "height": 480, + }, + "videoCodecs": ["H264"], + "audioCodecs": ["AAC"], + "supportedProtocols": ["WEB_RTC"], + }, + } + await async_setup_camera(hass, device_traits, auth=auth) + + assert len(hass.states.async_all()) == 1 + cam = hass.states.get("camera.my_camera") + assert cam is not None + assert cam.state == STATE_IDLE + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 5, + "type": "camera/web_rtc_offer", + "entity_id": "camera.my_camera", + "offer": "a=recvonly", + } + ) + + msg = await client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == TYPE_RESULT + assert not msg["success"] + assert msg["error"]["code"] == "web_rtc_offer_failed" + assert msg["error"]["message"].startswith("Nest API error") From 8f6ed2d27e0c856dead30c51a67c15599168402f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sun, 17 Oct 2021 19:47:07 +0200 Subject: [PATCH 0471/1038] Add category diagnostic to Surepetcare binary sensor (#57908) --- homeassistant/components/surepetcare/binary_sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/surepetcare/binary_sensor.py b/homeassistant/components/surepetcare/binary_sensor.py index a75addb11d3..eecf82abc52 100644 --- a/homeassistant/components/surepetcare/binary_sensor.py +++ b/homeassistant/components/surepetcare/binary_sensor.py @@ -13,6 +13,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -67,6 +68,7 @@ class Hub(SurePetcareBinarySensor): """Sure Petcare Hub.""" _attr_device_class = DEVICE_CLASS_CONNECTIVITY + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC @property def available(self) -> bool: @@ -116,6 +118,7 @@ class DeviceConnectivity(SurePetcareBinarySensor): """Sure Petcare Device.""" _attr_device_class = DEVICE_CLASS_CONNECTIVITY + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__( self, From e9d601a688966758554dd1dfa2c4f9cd18d11d5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sun, 17 Oct 2021 19:48:45 +0200 Subject: [PATCH 0472/1038] Opengarage dataupdater (#56931) --- .../components/opengarage/__init__.py | 43 ++++++++++++++++- homeassistant/components/opengarage/cover.py | 46 ++++++++++--------- 2 files changed, 67 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/opengarage/__init__.py b/homeassistant/components/opengarage/__init__.py index 5ea3af79ae4..d64a608a3ef 100644 --- a/homeassistant/components/opengarage/__init__.py +++ b/homeassistant/components/opengarage/__init__.py @@ -1,15 +1,21 @@ """The OpenGarage integration.""" from __future__ import annotations +from datetime import timedelta +import logging + import opengarage from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant +from homeassistant.helpers import update_coordinator from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_DEVICE_KEY, DOMAIN +_LOGGER = logging.getLogger(__name__) + PLATFORMS = ["cover"] @@ -17,12 +23,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up OpenGarage from a config entry.""" hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = opengarage.OpenGarage( + open_garage_connection = opengarage.OpenGarage( f"{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}", entry.data[CONF_DEVICE_KEY], entry.data[CONF_VERIFY_SSL], async_get_clientsession(hass), ) + open_garage_data_coordinator = OpenGarageDataUpdateCoordinator( + hass, + open_garage_connection=open_garage_connection, + ) + await open_garage_data_coordinator.async_config_entry_first_refresh() + hass.data[DOMAIN][entry.entry_id] = open_garage_data_coordinator hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -36,3 +48,32 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +class OpenGarageDataUpdateCoordinator(update_coordinator.DataUpdateCoordinator): + """Class to manage fetching Opengarage data.""" + + def __init__( + self, + hass: HomeAssistant, + *, + open_garage_connection: opengarage.OpenGarage, + ) -> None: + """Initialize global Opengarage data updater.""" + self.open_garage_connection = open_garage_connection + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=30), + ) + + async def _async_update_data(self) -> None: + """Fetch data.""" + data = await self.open_garage_connection.update_state() + if data is None: + raise update_coordinator.UpdateFailed( + "Unable to connect to OpenGarage device" + ) + return data diff --git a/homeassistant/components/opengarage/cover.py b/homeassistant/components/opengarage/cover.py index ad3b2a4f74f..b80e1c82079 100644 --- a/homeassistant/components/opengarage/cover.py +++ b/homeassistant/components/opengarage/cover.py @@ -23,8 +23,10 @@ from homeassistant.const import ( STATE_OPEN, STATE_OPENING, ) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTR_DISTANCE_SENSOR, @@ -75,28 +77,26 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, entry, async_add_entities): """Set up the OpenGarage covers.""" async_add_entities( - [OpenGarageCover(hass.data[DOMAIN][entry.entry_id], entry.unique_id)], True + [OpenGarageCover(hass.data[DOMAIN][entry.entry_id], entry.unique_id)] ) -class OpenGarageCover(CoverEntity): +class OpenGarageCover(CoordinatorEntity, CoverEntity): """Representation of a OpenGarage cover.""" _attr_device_class = DEVICE_CLASS_GARAGE _attr_supported_features = SUPPORT_OPEN | SUPPORT_CLOSE - def __init__(self, open_garage, device_id): + def __init__(self, open_garage_data_coordinator, device_id): """Initialize the cover.""" - self._open_garage = open_garage + super().__init__(open_garage_data_coordinator) + self._state = None self._state_before_move = None - self._extra_state_attributes = {} + self._attr_extra_state_attributes = {} self._attr_unique_id = self._device_id = device_id - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - return self._extra_state_attributes + self._device_name = None + self._update_attr() @property def is_closed(self): @@ -135,16 +135,16 @@ class OpenGarageCover(CoverEntity): self._state = STATE_OPENING await self._push_button() - async def async_update(self): - """Get updated status from API.""" - status = await self._open_garage.update_state() + @callback + def _update_attr(self) -> None: + """Update the state and attributes.""" + status = self.coordinator.data if status is None: _LOGGER.error("Unable to connect to OpenGarage device") self._attr_available = False return - if self.name is None and status["name"] is not None: - self._attr_name = status["name"] + self._device_name = self._attr_name = status["name"] state = STATES_MAP.get(status.get("door")) if self._state_before_move is not None: if self._state_before_move != state: @@ -155,17 +155,21 @@ class OpenGarageCover(CoverEntity): _LOGGER.debug("%s status: %s", self.name, self._state) if status.get("rssi") is not None: - self._extra_state_attributes[ATTR_SIGNAL_STRENGTH] = status.get("rssi") + self._attr_extra_state_attributes[ATTR_SIGNAL_STRENGTH] = status.get("rssi") if status.get("dist") is not None: - self._extra_state_attributes[ATTR_DISTANCE_SENSOR] = status.get("dist") + self._attr_extra_state_attributes[ATTR_DISTANCE_SENSOR] = status.get("dist") if self._state is not None: - self._extra_state_attributes[ATTR_DOOR_STATE] = self._state + self._attr_extra_state_attributes[ATTR_DOOR_STATE] = self._state - self._attr_available = True + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_attr() + self.async_write_ha_state() async def _push_button(self): """Send commands to API.""" - result = await self._open_garage.push_button() + result = await self.coordinator.open_garage_connection.push_button() if result is None: _LOGGER.error("Unable to connect to OpenGarage device") if result == 1: @@ -184,7 +188,7 @@ class OpenGarageCover(CoverEntity): """Return the device_info of the device.""" device_info = DeviceInfo( identifiers={(DOMAIN, self._device_id)}, - name=self.name, + name=self._device_name, manufacturer="Open Garage", ) return device_info From d09ee11c5416ce8f93f7d3b1bb5c6e84981a6117 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 Oct 2021 07:50:13 -1000 Subject: [PATCH 0473/1038] Fix bond reloading on zeroconf discovery when host has not changed (#57799) --- homeassistant/components/bond/__init__.py | 22 +++++---------- homeassistant/components/bond/config_flow.py | 14 ++++++---- homeassistant/components/bond/const.py | 1 - tests/components/bond/test_config_flow.py | 29 ++++++++++++++++++++ 4 files changed, 44 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index 90e2838ff36..ecf5ea526b9 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -1,6 +1,7 @@ """The Bond integration.""" from asyncio import TimeoutError as AsyncIOTimeoutError import logging +from typing import Any from aiohttp import ClientError, ClientResponseError, ClientTimeout from bond_api import Bond, BPUPSubscriptions, start_bpup @@ -12,18 +13,17 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, HTTP_UNAUTHORIZED, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import SLOW_UPDATE_WARNING -from .const import BPUP_STOP, BPUP_SUBS, BRIDGE_MAKE, DOMAIN, HUB +from .const import BPUP_SUBS, BRIDGE_MAKE, DOMAIN, HUB from .utils import BondHub PLATFORMS = ["cover", "fan", "light", "switch"] _API_TIMEOUT = SLOW_UPDATE_WARNING - 1 -_STOP_CANCEL = "stop_cancel" _LOGGER = logging.getLogger(__name__) @@ -55,18 +55,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: stop_bpup = await start_bpup(host, bpup_subs) @callback - def _async_stop_event(event: Event) -> None: + def _async_stop_event(*_: Any) -> None: stop_bpup() - stop_event_cancel = hass.bus.async_listen( - EVENT_HOMEASSISTANT_STOP, _async_stop_event + entry.async_on_unload(_async_stop_event) + entry.async_on_unload( + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, _async_stop_event) ) hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { HUB: hub, BPUP_SUBS: bpup_subs, - BPUP_STOP: stop_bpup, - _STOP_CANCEL: stop_event_cancel, } if not entry.unique_id: @@ -95,15 +94,8 @@ 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) - - data = hass.data[DOMAIN][entry.entry_id] - data[_STOP_CANCEL]() - if BPUP_STOP in data: - data[BPUP_STOP]() - if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index da8f51227dd..073a91d54fb 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -110,12 +110,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): token := await async_get_token(self.hass, host) ): updates[CONF_ACCESS_TOKEN] = token - self.hass.config_entries.async_update_entry( - entry, data={**entry.data, **updates} - ) - self.hass.async_create_task( - self.hass.config_entries.async_reload(entry.entry_id) - ) + new_data = {**entry.data, **updates} + if new_data != dict(entry.data): + self.hass.config_entries.async_update_entry( + entry, data={**entry.data, **updates} + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) raise AbortFlow("already_configured") self._discovered = {CONF_HOST: host, CONF_NAME: bond_id} diff --git a/homeassistant/components/bond/const.py b/homeassistant/components/bond/const.py index 4d886c2ee77..778dcbc1a1f 100644 --- a/homeassistant/components/bond/const.py +++ b/homeassistant/components/bond/const.py @@ -9,7 +9,6 @@ CONF_BOND_ID: str = "bond_id" HUB = "hub" BPUP_SUBS = "bpup_subs" -BPUP_STOP = "bpup_stop" SERVICE_SET_FAN_SPEED_TRACKED_STATE = "set_fan_speed_tracked_state" SERVICE_SET_POWER_TRACKED_STATE = "set_switch_power_tracked_state" diff --git a/tests/components/bond/test_config_flow.py b/tests/components/bond/test_config_flow.py index db9652bf0be..27f0e69e79b 100644 --- a/tests/components/bond/test_config_flow.py +++ b/tests/components/bond/test_config_flow.py @@ -352,6 +352,35 @@ async def test_zeroconf_already_configured_refresh_token(hass: core.HomeAssistan assert len(mock_setup_entry.mock_calls) == 1 +async def test_zeroconf_already_configured_no_reload_same_host( + hass: core.HomeAssistant, +): + """Test starting a flow from zeroconf when already configured does not reload if the host is the same.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="already-registered-bond-id", + data={CONF_HOST: "stored-host", CONF_ACCESS_TOKEN: "correct-token"}, + ) + entry.add_to_hass(hass) + + with _patch_async_setup_entry() as mock_setup_entry, patch_bond_token( + return_value={"token": "correct-token"} + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data={ + "name": "already-registered-bond-id.some-other-tail-info", + "host": "stored-host", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert len(mock_setup_entry.mock_calls) == 0 + + async def test_zeroconf_form_unexpected_error(hass: core.HomeAssistant): """Test we handle unexpected error gracefully.""" await _help_test_form_unexpected_error( From 5048bad050dff29b1d8a05a4dcc86bceb1d79ad5 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 17 Oct 2021 19:56:00 +0200 Subject: [PATCH 0474/1038] Use assignment expressions 05 (#57785) --- homeassistant/components/alexa/capabilities.py | 6 ++---- homeassistant/components/alexa/handlers.py | 3 +-- .../components/amcrest/binary_sensor.py | 13 +++++-------- homeassistant/components/amcrest/sensor.py | 16 ++++++---------- homeassistant/components/aqualogic/sensor.py | 3 +-- homeassistant/components/aqualogic/switch.py | 9 +++------ .../components/arcam_fmj/media_player.py | 4 +--- homeassistant/components/arlo/camera.py | 3 +-- homeassistant/components/blebox/climate.py | 3 +-- homeassistant/components/blebox/light.py | 3 +-- homeassistant/components/calendar/__init__.py | 6 ++---- homeassistant/components/cloud/__init__.py | 4 +--- homeassistant/components/cloud/alexa_config.py | 4 +--- homeassistant/components/cloud/utils.py | 4 +--- .../config/auth_provider_homeassistant.py | 3 +-- homeassistant/components/coolmaster/climate.py | 3 +-- homeassistant/components/cover/__init__.py | 14 +++++--------- homeassistant/components/currencylayer/sensor.py | 3 +-- homeassistant/components/daikin/switch.py | 3 +-- 19 files changed, 36 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 46f421963ca..dad6e00ff8e 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -182,8 +182,7 @@ class AlexaCapability: """Serialize according to the Discovery API.""" result = {"type": "AlexaInterface", "interface": self.name(), "version": "3"} - instance = self.instance - if instance is not None: + if (instance := self.instance) is not None: result["instance"] = instance properties_supported = self.properties_supported() @@ -264,8 +263,7 @@ class AlexaCapability: "timeOfSample": dt_util.utcnow().strftime(DATE_FORMAT), "uncertaintyInMilliseconds": 0, } - instance = self.instance - if instance is not None: + if (instance := self.instance) is not None: result["instance"] = instance yield result diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 5a23f5d1bc2..21ad6648a5a 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -117,8 +117,7 @@ async def async_api_accept_grant(hass, config, directive, context): async def async_api_turn_on(hass, config, directive, context): """Process a turn on request.""" entity = directive.entity - domain = entity.domain - if domain == group.DOMAIN: + if (domain := entity.domain) == group.DOMAIN: domain = ha.DOMAIN service = SERVICE_TURN_ON diff --git a/homeassistant/components/amcrest/binary_sensor.py b/homeassistant/components/amcrest/binary_sensor.py index 8d2535a142b..4fc810fd773 100644 --- a/homeassistant/components/amcrest/binary_sensor.py +++ b/homeassistant/components/amcrest/binary_sensor.py @@ -215,8 +215,7 @@ class AmcrestBinarySensor(BinarySensorEntity): log_update_error(_LOGGER, "update", self.name, "binary sensor", error) return - event_code = self.entity_description.event_code - if event_code is None: + if (event_code := self.entity_description.event_code) is None: _LOGGER.error("Binary sensor %s event code not set", self.name) return @@ -228,12 +227,10 @@ class AmcrestBinarySensor(BinarySensorEntity): def _update_unique_id(self) -> None: """Set the unique id.""" - if self._attr_unique_id is None: - serial_number = self._api.serial_number - if serial_number: - self._attr_unique_id = ( - f"{serial_number}-{self.entity_description.key}-{self._channel}" - ) + if self._attr_unique_id is None and (serial_number := self._api.serial_number): + self._attr_unique_id = ( + f"{serial_number}-{self.entity_description.key}-{self._channel}" + ) async def async_on_demand_update(self) -> None: """Update state.""" diff --git a/homeassistant/components/amcrest/sensor.py b/homeassistant/components/amcrest/sensor.py index f2048654da6..c262e16ec7c 100644 --- a/homeassistant/components/amcrest/sensor.py +++ b/homeassistant/components/amcrest/sensor.py @@ -96,18 +96,14 @@ class AmcrestSensor(SensorEntity): _LOGGER.debug("Updating %s sensor", self.name) sensor_type = self.entity_description.key - if self._attr_unique_id is None: - serial_number = self._api.serial_number - if serial_number: - self._attr_unique_id = f"{serial_number}-{sensor_type}-{self._channel}" + if self._attr_unique_id is None and (serial_number := self._api.serial_number): + self._attr_unique_id = f"{serial_number}-{sensor_type}-{self._channel}" try: - if self._attr_unique_id is None: - serial_number = self._api.serial_number - if serial_number: - self._attr_unique_id = ( - f"{serial_number}-{sensor_type}-{self._channel}" - ) + if self._attr_unique_id is None and ( + serial_number := self._api.serial_number + ): + self._attr_unique_id = f"{serial_number}-{sensor_type}-{self._channel}" if sensor_type == SENSOR_PTZ_PRESET: self._attr_native_value = self._api.ptz_presets_count diff --git a/homeassistant/components/aqualogic/sensor.py b/homeassistant/components/aqualogic/sensor.py index 4c46a7aa5eb..c1823ca2bb5 100644 --- a/homeassistant/components/aqualogic/sensor.py +++ b/homeassistant/components/aqualogic/sensor.py @@ -147,8 +147,7 @@ class AquaLogicSensor(SensorEntity): @callback def async_update_callback(self): """Update callback.""" - panel = self._processor.panel - if panel is not None: + if (panel := self._processor.panel) is not None: if panel.is_metric: self._attr_native_unit_of_measurement = ( self.entity_description.unit_metric diff --git a/homeassistant/components/aqualogic/switch.py b/homeassistant/components/aqualogic/switch.py index c05bacc5f03..157688c7576 100644 --- a/homeassistant/components/aqualogic/switch.py +++ b/homeassistant/components/aqualogic/switch.py @@ -66,23 +66,20 @@ class AquaLogicSwitch(SwitchEntity): @property def is_on(self): """Return true if device is on.""" - panel = self._processor.panel - if panel is None: + if (panel := self._processor.panel) is None: return False state = panel.get_state(self._state_name) return state def turn_on(self, **kwargs): """Turn the device on.""" - panel = self._processor.panel - if panel is None: + if (panel := self._processor.panel) is None: return panel.set_state(self._state_name, True) def turn_off(self, **kwargs): """Turn the device off.""" - panel = self._processor.panel - if panel is None: + if (panel := self._processor.panel) is None: return panel.set_state(self._state_name, False) diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index e9e5e29a1c7..115675dfb89 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -375,9 +375,7 @@ class ArcamFmj(MediaPlayerEntity): if source is None: return None - channel = self.media_channel - - if channel: + if channel := self.media_channel: value = f"{source.name} - {channel}" else: value = source.name diff --git a/homeassistant/components/arlo/camera.py b/homeassistant/components/arlo/camera.py index 6b14f0cee0c..146970a8610 100644 --- a/homeassistant/components/arlo/camera.py +++ b/homeassistant/components/arlo/camera.py @@ -145,13 +145,12 @@ class ArloCam(Camera): def set_base_station_mode(self, mode): """Set the mode in the base station.""" # Get the list of base stations identified by library - base_stations = self.hass.data[DATA_ARLO].base_stations # Some Arlo cameras does not have base station # So check if there is base station detected first # if yes, then choose the primary base station # Set the mode on the chosen base station - if base_stations: + if base_stations := self.hass.data[DATA_ARLO].base_stations: primary_base_station = base_stations[0] primary_base_station.mode = mode diff --git a/homeassistant/components/blebox/climate.py b/homeassistant/components/blebox/climate.py index 59e64b772ef..5dec5725607 100644 --- a/homeassistant/components/blebox/climate.py +++ b/homeassistant/components/blebox/climate.py @@ -40,8 +40,7 @@ class BleBoxClimateEntity(BleBoxEntity, ClimateEntity): @property def hvac_action(self): """Return the actual current HVAC action.""" - is_on = self._feature.is_on - if not is_on: + if not (is_on := self._feature.is_on): return None if is_on is None else CURRENT_HVAC_OFF # NOTE: In practice, there's no need to handle case when is_heating is None diff --git a/homeassistant/components/blebox/light.py b/homeassistant/components/blebox/light.py index b03cc16112c..efbdb038794 100644 --- a/homeassistant/components/blebox/light.py +++ b/homeassistant/components/blebox/light.py @@ -56,8 +56,7 @@ class BleBoxLightEntity(BleBoxEntity, LightEntity): @property def rgbw_color(self): """Return the hue and saturation.""" - rgbw_hex = self._feature.rgbw_hex - if rgbw_hex is None: + if (rgbw_hex := self._feature.rgbw_hex) is None: return None return tuple(rgb_hex_to_rgb_list(rgbw_hex)[0:4]) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 10580339329..1666a30a1f5 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -145,8 +145,7 @@ class CalendarEventDevice(Entity): @property def state_attributes(self): """Return the entity state attributes.""" - event = self.event - if event is None: + if (event := self.event) is None: return None event = normalize_event(event) @@ -162,8 +161,7 @@ class CalendarEventDevice(Entity): @property def state(self): """Return the state of the calendar event.""" - event = self.event - if event is None: + if (event := self.event) is None: return STATE_OFF event = normalize_event(event) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index f1833899fec..f99ac4c7b0a 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -253,9 +253,7 @@ def _remote_handle_prefs_updated(cloud: Cloud) -> None: if prefs.remote_enabled == cur_pref: return - cur_pref = prefs.remote_enabled - - if cur_pref: + if cur_pref := prefs.remote_enabled: await cloud.remote.connect() else: await cloud.remote.disconnect() diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index c14b4d7a4e7..f578a114dfc 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -143,10 +143,8 @@ class AlexaConfig(alexa_config.AbstractConfig): else: auxiliary_entity = False - default_expose = self._prefs.alexa_default_expose - # Backwards compat - if default_expose is None: + if (default_expose := self._prefs.alexa_default_expose) is None: return not auxiliary_entity return not auxiliary_entity and split_entity_id(entity_id)[0] in default_expose diff --git a/homeassistant/components/cloud/utils.py b/homeassistant/components/cloud/utils.py index 57f84b057f7..715a8119af9 100644 --- a/homeassistant/components/cloud/utils.py +++ b/homeassistant/components/cloud/utils.py @@ -8,9 +8,7 @@ from aiohttp import payload, web def aiohttp_serialize_response(response: web.Response) -> dict[str, Any]: """Serialize an aiohttp response to a dictionary.""" - body = response.body - - if body is None: + if (body := response.body) is None: pass elif isinstance(body, payload.StringPayload): # pylint: disable=protected-access diff --git a/homeassistant/components/config/auth_provider_homeassistant.py b/homeassistant/components/config/auth_provider_homeassistant.py index a8421c4c0f6..78175678a58 100644 --- a/homeassistant/components/config/auth_provider_homeassistant.py +++ b/homeassistant/components/config/auth_provider_homeassistant.py @@ -103,8 +103,7 @@ async def websocket_delete(hass, connection, msg): @websocket_api.async_response async def websocket_change_password(hass, connection, msg): """Change current user password.""" - user = connection.user - if user is None: + if (user := connection.user) is None: connection.send_error(msg["id"], "user_not_found", "User not found") return diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py index 7077854a768..0b717697a1a 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -120,8 +120,7 @@ class CoolmasterClimate(CoordinatorEntity, ClimateEntity): def hvac_mode(self): """Return hvac target hvac state.""" mode = self._unit.mode - is_on = self._unit.is_on - if not is_on: + if not self._unit.is_on: return HVAC_MODE_OFF return CM_TO_HA_STATE[mode] diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 00fef5c6485..7a3061d24c8 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -212,9 +212,7 @@ class CoverEntity(Entity): if self.is_closing: return STATE_CLOSING - closed = self.is_closed - - if closed is None: + if (closed := self.is_closed) is None: return None return STATE_CLOSED if closed else STATE_OPEN @@ -225,13 +223,11 @@ class CoverEntity(Entity): """Return the state attributes.""" data = {} - current = self.current_cover_position - if current is not None: - data[ATTR_CURRENT_POSITION] = self.current_cover_position + if (current := self.current_cover_position) is not None: + data[ATTR_CURRENT_POSITION] = current - current_tilt = self.current_cover_tilt_position - if current_tilt is not None: - data[ATTR_CURRENT_TILT_POSITION] = self.current_cover_tilt_position + if (current_tilt := self.current_cover_tilt_position) is not None: + data[ATTR_CURRENT_TILT_POSITION] = current_tilt return data diff --git a/homeassistant/components/currencylayer/sensor.py b/homeassistant/components/currencylayer/sensor.py index fd3f3b2f8c5..2f0461cc8c4 100644 --- a/homeassistant/components/currencylayer/sensor.py +++ b/homeassistant/components/currencylayer/sensor.py @@ -92,8 +92,7 @@ class CurrencylayerSensor(SensorEntity): def update(self): """Update current date.""" self.rest.update() - value = self.rest.data - if value is not None: + if (value := self.rest.data) is not None: self._state = round(value[f"{self._base}{self._quote}"], 4) diff --git a/homeassistant/components/daikin/switch.py b/homeassistant/components/daikin/switch.py index 5e0e1b5761a..647ee0689e6 100644 --- a/homeassistant/components/daikin/switch.py +++ b/homeassistant/components/daikin/switch.py @@ -22,8 +22,7 @@ async def async_setup_entry(hass, entry, async_add_entities): """Set up Daikin climate based on config_entry.""" daikin_api = hass.data[DAIKIN_DOMAIN][entry.entry_id] switches = [] - zones = daikin_api.device.zones - if zones: + if zones := daikin_api.device.zones: switches.extend( [ DaikinZoneSwitch(daikin_api, zone_id) From d5116810d4bd8ae2209f84c34c30af8117ed10bf Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 17 Oct 2021 20:02:42 +0200 Subject: [PATCH 0475/1038] Use assignment expressions 08 (#57788) --- .../components/proxmoxve/binary_sensor.py | 10 +++------- homeassistant/components/ps4/media_player.py | 20 +++++++++---------- .../components/rainforest_eagle/data.py | 4 +--- .../components/seventeentrack/sensor.py | 3 +-- .../components/smarttub/binary_sensor.py | 5 +---- homeassistant/components/snmp/sensor.py | 3 +-- .../components/tahoma/binary_sensor.py | 3 +-- homeassistant/components/tahoma/cover.py | 3 +-- homeassistant/components/tahoma/lock.py | 3 +-- homeassistant/components/tahoma/sensor.py | 3 +-- homeassistant/components/tahoma/switch.py | 3 +-- homeassistant/components/tellstick/light.py | 3 +-- homeassistant/components/tradfri/light.py | 3 +-- .../components/trafikverket_train/sensor.py | 3 +-- .../components/transmission/sensor.py | 6 ++---- homeassistant/components/travisci/sensor.py | 3 +-- .../components/universal/media_player.py | 6 ++---- homeassistant/components/vallox/sensor.py | 4 +--- homeassistant/components/vera/__init__.py | 9 +++------ homeassistant/components/vera/climate.py | 4 ++-- homeassistant/components/vera/switch.py | 4 ++-- homeassistant/components/vesync/fan.py | 9 +++++---- homeassistant/components/weather/__init__.py | 18 ++++++----------- homeassistant/components/wemo/entity.py | 6 ++---- homeassistant/components/wiffi/__init__.py | 3 +-- homeassistant/components/xbox/media_player.py | 6 ++---- .../yamaha_musiccast/media_player.py | 3 +-- homeassistant/components/zha/climate.py | 3 +-- homeassistant/components/zha/sensor.py | 6 ++---- homeassistant/components/zoneminder/sensor.py | 3 +-- homeassistant/components/zwave/climate.py | 6 ++---- homeassistant/helpers/config_entry_flow.py | 3 +-- 32 files changed, 62 insertions(+), 109 deletions(-) diff --git a/homeassistant/components/proxmoxve/binary_sensor.py b/homeassistant/components/proxmoxve/binary_sensor.py index fedb513e5b4..eb4bf77fa5e 100644 --- a/homeassistant/components/proxmoxve/binary_sensor.py +++ b/homeassistant/components/proxmoxve/binary_sensor.py @@ -25,10 +25,9 @@ async def async_setup_platform(hass, config, add_entities, discovery_info=None): for vm_id in node_config["vms"]: coordinator = host_name_coordinators[node_name][vm_id] - coordinator_data = coordinator.data # unfound vm case - if coordinator_data is None: + if (coordinator_data := coordinator.data) is None: continue vm_name = coordinator_data["name"] @@ -39,10 +38,9 @@ async def async_setup_platform(hass, config, add_entities, discovery_info=None): for container_id in node_config["containers"]: coordinator = host_name_coordinators[node_name][container_id] - coordinator_data = coordinator.data # unfound container case - if coordinator_data is None: + if (coordinator_data := coordinator.data) is None: continue container_name = coordinator_data["name"] @@ -88,9 +86,7 @@ class ProxmoxBinarySensor(ProxmoxEntity, BinarySensorEntity): @property def is_on(self): """Return the state of the binary sensor.""" - data = self.coordinator.data - - if data is None: + if (data := self.coordinator.data) is None: return None return data["status"] == "running" diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index be77ea04f1c..512894a9889 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -160,9 +160,7 @@ class PS4Device(MediaPlayerEntity): def _parse_status(self): """Parse status.""" - status = self._ps4.status - - if status is not None: + if (status := self._ps4.status) is not None: self._games = load_games(self.hass, self._unique_id) if self._games: self.get_source_list() @@ -384,13 +382,15 @@ class PS4Device(MediaPlayerEntity): @property def entity_picture(self): """Return picture.""" - if self._state == STATE_PLAYING and self._media_content_id is not None: - image_hash = self.media_image_hash - if image_hash is not None: - return ( - f"/api/media_player_proxy/{self.entity_id}?" - f"token={self.access_token}&cache={image_hash}" - ) + if ( + self._state == STATE_PLAYING + and self._media_content_id is not None + and (image_hash := self.media_image_hash) is not None + ): + return ( + f"/api/media_player_proxy/{self.entity_id}?" + f"token={self.access_token}&cache={image_hash}" + ) return MEDIA_IMAGE_DEFAULT @property diff --git a/homeassistant/components/rainforest_eagle/data.py b/homeassistant/components/rainforest_eagle/data.py index 70c2bddb4b3..91447392ea8 100644 --- a/homeassistant/components/rainforest_eagle/data.py +++ b/homeassistant/components/rainforest_eagle/data.py @@ -136,9 +136,7 @@ class EagleDataCoordinator(DataUpdateCoordinator): async def _async_update_data_200(self): """Get the latest data from the Eagle-200 device.""" - eagle200_meter = self.eagle200_meter - - if eagle200_meter is None: + if (eagle200_meter := self.eagle200_meter) is None: hub = aioeagle.EagleHub( aiohttp_client.async_get_clientsession(self.hass), self.cloud_id, diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py index 9087cff8a97..87546d09004 100644 --- a/homeassistant/components/seventeentrack/sensor.py +++ b/homeassistant/components/seventeentrack/sensor.py @@ -180,8 +180,7 @@ class SeventeenTrackPackageSensor(SensorEntity): @property def name(self): """Return the name.""" - name = self._friendly_name - if not name: + if not (name := self._friendly_name): name = self._tracking_number return f"Seventeentrack Package: {name}" diff --git a/homeassistant/components/smarttub/binary_sensor.py b/homeassistant/components/smarttub/binary_sensor.py index a3422e27d8d..ead0e6a0dce 100644 --- a/homeassistant/components/smarttub/binary_sensor.py +++ b/homeassistant/components/smarttub/binary_sensor.py @@ -171,10 +171,7 @@ class SmartTubError(SmartTubEntity, BinarySensorEntity): @property def extra_state_attributes(self): """Return the state attributes.""" - - error = self.error - - if error is None: + if (error := self.error) is None: return {} return { diff --git a/homeassistant/components/snmp/sensor.py b/homeassistant/components/snmp/sensor.py index 09bfe3856cc..fb01be229ec 100644 --- a/homeassistant/components/snmp/sensor.py +++ b/homeassistant/components/snmp/sensor.py @@ -167,9 +167,8 @@ class SnmpSensor(SensorEntity): async def async_update(self): """Get the latest data and updates the states.""" await self.data.async_update() - value = self.data.value - if value is None: + if (value := self.data.value) is None: value = STATE_UNKNOWN elif self._value_template is not None: value = self._value_template.async_render_with_possible_json_value( diff --git a/homeassistant/components/tahoma/binary_sensor.py b/homeassistant/components/tahoma/binary_sensor.py index c0946013469..55be39377bf 100644 --- a/homeassistant/components/tahoma/binary_sensor.py +++ b/homeassistant/components/tahoma/binary_sensor.py @@ -60,8 +60,7 @@ class TahomaBinarySensor(TahomaDevice, BinarySensorEntity): def extra_state_attributes(self): """Return the device state attributes.""" attr = {} - super_attr = super().extra_state_attributes - if super_attr is not None: + if (super_attr := super().extra_state_attributes) is not None: attr.update(super_attr) if self._battery is not None: diff --git a/homeassistant/components/tahoma/cover.py b/homeassistant/components/tahoma/cover.py index a02f21fb5e1..c7bd1540769 100644 --- a/homeassistant/components/tahoma/cover.py +++ b/homeassistant/components/tahoma/cover.py @@ -197,8 +197,7 @@ class TahomaCover(TahomaDevice, CoverEntity): def extra_state_attributes(self): """Return the device state attributes.""" attr = {} - super_attr = super().extra_state_attributes - if super_attr is not None: + if (super_attr := super().extra_state_attributes) is not None: attr.update(super_attr) if "core:Memorized1PositionState" in self.tahoma_device.active_states: diff --git a/homeassistant/components/tahoma/lock.py b/homeassistant/components/tahoma/lock.py index 3d160cd95b3..dde1227621c 100644 --- a/homeassistant/components/tahoma/lock.py +++ b/homeassistant/components/tahoma/lock.py @@ -83,7 +83,6 @@ class TahomaLock(TahomaDevice, LockEntity): attr = { ATTR_BATTERY_LEVEL: self._battery_level, } - super_attr = super().extra_state_attributes - if super_attr is not None: + if (super_attr := super().extra_state_attributes) is not None: attr.update(super_attr) return attr diff --git a/homeassistant/components/tahoma/sensor.py b/homeassistant/components/tahoma/sensor.py index 35a51b03805..6a7847ec06e 100644 --- a/homeassistant/components/tahoma/sensor.py +++ b/homeassistant/components/tahoma/sensor.py @@ -113,8 +113,7 @@ class TahomaSensor(TahomaDevice, SensorEntity): def extra_state_attributes(self): """Return the device state attributes.""" attr = {} - super_attr = super().extra_state_attributes - if super_attr is not None: + if (super_attr := super().extra_state_attributes) is not None: attr.update(super_attr) if "core:RSSILevelState" in self.tahoma_device.active_states: diff --git a/homeassistant/components/tahoma/switch.py b/homeassistant/components/tahoma/switch.py index 2ea68b93e6b..e75c72e8d4b 100644 --- a/homeassistant/components/tahoma/switch.py +++ b/homeassistant/components/tahoma/switch.py @@ -108,8 +108,7 @@ class TahomaSwitch(TahomaDevice, SwitchEntity): def extra_state_attributes(self): """Return the device state attributes.""" attr = {} - super_attr = super().extra_state_attributes - if super_attr is not None: + if (super_attr := super().extra_state_attributes) is not None: attr.update(super_attr) if "core:RSSILevelState" in self.tahoma_device.active_states: diff --git a/homeassistant/components/tellstick/light.py b/homeassistant/components/tellstick/light.py index 15b15112d14..a355644da41 100644 --- a/homeassistant/components/tellstick/light.py +++ b/homeassistant/components/tellstick/light.py @@ -66,8 +66,7 @@ class TellstickLight(TellstickDevice, LightEntity): def _update_model(self, new_state, data): """Update the device entity state to match the arguments.""" if new_state: - brightness = data - if brightness is not None: + if (brightness := data) is not None: self._brightness = brightness # _brightness is not defined when called from super diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index c41bc55bcc8..b3f73cebc82 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -259,8 +259,7 @@ class TradfriLight(TradfriBaseDevice, LightEntity): transition_time = None # HSB can always be set, but color temp + brightness is bulb dependent - command = dimmer_command - if command is not None: + if (command := dimmer_command) is not None: command += color_command else: command = color_command diff --git a/homeassistant/components/trafikverket_train/sensor.py b/homeassistant/components/trafikverket_train/sensor.py index cd5cdf29521..d67fef5a0df 100644 --- a/homeassistant/components/trafikverket_train/sensor.py +++ b/homeassistant/components/trafikverket_train/sensor.py @@ -191,8 +191,7 @@ class TrainSensor(SensorEntity): @property def native_value(self): """Return the departure state.""" - state = self._state - if state is not None: + if (state := self._state) is not None: if state.time_at_location is not None: return state.time_at_location if state.estimated_time_at_location is not None: diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index e5f827d1e52..169aeed363b 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -101,8 +101,7 @@ class TransmissionSpeedSensor(TransmissionSensor): def update(self): """Get the latest data from Transmission and updates the state.""" - data = self._tm_client.api.data - if data: + if data := self._tm_client.api.data: mb_spd = ( float(data.downloadSpeed) if self._sub_type == "download" @@ -117,8 +116,7 @@ class TransmissionStatusSensor(TransmissionSensor): def update(self): """Get the latest data from Transmission and updates the state.""" - data = self._tm_client.api.data - if data: + if data := self._tm_client.api.data: upload = data.uploadSpeed download = data.downloadSpeed if upload > 0 and download > 0: diff --git a/homeassistant/components/travisci/sensor.py b/homeassistant/components/travisci/sensor.py index 427283260e0..826503391b5 100644 --- a/homeassistant/components/travisci/sensor.py +++ b/homeassistant/components/travisci/sensor.py @@ -172,8 +172,7 @@ class TravisCISensor(SensorEntity): self._build = self._data.build(repo.last_build_id) if self._build: - sensor_type = self.entity_description.key - if sensor_type == "state": + if (sensor_type := self.entity_description.key) == "state": branch_stats = self._data.branch(self._branch, self._repo_name) self._attr_native_value = branch_stats.state diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index d658a44a117..59cef93de49 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -256,8 +256,7 @@ class UniversalMediaPlayer(MediaPlayerEntity): ) return - active_child = self._child_state - if active_child is None: + if (active_child := self._child_state) is None: # No child to call service on return @@ -307,8 +306,7 @@ class UniversalMediaPlayer(MediaPlayerEntity): if (master_state == STATE_OFF) or (self._state_template is not None): return master_state - active_child = self._child_state - if active_child: + if active_child := self._child_state: return active_child.state return master_state if master_state else STATE_OFF diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index ff22c317bc1..7bf9dca700f 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -71,9 +71,7 @@ class ValloxSensor(SensorEntity): async def async_update(self) -> None: """Fetch state from the ventilation unit.""" - metric_key = self.entity_description.metric_key - - if metric_key is None: + if (metric_key := self.entity_description.metric_key) is None: self._attr_available = False _LOGGER.error("Error updating sensor. Empty metric key") return diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index 9a153841718..b6e13eb3832 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -270,8 +270,7 @@ class VeraDevice(Generic[DeviceType], Entity): attr[ATTR_ARMED] = "True" if armed else "False" if self.vera_device.is_trippable: - last_tripped = self.vera_device.last_trip - if last_tripped is not None: + if (last_tripped := self.vera_device.last_trip) is not None: utc_time = utc_from_timestamp(int(last_tripped)) attr[ATTR_LAST_TRIP_TIME] = utc_time.isoformat() else: @@ -279,12 +278,10 @@ class VeraDevice(Generic[DeviceType], Entity): tripped = self.vera_device.is_tripped attr[ATTR_TRIPPED] = "True" if tripped else "False" - power = self.vera_device.power - if power: + if power := self.vera_device.power: attr[ATTR_CURRENT_POWER_W] = convert(power, float, 0.0) - energy = self.vera_device.energy - if energy: + if energy := self.vera_device.energy: attr[ATTR_CURRENT_ENERGY_KWH] = convert(energy, float, 0.0) attr["Vera Device Id"] = self.vera_device.vera_device_id diff --git a/homeassistant/components/vera/climate.py b/homeassistant/components/vera/climate.py index cde36dcc623..69d5a3ccbfa 100644 --- a/homeassistant/components/vera/climate.py +++ b/homeassistant/components/vera/climate.py @@ -114,9 +114,9 @@ class VeraThermostat(VeraDevice[veraApi.VeraThermostat], ClimateEntity): @property def current_power_w(self) -> float | None: """Return the current power usage in W.""" - power = self.vera_device.power - if power: + if power := self.vera_device.power: return convert(power, float, 0.0) + return None @property def temperature_unit(self) -> str: diff --git a/homeassistant/components/vera/switch.py b/homeassistant/components/vera/switch.py index 304441037ec..9f44bb28286 100644 --- a/homeassistant/components/vera/switch.py +++ b/homeassistant/components/vera/switch.py @@ -61,9 +61,9 @@ class VeraSwitch(VeraDevice[veraApi.VeraSwitch], SwitchEntity): @property def current_power_w(self) -> float | None: """Return the current power usage in W.""" - power = self.vera_device.power - if power: + if power := self.vera_device.power: return convert(power, float, 0.0) + return None @property def is_on(self) -> bool: diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index 6641f43d17b..c32ac6d2a25 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -76,10 +76,11 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): @property def percentage(self): """Return the current speed.""" - if self.smartfan.mode == "manual": - current_level = self.smartfan.fan_level - if current_level is not None: - return ranged_value_to_percentage(SPEED_RANGE, current_level) + if ( + self.smartfan.mode == "manual" + and (current_level := self.smartfan.fan_level) is not None + ): + return ranged_value_to_percentage(SPEED_RANGE, current_level) return None @property diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index d4965be841d..81d245c19bb 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -185,28 +185,22 @@ class WeatherEntity(Entity): self.hass, self.temperature, self.temperature_unit, self.precision ) - humidity = self.humidity - if humidity is not None: + if (humidity := self.humidity) is not None: data[ATTR_WEATHER_HUMIDITY] = round(humidity) - ozone = self.ozone - if ozone is not None: + if (ozone := self.ozone) is not None: data[ATTR_WEATHER_OZONE] = ozone - pressure = self.pressure - if pressure is not None: + if (pressure := self.pressure) is not None: data[ATTR_WEATHER_PRESSURE] = pressure - wind_bearing = self.wind_bearing - if wind_bearing is not None: + if (wind_bearing := self.wind_bearing) is not None: data[ATTR_WEATHER_WIND_BEARING] = wind_bearing - wind_speed = self.wind_speed - if wind_speed is not None: + if (wind_speed := self.wind_speed) is not None: data[ATTR_WEATHER_WIND_SPEED] = wind_speed - visibility = self.visibility - if visibility is not None: + if (visibility := self.visibility) is not None: data[ATTR_WEATHER_VISIBILITY] = visibility if self.forecast is not None: diff --git a/homeassistant/components/wemo/entity.py b/homeassistant/components/wemo/entity.py index 62b23b78bd7..2811d371f6b 100644 --- a/homeassistant/components/wemo/entity.py +++ b/homeassistant/components/wemo/entity.py @@ -40,8 +40,7 @@ class WemoEntity(CoordinatorEntity): @property def name(self) -> str: """Return the name of the device if any.""" - suffix = self.name_suffix - if suffix: + if suffix := self.name_suffix: return f"{self.wemo.name} {suffix}" return self.wemo.name @@ -60,8 +59,7 @@ class WemoEntity(CoordinatorEntity): @property def unique_id(self) -> str: """Return the id of this WeMo device.""" - suffix = self.unique_id_suffix - if suffix: + if suffix := self.unique_id_suffix: return f"{self.wemo.serialnumber}_{suffix}" return self.wemo.serialnumber diff --git a/homeassistant/components/wiffi/__init__.py b/homeassistant/components/wiffi/__init__.py index e19fe227ddb..5f87141e423 100644 --- a/homeassistant/components/wiffi/__init__.py +++ b/homeassistant/components/wiffi/__init__.py @@ -103,8 +103,7 @@ class WiffiIntegrationApi: Remove listener for periodic callbacks. """ - remove_listener = self._periodic_callback - if remove_listener is not None: + if (remove_listener := self._periodic_callback) is not None: remove_listener() async def __call__(self, device, metrics): diff --git a/homeassistant/components/xbox/media_player.py b/homeassistant/components/xbox/media_player.py index be798cd999a..17390f81fad 100644 --- a/homeassistant/components/xbox/media_player.py +++ b/homeassistant/components/xbox/media_player.py @@ -127,8 +127,7 @@ class XboxMediaPlayer(CoordinatorEntity, MediaPlayerEntity): @property def media_title(self): """Title of current playing media.""" - app_details = self.data.app_details - if not app_details: + if not (app_details := self.data.app_details): return None return ( app_details.localized_properties[0].product_title @@ -138,8 +137,7 @@ class XboxMediaPlayer(CoordinatorEntity, MediaPlayerEntity): @property def media_image_url(self): """Image url of current playing media.""" - app_details = self.data.app_details - if not app_details: + if not (app_details := self.data.app_details): return None image = _find_media_image(app_details.localized_properties[0].images) diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index 5081a716357..758d39a8dfb 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -664,8 +664,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): """Return all media players of the current group, if the media player is server.""" if self.is_client: # If we are a client we can still share group information, but we will take them from the server. - server = self.group_server - if server != self: + if (server := self.group_server) != self: return server.musiccast_group return [self] diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index e734c9cb415..b82cccd5324 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -580,8 +580,7 @@ class ZenWithinThermostat(Thermostat): def _rm_rs_action(self) -> str | None: """Return the current HVAC action based on running mode and running state.""" - running_state = self._thrm.running_state - if running_state is None: + if (running_state := self._thrm.running_state) is None: return None if running_state & (RunningState.HEAT | RunningState.HEAT_STAGE_2): return CURRENT_HVAC_HEAT diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 18df552986d..8e8a92c099a 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -388,8 +388,7 @@ class SmartEnergyMetering(Sensor): attrs = {} if self._channel.device_type is not None: attrs["device_type"] = self._channel.device_type - status = self._channel.status - if status is not None: + if (status := self._channel.status) is not None: attrs["status"] = str(status)[len(status.__class__.__name__) + 1 :] return attrs @@ -578,8 +577,7 @@ class ZenHVACAction(ThermostatHVACAction): def _rm_rs_action(self) -> str | None: """Return the current HVAC action based on running mode and running state.""" - running_state = self._channel.running_state - if running_state is None: + if (running_state := self._channel.running_state) is None: return None rs_heat = ( diff --git a/homeassistant/components/zoneminder/sensor.py b/homeassistant/components/zoneminder/sensor.py index 3384bad758c..0eb3e9d63a2 100644 --- a/homeassistant/components/zoneminder/sensor.py +++ b/homeassistant/components/zoneminder/sensor.py @@ -111,8 +111,7 @@ class ZMSensorMonitors(SensorEntity): def update(self): """Update the sensor.""" - state = self._monitor.function - if not state: + if not (state := self._monitor.function): self._state = None else: self._state = state.value diff --git a/homeassistant/components/zwave/climate.py b/homeassistant/components/zwave/climate.py index a09f839e6c4..0519d42a59c 100644 --- a/homeassistant/components/zwave/climate.py +++ b/homeassistant/components/zwave/climate.py @@ -248,8 +248,7 @@ class ZWaveClimateBase(ZWaveDeviceEntity, ClimateEntity): self._preset_list = [] self._preset_mapping = {} - mode_list = self._mode().data_items - if mode_list: + if mode_list := self._mode().data_items: for mode in mode_list: ha_mode = HVAC_STATE_MAPPINGS.get(str(mode).lower()) ha_preset = PRESET_MAPPINGS.get(str(mode).lower()) @@ -342,8 +341,7 @@ class ZWaveClimateBase(ZWaveDeviceEntity, ClimateEntity): """Update fan mode.""" if self.values.fan_mode: self._current_fan_mode = self.values.fan_mode.data - fan_modes = self.values.fan_mode.data_items - if fan_modes: + if fan_modes := self.values.fan_mode.data_items: self._fan_modes = list(fan_modes) _LOGGER.debug("self._fan_modes=%s", self._fan_modes) diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 05365b85645..2f9f0b52839 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -53,8 +53,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): # Get current discovered entries. in_progress = self._async_in_progress() - has_devices = in_progress - if not has_devices: + if not (has_devices := in_progress): has_devices = await self.hass.async_add_job( # type: ignore self._discovery_function, self.hass ) From 2a8eaf0e0fc07eb617cdcdefbe7e4b94574b8391 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 17 Oct 2021 20:05:11 +0200 Subject: [PATCH 0476/1038] Use assignment expressions 06 (#57786) --- .../components/entur_public_transport/sensor.py | 3 +-- .../components/environment_canada/weather.py | 3 +-- homeassistant/components/esphome/light.py | 3 +-- homeassistant/components/fan/__init__.py | 9 +++------ homeassistant/components/fastdotcom/sensor.py | 3 +-- homeassistant/components/fibaro/__init__.py | 14 ++++++-------- homeassistant/components/fido/sensor.py | 3 +-- .../components/fireservicerota/sensor.py | 3 +-- .../components/fortios/device_tracker.py | 4 +--- homeassistant/components/frontend/__init__.py | 3 +-- homeassistant/components/gdacs/geo_location.py | 3 +-- homeassistant/components/glances/sensor.py | 3 +-- .../components/google_assistant/helpers.py | 4 +--- .../components/google_assistant/trait.py | 15 ++++----------- homeassistant/components/harmony/remote.py | 6 ++---- .../components/here_travel_time/sensor.py | 6 ++---- homeassistant/components/homeassistant/scene.py | 3 +-- .../components/homekit/type_thermostats.py | 3 +-- .../components/homekit_controller/air_quality.py | 3 +-- .../components/homematicip_cloud/binary_sensor.py | 4 ++-- .../homematicip_cloud/generic_entity.py | 6 ++---- 21 files changed, 35 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/entur_public_transport/sensor.py b/homeassistant/components/entur_public_transport/sensor.py index 3256b26171b..776d1c17618 100644 --- a/homeassistant/components/entur_public_transport/sensor.py +++ b/homeassistant/components/entur_public_transport/sensor.py @@ -206,8 +206,7 @@ class EnturPublicTransportSensor(SensorEntity): self._attributes[CONF_LATITUDE] = data.latitude self._attributes[CONF_LONGITUDE] = data.longitude - calls = data.estimated_calls - if not calls: + if not (calls := data.estimated_calls): self._state = None return diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index 281cf117426..e54e9f8767d 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -202,8 +202,7 @@ def get_forecast(ec_data, forecast_type): forecast_array = [] if forecast_type == "daily": - half_days = ec_data.daily_forecasts - if not half_days: + if not (half_days := ec_data.daily_forecasts): return None today = { diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index b8fe4bd74c7..eb5e258f079 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -281,8 +281,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): def color_mode(self) -> str | None: """Return the color mode of the light.""" if not self._supports_color_mode: - supported = self.supported_color_modes - if not supported: + if not (supported := self.supported_color_modes): return None return next(iter(supported)) diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index a05505e8112..3a5b9bcda67 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -458,13 +458,10 @@ class FanEntity(ToggleEntity): @property def speed(self) -> str | None: """Return the current speed.""" - if self._implemented_preset_mode: - preset_mode = self.preset_mode - if preset_mode: - return preset_mode + if self._implemented_preset_mode and (preset_mode := self.preset_mode): + return preset_mode if self._implemented_percentage: - percentage = self.percentage - if percentage is None: + if (percentage := self.percentage) is None: return None return self.percentage_to_speed(percentage) return None diff --git a/homeassistant/components/fastdotcom/sensor.py b/homeassistant/components/fastdotcom/sensor.py index fa1f18815f1..2a82cee7cea 100644 --- a/homeassistant/components/fastdotcom/sensor.py +++ b/homeassistant/components/fastdotcom/sensor.py @@ -56,8 +56,7 @@ class SpeedtestSensor(RestoreEntity, SensorEntity): def update(self) -> None: """Get the latest data and update the states.""" - data = self._speedtest_data.data # type: ignore[attr-defined] - if data is None: + if (data := self._speedtest_data.data) is None: # type: ignore[attr-defined] return self._attr_native_value = data["download"] diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index 964c1112840..cff4e153d98 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -307,8 +307,7 @@ class FibaroController: device.device_config = self._device_config.get(device.ha_id, {}) else: device.mapped_type = None - dtype = device.mapped_type - if dtype is None: + if (dtype := device.mapped_type) is None: continue device.unique_id_str = f"{self.hub_serial}.{device.id}" self._device_map[device.id] = device @@ -472,12 +471,11 @@ class FibaroDevice(Entity): @property def current_power_w(self): """Return the current power usage in W.""" - if "power" in self.fibaro_device.properties: - power = self.fibaro_device.properties.power - if power: - return convert(power, float, 0.0) - else: - return None + if "power" in self.fibaro_device.properties and ( + power := self.fibaro_device.properties.power + ): + return convert(power, float, 0.0) + return None @property def current_binary_state(self): diff --git a/homeassistant/components/fido/sensor.py b/homeassistant/components/fido/sensor.py index 6964abd9b3d..0a06c7a2b07 100644 --- a/homeassistant/components/fido/sensor.py +++ b/homeassistant/components/fido/sensor.py @@ -218,8 +218,7 @@ class FidoSensor(SensorEntity): async def async_update(self): """Get the latest data from Fido and update the state.""" await self.fido_data.async_update() - sensor_type = self.entity_description.key - if sensor_type == "balance": + if (sensor_type := self.entity_description.key) == "balance": if self.fido_data.data.get(sensor_type) is not None: self._attr_native_value = round(self.fido_data.data[sensor_type], 2) else: diff --git a/homeassistant/components/fireservicerota/sensor.py b/homeassistant/components/fireservicerota/sensor.py index ec446621212..a87b1609ec9 100644 --- a/homeassistant/components/fireservicerota/sensor.py +++ b/homeassistant/components/fireservicerota/sensor.py @@ -67,9 +67,8 @@ class IncidentsSensor(RestoreEntity, SensorEntity): def extra_state_attributes(self) -> object: """Return available attributes for sensor.""" attr = {} - data = self._state_attributes - if not data: + if not (data := self._state_attributes): return attr for value in ( diff --git a/homeassistant/components/fortios/device_tracker.py b/homeassistant/components/fortios/device_tracker.py index 1b9134bee44..a1507af99dc 100644 --- a/homeassistant/components/fortios/device_tracker.py +++ b/homeassistant/components/fortios/device_tracker.py @@ -102,9 +102,7 @@ class FortiOSDeviceScanner(DeviceScanner): device = device.lower() - data = self._clients_json - - if data == 0: + if (data := self._clients_json) == 0: _LOGGER.error("No json results to get device names") return None diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 8b92745f4d4..005384b6beb 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -567,8 +567,7 @@ class IndexView(web_urldispatcher.AbstractResource): def get_template(self) -> jinja2.Template: """Get template.""" - tpl = self._template_cache - if tpl is None: + if (tpl := self._template_cache) is None: with (_frontend_root(self.repo_path) / "index.html").open( encoding="utf8" ) as file: diff --git a/homeassistant/components/gdacs/geo_location.py b/homeassistant/components/gdacs/geo_location.py index 7e3dc7484bb..fb0782de525 100644 --- a/homeassistant/components/gdacs/geo_location.py +++ b/homeassistant/components/gdacs/geo_location.py @@ -138,8 +138,7 @@ class GdacsEvent(GeolocationEvent): def _update_from_feed(self, feed_entry): """Update the internal state from the provided feed entry.""" - event_name = feed_entry.event_name - if not event_name: + if not (event_name := feed_entry.event_name): # Earthquakes usually don't have an event name. event_name = f"{feed_entry.country} ({feed_entry.event_id})" self._title = f"{feed_entry.event_type}: {event_name}" diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 92173f9d143..b0960b3531a 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -113,8 +113,7 @@ class GlancesSensor(SensorEntity): async def async_update(self): # noqa: C901 """Get the latest data from REST API.""" - value = self.glances_data.api.data - if value is None: + if (value := self.glances_data.api.data) is None: return if self.entity_description.type == "fs": diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 534fdcfac1e..67f4e0c60df 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -262,9 +262,7 @@ class AbstractConfig(ABC): @callback def async_enable_local_sdk(self): """Enable the local SDK.""" - webhook_id = self.local_sdk_webhook_id - - if webhook_id is None: + if (webhook_id := self.local_sdk_webhook_id) is None: return try: diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 0301769aea7..9f79f0f7d9b 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -255,9 +255,7 @@ class BrightnessTrait(_Trait): async def execute(self, command, data, params, challenge): """Execute a brightness command.""" - domain = self.state.domain - - if domain == light.DOMAIN: + if self.state.domain == light.DOMAIN: await self.hass.services.async_call( light.DOMAIN, light.SERVICE_TURN_ON, @@ -348,9 +346,7 @@ class OnOffTrait(_Trait): async def execute(self, command, data, params, challenge): """Execute an OnOff command.""" - domain = self.state.domain - - if domain == group.DOMAIN: + if (domain := self.state.domain) == group.DOMAIN: service_domain = HA_DOMAIN service = SERVICE_TURN_ON if params["on"] else SERVICE_TURN_OFF @@ -1156,9 +1152,7 @@ class HumiditySettingTrait(_Trait): async def execute(self, command, data, params, challenge): """Execute a humidity command.""" - domain = self.state.domain - - if domain == sensor.DOMAIN: + if self.state.domain == sensor.DOMAIN: raise SmartHomeError( ERR_NOT_SUPPORTED, "Execute is not supported by sensor" ) @@ -1449,8 +1443,7 @@ class FanSpeedTrait(_Trait): async def execute_reverse(self, data, params): """Execute a Reverse command.""" - domain = self.state.domain - if domain == fan.DOMAIN: + if self.state.domain == fan.DOMAIN: if self.state.attributes.get(fan.ATTR_DIRECTION) == fan.DIRECTION_FORWARD: direction = fan.DIRECTION_REVERSE else: diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index 806b638aee8..6a13093ec25 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -212,8 +212,7 @@ class HarmonyRemote(HarmonyEntity, remote.RemoteEntity, RestoreEntity): if self._last_activity: activity = self._last_activity else: - all_activities = self._data.activity_names - if all_activities: + if all_activities := self._data.activity_names: activity = all_activities[0] if activity: @@ -257,8 +256,7 @@ class HarmonyRemote(HarmonyEntity, remote.RemoteEntity, RestoreEntity): _LOGGER.debug( "%s: Writing hub configuration to file: %s", self.name, self._config_path ) - json_config = self._data.json_config - if json_config is None: + if (json_config := self._data.json_config) is None: _LOGGER.warning("%s: No configuration received from hub", self.name) return diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 7606a2772d6..a47e28179b3 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -416,11 +416,9 @@ class HERETravelTimeData: # Convert location to HERE friendly location destination = self.destination.split(",") origin = self.origin.split(",") - arrival = self.arrival - if arrival is not None: + if (arrival := self.arrival) is not None: arrival = convert_time_to_isodate(arrival) - departure = self.departure - if departure is not None: + if (departure := self.departure) is not None: departure = convert_time_to_isodate(departure) if departure is None and arrival is None: diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index 9ae271baa72..e5b37dd3f01 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -311,8 +311,7 @@ class HomeAssistantScene(Scene): def extra_state_attributes(self): """Return the scene state attributes.""" attributes = {ATTR_ENTITY_ID: list(self.scene_config.states)} - unique_id = self.unique_id - if unique_id is not None: + if (unique_id := self.unique_id) is not None: attributes[CONF_ID] = unique_id return attributes diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 95c5f87b6c2..c75cc95b169 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -609,8 +609,7 @@ class WaterHeater(HomeAccessory): self.char_display_units.set_value(unit) # Update target operation mode - operation_mode = new_state.state - if operation_mode: + if new_state.state: self.char_target_heat_cool.set_value(1) # Heat diff --git a/homeassistant/components/homekit_controller/air_quality.py b/homeassistant/components/homekit_controller/air_quality.py index b4ca2f4918a..df5a89f179e 100644 --- a/homeassistant/components/homekit_controller/air_quality.py +++ b/homeassistant/components/homekit_controller/air_quality.py @@ -91,8 +91,7 @@ class HomeAirQualitySensor(HomeKitEntity, AirQualityEntity): """Return the device state attributes.""" data = {"air_quality_text": self.air_quality_text} - voc = self.volatile_organic_compounds - if voc: + if voc := self.volatile_organic_compounds: data["volatile_organic_compounds"] = voc return data diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 80dfa8316d0..3cf8fc72c56 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -544,8 +544,8 @@ class HomematicipSecuritySensorGroup( @property def is_on(self) -> bool: """Return true if safety issue detected.""" - parent_is_on = super().is_on - if parent_is_on: + if super().is_on: + # parent is on return True if ( diff --git a/homeassistant/components/homematicip_cloud/generic_entity.py b/homeassistant/components/homematicip_cloud/generic_entity.py index f1edff1854b..976650aa46b 100644 --- a/homeassistant/components/homematicip_cloud/generic_entity.py +++ b/homeassistant/components/homematicip_cloud/generic_entity.py @@ -153,8 +153,7 @@ class HomematicipGenericEntity(Entity): if not self.registry_entry: return - device_id = self.registry_entry.device_id - if device_id: + if device_id := self.registry_entry.device_id: # Remove from device registry. device_registry = await dr.async_get_registry(self.hass) if device_id in device_registry.devices: @@ -163,8 +162,7 @@ class HomematicipGenericEntity(Entity): else: # Remove from entity registry. # Only relevant for entities that do not belong to a device. - entity_id = self.registry_entry.entity_id - if entity_id: + if entity_id := self.registry_entry.entity_id: entity_registry = await er.async_get_registry(self.hass) if entity_id in entity_registry.entities: entity_registry.async_remove(entity_id) From 238b48864292cd2b6d58acfe263f143e786d6598 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 17 Oct 2021 20:08:11 +0200 Subject: [PATCH 0477/1038] Use assignment expressions 03 (#57710) --- homeassistant/helpers/aiohttp_client.py | 3 +- homeassistant/helpers/condition.py | 31 ++++++------------- .../helpers/config_entry_oauth2_flow.py | 12 ++----- homeassistant/helpers/data_entry_flow.py | 3 +- homeassistant/helpers/discovery.py | 4 +-- homeassistant/helpers/entity_platform.py | 3 +- homeassistant/helpers/intent.py | 3 +- homeassistant/helpers/location.py | 4 +-- homeassistant/helpers/network.py | 3 +- homeassistant/helpers/ratelimit.py | 3 +- homeassistant/helpers/script.py | 12 +++---- homeassistant/helpers/selector.py | 4 +-- homeassistant/helpers/service.py | 9 +++--- homeassistant/helpers/template.py | 13 +++----- homeassistant/helpers/trace.py | 18 ++++------- homeassistant/loader.py | 13 +++----- homeassistant/requirements.py | 10 ++---- homeassistant/runner.py | 3 +- 18 files changed, 49 insertions(+), 102 deletions(-) diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index c7f77bf086d..3e5496b4179 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -192,8 +192,7 @@ def _async_register_clientsession_shutdown( EVENT_HOMEASSISTANT_CLOSE, _async_close_websession ) - config_entry = config_entries.current_entry.get() - if not config_entry: + if not (config_entry := config_entries.current_entry.get()): return config_entry.async_on_unload(unsub) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index f90086f87ee..a7064640307 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -328,9 +328,8 @@ def async_numeric_state( # noqa: C901 if isinstance(entity, str): entity_id = entity - entity = hass.states.get(entity) - if entity is None: + if (entity := hass.states.get(entity)) is None: raise ConditionErrorMessage("numeric_state", f"unknown entity {entity_id}") else: entity_id = entity.entity_id @@ -371,8 +370,7 @@ def async_numeric_state( # noqa: C901 if below is not None: if isinstance(below, str): - below_entity = hass.states.get(below) - if not below_entity: + if not (below_entity := hass.states.get(below)): raise ConditionErrorMessage( "numeric_state", f"unknown 'below' entity {below}" ) @@ -400,8 +398,7 @@ def async_numeric_state( # noqa: C901 if above is not None: if isinstance(above, str): - above_entity = hass.states.get(above) - if not above_entity: + if not (above_entity := hass.states.get(above)): raise ConditionErrorMessage( "numeric_state", f"unknown 'above' entity {above}" ) @@ -497,9 +494,8 @@ def state( if isinstance(entity, str): entity_id = entity - entity = hass.states.get(entity) - if entity is None: + if (entity := hass.states.get(entity)) is None: raise ConditionErrorMessage("state", f"unknown entity {entity_id}") else: entity_id = entity.entity_id @@ -526,8 +522,7 @@ def state( isinstance(req_state_value, str) and INPUT_ENTITY_ID.match(req_state_value) is not None ): - state_entity = hass.states.get(req_state_value) - if not state_entity: + if not (state_entity := hass.states.get(req_state_value)): raise ConditionErrorMessage( "state", f"the 'state' entity {req_state_value} is unavailable" ) @@ -738,8 +733,7 @@ def time( if after is None: after = dt_util.dt.time(0) elif isinstance(after, str): - after_entity = hass.states.get(after) - if not after_entity: + if not (after_entity := hass.states.get(after)): raise ConditionErrorMessage("time", f"unknown 'after' entity {after}") if after_entity.domain == "input_datetime": after = dt_util.dt.time( @@ -763,8 +757,7 @@ def time( if before is None: before = dt_util.dt.time(23, 59, 59, 999999) elif isinstance(before, str): - before_entity = hass.states.get(before) - if not before_entity: + if not (before_entity := hass.states.get(before)): raise ConditionErrorMessage("time", f"unknown 'before' entity {before}") if before_entity.domain == "input_datetime": before = dt_util.dt.time( @@ -840,9 +833,8 @@ def zone( if isinstance(zone_ent, str): zone_ent_id = zone_ent - zone_ent = hass.states.get(zone_ent) - if zone_ent is None: + if (zone_ent := hass.states.get(zone_ent)) is None: raise ConditionErrorMessage("zone", f"unknown zone {zone_ent_id}") if entity is None: @@ -850,9 +842,8 @@ def zone( if isinstance(entity, str): entity_id = entity - entity = hass.states.get(entity) - if entity is None: + if (entity := hass.states.get(entity)) is None: raise ConditionErrorMessage("zone", f"unknown entity {entity_id}") else: entity_id = entity.entity_id @@ -1029,9 +1020,7 @@ def async_extract_devices(config: ConfigType | Template) -> set[str]: if condition != "device": continue - device_id = config.get(CONF_DEVICE_ID) - - if device_id is not None: + if (device_id := config.get(CONF_DEVICE_ID)) is not None: referenced.add(device_id) return referenced diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 014c1f4f272..54f257cb781 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -129,14 +129,10 @@ class LocalOAuth2Implementation(AbstractOAuth2Implementation): @property def redirect_uri(self) -> str: """Return the redirect uri.""" - req = http.current_request.get() - - if req is None: + if (req := http.current_request.get()) is None: raise RuntimeError("No current request in context") - ha_host = req.headers.get(HEADER_FRONTEND_BASE) - - if ha_host is None: + if (ha_host := req.headers.get(HEADER_FRONTEND_BASE)) is None: raise RuntimeError("No header in request") return f"{ha_host}{AUTH_CALLBACK_PATH}" @@ -501,9 +497,7 @@ async def async_oauth2_request( @callback def _encode_jwt(hass: HomeAssistant, data: dict) -> str: """JWT encode data.""" - secret = hass.data.get(DATA_JWT_SECRET) - - if secret is None: + if (secret := hass.data.get(DATA_JWT_SECRET)) is None: secret = hass.data[DATA_JWT_SECRET] = secrets.token_hex() return jwt.encode(data, secret, algorithm="HS256") diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 7cdb1823ae0..787041700f4 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -38,8 +38,7 @@ class _BaseFlowManagerView(HomeAssistantView): data = result.copy() - schema = data["data_schema"] - if schema is None: + if (schema := data["data_schema"]) is None: data["data_schema"] = [] else: data["data_schema"] = voluptuous_serialize.convert( diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index 3f7523e9299..1923ba9556c 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -111,9 +111,7 @@ def async_listen_platform( async def discovery_platform_listener(discovered: DiscoveryDict) -> None: """Listen for platform discovery events.""" - platform = discovered["platform"] - - if not platform: + if not (platform := discovered["platform"]): return task = hass.async_run_hass_job(job, platform, discovered.get("discovered")) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 51e29647a9e..ccc9b1663a8 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -727,8 +727,7 @@ current_platform: ContextVar[EntityPlatform | None] = ContextVar( @callback def async_get_current_platform() -> EntityPlatform: """Get the current platform from context.""" - platform = current_platform.get() - if platform is None: + if (platform := current_platform.get()) is None: raise RuntimeError("Cannot get non-set current platform") return platform diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index c25cc25e99b..a153d994471 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -33,8 +33,7 @@ SPEECH_TYPE_SSML = "ssml" @bind_hass def async_register(hass: HomeAssistant, handler: IntentHandler) -> None: """Register an intent with Home Assistant.""" - intents = hass.data.get(DATA_KEY) - if intents is None: + if (intents := hass.data.get(DATA_KEY)) is None: intents = hass.data[DATA_KEY] = {} assert handler.intent_type is not None, "intent_type cannot be None" diff --git a/homeassistant/helpers/location.py b/homeassistant/helpers/location.py index da81040185c..e9d3f34b970 100644 --- a/homeassistant/helpers/location.py +++ b/homeassistant/helpers/location.py @@ -51,9 +51,7 @@ def find_coordinates( hass: HomeAssistant, entity_id: str, recursion_history: list | None = None ) -> str | None: """Find the gps coordinates of the entity in the form of '90.000,180.000'.""" - entity_state = hass.states.get(entity_id) - - if entity_state is None: + if (entity_state := hass.states.get(entity_id)) is None: _LOGGER.error("Unable to find entity %s", entity_id) return None diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py index 6ed8084413f..0b2804f821d 100644 --- a/homeassistant/helpers/network.py +++ b/homeassistant/helpers/network.py @@ -118,8 +118,7 @@ def get_url( def _get_request_host() -> str | None: """Get the host address of the current request.""" - request = http.current_request.get() - if request is None: + if (request := http.current_request.get()) is None: raise NoURLAvailableError return yarl.URL(request.url).host diff --git a/homeassistant/helpers/ratelimit.py b/homeassistant/helpers/ratelimit.py index 350f50423e1..1da79eb5f7d 100644 --- a/homeassistant/helpers/ratelimit.py +++ b/homeassistant/helpers/ratelimit.py @@ -78,8 +78,7 @@ class KeyedRateLimit: if rate_limit is None: return None - last_triggered = self._last_triggered.get(key) - if not last_triggered: + if not (last_triggered := self._last_triggered.get(key)): return None next_call_time = last_triggered + rate_limit diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index b6651d1dd47..a79caad74b0 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -953,8 +953,7 @@ class Script: variables: ScriptVariables | None = None, ) -> None: """Initialize the script.""" - all_scripts = hass.data.get(DATA_SCRIPTS) - if not all_scripts: + if not (all_scripts := hass.data.get(DATA_SCRIPTS)): all_scripts = hass.data[DATA_SCRIPTS] = [] hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, partial(_async_stop_scripts_at_shutdown, hass) @@ -1273,8 +1272,7 @@ class Script: config_cache_key = config.template else: config_cache_key = frozenset((k, str(v)) for k, v in config.items()) - cond = self._config_cache.get(config_cache_key) - if not cond: + if not (cond := self._config_cache.get(config_cache_key)): cond = await condition.async_from_config(self._hass, config, False) self._config_cache[config_cache_key] = cond return cond @@ -1297,8 +1295,7 @@ class Script: return sub_script def _get_repeat_script(self, step: int) -> Script: - sub_script = self._repeat_script.get(step) - if not sub_script: + if not (sub_script := self._repeat_script.get(step)): sub_script = self._prep_repeat_script(step) self._repeat_script[step] = sub_script return sub_script @@ -1351,8 +1348,7 @@ class Script: return {"choices": choices, "default": default_script} async def _async_get_choose_data(self, step: int) -> _ChooseData: - choose_data = self._choose_data.get(step) - if not choose_data: + if not (choose_data := self._choose_data.get(step)): choose_data = await self._async_prep_choose_data(step) self._choose_data[step] = choose_data return choose_data diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 6258d6db1c3..f17b610ff23 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -22,9 +22,7 @@ def validate_selector(config: Any) -> dict: selector_type = list(config)[0] - selector_class = SELECTORS.get(selector_type) - - if selector_class is None: + if (selector_class := SELECTORS.get(selector_type)) is None: raise vol.Invalid(f"Unknown selector type {selector_type} found") # Selectors can be empty diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 800d494fdbb..57bcefe56e3 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -396,10 +396,11 @@ async def async_extract_config_entry_ids( # Some devices may have no entities for device_id in referenced.referenced_devices: - if device_id in dev_reg.devices: - device = dev_reg.async_get(device_id) - if device is not None: - config_entry_ids.update(device.config_entries) + if ( + device_id in dev_reg.devices + and (device := dev_reg.async_get(device_id)) is not None + ): + config_entry_ids.update(device.config_entries) for entity_id in referenced.referenced | referenced.indirectly_referenced: entry = ent_reg.async_get(entity_id) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 4a7676960b4..8a72f7bdda4 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -813,8 +813,7 @@ class TemplateState(State): def _collect_state(hass: HomeAssistant, entity_id: str) -> None: - entity_collect = hass.data.get(_RENDER_INFO) - if entity_collect is not None: + if (entity_collect := hass.data.get(_RENDER_INFO)) is not None: entity_collect.entities.add(entity_id) @@ -1188,8 +1187,7 @@ def state_attr(hass: HomeAssistant, entity_id: str, name: str) -> Any: def now(hass: HomeAssistant) -> datetime: """Record fetching now.""" - render_info = hass.data.get(_RENDER_INFO) - if render_info is not None: + if (render_info := hass.data.get(_RENDER_INFO)) is not None: render_info.has_time = True return dt_util.now() @@ -1197,8 +1195,7 @@ def now(hass: HomeAssistant) -> datetime: def utcnow(hass: HomeAssistant) -> datetime: """Record fetching utcnow.""" - render_info = hass.data.get(_RENDER_INFO) - if render_info is not None: + if (render_info := hass.data.get(_RENDER_INFO)) is not None: render_info.has_time = True return dt_util.utcnow() @@ -1843,9 +1840,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): # any instance of this. return super().compile(source, name, filename, raw, defer_init) - cached = self.template_cache.get(source) - - if cached is None: + if (cached := self.template_cache.get(source)) is None: cached = self.template_cache[source] = super().compile(source) return cached diff --git a/homeassistant/helpers/trace.py b/homeassistant/helpers/trace.py index 0c364124c50..f779ccb84c1 100644 --- a/homeassistant/helpers/trace.py +++ b/homeassistant/helpers/trace.py @@ -113,8 +113,7 @@ def trace_id_get() -> tuple[tuple[str, str], str] | None: def trace_stack_push(trace_stack_var: ContextVar, node: Any) -> None: """Push an element to the top of a trace stack.""" - trace_stack = trace_stack_var.get() - if trace_stack is None: + if (trace_stack := trace_stack_var.get()) is None: trace_stack = [] trace_stack_var.set(trace_stack) trace_stack.append(node) @@ -149,8 +148,7 @@ def trace_path_pop(count: int) -> None: def trace_path_get() -> str: """Return a string representing the current location in the config tree.""" - path = trace_path_stack_cv.get() - if not path: + if not (path := trace_path_stack_cv.get()): return "" return "/".join(path) @@ -160,12 +158,10 @@ def trace_append_element( maxlen: int | None = None, ) -> None: """Append a TraceElement to trace[path].""" - path = trace_element.path - trace = trace_cv.get() - if trace is None: + if (trace := trace_cv.get()) is None: trace = {} trace_cv.set(trace) - if path not in trace: + if (path := trace_element.path) not in trace: trace[path] = deque(maxlen=maxlen) trace[path].append(trace_element) @@ -213,16 +209,14 @@ class StopReason: def script_execution_set(reason: str) -> None: """Set stop reason.""" - data = script_execution_cv.get() - if data is None: + if (data := script_execution_cv.get()) is None: return data.script_execution = reason def script_execution_get() -> str | None: """Return the current trace.""" - data = script_execution_cv.get() - if data is None: + if (data := script_execution_cv.get()) is None: return None return data.script_execution diff --git a/homeassistant/loader.py b/homeassistant/loader.py index fbbd4ad3ae6..1d8d7e8e98f 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -146,9 +146,7 @@ async def async_get_custom_components( hass: HomeAssistant, ) -> dict[str, Integration]: """Return cached list of custom integrations.""" - reg_or_evt = hass.data.get(DATA_CUSTOM_COMPONENTS) - - if reg_or_evt is None: + if (reg_or_evt := hass.data.get(DATA_CUSTOM_COMPONENTS)) is None: evt = hass.data[DATA_CUSTOM_COMPONENTS] = asyncio.Event() reg = await _async_get_custom_components(hass) @@ -543,8 +541,7 @@ class Integration: async def async_get_integration(hass: HomeAssistant, domain: str) -> Integration: """Get an integration.""" - cache = hass.data.get(DATA_INTEGRATIONS) - if cache is None: + if (cache := hass.data.get(DATA_INTEGRATIONS)) is None: if not _async_mount_config_dir(hass): raise IntegrationNotFound(domain) cache = hass.data[DATA_INTEGRATIONS] = {} @@ -553,12 +550,11 @@ async def async_get_integration(hass: HomeAssistant, domain: str) -> Integration if isinstance(int_or_evt, asyncio.Event): await int_or_evt.wait() - int_or_evt = cache.get(domain, _UNDEF) # When we have waited and it's _UNDEF, it doesn't exist # We don't cache that it doesn't exist, or else people can't fix it # and then restart, because their config will never be valid. - if int_or_evt is _UNDEF: + if (int_or_evt := cache.get(domain, _UNDEF)) is _UNDEF: raise IntegrationNotFound(domain) if int_or_evt is not _UNDEF: @@ -630,8 +626,7 @@ def _load_file( with suppress(KeyError): return hass.data[DATA_COMPONENTS][comp_or_platform] # type: ignore - cache = hass.data.get(DATA_COMPONENTS) - if cache is None: + if (cache := hass.data.get(DATA_COMPONENTS)) is None: if not _async_mount_config_dir(hass): return None cache = hass.data[DATA_COMPONENTS] = {} diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index aad4a6b1f46..e98ba2afe68 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -60,8 +60,7 @@ async def async_get_integration_with_requirements( if hass.config.skip_pip: return integration - cache = hass.data.get(DATA_INTEGRATIONS_WITH_REQS) - if cache is None: + if (cache := hass.data.get(DATA_INTEGRATIONS_WITH_REQS)) is None: cache = hass.data[DATA_INTEGRATIONS_WITH_REQS] = {} int_or_evt: Integration | asyncio.Event | None | UndefinedType = cache.get( @@ -71,12 +70,10 @@ async def async_get_integration_with_requirements( if isinstance(int_or_evt, asyncio.Event): await int_or_evt.wait() - int_or_evt = cache.get(domain, UNDEFINED) - # When we have waited and it's UNDEFINED, it doesn't exist # We don't cache that it doesn't exist, or else people can't fix it # and then restart, because their config will never be valid. - if int_or_evt is UNDEFINED: + if (int_or_evt := cache.get(domain, UNDEFINED)) is UNDEFINED: raise IntegrationNotFound(domain) if int_or_evt is not UNDEFINED: @@ -154,8 +151,7 @@ async def async_process_requirements( This method is a coroutine. It will raise RequirementsNotFound if an requirement can't be satisfied. """ - pip_lock = hass.data.get(DATA_PIP_LOCK) - if pip_lock is None: + if (pip_lock := hass.data.get(DATA_PIP_LOCK)) is None: pip_lock = hass.data[DATA_PIP_LOCK] = asyncio.Lock() install_failure_history = hass.data.get(DATA_INSTALL_FAILURE_HISTORY) if install_failure_history is None: diff --git a/homeassistant/runner.py b/homeassistant/runner.py index 3fd4118a25b..60f278c4efe 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -83,8 +83,7 @@ class HassEventLoopPolicy(asyncio.DefaultEventLoopPolicy): # type: ignore[valid def _async_loop_exception_handler(_: Any, context: dict[str, Any]) -> None: """Handle all exception inside the core loop.""" kwargs = {} - exception = context.get("exception") - if exception: + if exception := context.get("exception"): kwargs["exc_info"] = (type(exception), exception, exception.__traceback__) logging.getLogger(__package__).error( From aa7dc78a1eed3f546f2f28f0fae78a74d694bfb1 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 17 Oct 2021 20:15:48 +0200 Subject: [PATCH 0478/1038] Use assignment expressions 11 (#57792) --- homeassistant/components/history/__init__.py | 9 +++------ .../components/homeassistant/scene.py | 14 ++++--------- .../components/homeassistant/triggers/time.py | 6 ++---- homeassistant/components/http/auth.py | 12 +++-------- homeassistant/components/http/view.py | 3 +-- .../input_boolean/reproduce_state.py | 4 +--- .../components/input_datetime/__init__.py | 6 ++---- .../input_datetime/reproduce_state.py | 4 +--- .../input_select/reproduce_state.py | 4 +--- .../components/input_text/reproduce_state.py | 4 +--- homeassistant/components/light/__init__.py | 10 +++------- .../components/light/reproduce_state.py | 4 +--- homeassistant/components/logbook/__init__.py | 20 ++++++------------- .../components/media_player/__init__.py | 3 +-- .../components/number/reproduce_state.py | 4 +--- .../persistent_notification/__init__.py | 6 ++---- homeassistant/components/person/__init__.py | 11 +++------- homeassistant/components/recorder/__init__.py | 7 ++----- .../components/recorder/statistics.py | 3 +-- .../components/switch/reproduce_state.py | 4 +--- .../components/timer/reproduce_state.py | 4 +--- .../components/vacuum/reproduce_state.py | 4 +--- homeassistant/components/webhook/__init__.py | 3 +-- .../components/websocket_api/__init__.py | 3 +-- .../components/websocket_api/commands.py | 4 +--- 25 files changed, 45 insertions(+), 111 deletions(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 7c3087d471f..2ac9a77c025 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -211,8 +211,7 @@ class HistoryPeriodView(HomeAssistantView): if start_time > now: return self.json([]) - end_time_str = request.query.get("end_time") - if end_time_str: + if end_time_str := request.query.get("end_time"): end_time = dt_util.parse_datetime(end_time_str) if end_time: end_time = dt_util.as_utc(end_time) @@ -304,13 +303,11 @@ class HistoryPeriodView(HomeAssistantView): def sqlalchemy_filter_from_include_exclude_conf(conf): """Build a sql filter from config.""" filters = Filters() - exclude = conf.get(CONF_EXCLUDE) - if exclude: + if exclude := conf.get(CONF_EXCLUDE): filters.excluded_entities = exclude.get(CONF_ENTITIES, []) filters.excluded_domains = exclude.get(CONF_DOMAINS, []) filters.excluded_entity_globs = exclude.get(CONF_ENTITY_GLOBS, []) - include = conf.get(CONF_INCLUDE) - if include: + if include := conf.get(CONF_INCLUDE): filters.included_entities = include.get(CONF_ENTITIES, []) filters.included_domains = include.get(CONF_DOMAINS, []) filters.included_entity_globs = include.get(CONF_ENTITY_GLOBS, []) diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index e5b37dd3f01..cd5da46a03a 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -150,9 +150,7 @@ def entities_in_scene(hass: HomeAssistant, entity_id: str) -> list[str]: platform = hass.data[DATA_PLATFORM] - entity = platform.entities.get(entity_id) - - if entity is None: + if (entity := platform.entities.get(entity_id)) is None: return [] return list(entity.scene_config.states) @@ -233,8 +231,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= entities = call.data[CONF_ENTITIES] for entity_id in snapshot: - state = hass.states.get(entity_id) - if state is None: + if (state := hass.states.get(entity_id)) is None: _LOGGER.warning( "Entity %s does not exist and therefore cannot be snapshotted", entity_id, @@ -248,8 +245,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= scene_config = SceneConfig(None, call.data[CONF_SCENE_ID], None, entities) entity_id = f"{SCENE_DOMAIN}.{scene_config.name}" - old = platform.entities.get(entity_id) - if old is not None: + if (old := platform.entities.get(entity_id)) is not None: if not old.from_service: _LOGGER.warning("The scene %s already exists", entity_id) return @@ -263,10 +259,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= def _process_scenes_config(hass, async_add_entities, config): """Process multiple scenes and add them.""" - scene_config = config[STATES] - # Check empty list - if not scene_config: + if not (scene_config := config[STATES]): return async_add_entities( diff --git a/homeassistant/components/homeassistant/triggers/time.py b/homeassistant/components/homeassistant/triggers/time.py index 6ca1998a5c3..90780489d7b 100644 --- a/homeassistant/components/homeassistant/triggers/time.py +++ b/homeassistant/components/homeassistant/triggers/time.py @@ -79,13 +79,11 @@ async def async_attach_trigger(hass, config, action, automation_info): # Check state of entity. If valid, set up a listener. if new_state.domain == "input_datetime": - has_date = new_state.attributes["has_date"] - if has_date: + if has_date := new_state.attributes["has_date"]: year = new_state.attributes["year"] month = new_state.attributes["month"] day = new_state.attributes["day"] - has_time = new_state.attributes["has_time"] - if has_time: + if has_time := new_state.attributes["has_time"]: hour = new_state.attributes["hour"] minute = new_state.attributes["minute"] second = new_state.attributes["second"] diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 43ea0522594..e4d7da6ac9b 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -29,9 +29,7 @@ def async_sign_path( hass: HomeAssistant, refresh_token_id: str, path: str, expiration: timedelta ) -> str: """Sign a path for temporary access without auth header.""" - secret = hass.data.get(DATA_SIGN_SECRET) - - if secret is None: + if (secret := hass.data.get(DATA_SIGN_SECRET)) is None: secret = hass.data[DATA_SIGN_SECRET] = secrets.token_hex() now = dt_util.utcnow() @@ -80,14 +78,10 @@ def setup_auth(hass: HomeAssistant, app: Application) -> None: async def async_validate_signed_request(request: Request) -> bool: """Validate a signed request.""" - secret = hass.data.get(DATA_SIGN_SECRET) - - if secret is None: + if (secret := hass.data.get(DATA_SIGN_SECRET)) is None: return False - signature = request.query.get(SIGN_QUERY_PARAM) - - if signature is None: + if (signature := request.query.get(SIGN_QUERY_PARAM)) is None: return False try: diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index adebf2bb46a..949813ca4ad 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -40,8 +40,7 @@ class HomeAssistantView: @staticmethod def context(request: web.Request) -> Context: """Generate a context from a request.""" - user = request.get("hass_user") - if user is None: + if (user := request.get("hass_user")) is None: return Context() return Context(user_id=user.id) diff --git a/homeassistant/components/input_boolean/reproduce_state.py b/homeassistant/components/input_boolean/reproduce_state.py index 961345b7429..6c68489e4cb 100644 --- a/homeassistant/components/input_boolean/reproduce_state.py +++ b/homeassistant/components/input_boolean/reproduce_state.py @@ -28,9 +28,7 @@ async def _async_reproduce_states( reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce input boolean states.""" - cur_state = hass.states.get(state.entity_id) - - if cur_state is None: + if (cur_state := hass.states.get(state.entity_id)) is None: _LOGGER.warning("Unable to find entity %s", state.entity_id) return diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index 2713eef17f8..13063910e2b 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -83,8 +83,7 @@ def has_date_or_time(conf): def valid_initial(conf): """Check the initial value is valid.""" - initial = conf.get(CONF_INITIAL) - if not initial: + if not (initial := conf.get(CONF_INITIAL)): return conf if conf[CONF_HAS_DATE] and conf[CONF_HAS_TIME]: @@ -226,8 +225,7 @@ class InputDatetime(RestoreEntity): self.editable = True self._current_datetime = None - initial = config.get(CONF_INITIAL) - if not initial: + if not (initial := config.get(CONF_INITIAL)): return if self.has_date and self.has_time: diff --git a/homeassistant/components/input_datetime/reproduce_state.py b/homeassistant/components/input_datetime/reproduce_state.py index 230a0ed235c..6f36be0850a 100644 --- a/homeassistant/components/input_datetime/reproduce_state.py +++ b/homeassistant/components/input_datetime/reproduce_state.py @@ -41,9 +41,7 @@ async def _async_reproduce_state( reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" - cur_state = hass.states.get(state.entity_id) - - if cur_state is None: + if (cur_state := hass.states.get(state.entity_id)) is None: _LOGGER.warning("Unable to find entity %s", state.entity_id) return diff --git a/homeassistant/components/input_select/reproduce_state.py b/homeassistant/components/input_select/reproduce_state.py index a2cb2cadd0b..493980337fe 100644 --- a/homeassistant/components/input_select/reproduce_state.py +++ b/homeassistant/components/input_select/reproduce_state.py @@ -31,10 +31,8 @@ async def _async_reproduce_state( reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" - cur_state = hass.states.get(state.entity_id) - # Return if we can't find entity - if cur_state is None: + if (cur_state := hass.states.get(state.entity_id)) is None: _LOGGER.warning("Unable to find entity %s", state.entity_id) return diff --git a/homeassistant/components/input_text/reproduce_state.py b/homeassistant/components/input_text/reproduce_state.py index 56a03b0d133..ef82579f4b7 100644 --- a/homeassistant/components/input_text/reproduce_state.py +++ b/homeassistant/components/input_text/reproduce_state.py @@ -22,10 +22,8 @@ async def _async_reproduce_state( reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" - cur_state = hass.states.get(state.entity_id) - # Return if we can't find the entity - if cur_state is None: + if (cur_state := hass.states.get(state.entity_id)) is None: _LOGGER.warning("Unable to find entity %s", state.entity_id) return diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 4a0025126c8..e195ab0f09b 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -125,13 +125,11 @@ def get_supported_color_modes(hass: HomeAssistant, entity_id: str) -> set | None First try the statemachine, then entity registry. This is the equivalent of entity helper get_supported_features. """ - state = hass.states.get(entity_id) - if state: + if state := hass.states.get(entity_id): return state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) entity_registry = er.async_get(hass) - entry = entity_registry.async_get(entity_id) - if not entry: + if not (entry := entity_registry.async_get(entity_id)): raise HomeAssistantError(f"Unknown entity {entity_id}") if not entry.capabilities: return None @@ -629,9 +627,7 @@ class Profiles: @callback def apply_profile(self, name: str, params: dict) -> None: """Apply a profile.""" - profile = self.data.get(name) - - if profile is None: + if (profile := self.data.get(name)) is None: return if profile.hs_color is not None: diff --git a/homeassistant/components/light/reproduce_state.py b/homeassistant/components/light/reproduce_state.py index 7cc6b9c572c..9c382fcb7fa 100644 --- a/homeassistant/components/light/reproduce_state.py +++ b/homeassistant/components/light/reproduce_state.py @@ -123,9 +123,7 @@ async def _async_reproduce_state( reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" - cur_state = hass.states.get(state.entity_id) - - if cur_state is None: + if (cur_state := hass.states.get(state.entity_id)) is None: _LOGGER.warning("Unable to find entity %s", state.entity_id) return diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 91739aa5990..9bab6d0812e 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -150,9 +150,7 @@ async def async_setup(hass, config): "logbook", "logbook", "hass:format-list-bulleted-type" ) - conf = config.get(DOMAIN, {}) - - if conf: + if conf := config.get(DOMAIN, {}): filters = sqlalchemy_filter_from_include_exclude_conf(conf) entities_filter = convert_include_exclude_filter(conf) else: @@ -202,8 +200,7 @@ class LogbookView(HomeAssistantView): else: datetime = dt_util.start_of_local_day() - period = request.query.get("period") - if period is None: + if (period := request.query.get("period")) is None: period = 1 else: period = int(period) @@ -218,8 +215,7 @@ class LogbookView(HomeAssistantView): "Format should be ." ) from vol.Invalid - end_time = request.query.get("end_time") - if end_time is None: + if (end_time := request.query.get("end_time")) is None: start_day = dt_util.as_utc(datetime) - timedelta(days=period - 1) end_day = start_day + timedelta(days=period) else: @@ -605,9 +601,7 @@ def _keep_event(hass, event, entities_filter): def _augment_data_with_context( data, entity_id, event, context_lookup, entity_attr_cache, external_events ): - context_event = context_lookup.get(event.context_id) - - if not context_event: + if not (context_event := context_lookup.get(event.context_id)): return if event == context_event: @@ -663,8 +657,7 @@ def _augment_data_with_context( if event_type in external_events: domain, describe_event = external_events[event_type] data["context_domain"] = domain - name = describe_event(context_event).get(ATTR_NAME) - if name: + if name := describe_event(context_event).get(ATTR_NAME): data["context_name"] = name @@ -789,8 +782,7 @@ class EntityAttributeCache: else: self._cache[entity_id] = {} - current_state = self._hass.states.get(entity_id) - if current_state: + if current_state := self._hass.states.get(entity_id): # Try the current state as its faster than decoding the # attributes self._cache[entity_id][attribute] = current_state.attributes.get(attribute) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index dbe7c0ef04d..c84ccdfc105 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -984,8 +984,7 @@ class MediaPlayerEntity(Entity): response = await websession.get(url) if response.status == HTTP_OK: content = await response.read() - content_type = response.headers.get(CONTENT_TYPE) - if content_type: + if content_type := response.headers.get(CONTENT_TYPE): content_type = content_type.split(";")[0] if content is None: diff --git a/homeassistant/components/number/reproduce_state.py b/homeassistant/components/number/reproduce_state.py index dbf4af1f860..380ca0eb080 100644 --- a/homeassistant/components/number/reproduce_state.py +++ b/homeassistant/components/number/reproduce_state.py @@ -22,9 +22,7 @@ async def _async_reproduce_state( reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" - cur_state = hass.states.get(state.entity_id) - - if cur_state is None: + if (cur_state := hass.states.get(state.entity_id)) is None: _LOGGER.warning("Unable to find entity %s", state.entity_id) return diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 8725878797b..23b3a6b07d8 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -71,8 +71,7 @@ def async_create( context: Context | None = None, ) -> None: """Generate a notification.""" - notifications = hass.data.get(DOMAIN) - if notifications is None: + if (notifications := hass.data.get(DOMAIN)) is None: notifications = hass.data[DOMAIN] = {} if notification_id is not None: @@ -134,8 +133,7 @@ def async_dismiss( hass: HomeAssistant, notification_id: str, *, context: Context | None = None ) -> None: """Remove a notification.""" - notifications = hass.data.get(DOMAIN) - if notifications is None: + if (notifications := hass.data.get(DOMAIN)) is None: notifications = hass.data[DOMAIN] = {} entity_id = ENTITY_ID_FORMAT.format(slugify(notification_id)) diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index ba1f0ced623..4b7d6a54b1f 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -226,9 +226,7 @@ class PersonStorageCollection(collection.StorageCollection): """Validate the config is valid.""" data = self.CREATE_SCHEMA(data) - user_id = data.get(CONF_USER_ID) - - if user_id is not None: + if (user_id := data.get(CONF_USER_ID)) is not None: await self._validate_user_id(user_id) return data @@ -410,8 +408,7 @@ class Person(RestoreEntity): data[ATTR_GPS_ACCURACY] = self._gps_accuracy if self._source is not None: data[ATTR_SOURCE] = self._source - user_id = self._config.get(CONF_USER_ID) - if user_id is not None: + if (user_id := self._config.get(CONF_USER_ID)) is not None: data[ATTR_USER_ID] = user_id return data @@ -448,9 +445,7 @@ class Person(RestoreEntity): self._unsub_track_device() self._unsub_track_device = None - trackers = self._config[CONF_DEVICE_TRACKERS] - - if trackers: + if trackers := self._config[CONF_DEVICE_TRACKERS]: _LOGGER.debug("Subscribe to device trackers for %s", self.entity_id) self._unsub_track_device = async_track_state_change_event( diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 7e9bab0ed4e..471e86609ae 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -463,9 +463,7 @@ class Recorder(threading.Thread): if event.event_type in self.exclude_t: return False - entity_id = event.data.get(ATTR_ENTITY_ID) - - if entity_id is None: + if (entity_id := event.data.get(ATTR_ENTITY_ID)) is None: return True if isinstance(entity_id, str): @@ -496,8 +494,7 @@ class Recorder(threading.Thread): def do_adhoc_statistics(self, **kwargs): """Trigger an adhoc statistics run.""" - start = kwargs.get("start") - if not start: + if not (start := kwargs.get("start")): start = statistics.get_start_time() self.queue.put(StatisticsTask(start)) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 2dc18d3aecb..402668e50d8 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -523,8 +523,7 @@ def list_statistic_ids( metadata = get_metadata_with_session(hass, session, None, statistic_type) for _, meta in metadata.values(): - unit = meta["unit_of_measurement"] - if unit is not None: + 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 diff --git a/homeassistant/components/switch/reproduce_state.py b/homeassistant/components/switch/reproduce_state.py index 4cc1ec1f693..0a6d0de9602 100644 --- a/homeassistant/components/switch/reproduce_state.py +++ b/homeassistant/components/switch/reproduce_state.py @@ -30,9 +30,7 @@ async def _async_reproduce_state( reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" - cur_state = hass.states.get(state.entity_id) - - if cur_state is None: + if (cur_state := hass.states.get(state.entity_id)) is None: _LOGGER.warning("Unable to find entity %s", state.entity_id) return diff --git a/homeassistant/components/timer/reproduce_state.py b/homeassistant/components/timer/reproduce_state.py index 33aed933a06..5628c0b4bbc 100644 --- a/homeassistant/components/timer/reproduce_state.py +++ b/homeassistant/components/timer/reproduce_state.py @@ -33,9 +33,7 @@ async def _async_reproduce_state( reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" - cur_state = hass.states.get(state.entity_id) - - if cur_state is None: + if (cur_state := hass.states.get(state.entity_id)) is None: _LOGGER.warning("Unable to find entity %s", state.entity_id) return diff --git a/homeassistant/components/vacuum/reproduce_state.py b/homeassistant/components/vacuum/reproduce_state.py index f8d718c9979..fbcc97445c8 100644 --- a/homeassistant/components/vacuum/reproduce_state.py +++ b/homeassistant/components/vacuum/reproduce_state.py @@ -50,9 +50,7 @@ async def _async_reproduce_state( reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" - cur_state = hass.states.get(state.entity_id) - - if cur_state is None: + if (cur_state := hass.states.get(state.entity_id)) is None: _LOGGER.warning("Unable to find entity %s", state.entity_id) return diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py index 52778600147..983ead616f2 100644 --- a/homeassistant/components/webhook/__init__.py +++ b/homeassistant/components/webhook/__init__.py @@ -81,10 +81,9 @@ def async_generate_path(webhook_id: str) -> str: async def async_handle_webhook(hass, webhook_id, request): """Handle a webhook.""" handlers = hass.data.setdefault(DOMAIN, {}) - webhook = handlers.get(webhook_id) # Always respond successfully to not give away if a hook exists or not. - if webhook is None: + if (webhook := handlers.get(webhook_id)) is None: if isinstance(request, MockRequest): received_from = request.mock_source else: diff --git a/homeassistant/components/websocket_api/__init__.py b/homeassistant/components/websocket_api/__init__.py index 2e44a0aa0cd..13939338c3e 100644 --- a/homeassistant/components/websocket_api/__init__.py +++ b/homeassistant/components/websocket_api/__init__.py @@ -58,8 +58,7 @@ def async_register_command( schema = handler._ws_schema # type: ignore[attr-defined] else: command = command_or_handler - handlers = hass.data.get(DOMAIN) - if handlers is None: + if (handlers := hass.data.get(DOMAIN)) is None: handlers = hass.data[DOMAIN] = {} handlers[command] = (handler, schema) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index fa8286084b6..4216eae5a09 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -420,9 +420,7 @@ def handle_entity_source( perm_category=CAT_ENTITIES, ) - source = raw_sources.get(entity_id) - - if source is None: + if (source := raw_sources.get(entity_id)) is None: connection.send_error(msg["id"], ERR_NOT_FOUND, "Entity not found") return From 2b72b7b7b912d0b272ecedddf21a44aa1d9f0440 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 17 Oct 2021 20:19:56 +0200 Subject: [PATCH 0479/1038] Use assignment expressions 09 (#57790) --- homeassistant/components/alert/__init__.py | 3 +-- homeassistant/components/alert/reproduce_state.py | 4 +--- homeassistant/components/alexa/capabilities.py | 3 +-- homeassistant/components/alexa/flash_briefings.py | 3 +-- homeassistant/components/alexa/handlers.py | 6 ++---- homeassistant/components/alexa/intent.py | 4 +--- homeassistant/components/alexa/logbook.py | 3 +-- homeassistant/components/api/__init__.py | 10 +++------- homeassistant/components/auth/__init__.py | 12 +++--------- homeassistant/components/auth/login_flow.py | 3 +-- homeassistant/components/auth/mfa_setup_flow.py | 6 ++---- homeassistant/components/automation/__init__.py | 6 ++---- .../components/automation/reproduce_state.py | 4 +--- homeassistant/components/camera/__init__.py | 7 ++----- .../components/climate/device_trigger.py | 4 +--- homeassistant/components/cloud/account_link.py | 4 +--- homeassistant/components/cloud/prefs.py | 8 ++------ homeassistant/components/cloud/tts.py | 8 ++------ .../components/config/entity_registry.py | 3 +-- homeassistant/components/config/zwave.py | 15 +++++---------- homeassistant/components/configurator/__init__.py | 4 +--- .../components/counter/reproduce_state.py | 4 +--- homeassistant/components/cover/reproduce_state.py | 4 +--- 23 files changed, 37 insertions(+), 91 deletions(-) diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index 73be34e6d33..e72a1bcffa4 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -221,8 +221,7 @@ class Alert(ToggleEntity): async def watched_entity_change(self, ev): """Determine if the alert should start or stop.""" - to_state = ev.data.get("new_state") - if to_state is None: + if (to_state := ev.data.get("new_state")) is None: return _LOGGER.debug("Watched entity (%s) has changed", ev.data.get("entity_id")) if to_state.state == self._alert_state and not self._firing: diff --git a/homeassistant/components/alert/reproduce_state.py b/homeassistant/components/alert/reproduce_state.py index 9c8cbd19810..49658ab2495 100644 --- a/homeassistant/components/alert/reproduce_state.py +++ b/homeassistant/components/alert/reproduce_state.py @@ -30,9 +30,7 @@ async def _async_reproduce_state( reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" - cur_state = hass.states.get(state.entity_id) - - if cur_state is None: + if (cur_state := hass.states.get(state.entity_id)) is None: _LOGGER.warning("Unable to find entity %s", state.entity_id) return diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index dad6e00ff8e..660ef46e478 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1096,8 +1096,7 @@ class AlexaThermostatController(AlexaCapability): supported_modes = [] hvac_modes = self.entity.attributes.get(climate.ATTR_HVAC_MODES) for mode in hvac_modes: - thermostat_mode = API_THERMOSTAT_MODES.get(mode) - if thermostat_mode: + if thermostat_mode := API_THERMOSTAT_MODES.get(mode): supported_modes.append(thermostat_mode) preset_modes = self.entity.attributes.get(climate.ATTR_PRESET_MODES) diff --git a/homeassistant/components/alexa/flash_briefings.py b/homeassistant/components/alexa/flash_briefings.py index 50463810bbf..68d6368a5e2 100644 --- a/homeassistant/components/alexa/flash_briefings.py +++ b/homeassistant/components/alexa/flash_briefings.py @@ -93,8 +93,7 @@ class AlexaFlashBriefingView(http.HomeAssistantView): else: output[ATTR_MAIN_TEXT] = item.get(CONF_TEXT) - uid = item.get(CONF_UID) - if uid is None: + if (uid := item.get(CONF_UID)) is None: uid = str(uuid.uuid4()) output[ATTR_UID] = uid diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 21ad6648a5a..edf900bb18f 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -1150,8 +1150,7 @@ async def async_api_adjust_range(hass, config, directive, context): if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": range_delta = int(range_delta * 20) if range_delta_default else int(range_delta) service = SERVICE_SET_COVER_POSITION - current = entity.attributes.get(cover.ATTR_POSITION) - if not current: + if not (current := entity.attributes.get(cover.ATTR_POSITION)): msg = f"Unable to determine {entity.entity_id} current position" raise AlexaInvalidValueError(msg) position = response_value = min(100, max(0, range_delta + current)) @@ -1187,8 +1186,7 @@ async def async_api_adjust_range(hass, config, directive, context): else int(range_delta) ) service = fan.SERVICE_SET_PERCENTAGE - current = entity.attributes.get(fan.ATTR_PERCENTAGE) - if not current: + if not (current := entity.attributes.get(fan.ATTR_PERCENTAGE)): msg = f"Unable to determine {entity.entity_id} current fan speed" raise AlexaInvalidValueError(msg) percentage = response_value = min(100, max(0, range_delta + current)) diff --git a/homeassistant/components/alexa/intent.py b/homeassistant/components/alexa/intent.py index f64031250e2..fede7d96810 100644 --- a/homeassistant/components/alexa/intent.py +++ b/homeassistant/components/alexa/intent.py @@ -120,9 +120,7 @@ async def async_handle_message(hass, message): req = message.get("request") req_type = req["type"] - handler = HANDLERS.get(req_type) - - if not handler: + if not (handler := HANDLERS.get(req_type)): raise UnknownRequest(f"Received unknown request {req_type}") return await handler(hass, message) diff --git a/homeassistant/components/alexa/logbook.py b/homeassistant/components/alexa/logbook.py index 153c7b7d61a..65fb410c601 100644 --- a/homeassistant/components/alexa/logbook.py +++ b/homeassistant/components/alexa/logbook.py @@ -12,9 +12,8 @@ def async_describe_events(hass, async_describe_event): def async_describe_logbook_event(event): """Describe a logbook event.""" data = event.data - entity_id = data["request"].get("entity_id") - if entity_id: + if entity_id := data["request"].get("entity_id"): state = hass.states.get(entity_id) name = state.name if state else entity_id message = f"sent command {data['request']['namespace']}/{data['request']['name']} for {name}" diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index 01d48a190fd..229311ff6d9 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -97,8 +97,7 @@ class APIEventStream(HomeAssistantView): stop_obj = object() to_write = asyncio.Queue() - restrict = request.query.get("restrict") - if restrict: + if restrict := request.query.get("restrict"): restrict = restrict.split(",") + [EVENT_HOMEASSISTANT_STOP] async def forward_events(event): @@ -225,8 +224,7 @@ class APIEntityStateView(HomeAssistantView): if not user.permissions.check_entity(entity_id, POLICY_READ): raise Unauthorized(entity_id=entity_id) - state = request.app["hass"].states.get(entity_id) - if state: + if state := request.app["hass"].states.get(entity_id): return self.json(state) return self.json_message("Entity not found.", HTTPStatus.NOT_FOUND) @@ -240,9 +238,7 @@ class APIEntityStateView(HomeAssistantView): except ValueError: return self.json_message("Invalid JSON specified.", HTTPStatus.BAD_REQUEST) - new_state = data.get("state") - - if new_state is None: + if (new_state := data.get("state")) is None: return self.json_message("No state specified.", HTTPStatus.BAD_REQUEST) attributes = data.get("attributes") diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index 5af5aea13c4..bcdcf4de747 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -270,9 +270,7 @@ class TokenView(HomeAssistantView): # 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. - token = data.get("token") - - if token is None: + 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) @@ -292,9 +290,7 @@ class TokenView(HomeAssistantView): status_code=HTTPStatus.BAD_REQUEST, ) - code = data.get("code") - - if code is None: + if (code := data.get("code")) is None: return self.json( {"error": "invalid_request", "error_description": "Invalid code"}, status_code=HTTPStatus.BAD_REQUEST, @@ -349,9 +345,7 @@ class TokenView(HomeAssistantView): status_code=HTTPStatus.BAD_REQUEST, ) - token = data.get("refresh_token") - - if token is None: + if (token := data.get("refresh_token")) is None: return self.json( {"error": "invalid_request"}, status_code=HTTPStatus.BAD_REQUEST ) diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index 7975e220acb..e660832487a 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -130,8 +130,7 @@ def _prepare_result_json(result): data = result.copy() - schema = data["data_schema"] - if schema is None: + if (schema := data["data_schema"]) is None: data["data_schema"] = [] else: data["data_schema"] = voluptuous_serialize.convert(schema) diff --git a/homeassistant/components/auth/mfa_setup_flow.py b/homeassistant/components/auth/mfa_setup_flow.py index 1b199551a14..61c06a3c16e 100644 --- a/homeassistant/components/auth/mfa_setup_flow.py +++ b/homeassistant/components/auth/mfa_setup_flow.py @@ -70,8 +70,7 @@ def websocket_setup_mfa( """Return a setup flow for mfa auth module.""" flow_manager = hass.data[DATA_SETUP_FLOW_MGR] - flow_id = msg.get("flow_id") - if flow_id is not None: + if (flow_id := msg.get("flow_id")) is not None: result = await flow_manager.async_configure(flow_id, msg.get("user_input")) connection.send_message( websocket_api.result_message(msg["id"], _prepare_result_json(result)) @@ -139,8 +138,7 @@ def _prepare_result_json(result): data = result.copy() - schema = data["data_schema"] - if schema is None: + if (schema := data["data_schema"]) is None: data["data_schema"] = [] else: data["data_schema"] = voluptuous_serialize.convert(schema) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 24090b79fa8..b6635d54d2e 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -460,8 +460,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self._trace_config, ) as automation_trace: this = None - state = self.hass.states.get(self.entity_id) - if state: + if state := self.hass.states.get(self.entity_id): this = state.as_dict() variables = {"this": this, **(run_variables or {})} if self._variables: @@ -589,8 +588,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): this = None self.async_write_ha_state() - state = self.hass.states.get(self.entity_id) - if state: + if state := self.hass.states.get(self.entity_id): this = state.as_dict() variables = {"this": this} if self._trigger_variables: diff --git a/homeassistant/components/automation/reproduce_state.py b/homeassistant/components/automation/reproduce_state.py index dd2ba824f8a..4318cdafa39 100644 --- a/homeassistant/components/automation/reproduce_state.py +++ b/homeassistant/components/automation/reproduce_state.py @@ -30,9 +30,7 @@ async def _async_reproduce_state( reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" - cur_state = hass.states.get(state.entity_id) - - if cur_state is None: + if (cur_state := hass.states.get(state.entity_id)) is None: _LOGGER.warning("Unable to find entity %s", state.entity_id) return diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 56f7c56008b..9275589f3c9 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -272,9 +272,7 @@ async def async_get_still_stream( def _get_camera_from_entity_id(hass: HomeAssistant, entity_id: str) -> Camera: """Get camera component from entity_id.""" - component = hass.data.get(DOMAIN) - - if component is None: + if (component := hass.data.get(DOMAIN)) is None: raise HomeAssistantError("Camera integration not set up") camera = component.get_entity(entity_id) @@ -653,8 +651,7 @@ class CameraMjpegStream(CameraView): async def handle(self, request: web.Request, camera: Camera) -> web.StreamResponse: """Serve camera stream, possibly with interval.""" - interval_str = request.query.get("interval") - if interval_str is None: + if (interval_str := request.query.get("interval")) is None: stream = await camera.handle_async_mjpeg_stream(request) if stream is None: raise web.HTTPBadGateway() diff --git a/homeassistant/components/climate/device_trigger.py b/homeassistant/components/climate/device_trigger.py index ce4e08f9fd2..05212e6ab99 100644 --- a/homeassistant/components/climate/device_trigger.py +++ b/homeassistant/components/climate/device_trigger.py @@ -118,9 +118,7 @@ async def async_attach_trigger( automation_info: AutomationTriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" - trigger_type = config[CONF_TYPE] - - if trigger_type == "hvac_mode_changed": + if (trigger_type := config[CONF_TYPE]) == "hvac_mode_changed": state_config = { state_trigger.CONF_PLATFORM: "state", state_trigger.CONF_ENTITY_ID: config[CONF_ENTITY_ID], diff --git a/homeassistant/components/cloud/account_link.py b/homeassistant/components/cloud/account_link.py index 5bb0db6d057..6dc0da82512 100644 --- a/homeassistant/components/cloud/account_link.py +++ b/homeassistant/components/cloud/account_link.py @@ -41,9 +41,7 @@ async def async_provide_implementation(hass: HomeAssistant, domain: str): async def _get_services(hass): """Get the available services.""" - services = hass.data.get(DATA_SERVICES) - - if services is not None: + if (services := hass.data.get(DATA_SERVICES)) is not None: return services try: diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index c51d5278730..a4c81bcc64f 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -216,9 +216,7 @@ class CloudPreferences: @property def remote_enabled(self): """Return if remote is enabled on start.""" - enabled = self._prefs.get(PREF_ENABLE_REMOTE, False) - - if not enabled: + if not self._prefs.get(PREF_ENABLE_REMOTE, False): return False if self._has_local_trusted_network or self._has_local_trusted_proxies: @@ -307,9 +305,7 @@ class CloudPreferences: async def _load_cloud_user(self) -> User | None: """Load cloud user if available.""" - user_id = self._prefs.get(PREF_CLOUD_USER) - - if user_id is None: + if (user_id := self._prefs.get(PREF_CLOUD_USER)) is None: return None # Fetch the user. It can happen that the user no longer exists if diff --git a/homeassistant/components/cloud/tts.py b/homeassistant/components/cloud/tts.py index 51c3e5f3a4e..00eacf7ca52 100644 --- a/homeassistant/components/cloud/tts.py +++ b/homeassistant/components/cloud/tts.py @@ -15,14 +15,10 @@ SUPPORT_LANGUAGES = list({key[0] for key in MAP_VOICE}) def validate_lang(value): """Validate chosen gender or language.""" - lang = value.get(CONF_LANG) - - if lang is None: + if (lang := value.get(CONF_LANG)) is None: return value - gender = value.get(CONF_GENDER) - - if gender is None: + if (gender := value.get(CONF_GENDER)) is None: gender = value[CONF_GENDER] = next( (chk_gender for chk_lang, chk_gender in MAP_VOICE if chk_lang == lang), None ) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 1040a500f3c..9b6fc2af82a 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -50,9 +50,8 @@ async def websocket_get_entity(hass, connection, msg): Async friendly. """ registry = await async_get_registry(hass) - entry = registry.entities.get(msg["entity_id"]) - if entry is None: + if (entry := registry.entities.get(msg["entity_id"])) is None: connection.send_message( websocket_api.error_message(msg["id"], ERR_NOT_FOUND, "Entity not found") ) diff --git a/homeassistant/components/config/zwave.py b/homeassistant/components/config/zwave.py index 6817b230fb8..63b7bdf9868 100644 --- a/homeassistant/components/config/zwave.py +++ b/homeassistant/components/config/zwave.py @@ -80,8 +80,7 @@ class ZWaveConfigWriteView(HomeAssistantView): def post(self, request): """Save cache configuration to zwcfg_xxxxx.xml.""" hass = request.app["hass"] - network = hass.data.get(const.DATA_NETWORK) - if network is None: + if (network := hass.data.get(const.DATA_NETWORK)) is None: return self.json_message( "No Z-Wave network data found", HTTPStatus.NOT_FOUND ) @@ -131,8 +130,7 @@ class ZWaveNodeGroupView(HomeAssistantView): nodeid = int(node_id) hass = request.app["hass"] network = hass.data.get(const.DATA_NETWORK) - node = network.nodes.get(nodeid) - if node is None: + if (node := network.nodes.get(nodeid)) is None: return self.json_message("Node not found", HTTPStatus.NOT_FOUND) groupdata = node.groups groups = {} @@ -158,8 +156,7 @@ class ZWaveNodeConfigView(HomeAssistantView): nodeid = int(node_id) hass = request.app["hass"] network = hass.data.get(const.DATA_NETWORK) - node = network.nodes.get(nodeid) - if node is None: + if (node := network.nodes.get(nodeid)) is None: return self.json_message("Node not found", HTTPStatus.NOT_FOUND) config = {} for value in node.get_values( @@ -189,8 +186,7 @@ class ZWaveUserCodeView(HomeAssistantView): nodeid = int(node_id) hass = request.app["hass"] network = hass.data.get(const.DATA_NETWORK) - node = network.nodes.get(nodeid) - if node is None: + if (node := network.nodes.get(nodeid)) is None: return self.json_message("Node not found", HTTPStatus.NOT_FOUND) usercodes = {} if not node.has_command_class(const.COMMAND_CLASS_USER_CODE): @@ -220,8 +216,7 @@ class ZWaveProtectionView(HomeAssistantView): def _fetch_protection(): """Get protection data.""" - node = network.nodes.get(nodeid) - if node is None: + if (node := network.nodes.get(nodeid)) is None: return self.json_message("Node not found", HTTPStatus.NOT_FOUND) protection_options = {} if not node.has_command_class(const.COMMAND_CLASS_PROTECTION): diff --git a/homeassistant/components/configurator/__init__.py b/homeassistant/components/configurator/__init__.py index f06ec330815..fde0cdc590d 100644 --- a/homeassistant/components/configurator/__init__.py +++ b/homeassistant/components/configurator/__init__.py @@ -65,9 +65,7 @@ def async_request_config( if description_image is not None: description += f"\n\n![Description image]({description_image})" - instance = hass.data.get(_KEY_INSTANCE) - - if instance is None: + if (instance := hass.data.get(_KEY_INSTANCE)) is None: instance = hass.data[_KEY_INSTANCE] = Configurator(hass) request_id = instance.async_request_config( diff --git a/homeassistant/components/counter/reproduce_state.py b/homeassistant/components/counter/reproduce_state.py index 0ced9bad06d..2029321c430 100644 --- a/homeassistant/components/counter/reproduce_state.py +++ b/homeassistant/components/counter/reproduce_state.py @@ -30,9 +30,7 @@ async def _async_reproduce_state( reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" - cur_state = hass.states.get(state.entity_id) - - if cur_state is None: + if (cur_state := hass.states.get(state.entity_id)) is None: _LOGGER.warning("Unable to find entity %s", state.entity_id) return diff --git a/homeassistant/components/cover/reproduce_state.py b/homeassistant/components/cover/reproduce_state.py index c96b9ec5acc..1be68bcfeba 100644 --- a/homeassistant/components/cover/reproduce_state.py +++ b/homeassistant/components/cover/reproduce_state.py @@ -42,9 +42,7 @@ async def _async_reproduce_state( reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" - cur_state = hass.states.get(state.entity_id) - - if cur_state is None: + if (cur_state := hass.states.get(state.entity_id)) is None: _LOGGER.warning("Unable to find entity %s", state.entity_id) return From 4f8148f9eab51b717594bec0aaa034e68fd46cc4 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 17 Oct 2021 20:24:34 +0200 Subject: [PATCH 0480/1038] Use assignment expressions 07 (#57787) --- homeassistant/components/ihc/light.py | 3 +-- homeassistant/components/influxdb/sensor.py | 3 +-- .../components/insteon/insteon_entity.py | 3 +-- homeassistant/components/ipp/sensor.py | 4 +-- homeassistant/components/isy994/sensor.py | 3 +-- homeassistant/components/izone/climate.py | 3 +-- homeassistant/components/kodi/media_player.py | 3 +-- homeassistant/components/lifx/light.py | 3 +-- homeassistant/components/light/__init__.py | 4 +-- homeassistant/components/lock/__init__.py | 3 +-- homeassistant/components/logbook/__init__.py | 9 +++---- .../components/media_player/__init__.py | 26 ++++++++----------- homeassistant/components/melcloud/__init__.py | 3 +-- homeassistant/components/melcloud/climate.py | 6 ++--- .../components/melcloud/config_flow.py | 3 +-- .../components/mqtt/light/schema_basic.py | 9 +++---- homeassistant/components/mvglive/sensor.py | 3 +-- homeassistant/components/nest/__init__.py | 3 +-- homeassistant/components/nest/camera_sdm.py | 3 +-- homeassistant/components/nest/climate_sdm.py | 9 +++---- homeassistant/components/nest/device_info.py | 3 +-- homeassistant/components/nut/config_flow.py | 3 +-- 22 files changed, 39 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/ihc/light.py b/homeassistant/components/ihc/light.py index c8fe9ef54de..d4a3bd8b1f3 100644 --- a/homeassistant/components/ihc/light.py +++ b/homeassistant/components/ihc/light.py @@ -84,8 +84,7 @@ class IhcLight(IHCDevice, LightEntity): if ATTR_BRIGHTNESS in kwargs: brightness = kwargs[ATTR_BRIGHTNESS] else: - brightness = self._brightness - if brightness == 0: + if (brightness := self._brightness) == 0: brightness = 255 if self._dimmable: diff --git a/homeassistant/components/influxdb/sensor.py b/homeassistant/components/influxdb/sensor.py index bdbfafaf790..24ef6a1020e 100644 --- a/homeassistant/components/influxdb/sensor.py +++ b/homeassistant/components/influxdb/sensor.py @@ -234,8 +234,7 @@ class InfluxSensor(SensorEntity): def update(self): """Get the latest data from Influxdb and updates the states.""" self.data.update() - value = self.data.value - if value is None: + if (value := self.data.value) is None: value = STATE_UNKNOWN if self._value_template is not None: value = self._value_template.render_with_possible_json_value( diff --git a/homeassistant/components/insteon/insteon_entity.py b/homeassistant/components/insteon/insteon_entity.py index 3f83440d690..e555725a188 100644 --- a/homeassistant/components/insteon/insteon_entity.py +++ b/homeassistant/components/insteon/insteon_entity.py @@ -65,8 +65,7 @@ class InsteonEntity(Entity): def name(self): """Return the name of the node (used for Entity_ID).""" # Set a base description - description = self._insteon_device.description - if description is None: + if (description := self._insteon_device.description) is None: description = "Unknown Device" # Get an extension label if there is one extension = self._get_label() diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py index e7c0d5c38f5..02e2082bbd3 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -36,9 +36,7 @@ async def async_setup_entry( coordinator: IPPDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] # config flow sets this to either UUID, serial number or None - unique_id = entry.unique_id - - if unique_id is None: + if (unique_id := entry.unique_id) is None: unique_id = entry.entry_id sensors = [] diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index f12f3cb6bdd..c15da2af0da 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -69,8 +69,7 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity): @property def native_value(self) -> str: """Get the state of the ISY994 sensor device.""" - value = self._node.status - if value == ISY_VALUE_UNKNOWN: + if (value := self._node.status) == ISY_VALUE_UNKNOWN: return None # Get the translated ISY Unit of Measurement diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index 968f13748b2..06dfee0e7fb 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -316,8 +316,7 @@ class ControllerDevice(ClimateEntity): """Return current operation ie. heat, cool, idle.""" if not self._controller.is_on: return HVAC_MODE_OFF - mode = self._controller.mode - if mode == Controller.Mode.FREE_AIR: + if (mode := self._controller.mode) == Controller.Mode.FREE_AIR: return HVAC_MODE_FAN_ONLY for (key, value) in self._state_to_pizone.items(): if value == mode: diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 42943cffb13..9d23a5fa212 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -238,8 +238,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): connection = data[DATA_CONNECTION] kodi = data[DATA_KODI] name = config_entry.data[CONF_NAME] - uid = config_entry.unique_id - if uid is None: + if (uid := config_entry.unique_id) is None: uid = config_entry.entry_id entity = KodiEntity(connection, kodi, name, uid) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index a4412d042a8..48896cea8a5 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -462,8 +462,7 @@ class LIFXLight(LightEntity): "manufacturer": "LIFX", } - version = self.bulb.host_firmware_version - if version is not None: + if (version := self.bulb.host_firmware_version) is not None: info["sw_version"] = version product_map = aiolifx().products.product_map diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index e195ab0f09b..a09a2fcd58e 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -675,9 +675,7 @@ class LightEntity(ToggleEntity): @property def _light_internal_color_mode(self) -> str: """Return the color mode of the light with backwards compatibility.""" - color_mode = self.color_mode - - if color_mode is None: + if (color_mode := self.color_mode) is None: # Backwards compatibility for color_mode added in 2021.4 # Add warning in 2021.6, remove in 2021.10 supported = self._light_internal_supported_color_modes diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 860778d61f3..aa3662da0c8 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -177,8 +177,7 @@ class LockEntity(Entity): return STATE_LOCKING if self.is_unlocking: return STATE_UNLOCKING - locked = self.is_locked - if locked is None: + if (locked := self.is_locked) is None: return None return STATE_LOCKED if locked else STATE_UNLOCKED diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 9bab6d0812e..1dc13c9fb14 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -315,8 +315,7 @@ def humanify(hass, events, entity_attr_cache, context_lookup): "entity_id": entity_id, } - icon = event.attributes_icon - if icon: + if icon := event.attributes_icon: data["icon"] = icon if event.context_user_id: @@ -581,8 +580,7 @@ def _keep_event(hass, event, entities_filter): if event.event_type in HOMEASSISTANT_EVENTS: return entities_filter is None or entities_filter(HA_DOMAIN_ENTITY_ID) - entity_id = event.data_entity_id - if entity_id: + if entity_id := event.data_entity_id: return entities_filter is None or entities_filter(entity_id) if event.event_type in hass.data[DOMAIN]: @@ -615,10 +613,9 @@ def _augment_data_with_context( return event_type = context_event.event_type - context_entity_id = context_event.entity_id # State change - if context_entity_id: + if context_entity_id := context_event.entity_id: data["context_entity_id"] = context_entity_id data["context_entity_id_name"] = _entity_name_from_event( context_entity_id, context_event, entity_attr_cache diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index c84ccdfc105..8cf271d09cf 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -482,16 +482,14 @@ class MediaPlayerEntity(Entity): if hasattr(self, "_attr_media_image_hash"): return self._attr_media_image_hash - url = self.media_image_url - if url is not None: + if (url := self.media_image_url) is not None: return hashlib.sha256(url.encode("utf-8")).hexdigest()[:16] return None async def async_get_media_image(self): """Fetch media image of current playing image.""" - url = self.media_image_url - if url is None: + if (url := self.media_image_url) is None: return None, None return await self._async_fetch_image_from_cache(url) @@ -871,9 +869,7 @@ class MediaPlayerEntity(Entity): @property def media_image_local(self): """Return local url to media image.""" - image_hash = self.media_image_hash - - if image_hash is None: + if (image_hash := self.media_image_hash) is None: return None return ( @@ -887,15 +883,15 @@ class MediaPlayerEntity(Entity): supported_features = self.supported_features or 0 data = {} - if supported_features & SUPPORT_SELECT_SOURCE: - source_list = self.source_list - if source_list: - data[ATTR_INPUT_SOURCE_LIST] = source_list + if supported_features & SUPPORT_SELECT_SOURCE and ( + source_list := self.source_list + ): + data[ATTR_INPUT_SOURCE_LIST] = source_list - if supported_features & SUPPORT_SELECT_SOUND_MODE: - sound_mode_list = self.sound_mode_list - if sound_mode_list: - data[ATTR_SOUND_MODE_LIST] = sound_mode_list + if supported_features & SUPPORT_SELECT_SOUND_MODE and ( + sound_mode_list := self.sound_mode_list + ): + data[ATTR_SOUND_MODE_LIST] = sound_mode_list return data diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index 69efa26ac44..4547e690eb8 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -134,8 +134,7 @@ class MelCloudDevice: "manufacturer": "Mitsubishi Electric", "name": self.name, } - unit_infos = self.device.units - if unit_infos is not None: + if (unit_infos := self.device.units) is not None: _device_info["model"] = ", ".join( [x["model"] for x in unit_infos if x["model"]] ) diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py index d4eeb9354c8..25165cf1a71 100644 --- a/homeassistant/components/melcloud/climate.py +++ b/homeassistant/components/melcloud/climate.py @@ -143,8 +143,7 @@ class AtaDeviceClimate(MelCloudClimate): """Return the optional state attributes with device specific additions.""" attr = {} - vane_horizontal = self._device.vane_horizontal - if vane_horizontal: + if vane_horizontal := self._device.vane_horizontal: attr.update( { ATTR_VANE_HORIZONTAL: vane_horizontal, @@ -152,8 +151,7 @@ class AtaDeviceClimate(MelCloudClimate): } ) - vane_vertical = self._device.vane_vertical - if vane_vertical: + if vane_vertical := self._device.vane_vertical: attr.update( { ATTR_VANE_VERTICAL: vane_vertical, diff --git a/homeassistant/components/melcloud/config_flow.py b/homeassistant/components/melcloud/config_flow.py index 48ee84382fa..a04364ea20f 100644 --- a/homeassistant/components/melcloud/config_flow.py +++ b/homeassistant/components/melcloud/config_flow.py @@ -48,8 +48,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): try: with timeout(10): - acquired_token = token - if acquired_token is None: + if (acquired_token := token) is None: acquired_token = await pymelcloud.login( username, password, diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 9a900c296d3..409eb9d648c 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -656,8 +656,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): @property def brightness(self): """Return the brightness of this light between 0..255.""" - brightness = self._brightness - if brightness: + if brightness := self._brightness: brightness = min(round(brightness), 255) return brightness @@ -728,10 +727,8 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): @property def white_value(self): """Return the white property.""" - white_value = self._white_value - if white_value: - white_value = min(round(white_value), 255) - return white_value + if white_value := self._white_value: + return min(round(white_value), 255) return None @property diff --git a/homeassistant/components/mvglive/sensor.py b/homeassistant/components/mvglive/sensor.py index 416ce21cbaf..29554e89c5d 100644 --- a/homeassistant/components/mvglive/sensor.py +++ b/homeassistant/components/mvglive/sensor.py @@ -115,8 +115,7 @@ class MVGLiveSensor(SensorEntity): @property def extra_state_attributes(self): """Return the state attributes.""" - dep = self.data.departures - if not dep: + if not (dep := self.data.departures): return None attr = dep[0] # next depature attributes attr["departures"] = deepcopy(dep) # all departures dictionary diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index ff340d38424..9ca7d530f79 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -115,8 +115,7 @@ class SignalUpdateCallback: if not event_message.resource_update_name: return device_id = event_message.resource_update_name - events = event_message.resource_update_events - if not events: + if not (events := event_message.resource_update_events): return _LOGGER.debug("Event Update %s", events.keys()) device_registry = await self._hass.helpers.device_registry.async_get_registry() diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index 5620653819f..9ce485cee56 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -217,8 +217,7 @@ class NestCamera(Camera): """Return image from any active events happening.""" if CameraEventImageTrait.NAME not in self._device.traits: return None - trait = self._device.active_event_trait - if not trait: + if not (trait := self._device.active_event_trait): return None # Reuse image bytes if they have already been fetched if not isinstance(trait, EventImageGenerator): diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index 04954cc7a07..fe1c3034fc8 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -153,8 +153,7 @@ class ThermostatEntity(ClimateEntity): @property def target_temperature(self) -> float | None: """Return the temperature currently set to be reached.""" - trait = self._target_temperature_trait - if not trait: + if not (trait := self._target_temperature_trait): return None if self.hvac_mode == HVAC_MODE_HEAT: return trait.heat_celsius @@ -167,8 +166,7 @@ class ThermostatEntity(ClimateEntity): """Return the upper bound target temperature.""" if self.hvac_mode != HVAC_MODE_HEAT_COOL: return None - trait = self._target_temperature_trait - if not trait: + if not (trait := self._target_temperature_trait): return None return trait.cool_celsius @@ -177,8 +175,7 @@ class ThermostatEntity(ClimateEntity): """Return the lower bound target temperature.""" if self.hvac_mode != HVAC_MODE_HEAT_COOL: return None - trait = self._target_temperature_trait - if not trait: + if not (trait := self._target_temperature_trait): return None return trait.heat_celsius diff --git a/homeassistant/components/nest/device_info.py b/homeassistant/components/nest/device_info.py index 383c6d22258..605f3c48446 100644 --- a/homeassistant/components/nest/device_info.py +++ b/homeassistant/components/nest/device_info.py @@ -48,8 +48,7 @@ class NestDeviceInfo: return trait.custom_name # Build a name from the room/structure. Note: This room/structure name # is not associated with a home assistant Area. - parent_relations = self._device.parent_relations - if parent_relations: + if parent_relations := self._device.parent_relations: items = sorted(parent_relations.items()) names = [name for id, name in items] return " ".join(names) diff --git a/homeassistant/components/nut/config_flow.py b/homeassistant/components/nut/config_flow.py index 9b45e270448..ed490eddd37 100644 --- a/homeassistant/components/nut/config_flow.py +++ b/homeassistant/components/nut/config_flow.py @@ -88,8 +88,7 @@ async def validate_input(hass: core.HomeAssistant, data): data = PyNUTData(host, port, alias, username, password) await hass.async_add_executor_job(data.update) - status = data.status - if not status: + if not (status := data.status): raise CannotConnect return {"ups_list": data.ups_list, "available_resources": status} From 284861c8bbf9550bb794a5dd2256ebd71c37f4bc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 Oct 2021 08:47:35 -1000 Subject: [PATCH 0481/1038] Add support for push updates to flux_led (#57890) --- homeassistant/components/flux_led/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index fd194ac90b9..b2ebbbb3a3a 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,9 +3,9 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.24.5"], + "requirements": ["flux_led==0.24.6"], "codeowners": ["@icemanch"], - "iot_class": "local_polling", + "iot_class": "local_push", "dhcp": [ { "macaddress": "18B905*", diff --git a/requirements_all.txt b/requirements_all.txt index 2b868e0cc34..bb9d8de572f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -652,7 +652,7 @@ fjaraskupan==1.0.1 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.5 +flux_led==0.24.6 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 88738728123..8636bf47889 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -384,7 +384,7 @@ fjaraskupan==1.0.1 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.5 +flux_led==0.24.6 # homeassistant.components.homekit fnvhash==0.1.0 From f390812183d93475b4c269c723f541847badc5d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sun, 17 Oct 2021 20:56:03 +0200 Subject: [PATCH 0482/1038] Adax attr (#57867) --- homeassistant/components/adax/climate.py | 63 +++++++++--------------- 1 file changed, 22 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/adax/climate.py b/homeassistant/components/adax/climate.py index 6de31e289b6..7cc23c048fe 100644 --- a/homeassistant/components/adax/climate.py +++ b/homeassistant/components/adax/climate.py @@ -39,8 +39,11 @@ async def async_setup_entry( ) async_add_entities( - AdaxDevice(room, adax_data_handler) - for room in await adax_data_handler.get_rooms() + ( + AdaxDevice(room, adax_data_handler) + for room in await adax_data_handler.get_rooms() + ), + True, ) @@ -56,7 +59,7 @@ class AdaxDevice(ClimateEntity): def __init__(self, heater_data: dict[str, Any], adax_data_handler: Adax) -> None: """Initialize the heater.""" - self._heater_data = heater_data + self._device_id = heater_data["id"] self._adax_data_handler = adax_data_handler self._attr_unique_id = f"{heater_data['homeId']}_{heater_data['id']}" @@ -66,64 +69,42 @@ class AdaxDevice(ClimateEntity): manufacturer="Adax", ) - @property - def name(self) -> str: - """Return the name of the device, if any.""" - return self._heater_data["name"] - - @property - def hvac_mode(self) -> str: - """Return hvac operation ie. heat, cool mode.""" - if self._heater_data["heatingEnabled"]: - return HVAC_MODE_HEAT - return HVAC_MODE_OFF - - @property - def icon(self) -> str: - """Return nice icon for heater.""" - if self.hvac_mode == HVAC_MODE_HEAT: - return "mdi:radiator" - return "mdi:radiator-off" - async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set hvac mode.""" if hvac_mode == HVAC_MODE_HEAT: - temperature = max( - self.min_temp, self._heater_data.get("targetTemperature", self.min_temp) - ) + temperature = max(self.min_temp, self.target_temperature or self.min_temp) await self._adax_data_handler.set_room_target_temperature( - self._heater_data["id"], temperature, True + self._device_id, temperature, True ) elif hvac_mode == HVAC_MODE_OFF: await self._adax_data_handler.set_room_target_temperature( - self._heater_data["id"], self.min_temp, False + self._device_id, self.min_temp, False ) else: return await self._adax_data_handler.update() - @property - def current_temperature(self) -> float | None: - """Return the current temperature.""" - return self._heater_data.get("temperature") - - @property - def target_temperature(self) -> int | None: - """Return the temperature we try to reach.""" - return self._heater_data.get("targetTemperature") - async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) if temperature is None: return await self._adax_data_handler.set_room_target_temperature( - self._heater_data["id"], temperature, True + self._device_id, temperature, True ) async def async_update(self) -> None: """Get the latest data.""" for room in await self._adax_data_handler.get_rooms(): - if room["id"] == self._heater_data["id"]: - self._heater_data = room - return + if room["id"] != self._device_id: + continue + self._attr_name = room["name"] + self._attr_current_temperature = room.get("temperature") + self._attr_target_temperature = room.get("targetTemperature") + if room["heatingEnabled"]: + self._attr_hvac_mode = HVAC_MODE_HEAT + self._attr_icon = "mdi:radiator" + else: + self._attr_hvac_mode = HVAC_MODE_OFF + self._attr_icon = "mdi:radiator-off" + return From 4fd8b27ce68692ac916ac13b03d5846f832259d5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 17 Oct 2021 20:56:15 +0200 Subject: [PATCH 0483/1038] Add Vibration Sensor (zd) device support to Tuya (#57795) --- .../components/tuya/binary_sensor.py | 44 +++++++++++++++---- homeassistant/components/tuya/const.py | 3 ++ homeassistant/components/tuya/number.py | 9 ++++ homeassistant/components/tuya/sensor.py | 23 +++++++++- 4 files changed, 69 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index d2047f818ae..a851cbdbce5 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -9,6 +9,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_DOOR, DEVICE_CLASS_MOTION, DEVICE_CLASS_SAFETY, + DEVICE_CLASS_VIBRATION, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -27,6 +28,10 @@ from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode class TuyaBinarySensorEntityDescription(BinarySensorEntityDescription): """Describes a Tuya binary sensor.""" + # 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 @@ -84,6 +89,31 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ), + # Vibration Sensor + # https://developer.tuya.com/en/docs/iot/categoryzd?id=Kaiuz3a5vrzno + "zd": ( + TuyaBinarySensorEntityDescription( + key=f"{DPCode.SHOCK_STATE}_vibration", + dpcode=DPCode.SHOCK_STATE, + name="Vibration", + device_class=DEVICE_CLASS_VIBRATION, + on_value="vibration", + ), + TuyaBinarySensorEntityDescription( + key=f"{DPCode.SHOCK_STATE}_drop", + dpcode=DPCode.SHOCK_STATE, + name="Drop", + icon="mdi:icon=package-down", + on_value="drop", + ), + TuyaBinarySensorEntityDescription( + key=f"{DPCode.SHOCK_STATE}_tilt", + dpcode=DPCode.SHOCK_STATE, + name="Tilt", + icon="mdi:spirit-level", + on_value="tilt", + ), + ), } @@ -101,10 +131,8 @@ async def async_setup_entry( device = hass_data.device_manager.device_map[device_id] if descriptions := BINARY_SENSORS.get(device.category): for description in descriptions: - if ( - description.key in device.function - or description.key in device.status - ): + dpcode = description.dpcode or description.key + if dpcode in device.status: entities.append( TuyaBinarySensorEntity( device, hass_data.device_manager, description @@ -139,9 +167,7 @@ class TuyaBinarySensorEntity(TuyaEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return true if sensor is on.""" - if self.entity_description.key not in self.device.status: + dpcode = self.entity_description.dpcode or self.entity_description.key + if dpcode not in self.device.status: return False - return ( - self.device.status[self.entity_description.key] - == self.entity_description.on_value - ) + return self.device.status[dpcode] == self.entity_description.on_value diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 3a9dd15ee55..0bc5dcfa575 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -111,6 +111,7 @@ TUYA_SUPPORTED_PRODUCT_CATEGORIES = ( "wk", # Thermostat "xdd", # Ceiling Light "xxj", # Diffuser + "zd", # Vibration Sensor ) TUYA_SMART_APP = "tuyaSmart" @@ -173,9 +174,11 @@ class DPCode(str, Enum): POWDER_SET = "powder_set" # Powder PUMP_RESET = "pump_reset" # Water pump reset RECORD_SWITCH = "record_switch" # Recording switch + SENSITIVITY = "sensitivity" # Sensitivity SHAKE = "shake" # Oscillating SOS = "sos" # Emergency State SOS_STATE = "sos_state" # Emergency mode + SHOCK_STATE = "shock_state" # Vibration status SPEED = "speed" # Speed level START = "start" # Start SWING = "swing" # Swing mode diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index 15c7bf3150d..63d94195233 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -57,6 +57,15 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { entity_category=ENTITY_CATEGORY_CONFIG, ), ), + # Vibration Sensor + # https://developer.tuya.com/en/docs/iot/categoryzd?id=Kaiuz3a5vrzno + "zd": ( + NumberEntityDescription( + key=DPCode.SENSITIVITY, + name="Sensitivity", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), } diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 3d3647b0cc4..4bba17284cd 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -53,11 +53,13 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_BATTERY, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key=DPCode.BATTERY_STATE, name="Battery State", - entity_registry_enabled_default=False, + icon="mdi:battery", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ), # Switch @@ -146,6 +148,25 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { SensorEntityDescription( key=DPCode.BATTERY_STATE, name="Battery State", + icon="mdi:battery", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + ), + # Vibration Sensor + # https://developer.tuya.com/en/docs/iot/categoryzd?id=Kaiuz3a5vrzno + "zd": ( + SensorEntityDescription( + key=DPCode.BATTERY_PERCENTAGE, + name="Battery", + native_unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key=DPCode.BATTERY_STATE, + name="Battery State", + icon="mdi:battery", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ), From 6ca23c67ff508d14e42985b1463f8424868f16ab Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 17 Oct 2021 21:05:06 +0200 Subject: [PATCH 0484/1038] Use EntityDescription - bmw_connected_drive sensor (#57796) --- .../components/bmw_connected_drive/sensor.py | 767 +++++++++--------- 1 file changed, 400 insertions(+), 367 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 1faa858e9d8..daf7569bc77 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -1,12 +1,16 @@ """Support for reading vehicle status from BMW connected drive portal.""" from __future__ import annotations +from copy import copy +from dataclasses import dataclass import logging from bimmer_connected.const import SERVICE_ALL_TRIPS, SERVICE_LAST_TRIP, SERVICE_STATUS from bimmer_connected.state import ChargingState +from bimmer_connected.vehicle import ConnectedDriveVehicle -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_UNIT_SYSTEM_IMPERIAL, DEVICE_CLASS_TIMESTAMP, @@ -21,390 +25,399 @@ from homeassistant.const import ( VOLUME_GALLONS, VOLUME_LITERS, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.icon import icon_for_battery_level import homeassistant.util.dt as dt_util +from homeassistant.util.unit_system import UnitSystem -from . import DOMAIN as BMW_DOMAIN, BMWConnectedDriveBaseEntity +from . import ( + DOMAIN as BMW_DOMAIN, + BMWConnectedDriveAccount, + BMWConnectedDriveBaseEntity, +) from .const import CONF_ACCOUNT, DATA_ENTRIES _LOGGER = logging.getLogger(__name__) -SENSOR_TYPES: dict[str, tuple[str | None, str | None, str | None, str | None, bool]] = { - # "": (, , , , ), + +@dataclass +class BMWSensorEntityDescription(SensorEntityDescription): + """Describes BMW sensor entity.""" + + unit_metric: str | None = None + unit_imperial: str | None = None + + +SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { # --- Generic --- - "charging_time_remaining": ( - "mdi:update", - None, - TIME_HOURS, - TIME_HOURS, - True, + "charging_time_remaining": BMWSensorEntityDescription( + key="charging_time_remaining", + icon="mdi:update", + unit_metric=TIME_HOURS, + unit_imperial=TIME_HOURS, ), - "charging_status": ( - "mdi:battery-charging", - None, - None, - None, - True, + "charging_status": BMWSensorEntityDescription( + key="charging_status", + icon="mdi:battery-charging", ), # No icon as this is dealt with directly as a special case in icon() - "charging_level_hv": ( - None, - None, - PERCENTAGE, - PERCENTAGE, - True, + "charging_level_hv": BMWSensorEntityDescription( + key="charging_level_hv", + unit_metric=PERCENTAGE, + unit_imperial=PERCENTAGE, ), # LastTrip attributes - "date_utc": ( - None, - DEVICE_CLASS_TIMESTAMP, - None, - None, - True, + "date_utc": BMWSensorEntityDescription( + key="date_utc", + device_class=DEVICE_CLASS_TIMESTAMP, ), - "duration": ( - "mdi:timer-outline", - None, - TIME_MINUTES, - TIME_MINUTES, - True, + "duration": BMWSensorEntityDescription( + key="duration", + icon="mdi:timer-outline", + unit_metric=TIME_MINUTES, + unit_imperial=TIME_MINUTES, ), - "electric_distance_ratio": ( - "mdi:percent-outline", - None, - PERCENTAGE, - PERCENTAGE, - False, + "electric_distance_ratio": BMWSensorEntityDescription( + key="electric_distance_ratio", + icon="mdi:percent-outline", + unit_metric=PERCENTAGE, + unit_imperial=PERCENTAGE, + entity_registry_enabled_default=False, ), # AllTrips attributes - "battery_size_max": ( - "mdi:battery-charging-high", - None, - ENERGY_WATT_HOUR, - ENERGY_WATT_HOUR, - False, + "battery_size_max": BMWSensorEntityDescription( + key="battery_size_max", + icon="mdi:battery-charging-high", + unit_metric=ENERGY_WATT_HOUR, + unit_imperial=ENERGY_WATT_HOUR, + entity_registry_enabled_default=False, ), - "reset_date_utc": ( - None, - DEVICE_CLASS_TIMESTAMP, - None, - None, - False, + "reset_date_utc": BMWSensorEntityDescription( + key="reset_date_utc", + device_class=DEVICE_CLASS_TIMESTAMP, + entity_registry_enabled_default=False, ), - "saved_co2": ( - "mdi:tree-outline", - None, - MASS_KILOGRAMS, - MASS_KILOGRAMS, - False, + "saved_co2": BMWSensorEntityDescription( + key="saved_co2", + icon="mdi:tree-outline", + unit_metric=MASS_KILOGRAMS, + unit_imperial=MASS_KILOGRAMS, + entity_registry_enabled_default=False, ), - "saved_co2_green_energy": ( - "mdi:tree-outline", - None, - MASS_KILOGRAMS, - MASS_KILOGRAMS, - False, + "saved_co2_green_energy": BMWSensorEntityDescription( + key="saved_co2_green_energy", + icon="mdi:tree-outline", + unit_metric=MASS_KILOGRAMS, + unit_imperial=MASS_KILOGRAMS, + entity_registry_enabled_default=False, ), # --- Specific --- - "mileage": ( - "mdi:speedometer", - None, - LENGTH_KILOMETERS, - LENGTH_MILES, - True, + "mileage": BMWSensorEntityDescription( + key="mileage", + icon="mdi:speedometer", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, ), - "remaining_range_total": ( - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - LENGTH_MILES, - True, + "remaining_range_total": BMWSensorEntityDescription( + key="remaining_range_total", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, ), - "remaining_range_electric": ( - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - LENGTH_MILES, - True, + "remaining_range_electric": BMWSensorEntityDescription( + key="remaining_range_electric", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, ), - "remaining_range_fuel": ( - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - LENGTH_MILES, - True, + "remaining_range_fuel": BMWSensorEntityDescription( + key="remaining_range_fuel", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, ), - "max_range_electric": ( - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - LENGTH_MILES, - True, + "max_range_electric": BMWSensorEntityDescription( + key="max_range_electric", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, ), - "remaining_fuel": ( - "mdi:gas-station", - None, - VOLUME_LITERS, - VOLUME_GALLONS, - True, + "remaining_fuel": BMWSensorEntityDescription( + key="remaining_fuel", + icon="mdi:gas-station", + unit_metric=VOLUME_LITERS, + unit_imperial=VOLUME_GALLONS, ), # LastTrip attributes - "average_combined_consumption": ( - "mdi:flash", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - True, + "average_combined_consumption": BMWSensorEntityDescription( + key="average_combined_consumption", + icon="mdi:flash", + unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", ), - "average_electric_consumption": ( - "mdi:power-plug-outline", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - True, + "average_electric_consumption": BMWSensorEntityDescription( + key="average_electric_consumption", + icon="mdi:power-plug-outline", + unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", ), - "average_recuperation": ( - "mdi:recycle-variant", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - True, + "average_recuperation": BMWSensorEntityDescription( + key="average_recuperation", + icon="mdi:recycle-variant", + unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", ), - "electric_distance": ( - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - LENGTH_MILES, - True, + "electric_distance": BMWSensorEntityDescription( + key="electric_distance", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, ), - "saved_fuel": ( - "mdi:fuel", - None, - VOLUME_LITERS, - VOLUME_GALLONS, - False, + "saved_fuel": BMWSensorEntityDescription( + key="saved_fuel", + icon="mdi:fuel", + unit_metric=VOLUME_LITERS, + unit_imperial=VOLUME_GALLONS, + entity_registry_enabled_default=False, ), - "total_distance": ( - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - LENGTH_MILES, - True, + "total_distance": BMWSensorEntityDescription( + key="total_distance", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, ), # AllTrips attributes - "average_combined_consumption_community_average": ( - "mdi:flash", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - False, + "average_combined_consumption_community_average": BMWSensorEntityDescription( + key="average_combined_consumption_community_average", + icon="mdi:flash", + unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + entity_registry_enabled_default=False, ), - "average_combined_consumption_community_high": ( - "mdi:flash", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - False, + "average_combined_consumption_community_high": BMWSensorEntityDescription( + key="average_combined_consumption_community_high", + icon="mdi:flash", + unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + entity_registry_enabled_default=False, ), - "average_combined_consumption_community_low": ( - "mdi:flash", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - False, + "average_combined_consumption_community_low": BMWSensorEntityDescription( + key="average_combined_consumption_community_low", + icon="mdi:flash", + unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + entity_registry_enabled_default=False, ), - "average_combined_consumption_user_average": ( - "mdi:flash", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - True, + "average_combined_consumption_user_average": BMWSensorEntityDescription( + key="average_combined_consumption_user_average", + icon="mdi:flash", + unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", ), - "average_electric_consumption_community_average": ( - "mdi:power-plug-outline", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - False, + "average_electric_consumption_community_average": BMWSensorEntityDescription( + key="average_electric_consumption_community_average", + icon="mdi:power-plug-outline", + unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + entity_registry_enabled_default=False, ), - "average_electric_consumption_community_high": ( - "mdi:power-plug-outline", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - False, + "average_electric_consumption_community_high": BMWSensorEntityDescription( + key="average_electric_consumption_community_high", + icon="mdi:power-plug-outline", + unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + entity_registry_enabled_default=False, ), - "average_electric_consumption_community_low": ( - "mdi:power-plug-outline", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - False, + "average_electric_consumption_community_low": BMWSensorEntityDescription( + key="average_electric_consumption_community_low", + icon="mdi:power-plug-outline", + unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + entity_registry_enabled_default=False, ), - "average_electric_consumption_user_average": ( - "mdi:power-plug-outline", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - True, + "average_electric_consumption_user_average": BMWSensorEntityDescription( + key="average_electric_consumption_user_average", + icon="mdi:power-plug-outline", + unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", ), - "average_recuperation_community_average": ( - "mdi:recycle-variant", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - False, + "average_recuperation_community_average": BMWSensorEntityDescription( + key="average_recuperation_community_average", + icon="mdi:recycle-variant", + unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + entity_registry_enabled_default=False, ), - "average_recuperation_community_high": ( - "mdi:recycle-variant", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - False, + "average_recuperation_community_high": BMWSensorEntityDescription( + key="average_recuperation_community_high", + icon="mdi:recycle-variant", + unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + entity_registry_enabled_default=False, ), - "average_recuperation_community_low": ( - "mdi:recycle-variant", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - False, + "average_recuperation_community_low": BMWSensorEntityDescription( + key="average_recuperation_community_low", + icon="mdi:recycle-variant", + unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", + entity_registry_enabled_default=False, ), - "average_recuperation_user_average": ( - "mdi:recycle-variant", - None, - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", - f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", - True, + "average_recuperation_user_average": BMWSensorEntityDescription( + key="average_recuperation_user_average", + icon="mdi:recycle-variant", + unit_metric=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_KILOMETERS}", + unit_imperial=f"{ENERGY_KILO_WATT_HOUR}/100{LENGTH_MILES}", ), - "chargecycle_range_community_average": ( - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - LENGTH_MILES, - False, + "chargecycle_range_community_average": BMWSensorEntityDescription( + key="chargecycle_range_community_average", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, + entity_registry_enabled_default=False, ), - "chargecycle_range_community_high": ( - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - LENGTH_MILES, - False, + "chargecycle_range_community_high": BMWSensorEntityDescription( + key="chargecycle_range_community_high", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, + entity_registry_enabled_default=False, ), - "chargecycle_range_community_low": ( - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - LENGTH_MILES, - False, + "chargecycle_range_community_low": BMWSensorEntityDescription( + key="chargecycle_range_community_low", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, + entity_registry_enabled_default=False, ), - "chargecycle_range_user_average": ( - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - LENGTH_MILES, - True, + "chargecycle_range_user_average": BMWSensorEntityDescription( + key="chargecycle_range_user_average", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, ), - "chargecycle_range_user_current_charge_cycle": ( - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - LENGTH_MILES, - True, + "chargecycle_range_user_current_charge_cycle": BMWSensorEntityDescription( + key="chargecycle_range_user_current_charge_cycle", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, ), - "chargecycle_range_user_high": ( - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - LENGTH_MILES, - True, + "chargecycle_range_user_high": BMWSensorEntityDescription( + key="chargecycle_range_user_high", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, ), - "total_electric_distance_community_average": ( - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - LENGTH_MILES, - False, + "total_electric_distance_community_average": BMWSensorEntityDescription( + key="total_electric_distance_community_average", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, + entity_registry_enabled_default=False, ), - "total_electric_distance_community_high": ( - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - LENGTH_MILES, - False, + "total_electric_distance_community_high": BMWSensorEntityDescription( + key="total_electric_distance_community_high", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, + entity_registry_enabled_default=False, ), - "total_electric_distance_community_low": ( - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - LENGTH_MILES, - False, + "total_electric_distance_community_low": BMWSensorEntityDescription( + key="total_electric_distance_community_low", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, + entity_registry_enabled_default=False, ), - "total_electric_distance_user_average": ( - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - LENGTH_MILES, - False, + "total_electric_distance_user_average": BMWSensorEntityDescription( + key="total_electric_distance_user_average", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, + entity_registry_enabled_default=False, ), - "total_electric_distance_user_total": ( - "mdi:map-marker-distance", - None, - LENGTH_KILOMETERS, - LENGTH_MILES, - False, + "total_electric_distance_user_total": BMWSensorEntityDescription( + key="total_electric_distance_user_total", + icon="mdi:map-marker-distance", + unit_metric=LENGTH_KILOMETERS, + unit_imperial=LENGTH_MILES, + entity_registry_enabled_default=False, ), - "total_saved_fuel": ( - "mdi:fuel", - None, - VOLUME_LITERS, - VOLUME_GALLONS, - False, + "total_saved_fuel": BMWSensorEntityDescription( + key="total_saved_fuel", + icon="mdi:fuel", + unit_metric=VOLUME_LITERS, + unit_imperial=VOLUME_GALLONS, + entity_registry_enabled_default=False, ), } -async def async_setup_entry(hass, config_entry, async_add_entities): +DEFAULT_BMW_DESCRIPTION = BMWSensorEntityDescription( + key="", + entity_registry_enabled_default=True, +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the BMW ConnectedDrive sensors from config entry.""" # pylint: disable=too-many-nested-blocks - account = hass.data[BMW_DOMAIN][DATA_ENTRIES][config_entry.entry_id][CONF_ACCOUNT] - entities = [] + unit_system = hass.config.units + account: BMWConnectedDriveAccount = hass.data[BMW_DOMAIN][DATA_ENTRIES][ + config_entry.entry_id + ][CONF_ACCOUNT] + entities: list[BMWConnectedDriveSensor] = [] for vehicle in account.account.vehicles: for service in vehicle.available_state_services: if service == SERVICE_STATUS: - for attribute_name in vehicle.drive_train_attributes: - if attribute_name in vehicle.available_attributes: - device = BMWConnectedDriveSensor( - hass, account, vehicle, attribute_name + entities.extend( + [ + BMWConnectedDriveSensor( + account, vehicle, description, unit_system ) - entities.append(device) + for attribute_name in vehicle.drive_train_attributes + if attribute_name in vehicle.available_attributes + and (description := SENSOR_TYPES.get(attribute_name)) + ] + ) if service == SERVICE_LAST_TRIP: - for attribute_name in vehicle.state.last_trip.available_attributes: - if attribute_name == "date": - device = BMWConnectedDriveSensor( - hass, + entities.extend( + [ + BMWConnectedDriveSensor( + account, vehicle, description, unit_system, service + ) + for attribute_name in vehicle.state.last_trip.available_attributes + if attribute_name != "date" + and (description := SENSOR_TYPES.get(attribute_name)) + ] + ) + if "date" in vehicle.state.last_trip.available_attributes: + entities.append( + BMWConnectedDriveSensor( account, vehicle, - "date_utc", + SENSOR_TYPES["date_utc"], + unit_system, service, ) - entities.append(device) - else: - device = BMWConnectedDriveSensor( - hass, account, vehicle, attribute_name, service - ) - entities.append(device) + ) if service == SERVICE_ALL_TRIPS: for attribute_name in vehicle.state.all_trips.available_attributes: if attribute_name == "reset_date": - device = BMWConnectedDriveSensor( - hass, - account, - vehicle, - "reset_date_utc", - service, + entities.append( + BMWConnectedDriveSensor( + account, + vehicle, + SENSOR_TYPES["reset_date_utc"], + unit_system, + service, + ) ) - entities.append(device) elif attribute_name in ( "average_combined_consumption", "average_electric_consumption", @@ -412,45 +425,60 @@ async def async_setup_entry(hass, config_entry, async_add_entities): "chargecycle_range", "total_electric_distance", ): - for attr in ( - "community_average", - "community_high", - "community_low", - "user_average", - ): - device = BMWConnectedDriveSensor( - hass, + entities.extend( + [ + BMWConnectedDriveSensor( + account, + vehicle, + SENSOR_TYPES[f"{attribute_name}_{attr}"], + unit_system, + service, + ) + for attr in ( + "community_average", + "community_high", + "community_low", + "user_average", + ) + ] + ) + if attribute_name == "chargecycle_range": + entities.extend( + BMWConnectedDriveSensor( + account, + vehicle, + SENSOR_TYPES[f"{attribute_name}_{attr}"], + unit_system, + service, + ) + for attr in ("user_current_charge_cycle", "user_high") + ) + elif attribute_name == "total_electric_distance": + entities.extend( + [ + BMWConnectedDriveSensor( + account, + vehicle, + SENSOR_TYPES[f"{attribute_name}_{attr}"], + unit_system, + service, + ) + for attr in ("user_total",) + ] + ) + else: + if (description := SENSOR_TYPES.get(attribute_name)) is None: + description = copy(DEFAULT_BMW_DESCRIPTION) + description.key = attribute_name + entities.append( + BMWConnectedDriveSensor( account, vehicle, - f"{attribute_name}_{attr}", + description, + unit_system, service, ) - entities.append(device) - if attribute_name == "chargecycle_range": - for attr in ("user_current_charge_cycle", "user_high"): - device = BMWConnectedDriveSensor( - hass, - account, - vehicle, - f"{attribute_name}_{attr}", - service, - ) - entities.append(device) - if attribute_name == "total_electric_distance": - for attr in ("user_total",): - device = BMWConnectedDriveSensor( - hass, - account, - vehicle, - f"{attribute_name}_{attr}", - service, - ) - entities.append(device) - else: - device = BMWConnectedDriveSensor( - hass, account, vehicle, attribute_name, service ) - entities.append(device) async_add_entities(entities, True) @@ -458,52 +486,57 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): """Representation of a BMW vehicle sensor.""" - def __init__(self, hass, account, vehicle, attribute: str, service=None): + entity_description: BMWSensorEntityDescription + + def __init__( + self, + account: BMWConnectedDriveAccount, + vehicle: ConnectedDriveVehicle, + description: BMWSensorEntityDescription, + unit_system: UnitSystem, + service: str | None = None, + ) -> None: """Initialize BMW vehicle sensor.""" super().__init__(account, vehicle) + self.entity_description = description - self._attribute = attribute self._service = service if service: - self._attr_name = f"{vehicle.name} {service.lower()}_{attribute}" - self._attr_unique_id = f"{vehicle.vin}-{service.lower()}-{attribute}" + self._attr_name = f"{vehicle.name} {service.lower()}_{description.key}" + self._attr_unique_id = f"{vehicle.vin}-{service.lower()}-{description.key}" else: - self._attr_name = f"{vehicle.name} {attribute}" - self._attr_unique_id = f"{vehicle.vin}-{attribute}" - self._attribute_info = SENSOR_TYPES.get( - attribute, (None, None, None, None, True) - ) - self._attr_entity_registry_enabled_default = self._attribute_info[4] - self._attr_icon = self._attribute_info[0] - self._attr_device_class = self._attribute_info[1] - if hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: - self._attr_native_unit_of_measurement = self._attribute_info[3] + self._attr_name = f"{vehicle.name} {description.key}" + self._attr_unique_id = f"{vehicle.vin}-{description.key}" + + if unit_system.name == CONF_UNIT_SYSTEM_IMPERIAL: + self._attr_native_unit_of_measurement = description.unit_imperial else: - self._attr_native_unit_of_measurement = self._attribute_info[2] + self._attr_native_unit_of_measurement = description.unit_metric def update(self) -> None: """Read new state data from the library.""" _LOGGER.debug("Updating %s", self._vehicle.name) vehicle_state = self._vehicle.state - if self._attribute == "charging_status": - self._attr_native_value = getattr(vehicle_state, self._attribute).value + sensor_key = self.entity_description.key + if sensor_key == "charging_status": + self._attr_native_value = getattr(vehicle_state, sensor_key).value elif self.unit_of_measurement == VOLUME_GALLONS: - value = getattr(vehicle_state, self._attribute) + value = getattr(vehicle_state, sensor_key) value_converted = self.hass.config.units.volume(value, VOLUME_LITERS) self._attr_native_value = round(value_converted) elif self.unit_of_measurement == LENGTH_MILES: - value = getattr(vehicle_state, self._attribute) + value = getattr(vehicle_state, sensor_key) value_converted = self.hass.config.units.length(value, LENGTH_KILOMETERS) self._attr_native_value = round(value_converted) elif self._service is None: - self._attr_native_value = getattr(vehicle_state, self._attribute) + self._attr_native_value = getattr(vehicle_state, sensor_key) elif self._service == SERVICE_LAST_TRIP: vehicle_last_trip = self._vehicle.state.last_trip - if self._attribute == "date_utc": + if sensor_key == "date_utc": date_str = getattr(vehicle_last_trip, "date") self._attr_native_value = dt_util.parse_datetime(date_str).isoformat() else: - self._attr_native_value = getattr(vehicle_last_trip, self._attribute) + self._attr_native_value = getattr(vehicle_last_trip, sensor_key) elif self._service == SERVICE_ALL_TRIPS: vehicle_all_trips = self._vehicle.state.all_trips for attribute in ( @@ -513,21 +546,21 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): "chargecycle_range", "total_electric_distance", ): - if self._attribute.startswith(f"{attribute}_"): + if sensor_key.startswith(f"{attribute}_"): attr = getattr(vehicle_all_trips, attribute) - sub_attr = self._attribute.replace(f"{attribute}_", "") + sub_attr = sensor_key.replace(f"{attribute}_", "") self._attr_native_value = getattr(attr, sub_attr) return - if self._attribute == "reset_date_utc": + if sensor_key == "reset_date_utc": date_str = getattr(vehicle_all_trips, "reset_date") self._attr_native_value = dt_util.parse_datetime(date_str).isoformat() else: - self._attr_native_value = getattr(vehicle_all_trips, self._attribute) + self._attr_native_value = getattr(vehicle_all_trips, sensor_key) vehicle_state = self._vehicle.state charging_state = vehicle_state.charging_status in [ChargingState.CHARGING] - if self._attribute == "charging_level_hv": + if sensor_key == "charging_level_hv": self._attr_icon = icon_for_battery_level( battery_level=vehicle_state.charging_level_hv, charging=charging_state ) From 4501906da369e23b304857b8a3512798696f26a0 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 17 Oct 2021 13:04:12 -0700 Subject: [PATCH 0485/1038] Fix additional nest camera_sdm_tests to use STATE_STREAMING (#57920) --- tests/components/nest/camera_sdm_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/nest/camera_sdm_test.py b/tests/components/nest/camera_sdm_test.py index 08797fb3c6a..79b3ef1a929 100644 --- a/tests/components/nest/camera_sdm_test.py +++ b/tests/components/nest/camera_sdm_test.py @@ -208,7 +208,7 @@ async def test_camera_ws_stream(hass, auth, hass_ws_client): assert len(hass.states.async_all()) == 1 cam = hass.states.get("camera.my_camera") assert cam is not None - assert cam.state == STATE_IDLE + assert cam.state == STATE_STREAMING with patch("homeassistant.components.camera.create_stream") as mock_stream: mock_stream().endpoint_url.return_value = "http://home.assistant/playlist.m3u8" @@ -236,7 +236,7 @@ async def test_camera_ws_stream_failure(hass, auth, hass_ws_client): assert len(hass.states.async_all()) == 1 cam = hass.states.get("camera.my_camera") assert cam is not None - assert cam.state == STATE_IDLE + assert cam.state == STATE_STREAMING client = await hass_ws_client(hass) await client.send_json( @@ -768,7 +768,7 @@ async def test_camera_web_rtc_offer_failure(hass, auth, hass_ws_client): assert len(hass.states.async_all()) == 1 cam = hass.states.get("camera.my_camera") assert cam is not None - assert cam.state == STATE_IDLE + assert cam.state == STATE_STREAMING client = await hass_ws_client(hass) await client.send_json( From 32d6c27ba01a65c9ed0a7c75bda26593c4f32655 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Sun, 17 Oct 2021 19:47:53 -0300 Subject: [PATCH 0486/1038] Bump broadlink to 0.18.0 (#57929) --- homeassistant/components/broadlink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/broadlink/manifest.json b/homeassistant/components/broadlink/manifest.json index c27b9276ec4..aa895582240 100644 --- a/homeassistant/components/broadlink/manifest.json +++ b/homeassistant/components/broadlink/manifest.json @@ -2,7 +2,7 @@ "domain": "broadlink", "name": "Broadlink", "documentation": "https://www.home-assistant.io/integrations/broadlink", - "requirements": ["broadlink==0.17.0"], + "requirements": ["broadlink==0.18.0"], "codeowners": ["@danielhiversen", "@felipediel"], "config_flow": true, "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index bb9d8de572f..ad08e06a848 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -428,7 +428,7 @@ boto3==1.16.52 bravia-tv==1.0.11 # homeassistant.components.broadlink -broadlink==0.17.0 +broadlink==0.18.0 # homeassistant.components.brother brother==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8636bf47889..2a7205a1893 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -266,7 +266,7 @@ boschshcpy==0.2.19 bravia-tv==1.0.11 # homeassistant.components.broadlink -broadlink==0.17.0 +broadlink==0.18.0 # homeassistant.components.brother brother==1.1.0 From 3c4b7155240fa56cc83082918a119db6f8b18205 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 18 Oct 2021 01:21:56 +0200 Subject: [PATCH 0487/1038] Revert "Fix bmw_conntected_drive check_control_message short description" (#57928) This reverts commit acda3afe63fd051d035192a3983d2d48ff35289c. --- homeassistant/components/bmw_connected_drive/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index d2d2aa9d42f..358cdb67970 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -137,7 +137,7 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity): if has_check_control_messages: cbs_list = [] for message in check_control_messages: - cbs_list.append(message.description_short) + cbs_list.append(message["ccmDescriptionShort"]) result["check_control_messages"] = cbs_list else: result["check_control_messages"] = "OK" From a1176cc79aebdce7d83cabae59e954e0ea445a78 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 18 Oct 2021 00:11:59 +0000 Subject: [PATCH 0488/1038] [ci skip] Translation update --- .../accuweather/translations/zh-Hant.json | 2 +- .../components/adguard/translations/bg.json | 1 + .../advantage_air/translations/bg.json | 3 ++ .../components/agent_dvr/translations/bg.json | 4 +++ .../components/airvisual/translations/bg.json | 10 +++++- .../components/apple_tv/translations/bg.json | 9 +++++ .../components/atag/translations/bg.json | 4 +++ .../aurora/translations/zh-Hant.json | 2 +- .../binary_sensor/translations/et.json | 4 +++ .../binary_sensor/translations/ru.json | 4 +++ .../binary_sensor/translations/zh-Hant.json | 6 +++- .../components/blebox/translations/bg.json | 9 +++++ .../translations/zh-Hant.json | 2 +- .../components/braviatv/translations/bg.json | 14 ++++++++ .../components/brother/translations/bg.json | 8 +++++ .../components/bsblan/translations/bg.json | 5 +++ .../coinbase/translations/zh-Hant.json | 2 +- .../components/daikin/translations/bg.json | 3 +- .../components/deconz/translations/bg.json | 1 + .../devolo_home_control/translations/bg.json | 14 ++++++++ .../components/directv/translations/bg.json | 11 ++++++ .../components/doorbird/translations/bg.json | 16 +++++++++ .../components/dsmr/translations/bg.json | 6 ++++ .../flick_electric/translations/bg.json | 22 ++++++++++++ .../forked_daapd/translations/bg.json | 23 ++++++++++++ .../components/freebox/translations/bg.json | 3 ++ .../components/fritzbox/translations/bg.json | 18 ++++++++++ .../components/gios/translations/bg.json | 11 ++++++ .../components/gios/translations/zh-Hant.json | 2 +- .../home_connect/translations/bg.json | 15 ++++++++ .../components/hue/translations/bg.json | 6 ++++ .../translations/bg.json | 18 ++++++++++ .../components/icloud/translations/bg.json | 15 ++++++++ .../components/ipma/translations/bg.json | 1 + .../components/isy994/translations/bg.json | 21 +++++++++++ .../isy994/translations/zh-Hant.json | 6 ++-- .../components/juicenet/translations/bg.json | 12 +++++++ .../components/konnected/translations/bg.json | 25 +++++++++++++ .../components/local_ip/translations/bg.json | 13 +++++++ .../lutron_caseta/translations/bg.json | 9 +++++ .../components/melcloud/translations/bg.json | 12 +++++++ .../meteo_france/translations/bg.json | 12 +++++++ .../components/mikrotik/translations/bg.json | 6 +++- .../components/mill/translations/bg.json | 11 ++++++ .../minecraft_server/translations/bg.json | 12 +++++++ .../components/netatmo/translations/bg.json | 12 +++++++ .../netatmo/translations/zh-Hant.json | 8 ++--- .../components/nut/translations/zh-Hant.json | 2 +- .../components/nws/translations/bg.json | 20 +++++++++++ .../components/onvif/translations/bg.json | 10 ++++++ .../panasonic_viera/translations/bg.json | 24 +++++++++++++ .../components/plex/translations/bg.json | 9 +++++ .../translations/zh-Hant.json | 6 ++-- .../components/roomba/translations/bg.json | 10 ++++++ .../components/samsungtv/translations/bg.json | 3 +- .../season/translations/sensor.bg.json | 6 ++++ .../sensor/translations/zh-Hant.json | 2 +- .../smartthings/translations/bg.json | 6 ++++ .../solarlog/translations/zh-Hant.json | 2 +- .../components/soma/translations/zh-Hant.json | 8 ++--- .../components/songpal/translations/bg.json | 11 ++++++ .../stookalert/translations/ja.json | 11 ++++++ .../subaru/translations/zh-Hant.json | 2 +- .../synology_dsm/translations/bg.json | 12 +++++++ .../components/tado/translations/bg.json | 20 +++++++++++ .../components/tibber/translations/bg.json | 7 ++++ .../totalconnect/translations/bg.json | 15 ++++++++ .../components/tuya/translations/bg.json | 14 ++++++-- .../components/upb/translations/bg.json | 16 +++++++++ .../components/upnp/translations/bg.json | 7 +++- .../uptimerobot/translations/zh-Hant.json | 4 +-- .../components/vilfo/translations/bg.json | 11 ++++++ .../components/vizio/translations/bg.json | 12 +++++++ .../vlc_telnet/translations/et.json | 8 ++++- .../vlc_telnet/translations/he.json | 5 ++- .../vlc_telnet/translations/pl.json | 5 ++- .../vlc_telnet/translations/ru.json | 8 ++++- .../vlc_telnet/translations/zh-Hant.json | 36 +++++++++++++++++++ .../components/wiffi/translations/bg.json | 4 +++ .../components/wled/translations/bg.json | 1 + .../xiaomi_aqara/translations/zh-Hant.json | 2 +- .../xiaomi_miio/translations/bg.json | 12 ++++++- .../xiaomi_miio/translations/zh-Hant.json | 3 +- .../components/zerproc/translations/bg.json | 13 +++++++ .../components/zha/translations/bg.json | 7 ++++ 85 files changed, 746 insertions(+), 41 deletions(-) create mode 100644 homeassistant/components/apple_tv/translations/bg.json create mode 100644 homeassistant/components/devolo_home_control/translations/bg.json create mode 100644 homeassistant/components/directv/translations/bg.json create mode 100644 homeassistant/components/doorbird/translations/bg.json create mode 100644 homeassistant/components/flick_electric/translations/bg.json create mode 100644 homeassistant/components/forked_daapd/translations/bg.json create mode 100644 homeassistant/components/gios/translations/bg.json create mode 100644 homeassistant/components/home_connect/translations/bg.json create mode 100644 homeassistant/components/hunterdouglas_powerview/translations/bg.json create mode 100644 homeassistant/components/icloud/translations/bg.json create mode 100644 homeassistant/components/isy994/translations/bg.json create mode 100644 homeassistant/components/juicenet/translations/bg.json create mode 100644 homeassistant/components/local_ip/translations/bg.json create mode 100644 homeassistant/components/melcloud/translations/bg.json create mode 100644 homeassistant/components/meteo_france/translations/bg.json create mode 100644 homeassistant/components/minecraft_server/translations/bg.json create mode 100644 homeassistant/components/netatmo/translations/bg.json create mode 100644 homeassistant/components/nws/translations/bg.json create mode 100644 homeassistant/components/panasonic_viera/translations/bg.json create mode 100644 homeassistant/components/songpal/translations/bg.json create mode 100644 homeassistant/components/stookalert/translations/ja.json create mode 100644 homeassistant/components/tado/translations/bg.json create mode 100644 homeassistant/components/tibber/translations/bg.json create mode 100644 homeassistant/components/totalconnect/translations/bg.json create mode 100644 homeassistant/components/upb/translations/bg.json create mode 100644 homeassistant/components/vilfo/translations/bg.json create mode 100644 homeassistant/components/vizio/translations/bg.json create mode 100644 homeassistant/components/vlc_telnet/translations/zh-Hant.json create mode 100644 homeassistant/components/zerproc/translations/bg.json diff --git a/homeassistant/components/accuweather/translations/zh-Hant.json b/homeassistant/components/accuweather/translations/zh-Hant.json index eb3729fd2c4..4ebb296d5d3 100644 --- a/homeassistant/components/accuweather/translations/zh-Hant.json +++ b/homeassistant/components/accuweather/translations/zh-Hant.json @@ -16,7 +16,7 @@ "longitude": "\u7d93\u5ea6", "name": "\u540d\u7a31" }, - "description": "\u5047\u5982\u4f60\u9700\u8981\u5354\u52a9\u9032\u884c\u8a2d\u5b9a\uff0c\u8acb\u53c3\u95b1\uff1ahttps://www.home-assistant.io/integrations/accuweather/\n\n\u67d0\u4e9b\u50b3\u611f\u5668\u9810\u8a2d\u70ba\u672a\u555f\u7528\uff0c\u53ef\u4ee5\u65bc\u6574\u5408\u8a2d\u5b9a\u4e2d\u555f\u7528\u9019\u4e9b\u5be6\u9ad4\u3002\u5929\u6c23\u9810\u5831\u9810\u8a2d\u672a\u958b\u555f\u3002\u53ef\u4ee5\u65bc\u6574\u5408\u9078\u9805\u4e2d\u958b\u555f\u3002", + "description": "\u5047\u5982\u4f60\u9700\u8981\u5354\u52a9\u9032\u884c\u8a2d\u5b9a\uff0c\u8acb\u53c3\u95b1\uff1ahttps://www.home-assistant.io/integrations/accuweather/\n\n\u67d0\u4e9b\u611f\u6e2c\u5668\u9810\u8a2d\u70ba\u672a\u555f\u7528\uff0c\u53ef\u4ee5\u65bc\u6574\u5408\u8a2d\u5b9a\u4e2d\u555f\u7528\u9019\u4e9b\u5be6\u9ad4\u3002\u5929\u6c23\u9810\u5831\u9810\u8a2d\u672a\u958b\u555f\u3002\u53ef\u4ee5\u65bc\u6574\u5408\u9078\u9805\u4e2d\u958b\u555f\u3002", "title": "AccuWeather" } } diff --git a/homeassistant/components/adguard/translations/bg.json b/homeassistant/components/adguard/translations/bg.json index 00ad33588a4..2eb6a211ecd 100644 --- a/homeassistant/components/adguard/translations/bg.json +++ b/homeassistant/components/adguard/translations/bg.json @@ -13,6 +13,7 @@ }, "user": { "data": { + "host": "\u0425\u043e\u0441\u0442", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "port": "\u041f\u043e\u0440\u0442", "ssl": "AdGuard Home \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 SSL \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442", diff --git a/homeassistant/components/advantage_air/translations/bg.json b/homeassistant/components/advantage_air/translations/bg.json index 426266d26be..9afe310830d 100644 --- a/homeassistant/components/advantage_air/translations/bg.json +++ b/homeassistant/components/advantage_air/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/agent_dvr/translations/bg.json b/homeassistant/components/agent_dvr/translations/bg.json index 4983c9a14b2..527adb67bf7 100644 --- a/homeassistant/components/agent_dvr/translations/bg.json +++ b/homeassistant/components/agent_dvr/translations/bg.json @@ -1,8 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "step": { "user": { "data": { + "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" } } diff --git a/homeassistant/components/airvisual/translations/bg.json b/homeassistant/components/airvisual/translations/bg.json index 065d5854420..b2c2b26bad3 100644 --- a/homeassistant/components/airvisual/translations/bg.json +++ b/homeassistant/components/airvisual/translations/bg.json @@ -4,7 +4,9 @@ "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\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "general_error": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430", + "invalid_api_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447" }, "step": { "geography_by_name": { @@ -13,6 +15,12 @@ "country": "\u0421\u0442\u0440\u0430\u043d\u0430" } }, + "node_pro": { + "data": { + "ip_address": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + }, "reauth_confirm": { "data": { "api_key": "API \u043a\u043b\u044e\u0447" diff --git a/homeassistant/components/apple_tv/translations/bg.json b/homeassistant/components/apple_tv/translations/bg.json new file mode 100644 index 00000000000..b5195ba46a5 --- /dev/null +++ b/homeassistant/components/apple_tv/translations/bg.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "service_problem": { + "title": "\u0414\u043e\u0431\u0430\u0432\u044f\u043d\u0435\u0442\u043e \u043d\u0430 \u0443\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u043d\u0435 \u0431\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/atag/translations/bg.json b/homeassistant/components/atag/translations/bg.json index 4983c9a14b2..527adb67bf7 100644 --- a/homeassistant/components/atag/translations/bg.json +++ b/homeassistant/components/atag/translations/bg.json @@ -1,8 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "step": { "user": { "data": { + "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" } } diff --git a/homeassistant/components/aurora/translations/zh-Hant.json b/homeassistant/components/aurora/translations/zh-Hant.json index e1824a7ff4a..d12e8332373 100644 --- a/homeassistant/components/aurora/translations/zh-Hant.json +++ b/homeassistant/components/aurora/translations/zh-Hant.json @@ -22,5 +22,5 @@ } } }, - "title": "NOAA Aurora \u50b3\u611f\u5668" + "title": "NOAA Aurora \u611f\u6e2c\u5668" } \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/et.json b/homeassistant/components/binary_sensor/translations/et.json index 2a0172300c9..0249e401565 100644 --- a/homeassistant/components/binary_sensor/translations/et.json +++ b/homeassistant/components/binary_sensor/translations/et.json @@ -31,6 +31,7 @@ "is_not_plugged_in": "{entity_name} on lahti \u00fchendatud", "is_not_powered": "{entity_name} ei ole voolu all", "is_not_present": "{entity_name} puudub", + "is_not_tampered": "{entity_name} ei tuvasta omavoli", "is_not_unsafe": "{entity_name} on turvaline", "is_occupied": "{entity_name} on h\u00f5ivatud", "is_off": "{entity_name} on v\u00e4lja l\u00fclitatud", @@ -42,6 +43,7 @@ "is_problem": "Olemil {entity_name} on probleem", "is_smoke": "{entity_name} tuvastab suitsu", "is_sound": "{entity_name} tuvastab heli", + "is_tampered": "{entity_name} tuvastab omavolilise muutmist", "is_unsafe": "{entity_name} on ebaturvaline", "is_update": "{entity_name} on saadaval uuendus", "is_vibration": "{entity_name} tuvastab vibratsiooni" @@ -52,6 +54,8 @@ "connected": "{entity_name} on \u00fchendatud", "gas": "{entity_name} tuvastas gaasi(leket)", "hot": "{entity_name} muutus kuumaks", + "is_not_tampered": "{entity_name} l\u00f5petas omavolilise muutmise tuvastamise", + "is_tampered": "{entity_name} alustas omavolilise muutmise tuvastamist", "light": "{entity_name} tuvastas valgust", "locked": "{entity_name} on lukus", "moist": "{entity_name} muutus niiskeks", diff --git a/homeassistant/components/binary_sensor/translations/ru.json b/homeassistant/components/binary_sensor/translations/ru.json index c245d2ba15a..2e340b249d0 100644 --- a/homeassistant/components/binary_sensor/translations/ru.json +++ b/homeassistant/components/binary_sensor/translations/ru.json @@ -31,6 +31,7 @@ "is_not_plugged_in": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", "is_not_powered": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0438\u0442\u0430\u043d\u0438\u0435", "is_not_present": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", + "is_not_tampered": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u043e\u043d\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u0438\u0435", "is_not_unsafe": "{entity_name} \u0432 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_occupied": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "is_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", @@ -42,6 +43,7 @@ "is_problem": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", "is_smoke": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u044b\u043c", "is_sound": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0437\u0432\u0443\u043a", + "is_tampered": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u043e\u043d\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u0438\u0435", "is_unsafe": "{entity_name} \u0432 \u043d\u0435\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_update": "\u0414\u043e\u0441\u0442\u0443\u043f\u043d\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 {entity_name}", "is_vibration": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044e" @@ -52,6 +54,8 @@ "connected": "{entity_name} \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", "gas": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0433\u0430\u0437", "hot": "{entity_name} \u043d\u0430\u0433\u0440\u0435\u0432\u0430\u0435\u0442\u0441\u044f", + "is_not_tampered": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u043d\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u0438\u0435", + "is_tampered": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u043d\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u0438\u0435", "light": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0441\u0432\u0435\u0442", "locked": "{entity_name} \u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0435\u0442\u0441\u044f", "moist": "{entity_name} \u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0441\u044f \u0432\u043b\u0430\u0436\u043d\u044b\u043c", diff --git a/homeassistant/components/binary_sensor/translations/zh-Hant.json b/homeassistant/components/binary_sensor/translations/zh-Hant.json index 4733d4d1dcc..0009037d262 100644 --- a/homeassistant/components/binary_sensor/translations/zh-Hant.json +++ b/homeassistant/components/binary_sensor/translations/zh-Hant.json @@ -31,6 +31,7 @@ "is_not_plugged_in": "{entity_name}\u672a\u63d2\u5165", "is_not_powered": "{entity_name}\u672a\u901a\u96fb", "is_not_present": "{entity_name}\u672a\u51fa\u73fe", + "is_not_tampered": "{entity_name}\u672a\u5075\u6e2c\u5230\u6e1b\u5f31", "is_not_unsafe": "{entity_name}\u5b89\u5168", "is_occupied": "{entity_name}\u6709\u4eba", "is_off": "{entity_name}\u95dc\u9589", @@ -42,6 +43,7 @@ "is_problem": "{entity_name}\u6b63\u5075\u6e2c\u5230\u554f\u984c", "is_smoke": "{entity_name}\u6b63\u5075\u6e2c\u5230\u7159\u9727", "is_sound": "{entity_name}\u6b63\u5075\u6e2c\u5230\u8072\u97f3", + "is_tampered": "{entity_name}\u5075\u6e2c\u5230\u6e1b\u5f31\u4f5c\u4e2d", "is_unsafe": "{entity_name}\u4e0d\u5b89\u5168", "is_update": "{entity_name} \u6709\u66f4\u65b0", "is_vibration": "{entity_name}\u6b63\u5075\u6e2c\u5230\u9707\u52d5" @@ -52,6 +54,8 @@ "connected": "{entity_name}\u5df2\u9023\u7dda", "gas": "{entity_name}\u5df2\u958b\u59cb\u5075\u6e2c\u6c23\u9ad4", "hot": "{entity_name}\u5df2\u8b8a\u71b1", + "is_not_tampered": "{entity_name}\u5df2\u505c\u6b62\u5075\u6e2c\u6e1b\u5f31", + "is_tampered": "{entity_name}\u5df2\u5075\u6e2c\u5230\u6e1b\u5f31", "light": "{entity_name}\u5df2\u958b\u59cb\u5075\u6e2c\u5149\u7dda", "locked": "{entity_name}\u5df2\u4e0a\u9396", "moist": "{entity_name}\u5df2\u8b8a\u6f6e\u6fd5", @@ -195,5 +199,5 @@ "on": "\u958b\u555f" } }, - "title": "\u4e8c\u9032\u4f4d\u50b3\u611f\u5668" + "title": "\u4e8c\u9032\u4f4d\u611f\u6e2c\u5668" } \ No newline at end of file diff --git a/homeassistant/components/blebox/translations/bg.json b/homeassistant/components/blebox/translations/bg.json index 4983c9a14b2..11108007b21 100644 --- a/homeassistant/components/blebox/translations/bg.json +++ b/homeassistant/components/blebox/translations/bg.json @@ -1,8 +1,17 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { + "host": "IP \u0430\u0434\u0440\u0435\u0441", "port": "\u041f\u043e\u0440\u0442" } } diff --git a/homeassistant/components/bmw_connected_drive/translations/zh-Hant.json b/homeassistant/components/bmw_connected_drive/translations/zh-Hant.json index fde5e1e3c94..4f62df586f5 100644 --- a/homeassistant/components/bmw_connected_drive/translations/zh-Hant.json +++ b/homeassistant/components/bmw_connected_drive/translations/zh-Hant.json @@ -21,7 +21,7 @@ "step": { "account_options": { "data": { - "read_only": "\u552f\u8b80\uff08\u50c5\u652f\u63f4\u50b3\u611f\u5668\u8207\u901a\u77e5\uff0c\u4e0d\n\u5305\u542b\u670d\u52d9\u8207\u9396\u5b9a\uff09", + "read_only": "\u552f\u8b80\uff08\u50c5\u652f\u63f4\u611f\u6e2c\u5668\u8207\u901a\u77e5\uff0c\u4e0d\n\u5305\u542b\u670d\u52d9\u8207\u9396\u5b9a\uff09", "use_location": "\u4f7f\u7528 Home Assistant \u4f4d\u7f6e\u53d6\u5f97\u6c7d\u8eca\u4f4d\u7f6e\uff08\u9700\u8981\u70ba2014/7 \u524d\u751f\u7522\u7684\u975ei3/i8 \u8eca\u6b3e\uff09" } } diff --git a/homeassistant/components/braviatv/translations/bg.json b/homeassistant/components/braviatv/translations/bg.json index 5ee1a63993d..d05511b8d29 100644 --- a/homeassistant/components/braviatv/translations/bg.json +++ b/homeassistant/components/braviatv/translations/bg.json @@ -1,10 +1,24 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_host": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0438\u043c\u0435 \u043d\u0430 \u0445\u043e\u0441\u0442 \u0438\u043b\u0438 IP \u0430\u0434\u0440\u0435\u0441", + "unsupported_model": "\u041c\u043e\u0434\u0435\u043b\u044a\u0442 \u043d\u0430 \u0432\u0430\u0448\u0438\u044f \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430." + }, "step": { "authorize": { "data": { "pin": "\u041f\u0418\u041d \u043a\u043e\u0434" } + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "title": "Sony Bravia TV" } } } diff --git a/homeassistant/components/brother/translations/bg.json b/homeassistant/components/brother/translations/bg.json index 2ac8a444100..afb3272e575 100644 --- a/homeassistant/components/brother/translations/bg.json +++ b/homeassistant/components/brother/translations/bg.json @@ -2,6 +2,14 @@ "config": { "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "flow_title": "{model} {serial_number}", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/bg.json b/homeassistant/components/bsblan/translations/bg.json index 4983c9a14b2..76be712c2d5 100644 --- a/homeassistant/components/bsblan/translations/bg.json +++ b/homeassistant/components/bsblan/translations/bg.json @@ -1,8 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "flow_title": "{name}", "step": { "user": { "data": { + "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" } } diff --git a/homeassistant/components/coinbase/translations/zh-Hant.json b/homeassistant/components/coinbase/translations/zh-Hant.json index 5db1da7d23b..e6c92f1cb75 100644 --- a/homeassistant/components/coinbase/translations/zh-Hant.json +++ b/homeassistant/components/coinbase/translations/zh-Hant.json @@ -31,7 +31,7 @@ "init": { "data": { "account_balance_currencies": "\u5e33\u6236\u9918\u984d\u56de\u5831\u503c\u3002", - "exchange_base": "\u532f\u7387\u50b3\u611f\u5668\u57fa\u6e96\u8ca8\u5e63\u3002", + "exchange_base": "\u532f\u7387\u611f\u6e2c\u5668\u57fa\u6e96\u8ca8\u5e63\u3002", "exchange_rate_currencies": "\u532f\u7387\u56de\u5831\u503c\u3002" }, "description": "\u8abf\u6574 Coinbase \u9078\u9805" diff --git a/homeassistant/components/daikin/translations/bg.json b/homeassistant/components/daikin/translations/bg.json index a9b3e51b37d..5a8e7d875f9 100644 --- a/homeassistant/components/daikin/translations/bg.json +++ b/homeassistant/components/daikin/translations/bg.json @@ -10,7 +10,8 @@ "step": { "user": { "data": { - "host": "\u0410\u0434\u0440\u0435\u0441" + "host": "\u0410\u0434\u0440\u0435\u0441", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" }, "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 IP \u0430\u0434\u0440\u0435\u0441 \u043d\u0430 \u0432\u0430\u0448\u0438\u044f \u043a\u043b\u0438\u043c\u0430\u0442\u0438\u043a Daikin.", "title": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043a\u043b\u0438\u043c\u0430\u0442\u0438\u043a Daikin" diff --git a/homeassistant/components/deconz/translations/bg.json b/homeassistant/components/deconz/translations/bg.json index ba2b02e9232..2f339bf73a7 100644 --- a/homeassistant/components/deconz/translations/bg.json +++ b/homeassistant/components/deconz/translations/bg.json @@ -22,6 +22,7 @@ }, "manual_input": { "data": { + "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" } } diff --git a/homeassistant/components/devolo_home_control/translations/bg.json b/homeassistant/components/devolo_home_control/translations/bg.json new file mode 100644 index 00000000000..b3b4632e98d --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/bg.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u0440\u043e\u0444\u0438\u043b\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/translations/bg.json b/homeassistant/components/directv/translations/bg.json new file mode 100644 index 00000000000..ffb69776060 --- /dev/null +++ b/homeassistant/components/directv/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/translations/bg.json b/homeassistant/components/doorbird/translations/bg.json new file mode 100644 index 00000000000..d152ddfcf20 --- /dev/null +++ b/homeassistant/components/doorbird/translations/bg.json @@ -0,0 +1,16 @@ +{ + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/bg.json b/homeassistant/components/dsmr/translations/bg.json index b3341fccde8..fafad0e471c 100644 --- a/homeassistant/components/dsmr/translations/bg.json +++ b/homeassistant/components/dsmr/translations/bg.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "cannot_communicate": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430 \u043a\u043e\u043c\u0443\u043d\u0438\u043a\u0430\u0446\u0438\u044f" + }, + "error": { + "cannot_communicate": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430 \u043a\u043e\u043c\u0443\u043d\u0438\u043a\u0430\u0446\u0438\u044f" + }, "step": { "setup_network": { "data": { diff --git a/homeassistant/components/flick_electric/translations/bg.json b/homeassistant/components/flick_electric/translations/bg.json new file mode 100644 index 00000000000..5e1ec63739a --- /dev/null +++ b/homeassistant/components/flick_electric/translations/bg.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + }, + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "client_id": "Client ID (\u043f\u043e \u0438\u0437\u0431\u043e\u0440)", + "client_secret": "Client Secret (\u043f\u043e \u0438\u0437\u0431\u043e\u0440)", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/bg.json b/homeassistant/components/forked_daapd/translations/bg.json new file mode 100644 index 00000000000..5a415dfbee2 --- /dev/null +++ b/homeassistant/components/forked_daapd/translations/bg.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "unknown_error": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430", + "wrong_host_or_port": "\u041d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0436\u0435. \u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430.", + "wrong_password": "\u0413\u0440\u0435\u0448\u043d\u0430 \u043f\u0430\u0440\u043e\u043b\u0430." + }, + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041f\u0440\u0438\u044f\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435", + "password": "API \u043f\u0430\u0440\u043e\u043b\u0430 (\u043e\u0441\u0442\u0430\u0432\u0435\u0442\u0435 \u043f\u0440\u0430\u0437\u043d\u043e, \u0430\u043a\u043e \u043d\u044f\u043c\u0430 \u043f\u0430\u0440\u043e\u043b\u0430)", + "port": "API \u043f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/translations/bg.json b/homeassistant/components/freebox/translations/bg.json index 4983c9a14b2..c8526b8367d 100644 --- a/homeassistant/components/freebox/translations/bg.json +++ b/homeassistant/components/freebox/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "register_failed": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0435 \u0431\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430, \u043c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/fritzbox/translations/bg.json b/homeassistant/components/fritzbox/translations/bg.json index ec678d2d76c..ac7e60b9afc 100644 --- a/homeassistant/components/fritzbox/translations/bg.json +++ b/homeassistant/components/fritzbox/translations/bg.json @@ -1,11 +1,29 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "flow_title": "{name}", "step": { + "confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + }, + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name}?" + }, "reauth_confirm": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } } } } diff --git a/homeassistant/components/gios/translations/bg.json b/homeassistant/components/gios/translations/bg.json new file mode 100644 index 00000000000..35cfa0ad1d7 --- /dev/null +++ b/homeassistant/components/gios/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u0418\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/translations/zh-Hant.json b/homeassistant/components/gios/translations/zh-Hant.json index d72bc9bc015..98a62385ee1 100644 --- a/homeassistant/components/gios/translations/zh-Hant.json +++ b/homeassistant/components/gios/translations/zh-Hant.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "invalid_sensors_data": "\u6b64\u76e3\u6e2c\u7ad9\u50b3\u611f\u5668\u8cc7\u6599\u7121\u6548\u3002", + "invalid_sensors_data": "\u6b64\u76e3\u6e2c\u7ad9\u611f\u6e2c\u5668\u8cc7\u6599\u7121\u6548\u3002", "wrong_station_id": "\u76e3\u6e2c\u7ad9 ID \u4e0d\u6b63\u78ba\u3002" }, "step": { diff --git a/homeassistant/components/home_connect/translations/bg.json b/homeassistant/components/home_connect/translations/bg.json new file mode 100644 index 00000000000..ac264191dcd --- /dev/null +++ b/homeassistant/components/home_connect/translations/bg.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "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\u0435\u043d" + }, + "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" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/translations/bg.json b/homeassistant/components/hue/translations/bg.json index 76db584bae7..864963b3da5 100644 --- a/homeassistant/components/hue/translations/bg.json +++ b/homeassistant/components/hue/translations/bg.json @@ -26,5 +26,11 @@ "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u0445\u044a\u0431" } } + }, + "device_automation": { + "trigger_subtype": { + "double_buttons_1_3": "\u041f\u044a\u0440\u0432\u0438 \u0438 \u0442\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d\u0438", + "double_buttons_2_4": "\u0412\u0442\u043e\u0440\u0438 \u0438 \u0447\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d\u0438" + } } } \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/bg.json b/homeassistant/components/hunterdouglas_powerview/translations/bg.json new file mode 100644 index 00000000000..4baee58f6bd --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/bg.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "IP \u0430\u0434\u0440\u0435\u0441" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/translations/bg.json b/homeassistant/components/icloud/translations/bg.json new file mode 100644 index 00000000000..d074f0ff9b9 --- /dev/null +++ b/homeassistant/components/icloud/translations/bg.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "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" + }, + "step": { + "user": { + "data": { + "username": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/translations/bg.json b/homeassistant/components/ipma/translations/bg.json index fcabe7be75c..71f5cc0dbab 100644 --- a/homeassistant/components/ipma/translations/bg.json +++ b/homeassistant/components/ipma/translations/bg.json @@ -8,6 +8,7 @@ "data": { "latitude": "\u0428\u0438\u0440\u0438\u043d\u0430", "longitude": "\u0414\u044a\u043b\u0436\u0438\u043d\u0430", + "mode": "\u0420\u0435\u0436\u0438\u043c", "name": "\u0418\u043c\u0435" }, "description": "Instituto Portugu\u00eas do Mar e Atmosfera", diff --git a/homeassistant/components/isy994/translations/bg.json b/homeassistant/components/isy994/translations/bg.json new file mode 100644 index 00000000000..64b2d259c20 --- /dev/null +++ b/homeassistant/components/isy994/translations/bg.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "URL", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/zh-Hant.json b/homeassistant/components/isy994/translations/zh-Hant.json index 2f955dd25e6..6370d7bbbf8 100644 --- a/homeassistant/components/isy994/translations/zh-Hant.json +++ b/homeassistant/components/isy994/translations/zh-Hant.json @@ -29,10 +29,10 @@ "data": { "ignore_string": "\u5ffd\u7565\u5b57\u4e32", "restore_light_state": "\u56de\u5fa9\u4eae\u5ea6", - "sensor_string": "\u7bc0\u9ede\u50b3\u611f\u5668\u5b57\u4e32", - "variable_sensor_string": "\u53ef\u8b8a\u50b3\u611f\u5668\u5b57\u4e32" + "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\u50b3\u611f\u5668\u5b57\u4e32\uff08Node Sensor String\uff09\uff1a\u4efb\u4f55\u540d\u7a31\u6216\u8cc7\u6599\u593e\u5305\u542b\u300cNode Sensor String\u300d\u7684\u88dd\u7f6e\u90fd\u6703\u88ab\u8996\u70ba\u50b3\u611f\u5668\u6216\u4e8c\u9032\u4f4d\u50b3\u611f\u5668\u3002\n \u2022 \u5ffd\u7565\u5b57\u4e32\uff08Ignore String\uff09\uff1a\u4efb\u4f55\u540d\u7a31\u5305\u542b\u300cIgnore String\u300d\u7684\u88dd\u7f6e\u90fd\u6703\u88ab\u5ffd\u7565\u3002\n \u2022 \u53ef\u8b8a\u50b3\u611f\u5668\u5b57\u4e32\uff08Variable Sensor String\uff09\uff1a\u4efb\u4f55\u5305\u542b\u300cVariable Sensor String\u300d\u7684\u8b8a\u6578\u90fd\u5c07\u65b0\u589e\u70ba\u50b3\u611f\u5668\u3002 \n \u2022 \u56de\u5fa9\u4eae\u5ea6\uff08Restore Light Brightness\uff09\uff1a\u958b\u5553\u5f8c\u3001\u7576\u71c8\u5149\u958b\u555f\u6642\u6703\u56de\u5fa9\u5148\u524d\u7684\u4eae\u5ea6\uff0c\u800c\u4e0d\u662f\u4f7f\u7528\u88dd\u7f6e\u9810\u8a2d\u4eae\u5ea6\u3002", + "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", "title": "ISY994 \u9078\u9805" } } diff --git a/homeassistant/components/juicenet/translations/bg.json b/homeassistant/components/juicenet/translations/bg.json new file mode 100644 index 00000000000..084c18c771d --- /dev/null +++ b/homeassistant/components/juicenet/translations/bg.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u0440\u043e\u0444\u0438\u043b\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + }, + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/bg.json b/homeassistant/components/konnected/translations/bg.json index 4983c9a14b2..6b2ccdd56ec 100644 --- a/homeassistant/components/konnected/translations/bg.json +++ b/homeassistant/components/konnected/translations/bg.json @@ -3,9 +3,34 @@ "step": { "user": { "data": { + "host": "IP \u0430\u0434\u0440\u0435\u0441", "port": "\u041f\u043e\u0440\u0442" } } } + }, + "options": { + "step": { + "options_io": { + "data": { + "1": "\u0417\u043e\u043d\u0430 1", + "2": "\u0417\u043e\u043d\u0430 2", + "3": "\u0417\u043e\u043d\u0430 3", + "4": "\u0417\u043e\u043d\u0430 4", + "5": "\u0417\u043e\u043d\u0430 5", + "6": "\u0417\u043e\u043d\u0430 6", + "7": "\u0417\u043e\u043d\u0430 7" + } + }, + "options_io_ext": { + "data": { + "10": "\u0417\u043e\u043d\u0430 10", + "11": "\u0417\u043e\u043d\u0430 11", + "12": "\u0417\u043e\u043d\u0430 12", + "8": "\u0417\u043e\u043d\u0430 8", + "9": "\u0417\u043e\u043d\u0430 9" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/local_ip/translations/bg.json b/homeassistant/components/local_ip/translations/bg.json new file mode 100644 index 00000000000..bbc8018aa4d --- /dev/null +++ b/homeassistant/components/local_ip/translations/bg.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, + "step": { + "user": { + "title": "\u041b\u043e\u043a\u0430\u043b\u0435\u043d IP \u0430\u0434\u0440\u0435\u0441" + } + } + }, + "title": "\u041b\u043e\u043a\u0430\u043b\u0435\u043d IP \u0430\u0434\u0440\u0435\u0441" +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/bg.json b/homeassistant/components/lutron_caseta/translations/bg.json index ba9f144cb0a..2f95a8d4d06 100644 --- a/homeassistant/components/lutron_caseta/translations/bg.json +++ b/homeassistant/components/lutron_caseta/translations/bg.json @@ -1,4 +1,13 @@ { + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } + }, "device_automation": { "trigger_subtype": { "button_1": "\u041f\u044a\u0440\u0432\u0438 \u0431\u0443\u0442\u043e\u043d", diff --git a/homeassistant/components/melcloud/translations/bg.json b/homeassistant/components/melcloud/translations/bg.json new file mode 100644 index 00000000000..b8a429313ec --- /dev/null +++ b/homeassistant/components/melcloud/translations/bg.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/translations/bg.json b/homeassistant/components/meteo_france/translations/bg.json new file mode 100644 index 00000000000..c8c6c11aa02 --- /dev/null +++ b/homeassistant/components/meteo_france/translations/bg.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "city": "\u0413\u0440\u0430\u0434" + }, + "title": "M\u00e9t\u00e9o-France" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/bg.json b/homeassistant/components/mikrotik/translations/bg.json index 4983c9a14b2..9d19b786d10 100644 --- a/homeassistant/components/mikrotik/translations/bg.json +++ b/homeassistant/components/mikrotik/translations/bg.json @@ -3,7 +3,11 @@ "step": { "user": { "data": { - "port": "\u041f\u043e\u0440\u0442" + "host": "\u0425\u043e\u0441\u0442", + "name": "\u0418\u043c\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } } } diff --git a/homeassistant/components/mill/translations/bg.json b/homeassistant/components/mill/translations/bg.json index 2ac8a444100..a93b899406a 100644 --- a/homeassistant/components/mill/translations/bg.json +++ b/homeassistant/components/mill/translations/bg.json @@ -1,7 +1,18 @@ { "config": { + "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/translations/bg.json b/homeassistant/components/minecraft_server/translations/bg.json new file mode 100644 index 00000000000..a051d6ca487 --- /dev/null +++ b/homeassistant/components/minecraft_server/translations/bg.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u0418\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/bg.json b/homeassistant/components/netatmo/translations/bg.json new file mode 100644 index 00000000000..83b32fc7c85 --- /dev/null +++ b/homeassistant/components/netatmo/translations/bg.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "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." + }, + "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" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/zh-Hant.json b/homeassistant/components/netatmo/translations/zh-Hant.json index e62836f9a7e..c89f91d1d91 100644 --- a/homeassistant/components/netatmo/translations/zh-Hant.json +++ b/homeassistant/components/netatmo/translations/zh-Hant.json @@ -49,16 +49,16 @@ "mode": "\u8a08\u7b97\u65b9\u5f0f", "show_on_map": "\u65bc\u5730\u5716\u986f\u793a" }, - "description": "\u8a2d\u5b9a\u5340\u57df\u516c\u773e\u6c23\u8c61\u50b3\u611f\u5668\u3002", - "title": "Netatmo \u516c\u773e\u6c23\u8c61\u50b3\u611f\u5668" + "description": "\u8a2d\u5b9a\u5340\u57df\u516c\u773e\u6c23\u8c61\u611f\u6e2c\u5668\u3002", + "title": "Netatmo \u516c\u773e\u6c23\u8c61\u611f\u6e2c\u5668" }, "public_weather_areas": { "data": { "new_area": "\u5340\u57df\u540d\u7a31", "weather_areas": "\u6c23\u8c61\u5340\u57df" }, - "description": "\u8a2d\u5b9a\u516c\u773e\u6c23\u8c61\u50b3\u611f\u5668\u3002", - "title": "Netatmo \u516c\u773e\u6c23\u8c61\u50b3\u611f\u5668" + "description": "\u8a2d\u5b9a\u516c\u773e\u6c23\u8c61\u611f\u6e2c\u5668\u3002", + "title": "Netatmo \u516c\u773e\u6c23\u8c61\u611f\u6e2c\u5668" } } } diff --git a/homeassistant/components/nut/translations/zh-Hant.json b/homeassistant/components/nut/translations/zh-Hant.json index 5f48541792d..3b1daf88bf1 100644 --- a/homeassistant/components/nut/translations/zh-Hant.json +++ b/homeassistant/components/nut/translations/zh-Hant.json @@ -43,7 +43,7 @@ "resources": "\u8cc7\u6e90", "scan_interval": "\u6383\u63cf\u9593\u8ddd\uff08\u79d2\uff09" }, - "description": "\u9078\u64c7\u50b3\u611f\u5668\u8cc7\u6e90\u3002" + "description": "\u9078\u64c7\u611f\u6e2c\u5668\u8cc7\u6e90\u3002" } } } diff --git a/homeassistant/components/nws/translations/bg.json b/homeassistant/components/nws/translations/bg.json new file mode 100644 index 00000000000..ddc09275a04 --- /dev/null +++ b/homeassistant/components/nws/translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447", + "latitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0448\u0438\u0440\u0438\u043d\u0430", + "longitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0434\u044a\u043b\u0436\u0438\u043d\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/bg.json b/homeassistant/components/onvif/translations/bg.json index 0cebe1fb7e9..6ef4c15dd8b 100644 --- a/homeassistant/components/onvif/translations/bg.json +++ b/homeassistant/components/onvif/translations/bg.json @@ -1,6 +1,15 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "step": { + "auth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "Username" + } + }, "configure": { "data": { "host": "\u0425\u043e\u0441\u0442", @@ -12,6 +21,7 @@ }, "manual_input": { "data": { + "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" } } diff --git a/homeassistant/components/panasonic_viera/translations/bg.json b/homeassistant/components/panasonic_viera/translations/bg.json new file mode 100644 index 00000000000..0ec64883f3e --- /dev/null +++ b/homeassistant/components/panasonic_viera/translations/bg.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "error": { + "invalid_pin_code": "\u0412\u044a\u0432\u0435\u0434\u0435\u043d\u0438\u044f\u0442 \u043e\u0442 \u0412\u0430\u0441 \u041f\u0418\u041d \u043a\u043e\u0434 \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d" + }, + "step": { + "pairing": { + "data": { + "pin": "\u041f\u0418\u041d \u043a\u043e\u0434" + } + }, + "user": { + "data": { + "host": "IP \u0430\u0434\u0440\u0435\u0441", + "name": "\u0418\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/translations/bg.json b/homeassistant/components/plex/translations/bg.json index c575c261fbd..4f1fdbe98c5 100644 --- a/homeassistant/components/plex/translations/bg.json +++ b/homeassistant/components/plex/translations/bg.json @@ -12,9 +12,11 @@ "no_servers": "\u041d\u044f\u043c\u0430 \u0441\u044a\u0440\u0432\u044a\u0440\u0438, \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u0438 \u0441 \u0442\u043e\u0437\u0438 \u0430\u043a\u0430\u0443\u043d\u0442", "not_found": "Plex \u0441\u044a\u0440\u0432\u044a\u0440\u044a\u0442 \u043d\u0435 \u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d" }, + "flow_title": "{name} ({host})", "step": { "manual_setup": { "data": { + "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" } }, @@ -24,6 +26,12 @@ }, "description": "\u041d\u0430\u043b\u0438\u0447\u043d\u0438 \u0441\u0430 \u043d\u044f\u043a\u043e\u043b\u043a\u043e \u0441\u044a\u0440\u0432\u044a\u0440\u0430, \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0435\u0434\u0438\u043d:", "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 Plex \u0441\u044a\u0440\u0432\u044a\u0440" + }, + "user": { + "title": "Plex Media Server" + }, + "user_advanced": { + "title": "Plex Media Server" } } }, @@ -31,6 +39,7 @@ "step": { "plex_mp_settings": { "data": { + "monitored_users": "\u041d\u0430\u0431\u043b\u044e\u0434\u0430\u0432\u0430\u043d\u0438 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0438", "use_episode_art": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u043f\u043b\u0430\u043a\u0430\u0442 \u0437\u0430 \u0435\u043f\u0438\u0437\u043e\u0434\u0430" }, "description": "\u041e\u043f\u0446\u0438\u0438 \u0437\u0430 Plex Media Players" diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/zh-Hant.json b/homeassistant/components/pvpc_hourly_pricing/translations/zh-Hant.json index 1cadebbc6be..ace4cce089a 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/zh-Hant.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/zh-Hant.json @@ -6,12 +6,12 @@ "step": { "user": { "data": { - "name": "\u50b3\u611f\u5668\u540d\u7a31", + "name": "\u611f\u6e2c\u5668\u540d\u7a31", "power": "\u5408\u7d04\u529f\u7387\uff08kW\uff09", "power_p3": "\u4f4e\u5cf0\u671f P3 \u5408\u7d04\u529f\u7387\uff08kW\uff09", "tariff": "\u5206\u5340\u9069\u7528\u8cbb\u7387" }, - "description": "\u6b64\u50b3\u611f\u5668\u4f7f\u7528\u4e86\u975e\u5b98\u65b9 API \u4ee5\u53d6\u5f97\u897f\u73ed\u7259 [\u8a08\u6642\u96fb\u50f9\uff08PVPC\uff09](https://www.esios.ree.es/es/pvpc)\u3002\n\u95dc\u65bc\u66f4\u8a73\u7d30\u7684\u8aaa\u660e\uff0c\u8acb\u53c3\u95b1 [\u6574\u5408\u6587\u4ef6](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/)\u3002", + "description": "\u6b64\u611f\u6e2c\u5668\u4f7f\u7528\u4e86\u975e\u5b98\u65b9 API \u4ee5\u53d6\u5f97\u897f\u73ed\u7259 [\u8a08\u6642\u96fb\u50f9\uff08PVPC\uff09](https://www.esios.ree.es/es/pvpc)\u3002\n\u95dc\u65bc\u66f4\u8a73\u7d30\u7684\u8aaa\u660e\uff0c\u8acb\u53c3\u95b1 [\u6574\u5408\u6587\u4ef6](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/)\u3002", "title": "\u611f\u61c9\u5668\u8a2d\u5b9a" } } @@ -24,7 +24,7 @@ "power_p3": "\u4f4e\u5cf0\u671f P3 \u5408\u7d04\u529f\u7387\uff08kW\uff09", "tariff": "\u5206\u5340\u9069\u7528\u8cbb\u7387" }, - "description": "\u6b64\u50b3\u611f\u5668\u4f7f\u7528\u4e86\u975e\u5b98\u65b9 API \u4ee5\u53d6\u5f97\u897f\u73ed\u7259 [\u8a08\u6642\u96fb\u50f9\uff08PVPC\uff09](https://www.esios.ree.es/es/pvpc)\u3002\n\u95dc\u65bc\u66f4\u8a73\u7d30\u7684\u8aaa\u660e\uff0c\u8acb\u53c3\u95b1 [\u6574\u5408\u6587\u4ef6](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/)\u3002", + "description": "\u6b64\u611f\u6e2c\u5668\u4f7f\u7528\u4e86\u975e\u5b98\u65b9 API \u4ee5\u53d6\u5f97\u897f\u73ed\u7259 [\u8a08\u6642\u96fb\u50f9\uff08PVPC\uff09](https://www.esios.ree.es/es/pvpc)\u3002\n\u95dc\u65bc\u66f4\u8a73\u7d30\u7684\u8aaa\u660e\uff0c\u8acb\u53c3\u95b1 [\u6574\u5408\u6587\u4ef6](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/)\u3002", "title": "\u611f\u61c9\u5668\u8a2d\u5b9a" } } diff --git a/homeassistant/components/roomba/translations/bg.json b/homeassistant/components/roomba/translations/bg.json index 10f778472e6..096efcb1ba2 100644 --- a/homeassistant/components/roomba/translations/bg.json +++ b/homeassistant/components/roomba/translations/bg.json @@ -1,11 +1,21 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "link_manual": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430" }, "title": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "title": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e" } } } diff --git a/homeassistant/components/samsungtv/translations/bg.json b/homeassistant/components/samsungtv/translations/bg.json index c30e629d8ad..a8133e96d15 100644 --- a/homeassistant/components/samsungtv/translations/bg.json +++ b/homeassistant/components/samsungtv/translations/bg.json @@ -2,6 +2,7 @@ "config": { "abort": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" - } + }, + "flow_title": "{device}" } } \ No newline at end of file diff --git a/homeassistant/components/season/translations/sensor.bg.json b/homeassistant/components/season/translations/sensor.bg.json index 9b2254c2421..d3348b2cf7b 100644 --- a/homeassistant/components/season/translations/sensor.bg.json +++ b/homeassistant/components/season/translations/sensor.bg.json @@ -1,5 +1,11 @@ { "state": { + "season__season": { + "autumn": "\u0415\u0441\u0435\u043d", + "spring": "\u041f\u0440\u043e\u043b\u0435\u0442", + "summer": "\u041b\u044f\u0442\u043e", + "winter": "\u0417\u0438\u043c\u0430" + }, "season__season__": { "autumn": "\u0415\u0441\u0435\u043d", "spring": "\u041f\u0440\u043e\u043b\u0435\u0442", diff --git a/homeassistant/components/sensor/translations/zh-Hant.json b/homeassistant/components/sensor/translations/zh-Hant.json index fb15fc70402..5c36491941a 100644 --- a/homeassistant/components/sensor/translations/zh-Hant.json +++ b/homeassistant/components/sensor/translations/zh-Hant.json @@ -59,5 +59,5 @@ "on": "\u958b\u5553" } }, - "title": "\u50b3\u611f\u5668" + "title": "\u611f\u6e2c\u5668" } \ No newline at end of file diff --git a/homeassistant/components/smartthings/translations/bg.json b/homeassistant/components/smartthings/translations/bg.json index 9def747bbb4..242fdcec985 100644 --- a/homeassistant/components/smartthings/translations/bg.json +++ b/homeassistant/components/smartthings/translations/bg.json @@ -8,6 +8,12 @@ "webhook_error": "SmartThings \u043d\u0435 \u043c\u043e\u0436\u0430 \u0434\u0430 \u0432\u0430\u043b\u0438\u0434\u0438\u0440\u0430 \u0430\u0434\u0440\u0435\u0441\u0430, \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d \u0432 `base_url`. \u041c\u043e\u043b\u044f, \u043f\u0440\u0435\u0433\u043b\u0435\u0434\u0430\u0439\u0442\u0435 \u0438\u0437\u0438\u0441\u043a\u0432\u0430\u043d\u0438\u044f\u0442\u0430 \u0437\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430." }, "step": { + "select_location": { + "data": { + "location_id": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435" + }, + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435" + }, "user": { "description": "\u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 SmartThings [Personal Access Token] ( {token_url} ), \u043a\u043e\u0439\u0442\u043e \u0435 \u0441\u044a\u0437\u0434\u0430\u0434\u0435\u043d \u0441\u043f\u043e\u0440\u0435\u0434 [\u0443\u043a\u0430\u0437\u0430\u043d\u0438\u044f\u0442\u0430] ( {component_url} ).", "title": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043a\u043e\u0434 \u0437\u0430 \u0434\u043e\u0441\u0442\u044a\u043f (Personal Access Token)" diff --git a/homeassistant/components/solarlog/translations/zh-Hant.json b/homeassistant/components/solarlog/translations/zh-Hant.json index 9782e22ee16..126e78e6e90 100644 --- a/homeassistant/components/solarlog/translations/zh-Hant.json +++ b/homeassistant/components/solarlog/translations/zh-Hant.json @@ -11,7 +11,7 @@ "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", - "name": "Solar-Log \u50b3\u611f\u5668\u6240\u4f7f\u7528\u5b57\u9996" + "name": "Solar-Log \u611f\u6e2c\u5668\u6240\u4f7f\u7528\u5b57\u9996" }, "title": "\u5b9a\u7fa9 Solar-Log \u9023\u7dda" } diff --git a/homeassistant/components/soma/translations/zh-Hant.json b/homeassistant/components/soma/translations/zh-Hant.json index 3dfb1649557..c4e2796e189 100644 --- a/homeassistant/components/soma/translations/zh-Hant.json +++ b/homeassistant/components/soma/translations/zh-Hant.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Soma \u5e33\u865f\u3002", - "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642", - "connection_error": "SOMA \u9023\u7dda\u5931\u6557\u3002", + "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", + "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", + "connection_error": "\u9023\u7dda\u5931\u6557", "missing_configuration": "Soma \u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", "result_error": "SOMA \u9023\u7dda\u56de\u61c9\u72c0\u614b\u932f\u8aa4\u3002" }, "create_entry": { - "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Soma \u88dd\u7f6e\u3002" + "default": "\u5df2\u6210\u529f\u8a8d\u8b49" }, "step": { "user": { diff --git a/homeassistant/components/songpal/translations/bg.json b/homeassistant/components/songpal/translations/bg.json new file mode 100644 index 00000000000..ec38c3842e6 --- /dev/null +++ b/homeassistant/components/songpal/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "flow_title": "{name} ({host})" + } +} \ No newline at end of file diff --git a/homeassistant/components/stookalert/translations/ja.json b/homeassistant/components/stookalert/translations/ja.json new file mode 100644 index 00000000000..297ea3baa71 --- /dev/null +++ b/homeassistant/components/stookalert/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "province": "\u5dde" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/subaru/translations/zh-Hant.json b/homeassistant/components/subaru/translations/zh-Hant.json index 15799492d85..6264f01791e 100644 --- a/homeassistant/components/subaru/translations/zh-Hant.json +++ b/homeassistant/components/subaru/translations/zh-Hant.json @@ -35,7 +35,7 @@ "data": { "update_enabled": "\u958b\u555f\u8eca\u8f1b\u8cc7\u6599\u4e0b\u8f09" }, - "description": "\u958b\u555f\u5f8c\uff0c\u5c07\u6703\u6bcf 2 \u5c0f\u6642\u50b3\u9001\u9060\u7aef\u547d\u4ee4\u81f3\u8eca\u8f1b\u4ee5\u7372\u5f97\u6700\u65b0\u50b3\u611f\u5668\u8cc7\u6599\u3002\u5982\u679c\u6c92\u6709\u958b\u555f\uff0c\u50b3\u611f\u5668\u65b0\u8cc7\u6599\u50c5\u6703\u65bc\u8eca\u8f1b\u81ea\u52d5\u63a8\u9001\u8cc7\u6599\u6642\u63a5\u6536\uff08\u901a\u5e38\u70ba\u5f15\u64ce\u7184\u706b\u4e4b\u5f8c\uff09\u3002", + "description": "\u958b\u555f\u5f8c\uff0c\u5c07\u6703\u6bcf 2 \u5c0f\u6642\u50b3\u9001\u9060\u7aef\u547d\u4ee4\u81f3\u8eca\u8f1b\u4ee5\u7372\u5f97\u6700\u65b0\u611f\u6e2c\u5668\u8cc7\u6599\u3002\u5982\u679c\u6c92\u6709\u958b\u555f\uff0c\u611f\u6e2c\u5668\u65b0\u8cc7\u6599\u50c5\u6703\u65bc\u8eca\u8f1b\u81ea\u52d5\u63a8\u9001\u8cc7\u6599\u6642\u63a5\u6536\uff08\u901a\u5e38\u70ba\u5f15\u64ce\u7184\u706b\u4e4b\u5f8c\uff09\u3002", "title": "Subaru Starlink \u9078\u9805" } } diff --git a/homeassistant/components/synology_dsm/translations/bg.json b/homeassistant/components/synology_dsm/translations/bg.json index 7b4805e3905..c750e92869c 100644 --- a/homeassistant/components/synology_dsm/translations/bg.json +++ b/homeassistant/components/synology_dsm/translations/bg.json @@ -3,7 +3,17 @@ "abort": { "reconfigure_successful": "\u041f\u0440\u0435\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, + "error": { + "otp_failed": "\u0414\u0432\u0443\u0441\u0442\u0435\u043f\u0435\u043d\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\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 \u0441 \u043d\u043e\u0432 \u043a\u043e\u0434 \u0437\u0430 \u0434\u043e\u0441\u0442\u044a\u043f", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "flow_title": "{name} ({host})", "step": { + "2sa": { + "data": { + "otp_code": "\u041a\u043e\u0434" + } + }, "link": { "data": { "port": "\u041f\u043e\u0440\u0442" @@ -17,6 +27,8 @@ }, "user": { "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "port": "\u041f\u043e\u0440\u0442" } } diff --git a/homeassistant/components/tado/translations/bg.json b/homeassistant/components/tado/translations/bg.json new file mode 100644 index 00000000000..be758888427 --- /dev/null +++ b/homeassistant/components/tado/translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tibber/translations/bg.json b/homeassistant/components/tibber/translations/bg.json new file mode 100644 index 00000000000..80a7cc489a9 --- /dev/null +++ b/homeassistant/components/tibber/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/bg.json b/homeassistant/components/totalconnect/translations/bg.json new file mode 100644 index 00000000000..0f1f04fa894 --- /dev/null +++ b/homeassistant/components/totalconnect/translations/bg.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/bg.json b/homeassistant/components/tuya/translations/bg.json index 4b2bf8f53fe..4be356255bd 100644 --- a/homeassistant/components/tuya/translations/bg.json +++ b/homeassistant/components/tuya/translations/bg.json @@ -2,12 +2,14 @@ "config": { "abort": { "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", + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." }, "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", "login_error": "\u0413\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0432\u043b\u0438\u0437\u0430\u043d\u0435 ({code}): {msg}" }, + "flow_title": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 Tuya", "step": { "login": { "data": { @@ -21,8 +23,14 @@ }, "user": { "data": { - "region": "\u0420\u0435\u0433\u0438\u043e\u043d" - } + "country_code": "\u0414\u044a\u0440\u0436\u0430\u0432\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "platform": "\u041f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e, \u0432 \u043a\u043e\u0435\u0442\u043e \u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d \u0412\u0430\u0448\u0438\u044f\u0442 \u0430\u043a\u0430\u0443\u043d\u0442", + "region": "\u0420\u0435\u0433\u0438\u043e\u043d", + "username": "\u0410\u043a\u0430\u0443\u043d\u0442" + }, + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0432\u0430\u0448\u0438\u0442\u0435 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438 \u0437\u0430 Tuya", + "title": "Tuya \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f" } } }, diff --git a/homeassistant/components/upb/translations/bg.json b/homeassistant/components/upb/translations/bg.json new file mode 100644 index 00000000000..c7fc1a35b8e --- /dev/null +++ b/homeassistant/components/upb/translations/bg.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "address": "\u0410\u0434\u0440\u0435\u0441 (\u0432\u0438\u0436\u0442\u0435 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0435\u0442\u043e \u043f\u043e-\u0433\u043e\u0440\u0435)", + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/bg.json b/homeassistant/components/upnp/translations/bg.json index cf55d95f9ab..82632bc19b5 100644 --- a/homeassistant/components/upnp/translations/bg.json +++ b/homeassistant/components/upnp/translations/bg.json @@ -8,10 +8,15 @@ "one": "\u0433\u0440\u0435\u0448\u043a\u0430", "other": "\u0433\u0440\u0435\u0448\u043a\u0438" }, + "flow_title": "{name}", "step": { + "ssdp_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 \u0442\u043e\u0432\u0430 UPnP/IGD \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e?" + }, "user": { "data": { - "unique_id": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + "unique_id": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", + "usn": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" } } } diff --git a/homeassistant/components/uptimerobot/translations/zh-Hant.json b/homeassistant/components/uptimerobot/translations/zh-Hant.json index 73d27aac1db..89977ea815f 100644 --- a/homeassistant/components/uptimerobot/translations/zh-Hant.json +++ b/homeassistant/components/uptimerobot/translations/zh-Hant.json @@ -17,14 +17,14 @@ "data": { "api_key": "API \u5bc6\u9470" }, - "description": "\u9700\u8981\u63d0\u4f9b\u7531 Uptime Robot \u53d6\u5f97\u4e00\u7d44\u65b0\u7684\u552f\u8b80 API \u5bc6\u9470", + "description": "\u9700\u8981\u63d0\u4f9b\u7531 UptimeRobot \u53d6\u5f97\u4e00\u7d44\u65b0\u7684\u552f\u8b80 API \u5bc6\u9470", "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" }, "user": { "data": { "api_key": "API \u5bc6\u9470" }, - "description": "\u9700\u8981\u63d0\u4f9b\u7531 Uptime Robot \u53d6\u5f97\u552f\u8b80 API \u5bc6\u9470" + "description": "\u9700\u8981\u63d0\u4f9b\u7531 UptimeRobot \u53d6\u5f97\u552f\u8b80 API \u5bc6\u9470" } } } diff --git a/homeassistant/components/vilfo/translations/bg.json b/homeassistant/components/vilfo/translations/bg.json new file mode 100644 index 00000000000..ffb69776060 --- /dev/null +++ b/homeassistant/components/vilfo/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/translations/bg.json b/homeassistant/components/vizio/translations/bg.json new file mode 100644 index 00000000000..a051d6ca487 --- /dev/null +++ b/homeassistant/components/vizio/translations/bg.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u0418\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vlc_telnet/translations/et.json b/homeassistant/components/vlc_telnet/translations/et.json index 54c28d2f084..1ba3cce70cd 100644 --- a/homeassistant/components/vlc_telnet/translations/et.json +++ b/homeassistant/components/vlc_telnet/translations/et.json @@ -2,7 +2,10 @@ "config": { "abort": { "already_configured": "Teenus on juba h\u00e4\u00e4lestatud", - "reauth_successful": "Taastuvastamine \u00f5nnestus" + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "unknown": "Ootamatu t\u00f5rge" }, "error": { "cannot_connect": "\u00dchendamine nurjus", @@ -11,6 +14,9 @@ }, "flow_title": "{host}", "step": { + "hassio_confirm": { + "description": "Kas loon \u00fchenduse lisandmooduliga {addon} ?" + }, "reauth_confirm": { "data": { "password": "Salas\u00f5na" diff --git a/homeassistant/components/vlc_telnet/translations/he.json b/homeassistant/components/vlc_telnet/translations/he.json index 2d7d7e546ad..c4d1bb1063e 100644 --- a/homeassistant/components/vlc_telnet/translations/he.json +++ b/homeassistant/components/vlc_telnet/translations/he.json @@ -2,7 +2,10 @@ "config": { "abort": { "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", - "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + "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", + "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": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", diff --git a/homeassistant/components/vlc_telnet/translations/pl.json b/homeassistant/components/vlc_telnet/translations/pl.json index 8ddfa57f6d5..aa4e9ed9c5f 100644 --- a/homeassistant/components/vlc_telnet/translations/pl.json +++ b/homeassistant/components/vlc_telnet/translations/pl.json @@ -2,7 +2,10 @@ "config": { "abort": { "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", - "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", diff --git a/homeassistant/components/vlc_telnet/translations/ru.json b/homeassistant/components/vlc_telnet/translations/ru.json index b83181482ac..c8446afd557 100644 --- a/homeassistant/components/vlc_telnet/translations/ru.json +++ b/homeassistant/components/vlc_telnet/translations/ru.json @@ -2,7 +2,10 @@ "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." + "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.", + "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.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", @@ -11,6 +14,9 @@ }, "flow_title": "{host}", "step": { + "hassio_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044e {addon}?" + }, "reauth_confirm": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c" diff --git a/homeassistant/components/vlc_telnet/translations/zh-Hant.json b/homeassistant/components/vlc_telnet/translations/zh-Hant.json new file mode 100644 index 00000000000..509510a7ce3 --- /dev/null +++ b/homeassistant/components/vlc_telnet/translations/zh-Hant.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "{host}", + "step": { + "hassio_confirm": { + "description": "\u662f\u5426\u8981\u9023\u7dda\u81f3\u9644\u52a0\u5143\u4ef6 {addon}\uff1f" + }, + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "\u8acb\u8f38\u5165\u4e3b\u6a5f\u7aef\u6b63\u78ba\u5bc6\u78bc\uff1a{host}" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "name": "\u540d\u7a31", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/bg.json b/homeassistant/components/wiffi/translations/bg.json index 4983c9a14b2..f7644524c15 100644 --- a/homeassistant/components/wiffi/translations/bg.json +++ b/homeassistant/components/wiffi/translations/bg.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "addr_in_use": "\u041f\u043e\u0440\u0442\u0430 \u043d\u0430 \u0441\u044a\u0440\u0432\u044a\u0440\u0430 \u0432\u0435\u0447\u0435 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430.", + "start_server_failed": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0441\u044a\u0440\u0432\u044a\u0440\u0430." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/wled/translations/bg.json b/homeassistant/components/wled/translations/bg.json index 77f63ca8684..a511aad9e42 100644 --- a/homeassistant/components/wled/translations/bg.json +++ b/homeassistant/components/wled/translations/bg.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json index 26c49e82c16..4fe5decd858 100644 --- a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json @@ -26,7 +26,7 @@ "key": "\u7db2\u95dc\u5bc6\u9470", "name": "\u7db2\u95dc\u540d\u7a31" }, - "description": "\u5bc6\u9470\uff08\u5bc6\u78bc\uff09\u53d6\u5f97\u8acb\u53c3\u8003\u4e0b\u65b9\u6559\u5b78\uff1ahttps://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz\u3002\u5047\u5982\u672a\u63d0\u4f9b\u5bc6\u9470\u3001\u5247\u50c5\u6703\u6536\u5230\u50b3\u611f\u5668\u88dd\u7f6e\u7684\u8cc7\u8a0a", + "description": "\u5bc6\u9470\uff08\u5bc6\u78bc\uff09\u53d6\u5f97\u8acb\u53c3\u8003\u4e0b\u65b9\u6559\u5b78\uff1ahttps://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz\u3002\u5047\u5982\u672a\u63d0\u4f9b\u5bc6\u9470\u3001\u5247\u50c5\u6703\u6536\u5230\u611f\u6e2c\u5668\u88dd\u7f6e\u7684\u8cc7\u8a0a", "title": "\u5c0f\u7c73 Aqara \u7db2\u95dc\u9078\u9805\u8a2d\u5b9a" }, "user": { diff --git a/homeassistant/components/xiaomi_miio/translations/bg.json b/homeassistant/components/xiaomi_miio/translations/bg.json index ee40fdacacf..3721d0a27c1 100644 --- a/homeassistant/components/xiaomi_miio/translations/bg.json +++ b/homeassistant/components/xiaomi_miio/translations/bg.json @@ -1,13 +1,23 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "no_device_selected": "\u041d\u0435 \u0435 \u0438\u0437\u0431\u0440\u0430\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u043c\u043e\u043b\u044f, \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0435\u0434\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e." }, "step": { "device": { "data": { "host": "IP \u0430\u0434\u0440\u0435\u0441" } + }, + "gateway": { + "data": { + "host": "IP \u0430\u0434\u0440\u0435\u0441", + "name": "\u0418\u043c\u0435 \u043d\u0430 \u0448\u043b\u044e\u0437\u0430" + } } } } diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json index fdbcde43114..db9db466f4e 100644 --- a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json @@ -13,7 +13,8 @@ "cloud_login_error": "\u7121\u6cd5\u767b\u5165\u5c0f\u7c73 Miio \u96f2\u670d\u52d9\uff0c\u8acb\u6aa2\u67e5\u6191\u8b49\u3002", "cloud_no_devices": "\u5c0f\u7c73 Miio \u96f2\u7aef\u5e33\u865f\u672a\u627e\u5230\u4efb\u4f55\u88dd\u7f6e\u3002", "no_device_selected": "\u672a\u9078\u64c7\u88dd\u7f6e\uff0c\u8acb\u9078\u64c7\u4e00\u9805\u88dd\u7f6e\u3002", - "unknown_device": "\u88dd\u7f6e\u578b\u865f\u672a\u77e5\uff0c\u7121\u6cd5\u4f7f\u7528\u8a2d\u5b9a\u6d41\u7a0b\u3002" + "unknown_device": "\u88dd\u7f6e\u578b\u865f\u672a\u77e5\uff0c\u7121\u6cd5\u4f7f\u7528\u8a2d\u5b9a\u6d41\u7a0b\u3002", + "wrong_token": "\u6838\u5c0d\u548c\u932f\u8aa4\u3001\u6b0a\u6756\u932f\u8aa4" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/zerproc/translations/bg.json b/homeassistant/components/zerproc/translations/bg.json new file mode 100644 index 00000000000..e7ed81d36f5 --- /dev/null +++ b/homeassistant/components/zerproc/translations/bg.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, + "step": { + "confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0442\u0430?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/bg.json b/homeassistant/components/zha/translations/bg.json index eeb1df867ad..feed380b03a 100644 --- a/homeassistant/components/zha/translations/bg.json +++ b/homeassistant/components/zha/translations/bg.json @@ -10,6 +10,12 @@ "confirm": { "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name}?" }, + "pick_radio": { + "data": { + "radio_type": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e" + }, + "title": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e" + }, "port_config": { "data": { "baudrate": "\u0441\u043a\u043e\u0440\u043e\u0441\u0442 \u043d\u0430 \u043f\u043e\u0440\u0442\u0430" @@ -17,6 +23,7 @@ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" }, "user": { + "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0441\u0435\u0440\u0438\u0435\u043d \u043f\u043e\u0440\u0442 \u0437\u0430 Zigbee \u0440\u0430\u0434\u0438\u043e", "title": "ZHA" } } From dafea00f41d002d4efd95c7028b4c729b73078da Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 17 Oct 2021 20:16:29 -0700 Subject: [PATCH 0489/1038] Rename `stream_type` to `frontend_stream_type` (#57923) Camera devices may support multiple stream sources so we want to clarify that this is meant to decide which stream source is used in the frontend only. Will set stream_type temporarily to allow rollout without breaking nightly, and this will be removed after frontend is updated. --- homeassistant/components/camera/__init__.py | 12 +++++++----- homeassistant/components/nest/camera_sdm.py | 6 ++++-- tests/components/camera/test_init.py | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 9275589f3c9..5a3d730e7d3 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -422,7 +422,7 @@ class Camera(Entity): return MIN_STREAM_INTERVAL @property - def stream_type(self) -> str | None: + def frontend_stream_type(self) -> str | None: """Return the type of stream supported by this camera. A camera may have a single stream type which is used to inform the @@ -570,8 +570,10 @@ class Camera(Entity): if self.motion_detection_enabled: attrs["motion_detection"] = self.motion_detection_enabled - if self.stream_type: - attrs["stream_type"] = self.stream_type + if self.frontend_stream_type: + attrs["frontend_stream_type"] = self.frontend_stream_type + # Remove after home-assistant/frontend#10298 is merged into nightly + attrs["stream_type"] = self.frontend_stream_type return attrs @@ -746,11 +748,11 @@ async def ws_camera_web_rtc_offer( entity_id = msg["entity_id"] offer = msg["offer"] camera = _get_camera_from_entity_id(hass, entity_id) - if camera.stream_type != STREAM_TYPE_WEB_RTC: + if camera.frontend_stream_type != STREAM_TYPE_WEB_RTC: connection.send_error( msg["id"], "web_rtc_offer_failed", - f"Camera does not support WebRTC, stream_type={camera.stream_type}", + f"Camera does not support WebRTC, frontend_stream_type={camera.frontend_stream_type}", ) return try: diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index 9ce485cee56..99234c3de8a 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -118,7 +118,7 @@ class NestCamera(Camera): return supported_features @property - def stream_type(self) -> str | None: + def frontend_stream_type(self) -> str | None: """Return the type of stream supported by this camera.""" if CameraLiveStreamTrait.NAME not in self._device.traits: return None @@ -131,9 +131,11 @@ class NestCamera(Camera): """Return the source of the stream.""" if not self.supported_features & SUPPORT_STREAM: return None - if self.stream_type != STREAM_TYPE_HLS: + if CameraLiveStreamTrait.NAME not in self._device.traits: return None trait = self._device.traits[CameraLiveStreamTrait.NAME] + if StreamingProtocol.RTSP not in trait.supported_protocols: + return None if not self._stream: _LOGGER.debug("Fetching stream url") try: diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 122fe13e2f1..c37c1b2909a 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -53,7 +53,7 @@ async def mock_camera_web_rtc_fixture(hass): await hass.async_block_till_done() with patch( - "homeassistant.components.camera.Camera.stream_type", + "homeassistant.components.camera.Camera.frontend_stream_type", new_callable=PropertyMock(return_value=STREAM_TYPE_WEB_RTC), ), patch( "homeassistant.components.camera.Camera.async_handle_web_rtc_offer", From 147febb18a3ea2defe8f895021cd1cd3ee25075f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 Oct 2021 17:30:43 -1000 Subject: [PATCH 0490/1038] Prevent yeelight discovery from overloading the bulb (#57820) --- .../components/yeelight/config_flow.py | 28 ++++++++++++++----- tests/components/yeelight/test_config_flow.py | 12 ++++++++ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py index d59e03c965d..f4aab12a34a 100644 --- a/homeassistant/components/yeelight/config_flow.py +++ b/homeassistant/components/yeelight/config_flow.py @@ -8,6 +8,7 @@ from yeelight.aio import AsyncBulb from homeassistant import config_entries, exceptions from homeassistant.components.dhcp import IP_ADDRESS +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_ID, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -66,18 +67,30 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id( "{0:#0{1}x}".format(int(discovery_info["name"][-26:-18]), 18) ) - self._abort_if_unique_id_configured( - updates={CONF_HOST: self._discovered_ip}, reload_on_update=False - ) - return await self._async_handle_discovery() + return await self._async_handle_discovery_with_unique_id() async def async_step_ssdp(self, discovery_info): """Handle discovery from ssdp.""" self._discovered_ip = urlparse(discovery_info["location"]).hostname await self.async_set_unique_id(discovery_info["id"]) - self._abort_if_unique_id_configured( - updates={CONF_HOST: self._discovered_ip}, reload_on_update=False - ) + return await self._async_handle_discovery_with_unique_id() + + async def _async_handle_discovery_with_unique_id(self): + """Handle any discovery with a unique id.""" + for entry in self._async_current_entries(): + if entry.unique_id != self.unique_id: + continue + reload = entry.state == ConfigEntryState.SETUP_RETRY + if entry.data[CONF_HOST] != self._discovered_ip: + self.hass.config_entries.async_update_entry( + entry, data={**entry.data, CONF_HOST: self._discovered_ip} + ) + reload = True + if reload: + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + return self.async_abort(reason="already_configured") return await self._async_handle_discovery() async def _async_handle_discovery(self): @@ -86,6 +99,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): for progress in self._async_in_progress(): if progress.get("context", {}).get(CONF_HOST) == self._discovered_ip: return self.async_abort(reason="already_in_progress") + self._async_abort_entries_match({CONF_HOST: self._discovered_ip}) try: self._discovered_model = await self._async_try_connect( diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index 99dd233678f..85d147ba697 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -500,6 +500,18 @@ async def test_discovered_by_dhcp_or_homekit(hass, source, data): assert mock_async_setup.called assert mock_async_setup_entry.called + with _patch_discovery( + no_device=True + ), _patch_discovery_timeout(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", side_effect=CannotConnect + ): + result3 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source}, data=data + ) + await hass.async_block_till_done() + assert result3["type"] == RESULT_TYPE_ABORT + assert result3["reason"] == "already_configured" + @pytest.mark.parametrize( "source, data", From cac0c04a91239eeb8c6ee912608b1e5b1cf52185 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 Oct 2021 17:32:02 -1000 Subject: [PATCH 0491/1038] Avoid setting up harmony websocket from discovery (#57589) --- homeassistant/components/harmony/__init__.py | 13 ++------- .../components/harmony/config_flow.py | 25 ++++++++++------ homeassistant/components/harmony/data.py | 29 +++++++++++++------ .../components/harmony/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/harmony/test_config_flow.py | 29 ++++++++++++++++--- 7 files changed, 66 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py index 00a0feca4f7..4ec610f1f75 100644 --- a/homeassistant/components/harmony/__init__.py +++ b/homeassistant/components/harmony/__init__.py @@ -1,12 +1,10 @@ """The Logitech Harmony Hub integration.""" -import asyncio import logging from homeassistant.components.remote import ATTR_ACTIVITY, ATTR_DELAY_SECS from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import entity_registry from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -34,13 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address = entry.data[CONF_HOST] name = entry.data[CONF_NAME] data = HarmonyData(hass, address, name, entry.unique_id) - try: - connected_ok = await data.connect() - except (asyncio.TimeoutError, ValueError, AttributeError) as err: - raise ConfigEntryNotReady from err - - if not connected_ok: - raise ConfigEntryNotReady + await data.connect() await _migrate_old_unique_ids(hass, entry.entry_id, data) @@ -51,8 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: cancel_stop = hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, _async_on_stop) - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { HARMONY_DATA: data, CANCEL_LISTENER: cancel_listener, CANCEL_STOP: cancel_stop, diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py index b1e71ac2dab..1c735b0747d 100644 --- a/homeassistant/components/harmony/config_flow.py +++ b/homeassistant/components/harmony/config_flow.py @@ -1,7 +1,10 @@ """Config flow for Logitech Harmony Hub integration.""" +import asyncio import logging from urllib.parse import urlparse +from aioharmony.hubconnector_websocket import HubConnector +import aiohttp import voluptuous as vol from homeassistant import config_entries, exceptions @@ -94,16 +97,20 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_NAME: friendly_name, } - harmony = await get_harmony_client_if_available(parsed_url.hostname) - - if harmony: - unique_id = find_unique_id_for_remote(harmony) - await self.async_set_unique_id(unique_id) - self._abort_if_unique_id_configured( - updates={CONF_HOST: self.harmony_config[CONF_HOST]} - ) - self.harmony_config[UNIQUE_ID] = unique_id + connector = HubConnector(parsed_url.hostname, asyncio.Queue()) + try: + remote_id = await connector.get_remote_id() + except aiohttp.ClientError: + return self.async_abort(reason="cannot_connect") + finally: + await connector.async_close_session() + unique_id = str(remote_id) + await self.async_set_unique_id(str(unique_id)) + self._abort_if_unique_id_configured( + updates={CONF_HOST: self.harmony_config[CONF_HOST]} + ) + self.harmony_config[UNIQUE_ID] = unique_id return await self.async_step_link() async def async_step_link(self, user_input=None): diff --git a/homeassistant/components/harmony/data.py b/homeassistant/components/harmony/data.py index 6fdf18df612..78377265c07 100644 --- a/homeassistant/components/harmony/data.py +++ b/homeassistant/components/harmony/data.py @@ -1,6 +1,7 @@ """Harmony data object which contains the Harmony Client.""" from __future__ import annotations +import asyncio from collections.abc import Iterable import logging @@ -8,6 +9,8 @@ from aioharmony.const import ClientCallbackType, SendCommandDevice import aioharmony.exceptions as aioexc from aioharmony.harmonyapi import HarmonyAPI as HarmonyClient +from homeassistant.exceptions import ConfigEntryNotReady + from .const import ACTIVITY_POWER_OFF from .subscriber import HarmonySubscriberMixin @@ -109,16 +112,24 @@ class HarmonyData(HarmonySubscriberMixin): ip_address=self._address, callbacks=ClientCallbackType(**callbacks) ) + connected = False try: - if not await self._client.connect(): - _LOGGER.warning("%s: Unable to connect to HUB", self._name) - await self._client.close() - return False - except aioexc.TimeOut: - _LOGGER.warning("%s: Connection timed-out", self._name) - return False - - return True + connected = await self._client.connect() + except (asyncio.TimeoutError, aioexc.TimeOut) as err: + await self._client.close() + raise ConfigEntryNotReady( + f"{self._name}: Connection timed-out to {self._address}:8088" + ) from err + except (ValueError, AttributeError) as err: + await self._client.close() + raise ConfigEntryNotReady( + f"{self._name}: Error {err} while connected HUB at: {self._address}:8088" + ) from err + if not connected: + await self._client.close() + raise ConfigEntryNotReady( + f"{self._name}: Unable to connect to HUB at: {self._address}:8088" + ) async def shutdown(self): """Close connection on shutdown.""" diff --git a/homeassistant/components/harmony/manifest.json b/homeassistant/components/harmony/manifest.json index f35f4e99303..d1b1073ebad 100644 --- a/homeassistant/components/harmony/manifest.json +++ b/homeassistant/components/harmony/manifest.json @@ -2,7 +2,7 @@ "domain": "harmony", "name": "Logitech Harmony Hub", "documentation": "https://www.home-assistant.io/integrations/harmony", - "requirements": ["aioharmony==0.2.7"], + "requirements": ["aioharmony==0.2.8"], "codeowners": [ "@ehendrix23", "@bramkragten", diff --git a/requirements_all.txt b/requirements_all.txt index ad08e06a848..b05c90d243e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -176,7 +176,7 @@ aiogithubapi==21.8.0 aioguardian==1.0.8 # homeassistant.components.harmony -aioharmony==0.2.7 +aioharmony==0.2.8 # homeassistant.components.homekit_controller aiohomekit==0.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2a7205a1893..3d0715314ef 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -118,7 +118,7 @@ aioflo==0.4.1 aioguardian==1.0.8 # homeassistant.components.harmony -aioharmony==0.2.7 +aioharmony==0.2.8 # homeassistant.components.homekit_controller aiohomekit==0.6.3 diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py index be0d78242ac..9195af40cf1 100644 --- a/tests/components/harmony/test_config_flow.py +++ b/tests/components/harmony/test_config_flow.py @@ -1,6 +1,8 @@ """Test the Logitech Harmony Hub config flow.""" from unittest.mock import AsyncMock, MagicMock, patch +import aiohttp + from homeassistant import config_entries, data_entry_flow from homeassistant.components.harmony.config_flow import CannotConnect from homeassistant.components.harmony.const import DOMAIN, PREVIOUS_ACTIVE_ACTIVITY @@ -49,11 +51,9 @@ async def test_user_form(hass): async def test_form_ssdp(hass): """Test we get the form with ssdp source.""" - harmonyapi = _get_mock_harmonyapi(connect=True) - with patch( - "homeassistant.components.harmony.util.HarmonyAPI", - return_value=harmonyapi, + "homeassistant.components.harmony.config_flow.HubConnector.get_remote_id", + return_value=1234, ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -75,6 +75,8 @@ async def test_form_ssdp(hass): assert progress[0]["flow_id"] == result["flow_id"] assert progress[0]["context"]["confirm_only"] is True + harmonyapi = _get_mock_harmonyapi(connect=True) + with patch( "homeassistant.components.harmony.util.HarmonyAPI", return_value=harmonyapi, @@ -94,6 +96,25 @@ async def test_form_ssdp(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_form_ssdp_fails_to_get_remote_id(hass): + """Test we abort if we cannot get the remote id.""" + + with patch( + "homeassistant.components.harmony.config_flow.HubConnector.get_remote_id", + side_effect=aiohttp.ClientError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data={ + "friendlyName": "Harmony Hub", + "ssdp_location": "http://192.168.1.12:8088/description", + }, + ) + assert result["type"] == "abort" + assert result["reason"] == "cannot_connect" + + async def test_form_ssdp_aborts_before_checking_remoteid_if_host_known(hass): """Test we abort without connecting if the host is already known.""" From 6a8ff9ffe746bc46f60eb78ed320c7ee61d11ff8 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 17 Oct 2021 21:32:18 -0600 Subject: [PATCH 0492/1038] Fix bug that prevents multiple instances of Tile (#57942) --- homeassistant/components/tile/__init__.py | 4 +++- homeassistant/components/tile/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tile/__init__.py b/homeassistant/components/tile/__init__.py index 65b86cd1c6d..6d16ea79b68 100644 --- a/homeassistant/components/tile/__init__.py +++ b/homeassistant/components/tile/__init__.py @@ -59,7 +59,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await async_migrate_entries(hass, entry.entry_id, async_migrate_callback) - websession = aiohttp_client.async_get_clientsession(hass) + # Tile's API uses cookies to identify a consumer; in order to allow for multiple + # instances of this config entry, we use a new session each time: + websession = aiohttp_client.async_create_clientsession(hass) try: client = await async_login( diff --git a/homeassistant/components/tile/manifest.json b/homeassistant/components/tile/manifest.json index 39295eed646..4e9913615a9 100644 --- a/homeassistant/components/tile/manifest.json +++ b/homeassistant/components/tile/manifest.json @@ -3,7 +3,7 @@ "name": "Tile", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tile", - "requirements": ["pytile==5.2.3"], + "requirements": ["pytile==5.2.4"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index b05c90d243e..cd8b959479f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1952,7 +1952,7 @@ python_opendata_transport==0.2.1 pythonegardia==1.0.40 # homeassistant.components.tile -pytile==5.2.3 +pytile==5.2.4 # homeassistant.components.touchline pytouchline==0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3d0715314ef..f5d64ac91be 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1136,7 +1136,7 @@ python-twitch-client==0.6.0 python_awair==0.2.1 # homeassistant.components.tile -pytile==5.2.3 +pytile==5.2.4 # homeassistant.components.traccar pytraccar==0.9.0 From ed37d2a794f028fe1bd5426e14ca7b25b66a2e54 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 18 Oct 2021 08:06:06 +0200 Subject: [PATCH 0493/1038] New service reconnect_client for UniFi integration (#57570) * Initial proposal of a client reconnect service * Slim setup and teardown of services * Minor improvements * Add tests --- homeassistant/components/unifi/services.py | 72 ++++++-- homeassistant/components/unifi/services.yaml | 12 ++ tests/components/unifi/test_services.py | 166 ++++++++++++++++++- 3 files changed, 231 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/unifi/services.py b/homeassistant/components/unifi/services.py index 2dce6f829b0..10d297df883 100644 --- a/homeassistant/components/unifi/services.py +++ b/homeassistant/components/unifi/services.py @@ -1,47 +1,89 @@ """UniFi services.""" +import voluptuous as vol + +from homeassistant.const import ATTR_DEVICE_ID from homeassistant.core import callback +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from .const import DOMAIN as UNIFI_DOMAIN +SERVICE_RECONNECT_CLIENT = "reconnect_client" SERVICE_REMOVE_CLIENTS = "remove_clients" +SERVICE_RECONNECT_CLIENT_SCHEMA = vol.All( + vol.Schema({vol.Required(ATTR_DEVICE_ID): str}) +) + +SUPPORTED_SERVICES = (SERVICE_RECONNECT_CLIENT, SERVICE_REMOVE_CLIENTS) + +SERVICE_TO_SCHEMA = { + SERVICE_RECONNECT_CLIENT: SERVICE_RECONNECT_CLIENT_SCHEMA, +} + @callback def async_setup_services(hass) -> None: """Set up services for UniFi integration.""" + services = { + SERVICE_RECONNECT_CLIENT: async_reconnect_client, + SERVICE_REMOVE_CLIENTS: async_remove_clients, + } + async def async_call_unifi_service(service_call) -> None: """Call correct UniFi service.""" - service = service_call.service - service_data = service_call.data + await services[service_call.service](hass, service_call.data) - controllers = hass.data[UNIFI_DOMAIN].values() - - if service == SERVICE_REMOVE_CLIENTS: - await async_remove_clients(controllers, service_data) - - hass.services.async_register( - UNIFI_DOMAIN, - SERVICE_REMOVE_CLIENTS, - async_call_unifi_service, - ) + for service in SUPPORTED_SERVICES: + hass.services.async_register( + UNIFI_DOMAIN, + service, + async_call_unifi_service, + schema=SERVICE_TO_SCHEMA.get(service), + ) @callback def async_unload_services(hass) -> None: """Unload UniFi services.""" - hass.services.async_remove(UNIFI_DOMAIN, SERVICE_REMOVE_CLIENTS) + for service in SUPPORTED_SERVICES: + hass.services.async_remove(UNIFI_DOMAIN, service) -async def async_remove_clients(controllers, data) -> None: +async def async_reconnect_client(hass, data) -> None: + """Try to get wireless client to reconnect to Wi-Fi.""" + device_registry = await hass.helpers.device_registry.async_get_registry() + device_entry = device_registry.async_get(data[ATTR_DEVICE_ID]) + + mac = "" + for connection in device_entry.connections: + if connection[0] == CONNECTION_NETWORK_MAC: + mac = connection[1] + break + + if mac == "": + return + + for controller in hass.data[UNIFI_DOMAIN].values(): + if ( + not controller.available + or (client := controller.api.clients[mac]) is None + or client.is_wired + ): + continue + + await controller.api.clients.async_reconnect(mac) + + +async def async_remove_clients(hass, data) -> None: """Remove select clients from controller. Validates based on: - Total time between first seen and last seen is less than 15 minutes. - Neither IP, hostname nor name is configured. """ - for controller in controllers: + for controller in hass.data[UNIFI_DOMAIN].values(): if not controller.available: continue diff --git a/homeassistant/components/unifi/services.yaml b/homeassistant/components/unifi/services.yaml index 435661afd4a..7f06adc88a2 100644 --- a/homeassistant/components/unifi/services.yaml +++ b/homeassistant/components/unifi/services.yaml @@ -1,3 +1,15 @@ +reconnect_client: + name: Reconnect wireless client + description: Try to get wireless client to reconnect to UniFi network + fields: + device_id: + name: Device + description: Try reconnect client to wireless network + required: true + selector: + device: + integration: unifi + remove_clients: name: Remove clients from the UniFi Controller description: Clean up clients that has only been associated with the controller for a short period of time. diff --git a/tests/components/unifi/test_services.py b/tests/components/unifi/test_services.py index d9989e8733a..8fe41d7a856 100644 --- a/tests/components/unifi/test_services.py +++ b/tests/components/unifi/test_services.py @@ -3,7 +3,13 @@ from unittest.mock import patch from homeassistant.components.unifi.const import DOMAIN as UNIFI_DOMAIN -from homeassistant.components.unifi.services import SERVICE_REMOVE_CLIENTS +from homeassistant.components.unifi.services import ( + SERVICE_RECONNECT_CLIENT, + SERVICE_REMOVE_CLIENTS, + SUPPORTED_SERVICES, +) +from homeassistant.const import ATTR_DEVICE_ID +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from .test_controller import setup_unifi_integration @@ -11,10 +17,12 @@ from .test_controller import setup_unifi_integration async def test_service_setup_and_unload(hass, aioclient_mock): """Verify service setup works.""" config_entry = await setup_unifi_integration(hass, aioclient_mock) - assert hass.services.has_service(UNIFI_DOMAIN, SERVICE_REMOVE_CLIENTS) + for service in SUPPORTED_SERVICES: + assert hass.services.has_service(UNIFI_DOMAIN, service) assert await hass.config_entries.async_unload(config_entry.entry_id) - assert not hass.services.has_service(UNIFI_DOMAIN, SERVICE_REMOVE_CLIENTS) + for service in SUPPORTED_SERVICES: + assert not hass.services.has_service(UNIFI_DOMAIN, service) @patch("homeassistant.core.ServiceRegistry.async_remove") @@ -33,7 +41,157 @@ async def test_service_setup_and_unload_not_called_if_multiple_integrations_dete assert await hass.config_entries.async_unload(config_entry_2.entry_id) remove_service_mock.assert_not_called() assert await hass.config_entries.async_unload(config_entry.entry_id) - remove_service_mock.assert_called_once() + assert remove_service_mock.call_count == 2 + + +async def test_reconnect_client(hass, aioclient_mock): + """Verify call to reconnect client is performed as expected.""" + clients = [ + { + "is_wired": False, + "mac": "00:00:00:00:00:01", + } + ] + config_entry = await setup_unifi_integration( + hass, aioclient_mock, clients_response=clients + ) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + + aioclient_mock.clear_requests() + aioclient_mock.post( + f"https://{controller.host}:1234/api/s/{controller.site}/cmd/stamgr", + ) + + device_registry = await hass.helpers.device_registry.async_get_registry() + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(CONNECTION_NETWORK_MAC, clients[0]["mac"])}, + ) + + await hass.services.async_call( + UNIFI_DOMAIN, + SERVICE_RECONNECT_CLIENT, + service_data={ATTR_DEVICE_ID: device_entry.id}, + blocking=True, + ) + assert aioclient_mock.call_count == 1 + + +async def test_reconnect_device_without_mac(hass, aioclient_mock): + """Verify no call is made if device does not have a known mac.""" + config_entry = await setup_unifi_integration(hass, aioclient_mock) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + + aioclient_mock.clear_requests() + aioclient_mock.post( + f"https://{controller.host}:1234/api/s/{controller.site}/cmd/stamgr", + ) + + device_registry = await hass.helpers.device_registry.async_get_registry() + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={("other connection", "not mac")}, + ) + + await hass.services.async_call( + UNIFI_DOMAIN, + SERVICE_RECONNECT_CLIENT, + service_data={ATTR_DEVICE_ID: device_entry.id}, + blocking=True, + ) + assert aioclient_mock.call_count == 0 + + +async def test_reconnect_client_controller_unavailable(hass, aioclient_mock): + """Verify no call is made if controller is unavailable.""" + clients = [ + { + "is_wired": False, + "mac": "00:00:00:00:00:01", + } + ] + config_entry = await setup_unifi_integration( + hass, aioclient_mock, clients_response=clients + ) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + controller.available = False + + aioclient_mock.clear_requests() + aioclient_mock.post( + f"https://{controller.host}:1234/api/s/{controller.site}/cmd/stamgr", + ) + + device_registry = await hass.helpers.device_registry.async_get_registry() + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(CONNECTION_NETWORK_MAC, clients[0]["mac"])}, + ) + + await hass.services.async_call( + UNIFI_DOMAIN, + SERVICE_RECONNECT_CLIENT, + service_data={ATTR_DEVICE_ID: device_entry.id}, + blocking=True, + ) + assert aioclient_mock.call_count == 0 + + +async def test_reconnect_client_unknown_mac(hass, aioclient_mock): + """Verify no call is made if trying to reconnect a mac unknown to controller.""" + config_entry = await setup_unifi_integration(hass, aioclient_mock) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + + aioclient_mock.clear_requests() + aioclient_mock.post( + f"https://{controller.host}:1234/api/s/{controller.site}/cmd/stamgr", + ) + + device_registry = await hass.helpers.device_registry.async_get_registry() + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(CONNECTION_NETWORK_MAC, "mac unknown to controller")}, + ) + + await hass.services.async_call( + UNIFI_DOMAIN, + SERVICE_RECONNECT_CLIENT, + service_data={ATTR_DEVICE_ID: device_entry.id}, + blocking=True, + ) + assert aioclient_mock.call_count == 0 + + +async def test_reconnect_wired_client(hass, aioclient_mock): + """Verify no call is made if client is wired.""" + clients = [ + { + "is_wired": True, + "mac": "00:00:00:00:00:01", + } + ] + config_entry = await setup_unifi_integration( + hass, aioclient_mock, clients_response=clients + ) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + + aioclient_mock.clear_requests() + aioclient_mock.post( + f"https://{controller.host}:1234/api/s/{controller.site}/cmd/stamgr", + ) + + device_registry = await hass.helpers.device_registry.async_get_registry() + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(CONNECTION_NETWORK_MAC, clients[0]["mac"])}, + ) + + await hass.services.async_call( + UNIFI_DOMAIN, + SERVICE_RECONNECT_CLIENT, + service_data={ATTR_DEVICE_ID: device_entry.id}, + blocking=True, + ) + assert aioclient_mock.call_count == 0 async def test_remove_clients(hass, aioclient_mock): From 81efdb2df23eef9dc5b5f1f0b206d23c974b2a34 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Oct 2021 08:50:33 +0200 Subject: [PATCH 0494/1038] Bump actions/checkout from 2.3.4 to 2.3.5 (#57947) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 12 ++++----- .github/workflows/ci.yaml | 38 ++++++++++++++--------------- .github/workflows/translations.yaml | 4 +-- .github/workflows/wheels.yml | 6 ++--- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index d8558c6fdff..708e360d59b 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -23,7 +23,7 @@ jobs: publish: ${{ steps.version.outputs.publish }} steps: - name: Checkout the repository - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 with: fetch-depth: 0 @@ -67,7 +67,7 @@ jobs: if: needs.init.outputs.publish == 'true' steps: - name: Checkout the repository - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 @@ -97,7 +97,7 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} if: needs.init.outputs.channel == 'dev' @@ -170,7 +170,7 @@ jobs: - tinker steps: - name: Checkout the repository - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Login to DockerHub uses: docker/login-action@v1.10.0 @@ -201,7 +201,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Initialize git uses: home-assistant/actions/helpers/git-init@master @@ -233,7 +233,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Login to DockerHub uses: docker/login-action@v1.10.0 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a04e815af7f..fe291e04c3c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -26,7 +26,7 @@ jobs: pre-commit-key: ${{ steps.generate-pre-commit-key.outputs.key }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python uses: actions/setup-python@v2.2.2 @@ -84,7 +84,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -124,7 +124,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -164,7 +164,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -207,7 +207,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Register hadolint problem matcher run: | echo "::add-matcher::.github/workflows/matchers/hadolint.json" @@ -226,7 +226,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -269,7 +269,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -312,7 +312,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -352,7 +352,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -395,7 +395,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -436,7 +436,7 @@ jobs: # needs: prepare-base # steps: # - name: Check out code from GitHub - # uses: actions/checkout@v2.3.4 + # uses: actions/checkout@v2.3.5 # - name: Run ShellCheck # uses: ludeeus/action-shellcheck@0.3.0 @@ -446,7 +446,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -493,7 +493,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2.1.6 @@ -517,7 +517,7 @@ jobs: needs: prepare-base steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 id: python @@ -551,7 +551,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Generate partial Python venv restore key id: generate-python-key run: >- @@ -595,7 +595,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2.1.6 @@ -626,7 +626,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2.1.6 @@ -660,7 +660,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2.1.6 @@ -718,7 +718,7 @@ jobs: container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2.1.6 diff --git a/.github/workflows/translations.yaml b/.github/workflows/translations.yaml index d2ccdc6c9ca..6e734528f0e 100644 --- a/.github/workflows/translations.yaml +++ b/.github/workflows/translations.yaml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 @@ -39,7 +39,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v2 + uses: actions/checkout@v2.3.5 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v2.2.2 diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 9cced377f8c..ebb4a65b80e 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -21,7 +21,7 @@ jobs: architectures: ${{ steps.info.outputs.architectures }} steps: - name: Checkout the repository - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Get information id: info @@ -68,7 +68,7 @@ jobs: - "3.9-alpine3.14" steps: - name: Checkout the repository - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Download env_file uses: actions/download-artifact@v2 @@ -108,7 +108,7 @@ jobs: - "3.9-alpine3.14" steps: - name: Checkout the repository - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Download env_file uses: actions/download-artifact@v2 From 55c80b409379dc9d8e4802eff24f7c7cbb2a2489 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 18 Oct 2021 09:24:59 +0200 Subject: [PATCH 0495/1038] Tuya tweaks to entity category, registry enabled, icons & device classes (#57949) --- .../components/tuya/binary_sensor.py | 5 + homeassistant/components/tuya/number.py | 8 +- homeassistant/components/tuya/select.py | 3 + homeassistant/components/tuya/sensor.py | 100 ++++-------------- homeassistant/components/tuya/switch.py | 18 +++- 5 files changed, 49 insertions(+), 85 deletions(-) diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index a851cbdbce5..a083cfd73f5 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -9,6 +9,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_DOOR, DEVICE_CLASS_MOTION, DEVICE_CLASS_SAFETY, + DEVICE_CLASS_TAMPER, DEVICE_CLASS_VIBRATION, BinarySensorEntity, BinarySensorEntityDescription, @@ -50,6 +51,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { TuyaBinarySensorEntityDescription( key=DPCode.TEMPER_ALARM, name="Tamper", + device_class=DEVICE_CLASS_TAMPER, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ), @@ -59,6 +61,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { TuyaBinarySensorEntityDescription( key=DPCode.TEMPER_ALARM, name="Tamper", + device_class=DEVICE_CLASS_TAMPER, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ), @@ -73,6 +76,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { TuyaBinarySensorEntityDescription( key=DPCode.TEMPER_ALARM, name="Tamper", + device_class=DEVICE_CLASS_TAMPER, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ), @@ -86,6 +90,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { TuyaBinarySensorEntityDescription( key=DPCode.TEMPER_ALARM, name="Tamper", + device_class=DEVICE_CLASS_TAMPER, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ), diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index 63d94195233..34325e63d98 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -28,24 +28,24 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { key=DPCode.WATER_SET, name="Water Level", icon="mdi:cup-water", - entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_CONFIG, ), NumberEntityDescription( key=DPCode.TEMP_SET, name="Temperature", icon="mdi:thermometer", - entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_CONFIG, ), NumberEntityDescription( key=DPCode.WARM_TIME, name="Heat Preservation Time", icon="mdi:timer", - entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_CONFIG, ), NumberEntityDescription( key=DPCode.POWDER_SET, name="Powder", - entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_CONFIG, ), ), # Siren Alarm diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index 3c99e131284..762c105632f 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -33,14 +33,17 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { key=DPCode.CONCENTRATION_SET, name="Concentration", icon="mdi:altimeter", + entity_category=ENTITY_CATEGORY_CONFIG, ), SelectEntityDescription( key=DPCode.MATERIAL, name="Material", + entity_category=ENTITY_CATEGORY_CONFIG, ), SelectEntityDescription( key=DPCode.MODE, name="Mode", + icon="mdi:coffee", ), ), # Siren Alarm diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 4bba17284cd..fc12b9b9ace 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -39,6 +39,24 @@ from .const import ( UnitOfMeasurement, ) +# Commonly used battery sensors, that are re-used in the sensors down below. +BATTERY_SENSORS: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key=DPCode.BATTERY_PERCENTAGE, + name="Battery", + native_unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + SensorEntityDescription( + key=DPCode.BATTERY_STATE, + name="Battery State", + icon="mdi:battery", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), +) + # All descriptions can be found here. Mostly the Integer data types in the # default status set of each category (that don't have a set instruction) # end up being a sensor. @@ -46,22 +64,7 @@ from .const import ( SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { # Door Window Sensor # https://developer.tuya.com/en/docs/iot/s?id=K9gf48hm02l8m - "mcs": ( - SensorEntityDescription( - key=DPCode.BATTERY_PERCENTAGE, - name="Battery", - native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_BATTERY, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=DPCode.BATTERY_STATE, - name="Battery State", - icon="mdi:battery", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - ), + "mcs": BATTERY_SENSORS, # Switch # https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s "kg": ( @@ -119,74 +122,17 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { device_class=DEVICE_CLASS_CO2, state_class=STATE_CLASS_MEASUREMENT, ), - SensorEntityDescription( - key=DPCode.BATTERY_PERCENTAGE, - name="Battery", - native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_BATTERY, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=DPCode.BATTERY_STATE, - name="Battery State", - icon="mdi:battery", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), + *BATTERY_SENSORS, ), # PIR Detector # https://developer.tuya.com/en/docs/iot/categorypir?id=Kaiuz3ss11b80 - "pir": ( - SensorEntityDescription( - key=DPCode.BATTERY_PERCENTAGE, - name="Battery", - native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_BATTERY, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=DPCode.BATTERY_STATE, - name="Battery State", - icon="mdi:battery", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - ), + "pir": BATTERY_SENSORS, # Vibration Sensor # https://developer.tuya.com/en/docs/iot/categoryzd?id=Kaiuz3a5vrzno - "zd": ( - SensorEntityDescription( - key=DPCode.BATTERY_PERCENTAGE, - name="Battery", - native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_BATTERY, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=DPCode.BATTERY_STATE, - name="Battery State", - icon="mdi:battery", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - ), + "zd": BATTERY_SENSORS, # Emergency Button # https://developer.tuya.com/en/docs/iot/categorysos?id=Kaiuz3oi6agjy - "sos": ( - SensorEntityDescription( - key=DPCode.BATTERY_PERCENTAGE, - name="Battery", - native_unit_of_measurement=PERCENTAGE, - device_class=DEVICE_CLASS_BATTERY, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - SensorEntityDescription( - key=DPCode.BATTERY_STATE, - name="Battery State", - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), - ), + "sos": BATTERY_SENSORS, } # Socket (duplicate of `kg`) diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index a828b6c6936..d4dddb5910a 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -35,6 +35,7 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.WARM, name="Heat preservation", + entity_category=ENTITY_CATEGORY_CONFIG, ), ), # Pet Water Feeder @@ -44,11 +45,13 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { key=DPCode.FILTER_RESET, name="Filter reset", icon="mdi:filter", + entity_category=ENTITY_CATEGORY_CONFIG, ), SwitchEntityDescription( key=DPCode.PUMP_RESET, name="Water pump reset", icon="mdi:pump", + entity_category=ENTITY_CATEGORY_CONFIG, ), SwitchEntityDescription( key=DPCode.SWITCH, @@ -58,7 +61,7 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { key=DPCode.WATER_RESET, name="Reset of water usage days", icon="mdi:water-sync", - entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_CONFIG, ), ), # Cirquit Breaker @@ -67,6 +70,7 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { key=DPCode.CHILD_LOCK, name="Child Lock", icon="mdi:account-lock", + entity_category=ENTITY_CATEGORY_CONFIG, ), SwitchEntityDescription( key=DPCode.SWITCH_1, @@ -80,6 +84,7 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { key=DPCode.CHILD_LOCK, name="Child Lock", icon="mdi:account-lock", + entity_category=ENTITY_CATEGORY_CONFIG, ), SwitchEntityDescription( key=DPCode.SWITCH_1, @@ -148,17 +153,19 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { key=DPCode.ANION, name="Ionizer", icon="mdi:minus-circle-outline", + entity_category=ENTITY_CATEGORY_CONFIG, ), SwitchEntityDescription( key=DPCode.FILTER_RESET, name="Filter cartridge reset", icon="mdi:filter", - entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_CONFIG, ), SwitchEntityDescription( key=DPCode.LOCK, name="Child lock", icon="mdi:account-lock", + entity_category=ENTITY_CATEGORY_CONFIG, ), SwitchEntityDescription( key=DPCode.SWITCH, @@ -168,6 +175,7 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { key=DPCode.WET, name="Humidification", icon="mdi:water-percent", + entity_category=ENTITY_CATEGORY_CONFIG, ), ), # Power Socket @@ -177,6 +185,7 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { key=DPCode.CHILD_LOCK, name="Child Lock", icon="mdi:account-lock", + entity_category=ENTITY_CATEGORY_CONFIG, ), SwitchEntityDescription( key=DPCode.SWITCH_1, @@ -243,11 +252,12 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "sgbj": ( SwitchEntityDescription( key=DPCode.MUFFLING, - name="Muffling", + name="Mute", entity_category=ENTITY_CATEGORY_CONFIG, ), ), # Diffuser + # https://developer.tuya.com/en/docs/iot/categoryxxj?id=Kaiuz1f9mo6bl "xxj": ( SwitchEntityDescription( key=DPCode.SWITCH, @@ -262,7 +272,7 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { key=DPCode.SWITCH_VOICE, name="Voice", icon="mdi:account-voice", - entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_CONFIG, ), ), } From 04f51e599a2ee8599143288a776caa365da41b0f Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 18 Oct 2021 12:01:58 +0200 Subject: [PATCH 0496/1038] Fix netgear NoneType and discovery (#57904) --- homeassistant/components/netgear/config_flow.py | 4 +++- homeassistant/components/netgear/router.py | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/netgear/config_flow.py b/homeassistant/components/netgear/config_flow.py index 871cba5a95d..6ce97fdbe60 100644 --- a/homeassistant/components/netgear/config_flow.py +++ b/homeassistant/components/netgear/config_flow.py @@ -142,7 +142,9 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): updated_data[CONF_PORT] = DEFAULT_PORT for model in MODELS_V2: - if discovery_info.get(ssdp.ATTR_UPNP_MODEL_NUMBER, "").startswith(model): + if discovery_info.get(ssdp.ATTR_UPNP_MODEL_NUMBER, "").startswith( + model + ) or discovery_info.get(ssdp.ATTR_UPNP_MODEL_NAME, "").startswith(model): updated_data[CONF_PORT] = ORBI_PORT self.placeholders.update(updated_data) diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index fc5e2c72e14..12abac53c38 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -199,6 +199,9 @@ class NetgearRouter: ntg_devices = await self.async_get_attached_devices() now = dt_util.utcnow() + if ntg_devices is None: + return + if _LOGGER.isEnabledFor(logging.DEBUG): _LOGGER.debug("Netgear scan result: \n%s", ntg_devices) From a421c524c1bd6d904006164c0723788a8255472e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 18 Oct 2021 13:27:44 +0200 Subject: [PATCH 0497/1038] Use pytest fixtures on Renault tests (#57955) Co-authored-by: epenet --- tests/components/renault/__init__.py | 322 +++----------- tests/components/renault/conftest.py | 211 +++++++++ tests/components/renault/const.py | 2 + .../components/renault/test_binary_sensor.py | 141 +++--- tests/components/renault/test_config_flow.py | 418 +++++++++--------- .../components/renault/test_device_tracker.py | 136 +++--- tests/components/renault/test_init.py | 35 +- tests/components/renault/test_select.py | 154 +++---- tests/components/renault/test_sensor.py | 209 ++++----- tests/components/renault/test_services.py | 76 +++- 10 files changed, 789 insertions(+), 915 deletions(-) create mode 100644 tests/components/renault/conftest.py diff --git a/tests/components/renault/__init__.py b/tests/components/renault/__init__.py index e95978222f6..7b3bb9e3d0a 100644 --- a/tests/components/renault/__init__.py +++ b/tests/components/renault/__init__.py @@ -1,16 +1,8 @@ """Tests for the Renault integration.""" from __future__ import annotations -import contextlib from types import MappingProxyType -from typing import Any -from unittest.mock import patch -from renault_api.kamereon import schemas -from renault_api.renault_account import RenaultAccount - -from homeassistant.components.renault.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( ATTR_ICON, ATTR_IDENTIFIERS, @@ -18,271 +10,23 @@ from homeassistant.const import ( ATTR_MODEL, ATTR_NAME, ATTR_SW_VERSION, + STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import aiohttp_client from homeassistant.helpers.device_registry import DeviceRegistry +from homeassistant.helpers.entity_registry import EntityRegistry -from .const import ICON_FOR_EMPTY_VALUES, MOCK_CONFIG, MOCK_VEHICLES - -from tests.common import MockConfigEntry, load_fixture - - -def get_mock_config_entry(): - """Create the Renault integration.""" - return MockConfigEntry( - domain=DOMAIN, - source=SOURCE_USER, - data=MOCK_CONFIG, - unique_id="account_id_1", - options={}, - entry_id="123456", - ) - - -def get_fixtures(vehicle_type: str) -> dict[str, Any]: - """Create a vehicle proxy for testing.""" - mock_vehicle = MOCK_VEHICLES.get(vehicle_type, {"endpoints": {}}) - return { - "battery_status": schemas.KamereonVehicleDataResponseSchema.loads( - load_fixture(f"renault/{mock_vehicle['endpoints']['battery_status']}") - if "battery_status" in mock_vehicle["endpoints"] - else load_fixture("renault/no_data.json") - ).get_attributes(schemas.KamereonVehicleBatteryStatusDataSchema), - "charge_mode": schemas.KamereonVehicleDataResponseSchema.loads( - load_fixture(f"renault/{mock_vehicle['endpoints']['charge_mode']}") - if "charge_mode" in mock_vehicle["endpoints"] - else load_fixture("renault/no_data.json") - ).get_attributes(schemas.KamereonVehicleChargeModeDataSchema), - "cockpit": schemas.KamereonVehicleDataResponseSchema.loads( - load_fixture(f"renault/{mock_vehicle['endpoints']['cockpit']}") - if "cockpit" in mock_vehicle["endpoints"] - else load_fixture("renault/no_data.json") - ).get_attributes(schemas.KamereonVehicleCockpitDataSchema), - "hvac_status": schemas.KamereonVehicleDataResponseSchema.loads( - load_fixture(f"renault/{mock_vehicle['endpoints']['hvac_status']}") - if "hvac_status" in mock_vehicle["endpoints"] - else load_fixture("renault/no_data.json") - ).get_attributes(schemas.KamereonVehicleHvacStatusDataSchema), - "location": schemas.KamereonVehicleDataResponseSchema.loads( - load_fixture(f"renault/{mock_vehicle['endpoints']['location']}") - if "location" in mock_vehicle["endpoints"] - else load_fixture("renault/no_data.json") - ).get_attributes(schemas.KamereonVehicleLocationDataSchema), - } +from .const import DYNAMIC_ATTRIBUTES, FIXED_ATTRIBUTES, ICON_FOR_EMPTY_VALUES def get_no_data_icon(expected_entity: MappingProxyType): - """Check attribute for icon for inactive sensors.""" + """Check icon attribute for inactive sensors.""" entity_id = expected_entity["entity_id"] return ICON_FOR_EMPTY_VALUES.get(entity_id, expected_entity.get(ATTR_ICON)) -async def setup_renault_integration_simple(hass: HomeAssistant): - """Create the Renault integration.""" - config_entry = get_mock_config_entry() - config_entry.add_to_hass(hass) - - renault_account = RenaultAccount( - config_entry.unique_id, - websession=aiohttp_client.async_get_clientsession(hass), - ) - - with patch("renault_api.renault_session.RenaultSession.login"), patch( - "renault_api.renault_client.RenaultClient.get_api_account", - return_value=renault_account, - ), patch("renault_api.renault_account.RenaultAccount.get_vehicles"): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - return config_entry - - -@contextlib.contextmanager -def patch_fixtures( - hass: HomeAssistant, config_entry: MockConfigEntry, vehicle_type: str -): - """Mock fixtures.""" - renault_account = RenaultAccount( - config_entry.unique_id, - websession=aiohttp_client.async_get_clientsession(hass), - ) - mock_vehicle = MOCK_VEHICLES[vehicle_type] - mock_fixtures = get_fixtures(vehicle_type) - - with patch("renault_api.renault_session.RenaultSession.login"), patch( - "renault_api.renault_client.RenaultClient.get_api_account", - return_value=renault_account, - ), patch( - "renault_api.renault_account.RenaultAccount.get_vehicles", - return_value=( - schemas.KamereonVehiclesResponseSchema.loads( - load_fixture(f"renault/vehicle_{vehicle_type}.json") - ) - ), - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.supports_endpoint", - side_effect=mock_vehicle["endpoints_available"], - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.has_contract_for_endpoint", - return_value=True, - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_battery_status", - return_value=mock_fixtures["battery_status"], - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_charge_mode", - return_value=mock_fixtures["charge_mode"], - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_cockpit", - return_value=mock_fixtures["cockpit"], - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_hvac_status", - return_value=mock_fixtures["hvac_status"], - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_location", - return_value=mock_fixtures["location"], - ): - yield - - -async def setup_renault_integration_vehicle(hass: HomeAssistant, vehicle_type: str): - """Create the Renault integration.""" - config_entry = get_mock_config_entry() - config_entry.add_to_hass(hass) - - with patch_fixtures(hass, config_entry, vehicle_type): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - return config_entry - - -@contextlib.contextmanager -def patch_fixtures_with_no_data( - hass: HomeAssistant, config_entry: MockConfigEntry, vehicle_type: str -): - """Mock fixtures.""" - renault_account = RenaultAccount( - config_entry.unique_id, - websession=aiohttp_client.async_get_clientsession(hass), - ) - mock_vehicle = MOCK_VEHICLES[vehicle_type] - mock_fixtures = get_fixtures("") - - with patch("renault_api.renault_session.RenaultSession.login"), patch( - "renault_api.renault_client.RenaultClient.get_api_account", - return_value=renault_account, - ), patch( - "renault_api.renault_account.RenaultAccount.get_vehicles", - return_value=( - schemas.KamereonVehiclesResponseSchema.loads( - load_fixture(f"renault/vehicle_{vehicle_type}.json") - ) - ), - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.supports_endpoint", - side_effect=mock_vehicle["endpoints_available"], - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.has_contract_for_endpoint", - return_value=True, - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_battery_status", - return_value=mock_fixtures["battery_status"], - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_charge_mode", - return_value=mock_fixtures["charge_mode"], - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_cockpit", - return_value=mock_fixtures["cockpit"], - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_hvac_status", - return_value=mock_fixtures["hvac_status"], - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_location", - return_value=mock_fixtures["location"], - ): - yield - - -async def setup_renault_integration_vehicle_with_no_data( - hass: HomeAssistant, vehicle_type: str -): - """Create the Renault integration.""" - config_entry = get_mock_config_entry() - config_entry.add_to_hass(hass) - - with patch_fixtures_with_no_data(hass, config_entry, vehicle_type): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - return config_entry - - -@contextlib.contextmanager -def patch_fixtures_with_side_effect( - hass: HomeAssistant, - config_entry: MockConfigEntry, - vehicle_type: str, - side_effect: Any, -): - """Mock fixtures.""" - renault_account = RenaultAccount( - config_entry.unique_id, - websession=aiohttp_client.async_get_clientsession(hass), - ) - mock_vehicle = MOCK_VEHICLES[vehicle_type] - - with patch("renault_api.renault_session.RenaultSession.login"), patch( - "renault_api.renault_client.RenaultClient.get_api_account", - return_value=renault_account, - ), patch( - "renault_api.renault_account.RenaultAccount.get_vehicles", - return_value=( - schemas.KamereonVehiclesResponseSchema.loads( - load_fixture(f"renault/vehicle_{vehicle_type}.json") - ) - ), - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.supports_endpoint", - side_effect=mock_vehicle["endpoints_available"], - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.has_contract_for_endpoint", - return_value=True, - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_battery_status", - side_effect=side_effect, - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_charge_mode", - side_effect=side_effect, - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_cockpit", - side_effect=side_effect, - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_hvac_status", - side_effect=side_effect, - ), patch( - "renault_api.renault_vehicle.RenaultVehicle.get_location", - side_effect=side_effect, - ): - yield - - -async def setup_renault_integration_vehicle_with_side_effect( - hass: HomeAssistant, vehicle_type: str, side_effect: Any -): - """Create the Renault integration.""" - config_entry = get_mock_config_entry() - config_entry.add_to_hass(hass) - - with patch_fixtures_with_side_effect(hass, config_entry, vehicle_type, side_effect): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - return config_entry - - def check_device_registry( - device_registry: DeviceRegistry, expected_device: dict[str, Any] + device_registry: DeviceRegistry, expected_device: MappingProxyType ) -> None: """Ensure that the expected_device is correctly registered.""" assert len(device_registry.devices) == 1 @@ -293,3 +37,59 @@ def check_device_registry( assert registry_entry.name == expected_device[ATTR_NAME] assert registry_entry.model == expected_device[ATTR_MODEL] assert registry_entry.sw_version == expected_device[ATTR_SW_VERSION] + + +def check_entities( + hass: HomeAssistant, + entity_registry: EntityRegistry, + expected_entities: MappingProxyType, +) -> None: + """Ensure that the expected_entities are correct.""" + for expected_entity in expected_entities: + entity_id = expected_entity["entity_id"] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry is not None + assert registry_entry.unique_id == expected_entity["unique_id"] + state = hass.states.get(entity_id) + assert state.state == expected_entity["result"] + for attr in FIXED_ATTRIBUTES + DYNAMIC_ATTRIBUTES: + assert state.attributes.get(attr) == expected_entity.get(attr) + + +def check_entities_no_data( + hass: HomeAssistant, + entity_registry: EntityRegistry, + expected_entities: MappingProxyType, + expected_state: str, +) -> None: + """Ensure that the expected_entities are correct.""" + for expected_entity in expected_entities: + entity_id = expected_entity["entity_id"] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry is not None + assert registry_entry.unique_id == expected_entity["unique_id"] + state = hass.states.get(entity_id) + assert state.state == expected_state + for attr in FIXED_ATTRIBUTES: + assert state.attributes.get(attr) == expected_entity.get(attr) + # Check dynamic attributes: + assert state.attributes.get(ATTR_ICON) == get_no_data_icon(expected_entity) + + +def check_entities_unavailable( + hass: HomeAssistant, + entity_registry: EntityRegistry, + expected_entities: MappingProxyType, +) -> None: + """Ensure that the expected_entities are correct.""" + for expected_entity in expected_entities: + entity_id = expected_entity["entity_id"] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry is not None + assert registry_entry.unique_id == expected_entity["unique_id"] + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE + for attr in FIXED_ATTRIBUTES: + assert state.attributes.get(attr) == expected_entity.get(attr) + # Check dynamic attributes: + assert state.attributes.get(ATTR_ICON) == get_no_data_icon(expected_entity) diff --git a/tests/components/renault/conftest.py b/tests/components/renault/conftest.py new file mode 100644 index 00000000000..a1f3b42167c --- /dev/null +++ b/tests/components/renault/conftest.py @@ -0,0 +1,211 @@ +"""Provide common Renault fixtures.""" +import contextlib +from types import MappingProxyType +from typing import Any +from unittest.mock import patch + +import pytest +from renault_api.kamereon import exceptions, schemas +from renault_api.renault_account import RenaultAccount + +from homeassistant.components.renault.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER, ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import aiohttp_client + +from .const import MOCK_ACCOUNT_ID, MOCK_CONFIG, MOCK_VEHICLES + +from tests.common import MockConfigEntry, load_fixture + + +@pytest.fixture(name="vehicle_type", params=MOCK_VEHICLES.keys()) +def get_vehicle_type(request: pytest.FixtureRequest) -> str: + """Parametrize vehicle type.""" + return request.param + + +@pytest.fixture(name="config_entry") +def get_config_entry(hass: HomeAssistant) -> ConfigEntry: + """Create and register mock config entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=MOCK_CONFIG, + unique_id=MOCK_ACCOUNT_ID, + options={}, + entry_id="123456", + ) + config_entry.add_to_hass(hass) + return config_entry + + +@pytest.fixture(name="patch_renault_account") +async def patch_renault_account(hass: HomeAssistant) -> RenaultAccount: + """Create a Renault account.""" + renault_account = RenaultAccount( + MOCK_ACCOUNT_ID, + websession=aiohttp_client.async_get_clientsession(hass), + ) + with patch("renault_api.renault_session.RenaultSession.login"), patch( + "renault_api.renault_client.RenaultClient.get_api_account", + return_value=renault_account, + ): + yield renault_account + + +@pytest.fixture(name="patch_get_vehicles") +def patch_get_vehicles(vehicle_type: str): + """Mock fixtures.""" + with patch( + "renault_api.renault_account.RenaultAccount.get_vehicles", + return_value=( + schemas.KamereonVehiclesResponseSchema.loads( + load_fixture(f"renault/vehicle_{vehicle_type}.json") + ) + ), + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.supports_endpoint", + side_effect=MOCK_VEHICLES[vehicle_type]["endpoints_available"], + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.has_contract_for_endpoint", + return_value=True, + ): + yield + + +def _get_fixtures(vehicle_type: str) -> MappingProxyType: + """Create a vehicle proxy for testing.""" + mock_vehicle = MOCK_VEHICLES.get(vehicle_type, {"endpoints": {}}) + return { + "battery_status": schemas.KamereonVehicleDataResponseSchema.loads( + load_fixture(f"renault/{mock_vehicle['endpoints']['battery_status']}") + if "battery_status" in mock_vehicle["endpoints"] + else load_fixture("renault/no_data.json") + ).get_attributes(schemas.KamereonVehicleBatteryStatusDataSchema), + "charge_mode": schemas.KamereonVehicleDataResponseSchema.loads( + load_fixture(f"renault/{mock_vehicle['endpoints']['charge_mode']}") + if "charge_mode" in mock_vehicle["endpoints"] + else load_fixture("renault/no_data.json") + ).get_attributes(schemas.KamereonVehicleChargeModeDataSchema), + "cockpit": schemas.KamereonVehicleDataResponseSchema.loads( + load_fixture(f"renault/{mock_vehicle['endpoints']['cockpit']}") + if "cockpit" in mock_vehicle["endpoints"] + else load_fixture("renault/no_data.json") + ).get_attributes(schemas.KamereonVehicleCockpitDataSchema), + "hvac_status": schemas.KamereonVehicleDataResponseSchema.loads( + load_fixture(f"renault/{mock_vehicle['endpoints']['hvac_status']}") + if "hvac_status" in mock_vehicle["endpoints"] + else load_fixture("renault/no_data.json") + ).get_attributes(schemas.KamereonVehicleHvacStatusDataSchema), + "location": schemas.KamereonVehicleDataResponseSchema.loads( + load_fixture(f"renault/{mock_vehicle['endpoints']['location']}") + if "location" in mock_vehicle["endpoints"] + else load_fixture("renault/no_data.json") + ).get_attributes(schemas.KamereonVehicleLocationDataSchema), + } + + +@pytest.fixture(name="fixtures_with_data") +def patch_fixtures_with_data(vehicle_type: str): + """Mock fixtures.""" + mock_fixtures = _get_fixtures(vehicle_type) + + with patch( + "renault_api.renault_vehicle.RenaultVehicle.get_battery_status", + return_value=mock_fixtures["battery_status"], + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_charge_mode", + return_value=mock_fixtures["charge_mode"], + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_cockpit", + return_value=mock_fixtures["cockpit"], + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_hvac_status", + return_value=mock_fixtures["hvac_status"], + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_location", + return_value=mock_fixtures["location"], + ): + yield + + +@pytest.fixture(name="fixtures_with_no_data") +def patch_fixtures_with_no_data(): + """Mock fixtures.""" + mock_fixtures = _get_fixtures("") + + with patch( + "renault_api.renault_vehicle.RenaultVehicle.get_battery_status", + return_value=mock_fixtures["battery_status"], + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_charge_mode", + return_value=mock_fixtures["charge_mode"], + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_cockpit", + return_value=mock_fixtures["cockpit"], + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_hvac_status", + return_value=mock_fixtures["hvac_status"], + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_location", + return_value=mock_fixtures["location"], + ): + yield + + +@contextlib.contextmanager +def _patch_fixtures_with_side_effect(side_effect: Any): + """Mock fixtures.""" + with patch( + "renault_api.renault_vehicle.RenaultVehicle.get_battery_status", + side_effect=side_effect, + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_charge_mode", + side_effect=side_effect, + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_cockpit", + side_effect=side_effect, + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_hvac_status", + side_effect=side_effect, + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_location", + side_effect=side_effect, + ): + yield + + +@pytest.fixture(name="fixtures_with_access_denied_exception") +def patch_fixtures_with_access_denied_exception(): + """Mock fixtures.""" + access_denied_exception = exceptions.AccessDeniedException( + "err.func.403", + "Access is denied for this resource", + ) + + with _patch_fixtures_with_side_effect(access_denied_exception): + yield + + +@pytest.fixture(name="fixtures_with_invalid_upstream_exception") +def patch_fixtures_with_invalid_upstream_exception(): + """Mock fixtures.""" + invalid_upstream_exception = exceptions.InvalidUpstreamException( + "err.tech.500", + "Invalid response from the upstream server (The request sent to the GDC is erroneous) ; 502 Bad Gateway", + ) + + with _patch_fixtures_with_side_effect(invalid_upstream_exception): + yield + + +@pytest.fixture(name="fixtures_with_not_supported_exception") +def patch_fixtures_with_not_supported_exception(): + """Mock fixtures.""" + not_supported_exception = exceptions.NotSupportedException( + "err.tech.501", + "This feature is not technically supported by this gateway", + ) + + with _patch_fixtures_with_side_effect(not_supported_exception): + yield diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py index 2bcab8ef47f..2e411500d62 100644 --- a/tests/components/renault/const.py +++ b/tests/components/renault/const.py @@ -61,6 +61,8 @@ ICON_FOR_EMPTY_VALUES = { "sensor.plug_state": "mdi:power-plug-off", } +MOCK_ACCOUNT_ID = "account_id_1" + # Mock config data to be used across multiple tests MOCK_CONFIG = { CONF_USERNAME: "email@test.com", diff --git a/tests/components/renault/test_binary_sensor.py b/tests/components/renault/test_binary_sensor.py index 10b9f768501..440018c01c2 100644 --- a/tests/components/renault/test_binary_sensor.py +++ b/tests/components/renault/test_binary_sensor.py @@ -2,133 +2,102 @@ from unittest.mock import patch import pytest -from renault_api.kamereon import exceptions from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.const import ATTR_ICON, STATE_OFF, STATE_UNAVAILABLE +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_OFF from homeassistant.core import HomeAssistant from . import ( check_device_registry, - get_no_data_icon, - setup_renault_integration_vehicle, - setup_renault_integration_vehicle_with_no_data, - setup_renault_integration_vehicle_with_side_effect, + check_entities, + check_entities_no_data, + check_entities_unavailable, ) -from .const import DYNAMIC_ATTRIBUTES, FIXED_ATTRIBUTES, MOCK_VEHICLES +from .const import MOCK_VEHICLES from tests.common import mock_device_registry, mock_registry +pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicles") -@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) -async def test_binary_sensors(hass: HomeAssistant, vehicle_type: str): + +@pytest.fixture(autouse=True) +def override_platforms(): + """Override PLATFORMS.""" + with patch("homeassistant.components.renault.PLATFORMS", [BINARY_SENSOR_DOMAIN]): + yield + + +@pytest.mark.usefixtures("fixtures_with_data") +async def test_binary_sensors( + hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str +): """Test for Renault binary sensors.""" - entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) - with patch("homeassistant.components.renault.PLATFORMS", [BINARY_SENSOR_DOMAIN]): - await setup_renault_integration_vehicle(hass, vehicle_type) - await hass.async_block_till_done() + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) expected_entities = mock_vehicle[BINARY_SENSOR_DOMAIN] assert len(entity_registry.entities) == len(expected_entities) - for expected_entity in expected_entities: - entity_id = expected_entity["entity_id"] - registry_entry = entity_registry.entities.get(entity_id) - assert registry_entry is not None - assert registry_entry.unique_id == expected_entity["unique_id"] - state = hass.states.get(entity_id) - assert state.state == expected_entity["result"] - for attr in FIXED_ATTRIBUTES + DYNAMIC_ATTRIBUTES: - assert state.attributes.get(attr) == expected_entity.get(attr) + + check_entities(hass, entity_registry, expected_entities) -@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) -async def test_binary_sensor_empty(hass: HomeAssistant, vehicle_type: str): +@pytest.mark.usefixtures("fixtures_with_no_data") +async def test_binary_sensor_empty( + hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str +): """Test for Renault binary sensors with empty data from Renault.""" - entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) - with patch("homeassistant.components.renault.PLATFORMS", [BINARY_SENSOR_DOMAIN]): - await setup_renault_integration_vehicle_with_no_data(hass, vehicle_type) - await hass.async_block_till_done() + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) expected_entities = mock_vehicle[BINARY_SENSOR_DOMAIN] assert len(entity_registry.entities) == len(expected_entities) - for expected_entity in expected_entities: - entity_id = expected_entity["entity_id"] - registry_entry = entity_registry.entities.get(entity_id) - assert registry_entry is not None - assert registry_entry.unique_id == expected_entity["unique_id"] - state = hass.states.get(entity_id) - assert state.state == STATE_OFF - for attr in FIXED_ATTRIBUTES: - assert state.attributes.get(attr) == expected_entity.get(attr) - # Check dynamic attributes: - assert state.attributes.get(ATTR_ICON) == get_no_data_icon(expected_entity) + check_entities_no_data(hass, entity_registry, expected_entities, STATE_OFF) -@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) -async def test_binary_sensor_errors(hass: HomeAssistant, vehicle_type: str): +@pytest.mark.usefixtures("fixtures_with_invalid_upstream_exception") +async def test_binary_sensor_errors( + hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str +): """Test for Renault binary sensors with temporary failure.""" - entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) - invalid_upstream_exception = exceptions.InvalidUpstreamException( - "err.tech.500", - "Invalid response from the upstream server (The request sent to the GDC is erroneous) ; 502 Bad Gateway", - ) - - with patch("homeassistant.components.renault.PLATFORMS", [BINARY_SENSOR_DOMAIN]): - await setup_renault_integration_vehicle_with_side_effect( - hass, vehicle_type, invalid_upstream_exception - ) - await hass.async_block_till_done() + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) expected_entities = mock_vehicle[BINARY_SENSOR_DOMAIN] assert len(entity_registry.entities) == len(expected_entities) - for expected_entity in expected_entities: - entity_id = expected_entity["entity_id"] - registry_entry = entity_registry.entities.get(entity_id) - assert registry_entry is not None - assert registry_entry.unique_id == expected_entity["unique_id"] - state = hass.states.get(entity_id) - assert state.state == STATE_UNAVAILABLE - for attr in FIXED_ATTRIBUTES: - assert state.attributes.get(attr) == expected_entity.get(attr) - # Check dynamic attributes: - assert state.attributes.get(ATTR_ICON) == get_no_data_icon(expected_entity) + + check_entities_unavailable(hass, entity_registry, expected_entities) -async def test_binary_sensor_access_denied(hass): +@pytest.mark.usefixtures("fixtures_with_access_denied_exception") +@pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) +async def test_binary_sensor_access_denied( + hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str +): """Test for Renault binary sensors with access denied failure.""" - entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) - vehicle_type = "zoe_40" - access_denied_exception = exceptions.AccessDeniedException( - "err.func.403", - "Access is denied for this resource", - ) - - with patch("homeassistant.components.renault.PLATFORMS", [BINARY_SENSOR_DOMAIN]): - await setup_renault_integration_vehicle_with_side_effect( - hass, vehicle_type, access_denied_exception - ) - await hass.async_block_till_done() + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) @@ -136,23 +105,17 @@ async def test_binary_sensor_access_denied(hass): assert len(entity_registry.entities) == 0 -async def test_binary_sensor_not_supported(hass): +@pytest.mark.usefixtures("fixtures_with_not_supported_exception") +@pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) +async def test_binary_sensor_not_supported( + hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str +): """Test for Renault binary sensors with not supported failure.""" - entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) - vehicle_type = "zoe_40" - not_supported_exception = exceptions.NotSupportedException( - "err.tech.501", - "This feature is not technically supported by this gateway", - ) - - with patch("homeassistant.components.renault.PLATFORMS", [BINARY_SENSOR_DOMAIN]): - await setup_renault_integration_vehicle_with_side_effect( - hass, vehicle_type, not_supported_exception - ) - await hass.async_block_till_done() + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) diff --git a/tests/components/renault/test_config_flow.py b/tests/components/renault/test_config_flow.py index ebf458541f0..ec5eae468fc 100644 --- a/tests/components/renault/test_config_flow.py +++ b/tests/components/renault/test_config_flow.py @@ -1,6 +1,7 @@ """Test the Renault config flow.""" from unittest.mock import AsyncMock, PropertyMock, patch +import pytest from renault_api.gigya.exceptions import InvalidCredentialsException from renault_api.kamereon import schemas from renault_api.renault_account import RenaultAccount @@ -11,251 +12,238 @@ from homeassistant.components.renault.const import ( CONF_LOCALE, DOMAIN, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client -from . import get_mock_config_entry from .const import MOCK_CONFIG from tests.common import load_fixture -async def test_config_flow_single_account(hass: HomeAssistant): +@pytest.fixture(autouse=True, name="mock_setup_entry") +def override_async_setup_entry() -> AsyncMock: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.renault.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +async def test_config_flow_single_account( + hass: HomeAssistant, mock_setup_entry: AsyncMock +): """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + # Failed credentials with patch( - "homeassistant.components.renault.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {} - - # Failed credentials - with patch( - "renault_api.renault_session.RenaultSession.login", - side_effect=InvalidCredentialsException( - 403042, "invalid loginID or password" - ), - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_LOCALE: "fr_FR", - CONF_USERNAME: "email@test.com", - CONF_PASSWORD: "test", - }, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "invalid_credentials"} - - renault_account = AsyncMock() - type(renault_account).account_id = PropertyMock(return_value="account_id_1") - renault_account.get_vehicles.return_value = ( - schemas.KamereonVehiclesResponseSchema.loads( - load_fixture("renault/vehicle_zoe_40.json") - ) - ) - - # Account list single - with patch("renault_api.renault_session.RenaultSession.login"), patch( - "renault_api.renault_account.RenaultAccount.account_id", return_value="123" - ), patch( - "renault_api.renault_client.RenaultClient.get_api_accounts", - return_value=[renault_account], - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_LOCALE: "fr_FR", - CONF_USERNAME: "email@test.com", - CONF_PASSWORD: "test", - }, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "account_id_1" - assert result["data"][CONF_USERNAME] == "email@test.com" - assert result["data"][CONF_PASSWORD] == "test" - assert result["data"][CONF_KAMEREON_ACCOUNT_ID] == "account_id_1" - assert result["data"][CONF_LOCALE] == "fr_FR" - - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_config_flow_no_account(hass: HomeAssistant): - """Test we get the form.""" - with patch( - "homeassistant.components.renault.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {} - - # Account list empty - with patch("renault_api.renault_session.RenaultSession.login"), patch( - "renault_api.renault_client.RenaultClient.get_api_accounts", - return_value=[], - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_LOCALE: "fr_FR", - CONF_USERNAME: "email@test.com", - CONF_PASSWORD: "test", - }, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "kamereon_no_account" - - assert len(mock_setup_entry.mock_calls) == 0 - - -async def test_config_flow_multiple_accounts(hass: HomeAssistant): - """Test what happens if multiple Kamereon accounts are available.""" - with patch( - "homeassistant.components.renault.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {} - - renault_account_1 = RenaultAccount( - "account_id_1", - websession=aiohttp_client.async_get_clientsession(hass), - ) - renault_account_2 = RenaultAccount( - "account_id_2", - websession=aiohttp_client.async_get_clientsession(hass), - ) - - # Multiple accounts - with patch("renault_api.renault_session.RenaultSession.login"), patch( - "renault_api.renault_client.RenaultClient.get_api_accounts", - return_value=[renault_account_1, renault_account_2], - ), patch("renault_api.renault_account.RenaultAccount.get_vehicles"): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_LOCALE: "fr_FR", - CONF_USERNAME: "email@test.com", - CONF_PASSWORD: "test", - }, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "kamereon" - - # Account selected + "renault_api.renault_session.RenaultSession.login", + side_effect=InvalidCredentialsException(403042, "invalid loginID or password"), + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={CONF_KAMEREON_ACCOUNT_ID: "account_id_2"}, + user_input={ + CONF_LOCALE: "fr_FR", + CONF_USERNAME: "email@test.com", + CONF_PASSWORD: "test", + }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "account_id_2" - assert result["data"][CONF_USERNAME] == "email@test.com" - assert result["data"][CONF_PASSWORD] == "test" - assert result["data"][CONF_KAMEREON_ACCOUNT_ID] == "account_id_2" - assert result["data"][CONF_LOCALE] == "fr_FR" + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_credentials"} + + renault_account = AsyncMock() + type(renault_account).account_id = PropertyMock(return_value="account_id_1") + renault_account.get_vehicles.return_value = ( + schemas.KamereonVehiclesResponseSchema.loads( + load_fixture("renault/vehicle_zoe_40.json") + ) + ) + + # Account list single + with patch("renault_api.renault_session.RenaultSession.login"), patch( + "renault_api.renault_account.RenaultAccount.account_id", return_value="123" + ), patch( + "renault_api.renault_client.RenaultClient.get_api_accounts", + return_value=[renault_account], + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LOCALE: "fr_FR", + CONF_USERNAME: "email@test.com", + CONF_PASSWORD: "test", + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "account_id_1" + assert result["data"][CONF_USERNAME] == "email@test.com" + assert result["data"][CONF_PASSWORD] == "test" + assert result["data"][CONF_KAMEREON_ACCOUNT_ID] == "account_id_1" + assert result["data"][CONF_LOCALE] == "fr_FR" assert len(mock_setup_entry.mock_calls) == 1 -async def test_config_flow_duplicate(hass: HomeAssistant): - """Test abort if unique_id configured.""" - with patch( - "homeassistant.components.renault.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - get_mock_config_entry().add_to_hass(hass) - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 +async def test_config_flow_no_account(hass: HomeAssistant, mock_setup_entry: AsyncMock): + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + # Account list empty + with patch("renault_api.renault_session.RenaultSession.login"), patch( + "renault_api.renault_client.RenaultClient.get_api_accounts", + return_value=[], + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LOCALE: "fr_FR", + CONF_USERNAME: "email@test.com", + CONF_PASSWORD: "test", + }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {} - renault_account = RenaultAccount( - "account_id_1", - websession=aiohttp_client.async_get_clientsession(hass), - ) - with patch("renault_api.renault_session.RenaultSession.login"), patch( - "renault_api.renault_client.RenaultClient.get_api_accounts", - return_value=[renault_account], - ), patch("renault_api.renault_account.RenaultAccount.get_vehicles"): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_LOCALE: "fr_FR", - CONF_USERNAME: "email@test.com", - CONF_PASSWORD: "test", - }, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "kamereon_no_account" assert len(mock_setup_entry.mock_calls) == 0 -async def test_reauth(hass): - """Test the start of the config flow.""" - with patch( - "homeassistant.components.renault.async_setup_entry", - return_value=True, - ): - original_entry = get_mock_config_entry() - original_entry.add_to_hass(hass) - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 +async def test_config_flow_multiple_accounts( + hass: HomeAssistant, mock_setup_entry: AsyncMock +): + """Test what happens if multiple Kamereon accounts are available.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": config_entries.SOURCE_REAUTH, - "entry_id": original_entry.entry_id, - "unique_id": original_entry.unique_id, + renault_account_1 = RenaultAccount( + "account_id_1", + websession=aiohttp_client.async_get_clientsession(hass), + ) + renault_account_2 = RenaultAccount( + "account_id_2", + websession=aiohttp_client.async_get_clientsession(hass), + ) + + # Multiple accounts + with patch("renault_api.renault_session.RenaultSession.login"), patch( + "renault_api.renault_client.RenaultClient.get_api_accounts", + return_value=[renault_account_1, renault_account_2], + ), patch("renault_api.renault_account.RenaultAccount.get_vehicles"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LOCALE: "fr_FR", + CONF_USERNAME: "email@test.com", + CONF_PASSWORD: "test", }, - data=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["description_placeholders"] == {CONF_USERNAME: "email@test.com"} - assert result["errors"] == {} + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "kamereon" - # Failed credentials - with patch( - "renault_api.renault_session.RenaultSession.login", - side_effect=InvalidCredentialsException( - 403042, "invalid loginID or password" - ), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_PASSWORD: "any"}, - ) + # Account selected + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_KAMEREON_ACCOUNT_ID: "account_id_2"}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "account_id_2" + assert result["data"][CONF_USERNAME] == "email@test.com" + assert result["data"][CONF_PASSWORD] == "test" + assert result["data"][CONF_KAMEREON_ACCOUNT_ID] == "account_id_2" + assert result["data"][CONF_LOCALE] == "fr_FR" - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["description_placeholders"] == {CONF_USERNAME: "email@test.com"} - assert result2["errors"] == {"base": "invalid_credentials"} + assert len(mock_setup_entry.mock_calls) == 1 - # Valid credentials - with patch("renault_api.renault_session.RenaultSession.login"): - result3 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_PASSWORD: "any"}, - ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result3["reason"] == "reauth_successful" +@pytest.mark.usefixtures("config_entry") +async def test_config_flow_duplicate(hass: HomeAssistant, mock_setup_entry: AsyncMock): + """Test abort if unique_id configured.""" + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + renault_account = RenaultAccount( + "account_id_1", + websession=aiohttp_client.async_get_clientsession(hass), + ) + with patch("renault_api.renault_session.RenaultSession.login"), patch( + "renault_api.renault_client.RenaultClient.get_api_accounts", + return_value=[renault_account], + ), patch("renault_api.renault_account.RenaultAccount.get_vehicles"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LOCALE: "fr_FR", + CONF_USERNAME: "email@test.com", + CONF_PASSWORD: "test", + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + await hass.async_block_till_done() + + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_reauth(hass: HomeAssistant, config_entry: ConfigEntry): + """Test the start of the config flow.""" + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": config_entry.entry_id, + "unique_id": config_entry.unique_id, + }, + data=MOCK_CONFIG, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["description_placeholders"] == {CONF_USERNAME: "email@test.com"} + assert result["errors"] == {} + + # Failed credentials + with patch( + "renault_api.renault_session.RenaultSession.login", + side_effect=InvalidCredentialsException(403042, "invalid loginID or password"), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: "any"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["description_placeholders"] == {CONF_USERNAME: "email@test.com"} + assert result2["errors"] == {"base": "invalid_credentials"} + + # Valid credentials + with patch("renault_api.renault_session.RenaultSession.login"): + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: "any"}, + ) + + assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["reason"] == "reauth_successful" diff --git a/tests/components/renault/test_device_tracker.py b/tests/components/renault/test_device_tracker.py index 54f25e4e8cd..a2c8b165b32 100644 --- a/tests/components/renault/test_device_tracker.py +++ b/tests/components/renault/test_device_tracker.py @@ -2,133 +2,106 @@ from unittest.mock import patch import pytest -from renault_api.kamereon import exceptions from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN -from homeassistant.const import ATTR_ICON, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant from . import ( check_device_registry, - get_no_data_icon, - setup_renault_integration_vehicle, - setup_renault_integration_vehicle_with_no_data, - setup_renault_integration_vehicle_with_side_effect, + check_entities, + check_entities_no_data, + check_entities_unavailable, ) -from .const import DYNAMIC_ATTRIBUTES, FIXED_ATTRIBUTES, MOCK_VEHICLES +from .const import MOCK_VEHICLES from tests.common import mock_device_registry, mock_registry +pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicles") -@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) -async def test_device_trackers(hass: HomeAssistant, vehicle_type: str): + +@pytest.fixture(autouse=True) +def override_platforms(): + """Override PLATFORMS.""" + with patch("homeassistant.components.renault.PLATFORMS", [DEVICE_TRACKER_DOMAIN]): + yield + + +@pytest.mark.usefixtures("fixtures_with_data") +async def test_device_trackers( + hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str +): """Test for Renault device trackers.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) - with patch("homeassistant.components.renault.PLATFORMS", [DEVICE_TRACKER_DOMAIN]): - await setup_renault_integration_vehicle(hass, vehicle_type) - await hass.async_block_till_done() + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) expected_entities = mock_vehicle[DEVICE_TRACKER_DOMAIN] assert len(entity_registry.entities) == len(expected_entities) - for expected_entity in expected_entities: - entity_id = expected_entity["entity_id"] - registry_entry = entity_registry.entities.get(entity_id) - assert registry_entry is not None - assert registry_entry.unique_id == expected_entity["unique_id"] - state = hass.states.get(entity_id) - assert state.state == expected_entity["result"] - for attr in FIXED_ATTRIBUTES + DYNAMIC_ATTRIBUTES: - assert state.attributes.get(attr) == expected_entity.get(attr) + + check_entities(hass, entity_registry, expected_entities) -@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) -async def test_device_tracker_empty(hass: HomeAssistant, vehicle_type: str): +@pytest.mark.usefixtures("fixtures_with_no_data") +async def test_device_tracker_empty( + hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str +): """Test for Renault device trackers with empty data from Renault.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) - with patch("homeassistant.components.renault.PLATFORMS", [DEVICE_TRACKER_DOMAIN]): - await setup_renault_integration_vehicle_with_no_data(hass, vehicle_type) - await hass.async_block_till_done() + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) expected_entities = mock_vehicle[DEVICE_TRACKER_DOMAIN] assert len(entity_registry.entities) == len(expected_entities) - for expected_entity in expected_entities: - entity_id = expected_entity["entity_id"] - registry_entry = entity_registry.entities.get(entity_id) - assert registry_entry is not None - assert registry_entry.unique_id == expected_entity["unique_id"] - state = hass.states.get(entity_id) - assert state.state == STATE_UNKNOWN - for attr in FIXED_ATTRIBUTES: - assert state.attributes.get(attr) == expected_entity.get(attr) - # Check dynamic attributes: - assert state.attributes.get(ATTR_ICON) == get_no_data_icon(expected_entity) + check_entities_no_data(hass, entity_registry, expected_entities, STATE_UNKNOWN) -@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) -async def test_device_tracker_errors(hass: HomeAssistant, vehicle_type: str): +@pytest.mark.usefixtures("fixtures_with_invalid_upstream_exception") +async def test_device_tracker_errors( + hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str +): """Test for Renault device trackers with temporary failure.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) - invalid_upstream_exception = exceptions.InvalidUpstreamException( - "err.tech.500", - "Invalid response from the upstream server (The request sent to the GDC is erroneous) ; 502 Bad Gateway", - ) - - with patch("homeassistant.components.renault.PLATFORMS", [DEVICE_TRACKER_DOMAIN]): - await setup_renault_integration_vehicle_with_side_effect( - hass, vehicle_type, invalid_upstream_exception - ) - await hass.async_block_till_done() + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) expected_entities = mock_vehicle[DEVICE_TRACKER_DOMAIN] assert len(entity_registry.entities) == len(expected_entities) - for expected_entity in expected_entities: - entity_id = expected_entity["entity_id"] - registry_entry = entity_registry.entities.get(entity_id) - assert registry_entry is not None - assert registry_entry.unique_id == expected_entity["unique_id"] - state = hass.states.get(entity_id) - assert state.state == STATE_UNAVAILABLE - for attr in FIXED_ATTRIBUTES: - assert state.attributes.get(attr) == expected_entity.get(attr) - # Check dynamic attributes: - assert state.attributes.get(ATTR_ICON) == get_no_data_icon(expected_entity) + + check_entities_unavailable(hass, entity_registry, expected_entities) -async def test_device_tracker_access_denied(hass: HomeAssistant): +@pytest.mark.usefixtures("fixtures_with_access_denied_exception") +@pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) +async def test_device_tracker_access_denied( + hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str +): """Test for Renault device trackers with access denied failure.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) - vehicle_type = "zoe_40" - access_denied_exception = exceptions.AccessDeniedException( - "err.func.403", - "Access is denied for this resource", - ) - - with patch("homeassistant.components.renault.PLATFORMS", [DEVICE_TRACKER_DOMAIN]): - await setup_renault_integration_vehicle_with_side_effect( - hass, vehicle_type, access_denied_exception - ) - await hass.async_block_till_done() + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) @@ -136,23 +109,18 @@ async def test_device_tracker_access_denied(hass: HomeAssistant): assert len(entity_registry.entities) == 0 -async def test_device_tracker_not_supported(hass: HomeAssistant): +@pytest.mark.usefixtures("fixtures_with_not_supported_exception") +@pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) +async def test_device_tracker_not_supported( + hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str +): """Test for Renault device trackers with not supported failure.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) - vehicle_type = "zoe_40" - not_supported_exception = exceptions.NotSupportedException( - "err.tech.501", - "This feature is not technically supported by this gateway", - ) - - with patch("homeassistant.components.renault.PLATFORMS", [DEVICE_TRACKER_DOMAIN]): - await setup_renault_integration_vehicle_with_side_effect( - hass, vehicle_type, not_supported_exception - ) - await hass.async_block_till_done() + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) diff --git a/tests/components/renault/test_init.py b/tests/components/renault/test_init.py index 3446bb1f9fa..58da09ccd0b 100644 --- a/tests/components/renault/test_init.py +++ b/tests/components/renault/test_init.py @@ -2,19 +2,32 @@ from unittest.mock import patch import aiohttp +import pytest from renault_api.gigya.exceptions import InvalidCredentialsException from homeassistant.components.renault.const import DOMAIN -from homeassistant.config_entries import ConfigEntryState +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant -from . import get_mock_config_entry, setup_renault_integration_simple - -async def test_setup_unload_entry(hass: HomeAssistant): - """Test entry setup and unload.""" +@pytest.fixture(autouse=True) +def override_platforms(): + """Override PLATFORMS.""" with patch("homeassistant.components.renault.PLATFORMS", []): - config_entry = await setup_renault_integration_simple(hass) + yield + + +@pytest.fixture(autouse=True, name="vehicle_type", params=["zoe_40"]) +def override_vehicle_type(request) -> str: + """Parametrize vehicle type.""" + return request.param + + +@pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicles") +async def test_setup_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): + """Test entry setup and unload.""" + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert config_entry.state is ConfigEntryState.LOADED @@ -27,12 +40,9 @@ async def test_setup_unload_entry(hass: HomeAssistant): assert config_entry.entry_id not in hass.data[DOMAIN] -async def test_setup_entry_bad_password(hass: HomeAssistant): +async def test_setup_entry_bad_password(hass: HomeAssistant, config_entry: ConfigEntry): """Test entry setup and unload.""" # Create a mock entry so we don't have to go through config flow - config_entry = get_mock_config_entry() - config_entry.add_to_hass(hass) - with patch( "renault_api.renault_session.RenaultSession.login", side_effect=InvalidCredentialsException(403042, "invalid loginID or password"), @@ -45,11 +55,8 @@ async def test_setup_entry_bad_password(hass: HomeAssistant): assert not hass.data.get(DOMAIN) -async def test_setup_entry_exception(hass: HomeAssistant): +async def test_setup_entry_exception(hass: HomeAssistant, config_entry: ConfigEntry): """Test ConfigEntryNotReady when API raises an exception during entry setup.""" - config_entry = get_mock_config_entry() - config_entry.add_to_hass(hass) - # In this case we are testing the condition where async_setup_entry raises # ConfigEntryNotReady. with patch( diff --git a/tests/components/renault/test_select.py b/tests/components/renault/test_select.py index 5090658464d..b7adaa0c637 100644 --- a/tests/components/renault/test_select.py +++ b/tests/components/renault/test_select.py @@ -2,139 +2,104 @@ from unittest.mock import patch import pytest -from renault_api.kamereon import exceptions, schemas +from renault_api.kamereon import schemas from homeassistant.components.select import DOMAIN as SELECT_DOMAIN from homeassistant.components.select.const import ATTR_OPTION, SERVICE_SELECT_OPTION -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_ICON, - STATE_UNAVAILABLE, - STATE_UNKNOWN, -) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant from . import ( check_device_registry, - get_no_data_icon, - setup_renault_integration_vehicle, - setup_renault_integration_vehicle_with_no_data, - setup_renault_integration_vehicle_with_side_effect, + check_entities, + check_entities_no_data, + check_entities_unavailable, ) -from .const import DYNAMIC_ATTRIBUTES, FIXED_ATTRIBUTES, MOCK_VEHICLES +from .const import MOCK_VEHICLES from tests.common import load_fixture, mock_device_registry, mock_registry +pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicles") -@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) -async def test_selects(hass: HomeAssistant, vehicle_type: str): + +@pytest.fixture(autouse=True) +def override_platforms(): + """Override PLATFORMS.""" + with patch("homeassistant.components.renault.PLATFORMS", [SELECT_DOMAIN]): + yield + + +@pytest.mark.usefixtures("fixtures_with_data") +async def test_selects( + hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str +): """Test for Renault selects.""" - entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) - with patch("homeassistant.components.renault.PLATFORMS", [SELECT_DOMAIN]): - await setup_renault_integration_vehicle(hass, vehicle_type) - await hass.async_block_till_done() + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) expected_entities = mock_vehicle[SELECT_DOMAIN] assert len(entity_registry.entities) == len(expected_entities) - for expected_entity in expected_entities: - entity_id = expected_entity["entity_id"] - registry_entry = entity_registry.entities.get(entity_id) - assert registry_entry is not None - assert registry_entry.unique_id == expected_entity["unique_id"] - state = hass.states.get(entity_id) - assert state.state == expected_entity["result"] - for attr in FIXED_ATTRIBUTES + DYNAMIC_ATTRIBUTES: - assert state.attributes.get(attr) == expected_entity.get(attr) + + check_entities(hass, entity_registry, expected_entities) -@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) -async def test_select_empty(hass: HomeAssistant, vehicle_type: str): +@pytest.mark.usefixtures("fixtures_with_no_data") +async def test_select_empty( + hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str +): """Test for Renault selects with empty data from Renault.""" - entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) - with patch("homeassistant.components.renault.PLATFORMS", [SELECT_DOMAIN]): - await setup_renault_integration_vehicle_with_no_data(hass, vehicle_type) - await hass.async_block_till_done() + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) expected_entities = mock_vehicle[SELECT_DOMAIN] assert len(entity_registry.entities) == len(expected_entities) - for expected_entity in expected_entities: - entity_id = expected_entity["entity_id"] - registry_entry = entity_registry.entities.get(entity_id) - assert registry_entry is not None - assert registry_entry.unique_id == expected_entity["unique_id"] - state = hass.states.get(entity_id) - assert state.state == STATE_UNKNOWN - for attr in FIXED_ATTRIBUTES: - assert state.attributes.get(attr) == expected_entity.get(attr) - # Check dynamic attributes: - assert state.attributes.get(ATTR_ICON) == get_no_data_icon(expected_entity) + check_entities_no_data(hass, entity_registry, expected_entities, STATE_UNKNOWN) -@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) -async def test_select_errors(hass: HomeAssistant, vehicle_type: str): +@pytest.mark.usefixtures("fixtures_with_invalid_upstream_exception") +async def test_select_errors( + hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str +): """Test for Renault selects with temporary failure.""" - entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) - invalid_upstream_exception = exceptions.InvalidUpstreamException( - "err.tech.500", - "Invalid response from the upstream server (The request sent to the GDC is erroneous) ; 502 Bad Gateway", - ) - - with patch("homeassistant.components.renault.PLATFORMS", [SELECT_DOMAIN]): - await setup_renault_integration_vehicle_with_side_effect( - hass, vehicle_type, invalid_upstream_exception - ) - await hass.async_block_till_done() + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) expected_entities = mock_vehicle[SELECT_DOMAIN] assert len(entity_registry.entities) == len(expected_entities) - for expected_entity in expected_entities: - entity_id = expected_entity["entity_id"] - registry_entry = entity_registry.entities.get(entity_id) - assert registry_entry is not None - assert registry_entry.unique_id == expected_entity["unique_id"] - state = hass.states.get(entity_id) - assert state.state == STATE_UNAVAILABLE - for attr in FIXED_ATTRIBUTES: - assert state.attributes.get(attr) == expected_entity.get(attr) - # Check dynamic attributes: - assert state.attributes.get(ATTR_ICON) == get_no_data_icon(expected_entity) + + check_entities_unavailable(hass, entity_registry, expected_entities) -async def test_select_access_denied(hass: HomeAssistant): +@pytest.mark.usefixtures("fixtures_with_access_denied_exception") +@pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) +async def test_select_access_denied( + hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str +): """Test for Renault selects with access denied failure.""" - entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) - vehicle_type = "zoe_40" - access_denied_exception = exceptions.AccessDeniedException( - "err.func.403", - "Access is denied for this resource", - ) - - with patch("homeassistant.components.renault.PLATFORMS", [SELECT_DOMAIN]): - await setup_renault_integration_vehicle_with_side_effect( - hass, vehicle_type, access_denied_exception - ) - await hass.async_block_till_done() + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) @@ -142,23 +107,17 @@ async def test_select_access_denied(hass: HomeAssistant): assert len(entity_registry.entities) == 0 -async def test_select_not_supported(hass: HomeAssistant): +@pytest.mark.usefixtures("fixtures_with_not_supported_exception") +@pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) +async def test_select_not_supported( + hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str +): """Test for Renault selects with access denied failure.""" - entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) - vehicle_type = "zoe_40" - not_supported_exception = exceptions.NotSupportedException( - "err.tech.501", - "This feature is not technically supported by this gateway", - ) - - with patch("homeassistant.components.renault.PLATFORMS", [SELECT_DOMAIN]): - await setup_renault_integration_vehicle_with_side_effect( - hass, vehicle_type, not_supported_exception - ) - await hass.async_block_till_done() + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) @@ -166,9 +125,12 @@ async def test_select_not_supported(hass: HomeAssistant): assert len(entity_registry.entities) == 0 -async def test_select_charge_mode(hass: HomeAssistant): +@pytest.mark.usefixtures("fixtures_with_data") +@pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) +async def test_select_charge_mode(hass: HomeAssistant, config_entry: ConfigEntry): """Test that service invokes renault_api with correct data.""" - await setup_renault_integration_vehicle(hass, "zoe_40") + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() data = { ATTR_ENTITY_ID: "select.charge_mode", diff --git a/tests/components/renault/test_sensor.py b/tests/components/renault/test_sensor.py index 79d53d896d3..9e83a131aa8 100644 --- a/tests/components/renault/test_sensor.py +++ b/tests/components/renault/test_sensor.py @@ -1,38 +1,58 @@ """Tests for Renault sensors.""" +from types import MappingProxyType from unittest.mock import patch import pytest -from renault_api.kamereon import exceptions from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import ATTR_ICON, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_registry import EntityRegistry from . import ( check_device_registry, - get_no_data_icon, - patch_fixtures, - patch_fixtures_with_no_data, - patch_fixtures_with_side_effect, - setup_renault_integration_vehicle, - setup_renault_integration_vehicle_with_no_data, - setup_renault_integration_vehicle_with_side_effect, + check_entities, + check_entities_no_data, + check_entities_unavailable, ) -from .const import DYNAMIC_ATTRIBUTES, FIXED_ATTRIBUTES, MOCK_VEHICLES +from .const import MOCK_VEHICLES from tests.common import mock_device_registry, mock_registry +pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicles") -@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) -async def test_sensors(hass: HomeAssistant, vehicle_type: str): + +@pytest.fixture(autouse=True) +def override_platforms(): + """Override PLATFORMS.""" + with patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): + yield + + +def _check_and_enable_disabled_entities( + entity_registry: EntityRegistry, expected_entities: MappingProxyType +) -> None: + """Ensure that the expected_entities are correctly disabled.""" + for expected_entity in expected_entities: + if expected_entity.get("default_disabled"): + entity_id = expected_entity["entity_id"] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry.disabled + assert registry_entry.disabled_by == "integration" + entity_registry.async_update_entity(entity_id, **{"disabled_by": None}) + + +@pytest.mark.usefixtures("fixtures_with_data") +async def test_sensors( + hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str +): """Test for Renault sensors.""" - entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) - with patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): - config_entry = await setup_renault_integration_vehicle(hass, vehicle_type) - await hass.async_block_till_done() + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) @@ -40,43 +60,23 @@ async def test_sensors(hass: HomeAssistant, vehicle_type: str): expected_entities = mock_vehicle[SENSOR_DOMAIN] assert len(entity_registry.entities) == len(expected_entities) - # Ensure all entities are enabled - for expected_entity in expected_entities: - if expected_entity.get("default_disabled"): - entity_id = expected_entity["entity_id"] - registry_entry = entity_registry.entities.get(entity_id) - assert registry_entry.disabled - assert registry_entry.disabled_by == "integration" - entity_registry.async_update_entity(entity_id, **{"disabled_by": None}) - with patch( - "homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN] - ), patch_fixtures(hass, config_entry, vehicle_type): - await hass.config_entries.async_reload(config_entry.entry_id) - await hass.async_block_till_done() + _check_and_enable_disabled_entities(entity_registry, expected_entities) + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() - for expected_entity in expected_entities: - entity_id = expected_entity["entity_id"] - registry_entry = entity_registry.entities.get(entity_id) - assert registry_entry is not None - assert registry_entry.unique_id == expected_entity["unique_id"] - state = hass.states.get(entity_id) - assert state.state == expected_entity["result"] - for attr in FIXED_ATTRIBUTES + DYNAMIC_ATTRIBUTES: - assert state.attributes.get(attr) == expected_entity.get(attr) + check_entities(hass, entity_registry, expected_entities) -@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) -async def test_sensor_empty(hass: HomeAssistant, vehicle_type: str): +@pytest.mark.usefixtures("fixtures_with_no_data") +async def test_sensor_empty( + hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str +): """Test for Renault sensors with empty data from Renault.""" - entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) - with patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): - config_entry = await setup_renault_integration_vehicle_with_no_data( - hass, vehicle_type - ) - await hass.async_block_till_done() + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) @@ -84,50 +84,23 @@ async def test_sensor_empty(hass: HomeAssistant, vehicle_type: str): expected_entities = mock_vehicle[SENSOR_DOMAIN] assert len(entity_registry.entities) == len(expected_entities) - # Ensure all entities are enabled - for expected_entity in expected_entities: - if expected_entity.get("default_disabled"): - entity_id = expected_entity["entity_id"] - registry_entry = entity_registry.entities.get(entity_id) - assert registry_entry.disabled - assert registry_entry.disabled_by == "integration" - entity_registry.async_update_entity(entity_id, **{"disabled_by": None}) - with patch( - "homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN] - ), patch_fixtures_with_no_data(hass, config_entry, vehicle_type): - await hass.config_entries.async_reload(config_entry.entry_id) - await hass.async_block_till_done() + _check_and_enable_disabled_entities(entity_registry, expected_entities) + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() - for expected_entity in expected_entities: - entity_id = expected_entity["entity_id"] - registry_entry = entity_registry.entities.get(entity_id) - assert registry_entry is not None - assert registry_entry.unique_id == expected_entity["unique_id"] - state = hass.states.get(entity_id) - assert state.state == STATE_UNKNOWN - for attr in FIXED_ATTRIBUTES: - assert state.attributes.get(attr) == expected_entity.get(attr) - # Check dynamic attributes: - assert state.attributes.get(ATTR_ICON) == get_no_data_icon(expected_entity) + check_entities_no_data(hass, entity_registry, expected_entities, STATE_UNKNOWN) -@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) -async def test_sensor_errors(hass: HomeAssistant, vehicle_type: str): +@pytest.mark.usefixtures("fixtures_with_invalid_upstream_exception") +async def test_sensor_errors( + hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str +): """Test for Renault sensors with temporary failure.""" - entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) - invalid_upstream_exception = exceptions.InvalidUpstreamException( - "err.tech.500", - "Invalid response from the upstream server (The request sent to the GDC is erroneous) ; 502 Bad Gateway", - ) - - with patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): - config_entry = await setup_renault_integration_vehicle_with_side_effect( - hass, vehicle_type, invalid_upstream_exception - ) - await hass.async_block_till_done() + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) @@ -135,52 +108,24 @@ async def test_sensor_errors(hass: HomeAssistant, vehicle_type: str): expected_entities = mock_vehicle[SENSOR_DOMAIN] assert len(entity_registry.entities) == len(expected_entities) - # Ensure all entities are enabled - for expected_entity in expected_entities: - if expected_entity.get("default_disabled"): - entity_id = expected_entity["entity_id"] - registry_entry = entity_registry.entities.get(entity_id) - assert registry_entry.disabled - assert registry_entry.disabled_by == "integration" - entity_registry.async_update_entity(entity_id, **{"disabled_by": None}) - with patch( - "homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN] - ), patch_fixtures_with_side_effect( - hass, config_entry, vehicle_type, invalid_upstream_exception - ): - await hass.config_entries.async_reload(config_entry.entry_id) - await hass.async_block_till_done() + _check_and_enable_disabled_entities(entity_registry, expected_entities) + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() - for expected_entity in expected_entities: - entity_id = expected_entity["entity_id"] - registry_entry = entity_registry.entities.get(entity_id) - assert registry_entry is not None - assert registry_entry.unique_id == expected_entity["unique_id"] - state = hass.states.get(entity_id) - assert state.state == STATE_UNAVAILABLE - for attr in FIXED_ATTRIBUTES: - assert state.attributes.get(attr) == expected_entity.get(attr) - # Check dynamic attributes: - assert state.attributes.get(ATTR_ICON) == get_no_data_icon(expected_entity) + check_entities_unavailable(hass, entity_registry, expected_entities) -async def test_sensor_access_denied(hass: HomeAssistant): +@pytest.mark.usefixtures("fixtures_with_access_denied_exception") +@pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) +async def test_sensor_access_denied( + hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str +): """Test for Renault sensors with access denied failure.""" - entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) - vehicle_type = "zoe_40" - access_denied_exception = exceptions.AccessDeniedException( - "err.func.403", - "Access is denied for this resource", - ) - - with patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): - await setup_renault_integration_vehicle_with_side_effect( - hass, vehicle_type, access_denied_exception - ) - await hass.async_block_till_done() + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) @@ -188,23 +133,17 @@ async def test_sensor_access_denied(hass: HomeAssistant): assert len(entity_registry.entities) == 0 -async def test_sensor_not_supported(hass: HomeAssistant): +@pytest.mark.usefixtures("fixtures_with_not_supported_exception") +@pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) +async def test_sensor_not_supported( + hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str +): """Test for Renault sensors with access denied failure.""" - entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) - vehicle_type = "zoe_40" - not_supported_exception = exceptions.NotSupportedException( - "err.tech.501", - "This feature is not technically supported by this gateway", - ) - - with patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]): - await setup_renault_integration_vehicle_with_side_effect( - hass, vehicle_type, not_supported_exception - ) - await hass.async_block_till_done() + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() mock_vehicle = MOCK_VEHICLES[vehicle_type] check_device_registry(device_registry, mock_vehicle["expected_device"]) diff --git a/tests/components/renault/test_services.py b/tests/components/renault/test_services.py index 37c3d71af61..5a02fd814b9 100644 --- a/tests/components/renault/test_services.py +++ b/tests/components/renault/test_services.py @@ -18,6 +18,7 @@ from homeassistant.components.renault.services import ( SERVICE_CHARGE_START, SERVICES, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_IDENTIFIERS, ATTR_MANUFACTURER, @@ -28,11 +29,24 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from . import setup_renault_integration_simple, setup_renault_integration_vehicle - from tests.common import load_fixture from tests.components.renault.const import MOCK_VEHICLES +pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicles") + + +@pytest.fixture(autouse=True) +def override_platforms(): + """Override PLATFORMS.""" + with patch("homeassistant.components.renault.PLATFORMS", []): + yield + + +@pytest.fixture(autouse=True, name="vehicle_type", params=["zoe_40"]) +def override_vehicle_type(request) -> str: + """Parametrize vehicle type.""" + return request.param + def get_device_id(hass: HomeAssistant) -> str: """Get device_id.""" @@ -42,10 +56,10 @@ def get_device_id(hass: HomeAssistant) -> str: return device.id -async def test_service_registration(hass: HomeAssistant): +async def test_service_registration(hass: HomeAssistant, config_entry: ConfigEntry): """Test entry setup and unload.""" - with patch("homeassistant.components.renault.PLATFORMS", []): - config_entry = await setup_renault_integration_simple(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() # Check that all services are registered. for service in SERVICES: @@ -59,9 +73,10 @@ async def test_service_registration(hass: HomeAssistant): assert not hass.services.has_service(DOMAIN, service) -async def test_service_set_ac_cancel(hass: HomeAssistant): +async def test_service_set_ac_cancel(hass: HomeAssistant, config_entry: ConfigEntry): """Test that service invokes renault_api with correct data.""" - await setup_renault_integration_vehicle(hass, "zoe_40") + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() data = { ATTR_VEHICLE: get_device_id(hass), @@ -82,9 +97,12 @@ async def test_service_set_ac_cancel(hass: HomeAssistant): assert mock_action.mock_calls[0][1] == () -async def test_service_set_ac_start_simple(hass: HomeAssistant): +async def test_service_set_ac_start_simple( + hass: HomeAssistant, config_entry: ConfigEntry +): """Test that service invokes renault_api with correct data.""" - await setup_renault_integration_vehicle(hass, "zoe_40") + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() temperature = 13.5 data = { @@ -107,9 +125,12 @@ async def test_service_set_ac_start_simple(hass: HomeAssistant): assert mock_action.mock_calls[0][1] == (temperature, None) -async def test_service_set_ac_start_with_date(hass: HomeAssistant): +async def test_service_set_ac_start_with_date( + hass: HomeAssistant, config_entry: ConfigEntry +): """Test that service invokes renault_api with correct data.""" - await setup_renault_integration_vehicle(hass, "zoe_40") + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() temperature = 13.5 when = datetime(2025, 8, 23, 17, 12, 45) @@ -134,9 +155,12 @@ async def test_service_set_ac_start_with_date(hass: HomeAssistant): assert mock_action.mock_calls[0][1] == (temperature, when) -async def test_service_set_charge_schedule(hass: HomeAssistant): +async def test_service_set_charge_schedule( + hass: HomeAssistant, config_entry: ConfigEntry +): """Test that service invokes renault_api with correct data.""" - await setup_renault_integration_vehicle(hass, "zoe_40") + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() schedules = {"id": 2} data = { @@ -165,9 +189,12 @@ async def test_service_set_charge_schedule(hass: HomeAssistant): assert mock_action.mock_calls[0][1] == (mock_call_data,) -async def test_service_set_charge_schedule_multi(hass: HomeAssistant): +async def test_service_set_charge_schedule_multi( + hass: HomeAssistant, config_entry: ConfigEntry +): """Test that service invokes renault_api with correct data.""" - await setup_renault_integration_vehicle(hass, "zoe_40") + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() schedules = [ { @@ -209,9 +236,10 @@ async def test_service_set_charge_schedule_multi(hass: HomeAssistant): assert mock_action.mock_calls[0][1] == (mock_call_data,) -async def test_service_set_charge_start(hass: HomeAssistant): +async def test_service_set_charge_start(hass: HomeAssistant, config_entry: ConfigEntry): """Test that service invokes renault_api with correct data.""" - await setup_renault_integration_vehicle(hass, "zoe_40") + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() data = { ATTR_VEHICLE: get_device_id(hass), @@ -232,9 +260,12 @@ async def test_service_set_charge_start(hass: HomeAssistant): assert mock_action.mock_calls[0][1] == () -async def test_service_invalid_device_id(hass: HomeAssistant): +async def test_service_invalid_device_id( + hass: HomeAssistant, config_entry: ConfigEntry +): """Test that service fails with ValueError if device_id not found in registry.""" - await setup_renault_integration_vehicle(hass, "zoe_40") + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() data = {ATTR_VEHICLE: "VF1AAAAA555777999"} @@ -244,9 +275,12 @@ async def test_service_invalid_device_id(hass: HomeAssistant): ) -async def test_service_invalid_device_id2(hass: HomeAssistant): +async def test_service_invalid_device_id2( + hass: HomeAssistant, config_entry: ConfigEntry +): """Test that service fails with ValueError if device_id not found in vehicles.""" - config_entry = await setup_renault_integration_vehicle(hass, "zoe_40") + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() extra_vehicle = MOCK_VEHICLES["captur_phev"]["expected_device"] From 1e98761f30b68f8a720b23b9883314f40e0edb74 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 18 Oct 2021 14:01:23 +0200 Subject: [PATCH 0498/1038] Use assignment expressions 15 (#57961) --- homeassistant/components/emulated_roku/__init__.py | 4 +--- homeassistant/components/ephember/climate.py | 3 +-- homeassistant/components/fjaraskupan/__init__.py | 4 +--- homeassistant/components/google_travel_time/helpers.py | 4 +--- homeassistant/components/graphite/__init__.py | 3 +-- homeassistant/components/harmony/remote.py | 3 +-- homeassistant/components/humidifier/device_trigger.py | 8 ++------ homeassistant/components/humidifier/reproduce_state.py | 4 +--- homeassistant/components/matrix/notify.py | 3 +-- homeassistant/components/onewire/onewirehub.py | 3 +-- homeassistant/components/opengarage/cover.py | 3 +-- homeassistant/components/rachio/webhooks.py | 6 ++---- homeassistant/components/slide/cover.py | 3 +-- homeassistant/components/statsd/__init__.py | 4 +--- homeassistant/components/stream/__init__.py | 3 +-- homeassistant/components/tellduslive/entry.py | 9 +++------ homeassistant/components/velbus/climate.py | 3 +-- homeassistant/components/viaggiatreno/sensor.py | 3 +-- homeassistant/components/vlc_telnet/media_player.py | 6 ++---- homeassistant/components/waze_travel_time/helpers.py | 4 +--- homeassistant/components/xiaomi_miio/config_flow.py | 3 +-- homeassistant/components/xiaomi_miio/fan.py | 3 +-- homeassistant/components/xiaomi_miio/light.py | 3 +-- homeassistant/components/xiaomi_miio/switch.py | 3 +-- 24 files changed, 29 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/emulated_roku/__init__.py b/homeassistant/components/emulated_roku/__init__.py index 45c9355603f..32e08342191 100644 --- a/homeassistant/components/emulated_roku/__init__.py +++ b/homeassistant/components/emulated_roku/__init__.py @@ -46,9 +46,7 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the emulated roku component.""" - conf = config.get(DOMAIN) - - if conf is None: + if (conf := config.get(DOMAIN)) is None: return True existing_servers = configured_servers(hass) diff --git a/homeassistant/components/ephember/climate.py b/homeassistant/components/ephember/climate.py index 787677a6605..022c91c96f3 100644 --- a/homeassistant/components/ephember/climate.py +++ b/homeassistant/components/ephember/climate.py @@ -161,8 +161,7 @@ class EphEmberThermostat(ClimateEntity): def set_temperature(self, **kwargs): """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return if self._hot_water: diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index ac22e788a6e..40113744977 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -64,9 +64,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "Detection: %s %s - %s", ble_device.name, ble_device, advertisement_data ) - data = state.devices.get(ble_device.address) - - if data: + if data := state.devices.get(ble_device.address): data.device.detection_callback(ble_device, advertisement_data) data.coordinator.async_set_updated_data(data.device.state) else: diff --git a/homeassistant/components/google_travel_time/helpers.py b/homeassistant/components/google_travel_time/helpers.py index cf5f6e8b0af..00d3119e868 100644 --- a/homeassistant/components/google_travel_time/helpers.py +++ b/homeassistant/components/google_travel_time/helpers.py @@ -30,9 +30,7 @@ def resolve_location(hass, logger, loc): def get_location_from_entity(hass, logger, entity_id): """Get the location from the entity state or attributes.""" - entity = hass.states.get(entity_id) - - if entity is None: + if (entity := hass.states.get(entity_id)) is None: logger.error("Unable to find entity %s", entity_id) return None diff --git a/homeassistant/components/graphite/__init__.py b/homeassistant/components/graphite/__init__.py index 9405b576b4d..b63e461e76a 100644 --- a/homeassistant/components/graphite/__init__.py +++ b/homeassistant/components/graphite/__init__.py @@ -149,8 +149,7 @@ class GraphiteFeeder(threading.Thread): def run(self): """Run the process to export the data.""" while True: - event = self._queue.get() - if event == self._quit_object: + if (event := self._queue.get()) == self._quit_object: _LOGGER.debug("Event processing thread stopped") self._queue.task_done() return diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index 6a13093ec25..3431eff7994 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -227,8 +227,7 @@ class HarmonyRemote(HarmonyEntity, remote.RemoteEntity, RestoreEntity): async def async_send_command(self, command, **kwargs): """Send a list of commands to one device.""" _LOGGER.debug("%s: Send Command", self.name) - device = kwargs.get(ATTR_DEVICE) - if device is None: + if (device := kwargs.get(ATTR_DEVICE)) is None: _LOGGER.error("%s: Missing required argument: device", self.name) return diff --git a/homeassistant/components/humidifier/device_trigger.py b/homeassistant/components/humidifier/device_trigger.py index a049af9afec..9c6ca5cea55 100644 --- a/homeassistant/components/humidifier/device_trigger.py +++ b/homeassistant/components/humidifier/device_trigger.py @@ -86,9 +86,7 @@ async def async_attach_trigger( automation_info: AutomationTriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" - trigger_type = config[CONF_TYPE] - - if trigger_type == "target_humidity_changed": + if config[CONF_TYPE] == "target_humidity_changed": numeric_state_config = { numeric_state_trigger.CONF_PLATFORM: "numeric_state", numeric_state_trigger.CONF_ENTITY_ID: config[CONF_ENTITY_ID], @@ -118,9 +116,7 @@ async def async_get_trigger_capabilities( hass: HomeAssistant, config: ConfigType ) -> dict[str, vol.Schema]: """List trigger capabilities.""" - trigger_type = config[CONF_TYPE] - - if trigger_type == "target_humidity_changed": + if config[CONF_TYPE] == "target_humidity_changed": return { "extra_fields": vol.Schema( { diff --git a/homeassistant/components/humidifier/reproduce_state.py b/homeassistant/components/humidifier/reproduce_state.py index 1303dee4518..e6d4fddafbc 100644 --- a/homeassistant/components/humidifier/reproduce_state.py +++ b/homeassistant/components/humidifier/reproduce_state.py @@ -28,9 +28,7 @@ async def _async_reproduce_states( reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce component states.""" - cur_state = hass.states.get(state.entity_id) - - if cur_state is None: + if (cur_state := hass.states.get(state.entity_id)) is None: _LOGGER.warning("Unable to find entity %s", state.entity_id) return diff --git a/homeassistant/components/matrix/notify.py b/homeassistant/components/matrix/notify.py index 8643d7511bc..197d84352df 100644 --- a/homeassistant/components/matrix/notify.py +++ b/homeassistant/components/matrix/notify.py @@ -33,8 +33,7 @@ class MatrixNotificationService(BaseNotificationService): """Send the message to the Matrix server.""" target_rooms = kwargs.get(ATTR_TARGET) or [self._default_room] service_data = {ATTR_TARGET: target_rooms, ATTR_MESSAGE: message} - data = kwargs.get(ATTR_DATA) - if data is not None: + if (data := kwargs.get(ATTR_DATA)) is not None: service_data[ATTR_DATA] = data return self.hass.services.call( DOMAIN, SERVICE_SEND_MESSAGE, service_data=service_data diff --git a/homeassistant/components/onewire/onewirehub.py b/homeassistant/components/onewire/onewirehub.py index 26d08594055..d3b6773e74e 100644 --- a/homeassistant/components/onewire/onewirehub.py +++ b/homeassistant/components/onewire/onewirehub.py @@ -79,8 +79,7 @@ class OneWireHub: for device_path in self.owproxy.dir(path): device_family = self.owproxy.read(f"{device_path}family").decode() device_type = self.owproxy.read(f"{device_path}type").decode() - device_branches = DEVICE_COUPLERS.get(device_family) - if device_branches: + if device_branches := DEVICE_COUPLERS.get(device_family): for branch in device_branches: devices += self._discover_devices_owserver(f"{device_path}{branch}") else: diff --git a/homeassistant/components/opengarage/cover.py b/homeassistant/components/opengarage/cover.py index b80e1c82079..6952e4bff24 100644 --- a/homeassistant/components/opengarage/cover.py +++ b/homeassistant/components/opengarage/cover.py @@ -138,8 +138,7 @@ class OpenGarageCover(CoordinatorEntity, CoverEntity): @callback def _update_attr(self) -> None: """Update the state and attributes.""" - status = self.coordinator.data - if status is None: + if (status := self.coordinator.data) is None: _LOGGER.error("Unable to connect to OpenGarage device") self._attr_available = False return diff --git a/homeassistant/components/rachio/webhooks.py b/homeassistant/components/rachio/webhooks.py index 94c79a1504f..b0ac1a86d4a 100644 --- a/homeassistant/components/rachio/webhooks.py +++ b/homeassistant/components/rachio/webhooks.py @@ -109,15 +109,13 @@ async def async_get_or_create_registered_webhook_id_and_url(hass, entry): updated_config = False webhook_url = None - webhook_id = config.get(CONF_WEBHOOK_ID) - if not webhook_id: + if not (webhook_id := config.get(CONF_WEBHOOK_ID)): webhook_id = hass.components.webhook.async_generate_id() config[CONF_WEBHOOK_ID] = webhook_id updated_config = True if hass.components.cloud.async_active_subscription(): - cloudhook_url = config.get(CONF_CLOUDHOOK_URL) - if not cloudhook_url: + if not (cloudhook_url := config.get(CONF_CLOUDHOOK_URL)): cloudhook_url = await hass.components.cloud.async_create_cloudhook( webhook_id ) diff --git a/homeassistant/components/slide/cover.py b/homeassistant/components/slide/cover.py index 9e925af3391..470cde8ac94 100644 --- a/homeassistant/components/slide/cover.py +++ b/homeassistant/components/slide/cover.py @@ -94,8 +94,7 @@ class SlideCover(CoverEntity): @property def current_cover_position(self): """Return the current position of cover shutter.""" - pos = self._slide["pos"] - if pos is not None: + if (pos := self._slide["pos"]) is not None: if (1 - pos) <= DEFAULT_OFFSET or pos <= DEFAULT_OFFSET: pos = round(pos) if not self._invert: diff --git a/homeassistant/components/statsd/__init__.py b/homeassistant/components/statsd/__init__.py index b4657838683..9830279a5e2 100644 --- a/homeassistant/components/statsd/__init__.py +++ b/homeassistant/components/statsd/__init__.py @@ -54,9 +54,7 @@ def setup(hass, config): def statsd_event_listener(event): """Listen for new messages on the bus and sends them to StatsD.""" - state = event.data.get("new_state") - - if state is None: + if (state := event.data.get("new_state")) is None: return try: diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index d06d60aa48b..cef70a2e809 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -348,8 +348,7 @@ class Stream: raise HomeAssistantError(f"Can't write {video_path}, no access to path!") # Add recorder - recorder = self.outputs().get(RECORDER_PROVIDER) - if recorder: + if recorder := self.outputs().get(RECORDER_PROVIDER): assert isinstance(recorder, RecorderOutput) raise HomeAssistantError( f"Stream already recording to {recorder.video_path}!" diff --git a/homeassistant/components/tellduslive/entry.py b/homeassistant/components/tellduslive/entry.py index 67a59fc8dab..edb4537aac3 100644 --- a/homeassistant/components/tellduslive/entry.py +++ b/homeassistant/components/tellduslive/entry.py @@ -123,13 +123,10 @@ class TelldusLiveEntity(Entity): "identifiers": {("tellduslive", self.device.device_id)}, "name": self.device.name, } - model = device.get("model") - if model is not None: + if (model := device.get("model")) is not None: device_info["model"] = model.title() - protocol = device.get("protocol") - if protocol is not None: + if (protocol := device.get("protocol")) is not None: device_info["manufacturer"] = protocol.title() - client = device.get("client") - if client is not None: + if (client := device.get("client")) is not None: device_info["via_device"] = ("tellduslive", client) return device_info diff --git a/homeassistant/components/velbus/climate.py b/homeassistant/components/velbus/climate.py index cdb049266b5..318dff463f0 100644 --- a/homeassistant/components/velbus/climate.py +++ b/homeassistant/components/velbus/climate.py @@ -61,8 +61,7 @@ class VelbusClimate(VelbusEntity, ClimateEntity): def set_temperature(self, **kwargs): """Set new target temperatures.""" - temp = kwargs.get(ATTR_TEMPERATURE) - if temp is None: + if (temp := kwargs.get(ATTR_TEMPERATURE)) is None: return self._channel.set_temp(temp) self.schedule_update_ha_state() diff --git a/homeassistant/components/viaggiatreno/sensor.py b/homeassistant/components/viaggiatreno/sensor.py index dc20ea3edbc..ee02eaa63e1 100644 --- a/homeassistant/components/viaggiatreno/sensor.py +++ b/homeassistant/components/viaggiatreno/sensor.py @@ -60,8 +60,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= """Set up the ViaggiaTreno platform.""" train_id = config.get(CONF_TRAIN_ID) station_id = config.get(CONF_STATION_ID) - name = config.get(CONF_NAME) - if not name: + if not (name := config.get(CONF_NAME)): name = DEFAULT_NAME.format(train_id) async_add_entities([ViaggiaTrenoSensor(train_id, station_id, name)]) diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index 75b55b5f77b..ad88cfc2627 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -179,8 +179,7 @@ class VlcDevice(MediaPlayerEntity): if not self._media_title: # Fall back to filename. - data_info = data.get("data") - if data_info: + if data_info := data.get("data"): self._media_title = data_info["filename"] except CommandError as err: @@ -282,8 +281,7 @@ class VlcDevice(MediaPlayerEntity): async def async_media_pause(self) -> None: """Send pause command.""" status = await self._vlc.status() - current_state = status.state - if current_state != "paused": + if status.state != "paused": # Make sure we're not already paused since VLCTelnet.pause() toggles # pause. await self._vlc.pause() diff --git a/homeassistant/components/waze_travel_time/helpers.py b/homeassistant/components/waze_travel_time/helpers.py index 326a6018c96..26e529b8e93 100644 --- a/homeassistant/components/waze_travel_time/helpers.py +++ b/homeassistant/components/waze_travel_time/helpers.py @@ -29,9 +29,7 @@ def resolve_location(hass, logger, loc): def get_location_from_entity(hass, logger, entity_id): """Get the location from the entity_id.""" - state = hass.states.get(entity_id) - - if state is None: + if (state := hass.states.get(entity_id)) is None: logger.error("Unable to find entity %s", entity_id) return None diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index 5256b37ccda..96a06f6a33d 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -256,8 +256,7 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.cloud_devices = {} for device in devices_raw: - parent_id = device.get("parent_id") - if not parent_id: + if not device.get("parent_id"): name = device["name"] model = device["model"] list_name = f"{name} - {model}" diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 04cdc4573db..01304008b76 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -229,8 +229,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): params = { key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID } - entity_ids = service.data.get(ATTR_ENTITY_ID) - if entity_ids: + if entity_ids := service.data.get(ATTR_ENTITY_ID): filtered_entities = [ entity for entity in hass.data[DATA_KEY].values() diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index b916de899b9..04597eadf81 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -194,8 +194,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for key, value in service.data.items() if key != ATTR_ENTITY_ID } - entity_ids = service.data.get(ATTR_ENTITY_ID) - if entity_ids: + if entity_ids := service.data.get(ATTR_ENTITY_ID): target_devices = [ dev for dev in hass.data[DATA_KEY].values() diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 6f68e5652db..5c29253ae73 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -414,8 +414,7 @@ async def async_setup_other_entry(hass, config_entry, async_add_entities): for key, value in service.data.items() if key != ATTR_ENTITY_ID } - entity_ids = service.data.get(ATTR_ENTITY_ID) - if entity_ids: + if entity_ids := service.data.get(ATTR_ENTITY_ID): devices = [ device for device in hass.data[DATA_KEY].values() From 786e1f9b6a18012318a22c721d0f4b5657593159 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 18 Oct 2021 14:53:47 +0200 Subject: [PATCH 0499/1038] Add configuration url to Tasmota (#57957) --- homeassistant/components/tasmota/__init__.py | 4 +++- tests/components/tasmota/test_discovery.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tasmota/__init__.py b/homeassistant/components/tasmota/__init__.py index 5599f887f8f..b863cda796d 100644 --- a/homeassistant/components/tasmota/__init__.py +++ b/homeassistant/components/tasmota/__init__.py @@ -5,6 +5,7 @@ import asyncio import logging from hatasmota.const import ( + CONF_IP, CONF_MAC, CONF_MANUFACTURER, CONF_MODEL, @@ -165,12 +166,13 @@ def _update_device( """Add or update device registry.""" _LOGGER.debug("Adding or updating tasmota device %s", config[CONF_MAC]) device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + configuration_url=f"http://{config[CONF_IP]}/", connections={(CONNECTION_NETWORK_MAC, config[CONF_MAC])}, manufacturer=config[CONF_MANUFACTURER], model=config[CONF_MODEL], name=config[CONF_NAME], sw_version=config[CONF_SW_VERSION], - config_entry_id=config_entry.entry_id, ) diff --git a/tests/components/tasmota/test_discovery.py b/tests/components/tasmota/test_discovery.py index 3e1175faa0f..713d0f5ae67 100644 --- a/tests/components/tasmota/test_discovery.py +++ b/tests/components/tasmota/test_discovery.py @@ -150,6 +150,7 @@ async def test_device_discover( set(), {(dr.CONNECTION_NETWORK_MAC, mac)} ) assert device_entry is not None + assert device_entry.configuration_url == f"http://{config['ip']}/" assert device_entry.manufacturer == "Tasmota" assert device_entry.model == config["md"] assert device_entry.name == config["dn"] From ff853b2d53782fb0a22dde2d2dba3f673bd96cd4 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 18 Oct 2021 15:54:38 +0200 Subject: [PATCH 0500/1038] Use assignment expressions 19 (#57968) --- homeassistant/components/acer_projector/switch.py | 3 +-- homeassistant/components/arlo/sensor.py | 3 +-- homeassistant/components/coolmaster/climate.py | 3 +-- homeassistant/components/elkm1/__init__.py | 3 +-- homeassistant/components/elkm1/alarm_control_panel.py | 3 +-- homeassistant/components/elkm1/config_flow.py | 3 +-- homeassistant/components/emoncms/sensor.py | 3 +-- homeassistant/components/hassio/http.py | 3 +-- homeassistant/components/hassio/ingress.py | 3 +-- homeassistant/components/input_number/reproduce_state.py | 4 +--- homeassistant/components/message_bird/notify.py | 3 +-- homeassistant/components/mysensors/__init__.py | 3 +-- homeassistant/components/mysensors/handler.py | 3 +-- homeassistant/components/nx584/binary_sensor.py | 3 +-- homeassistant/components/pyload/sensor.py | 3 +-- homeassistant/components/radarr/sensor.py | 3 +-- homeassistant/components/select/reproduce_state.py | 4 +--- homeassistant/components/sighthound/image_processing.py | 3 +-- homeassistant/components/tradfri/__init__.py | 4 +--- homeassistant/components/tradfri/light.py | 6 ++---- homeassistant/components/vicare/climate.py | 3 +-- homeassistant/components/vicare/water_heater.py | 3 +-- homeassistant/components/wallbox/__init__.py | 3 +-- 23 files changed, 24 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/acer_projector/switch.py b/homeassistant/components/acer_projector/switch.py index 947b774b7bd..747c7c98d73 100644 --- a/homeassistant/components/acer_projector/switch.py +++ b/homeassistant/components/acer_projector/switch.py @@ -129,8 +129,7 @@ class AcerSwitch(SwitchEntity): self._attr_available = False for key in self._attributes: - msg = CMD_DICT.get(key) - if msg: + if msg := CMD_DICT.get(key): awns = self._write_read_format(msg) self._attributes[key] = awns self._attr_extra_state_attributes = self._attributes diff --git a/homeassistant/components/arlo/sensor.py b/homeassistant/components/arlo/sensor.py index 0cbb7c95f65..7fbc57f9c6b 100644 --- a/homeassistant/components/arlo/sensor.py +++ b/homeassistant/components/arlo/sensor.py @@ -89,8 +89,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up an Arlo IP sensor.""" - arlo = hass.data.get(DATA_ARLO) - if not arlo: + if not (arlo := hass.data.get(DATA_ARLO)): return sensors = [] diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py index 0b717697a1a..ceab74b2139 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -142,8 +142,7 @@ class CoolmasterClimate(CoordinatorEntity, ClimateEntity): async def async_set_temperature(self, **kwargs): """Set new target temperatures.""" - temp = kwargs.get(ATTR_TEMPERATURE) - if temp is not None: + 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() diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 0eb34b2fc61..0b4fa26e833 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -236,8 +236,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: elk.connect() def _element_changed(element, changeset): - keypress = changeset.get("last_keypress") - if keypress is None: + if (keypress := changeset.get("last_keypress")) is None: return hass.bus.async_fire( diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py index c3ed6bbc40d..5b3a20b3448 100644 --- a/homeassistant/components/elkm1/alarm_control_panel.py +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -141,8 +141,7 @@ class ElkArea(ElkAttachedEntity, AlarmControlPanelEntity, RestoreEntity): self.async_write_ha_state() def _watch_area(self, area, changeset): - last_log = changeset.get("last_log") - if not last_log: + if not (last_log := changeset.get("last_log")): return # user_number only set for arm/disarm logs if not last_log.get("user_number"): diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py index f8cfdbe9851..905aa35ad19 100644 --- a/homeassistant/components/elkm1/config_flow.py +++ b/homeassistant/components/elkm1/config_flow.py @@ -79,8 +79,7 @@ async def validate_input(data): def _make_url_from_data(data): - host = data.get(CONF_HOST) - if host: + if host := data.get(CONF_HOST): return host protocol = PROTOCOL_MAP[data[CONF_PROTOCOL]] diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index 033f7878b5e..125e5e6c333 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -109,8 +109,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if sensor_names is not None: name = sensor_names.get(int(elem["id"]), None) - unit = elem.get("unit") - if unit: + if unit := elem.get("unit"): unit_of_measurement = unit else: unit_of_measurement = config_unit diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 2012725c7f4..532b947ac49 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -129,8 +129,7 @@ def _init_header(request: web.Request) -> dict[str, str]: } # Add user data - user = request.get("hass_user") - if user is not None: + if request.get("hass_user") is not None: headers[X_HASS_USER_ID] = request["hass_user"].id headers[X_HASS_IS_ADMIN] = str(int(request["hass_user"].is_admin)) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index e58c2d790f2..620c69f543d 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -197,8 +197,7 @@ def _init_header(request: web.Request, token: str) -> CIMultiDict | dict[str, st headers[hdrs.X_FORWARDED_FOR] = forward_for # Set X-Forwarded-Host - forward_host = request.headers.get(hdrs.X_FORWARDED_HOST) - if not forward_host: + if not (forward_host := request.headers.get(hdrs.X_FORWARDED_HOST)): forward_host = request.host headers[hdrs.X_FORWARDED_HOST] = forward_host diff --git a/homeassistant/components/input_number/reproduce_state.py b/homeassistant/components/input_number/reproduce_state.py index c198236789c..368f68b5178 100644 --- a/homeassistant/components/input_number/reproduce_state.py +++ b/homeassistant/components/input_number/reproduce_state.py @@ -24,9 +24,7 @@ async def _async_reproduce_state( reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" - cur_state = hass.states.get(state.entity_id) - - if cur_state is None: + if (cur_state := hass.states.get(state.entity_id)) is None: _LOGGER.warning("Unable to find entity %s", state.entity_id) return diff --git a/homeassistant/components/message_bird/notify.py b/homeassistant/components/message_bird/notify.py index ce1d275a832..9a542836011 100644 --- a/homeassistant/components/message_bird/notify.py +++ b/homeassistant/components/message_bird/notify.py @@ -48,8 +48,7 @@ class MessageBirdNotificationService(BaseNotificationService): def send_message(self, message=None, **kwargs): """Send a message to a specified target.""" - targets = kwargs.get(ATTR_TARGET) - if not targets: + if not (targets := kwargs.get(ATTR_TARGET)): _LOGGER.error("No target specified") return diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index b6ad78f5dc8..6c2d2f710e6 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -62,8 +62,7 @@ DEFAULT_VERSION = "1.4" def set_default_persistence_file(value: dict) -> dict: """Set default persistence file.""" for idx, gateway in enumerate(value): - fil = gateway.get(CONF_PERSISTENCE_FILE) - if fil is not None: + if gateway.get(CONF_PERSISTENCE_FILE) is not None: continue new_name = f"mysensors{idx + 1}.pickle" gateway[CONF_PERSISTENCE_FILE] = new_name diff --git a/homeassistant/components/mysensors/handler.py b/homeassistant/components/mysensors/handler.py index 0fb86fd0eec..98242615f67 100644 --- a/homeassistant/components/mysensors/handler.py +++ b/homeassistant/components/mysensors/handler.py @@ -27,8 +27,7 @@ async def handle_internal( ) -> None: """Handle a mysensors internal message.""" internal = msg.gateway.const.Internal(msg.sub_type) - handler = HANDLERS.get(internal.name) - if handler is None: + if (handler := HANDLERS.get(internal.name)) is None: return await handler(hass, gateway_id, msg) diff --git a/homeassistant/components/nx584/binary_sensor.py b/homeassistant/components/nx584/binary_sensor.py index 058ac6c5795..7a999263332 100644 --- a/homeassistant/components/nx584/binary_sensor.py +++ b/homeassistant/components/nx584/binary_sensor.py @@ -122,9 +122,8 @@ class NX584Watcher(threading.Thread): def _process_zone_event(self, event): zone = event["zone"] - zone_sensor = self._zone_sensors.get(zone) # pylint: disable=protected-access - if not zone_sensor: + if not (zone_sensor := self._zone_sensors.get(zone)): return zone_sensor._zone["state"] = event["zone_state"] zone_sensor.schedule_update_ha_state() diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index f568b41776f..b10abd282b4 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -116,8 +116,7 @@ class PyLoadSensor(SensorEntity): ) return - value = self.api.status.get(self.type) - if value is None: + if (value := self.api.status.get(self.type)) is None: _LOGGER.warning("Unable to locate value for %s", self.type) return diff --git a/homeassistant/components/radarr/sensor.py b/homeassistant/components/radarr/sensor.py index fc4fff6c274..8364dd670e4 100644 --- a/homeassistant/components/radarr/sensor.py +++ b/homeassistant/components/radarr/sensor.py @@ -248,8 +248,7 @@ def get_date(zone, offset=0): def get_release_date(data): """Get release date.""" - date = data.get("physicalRelease") - if not date: + if not (date := data.get("physicalRelease")): date = data.get("inCinemas") return date diff --git a/homeassistant/components/select/reproduce_state.py b/homeassistant/components/select/reproduce_state.py index 8af4b94fd6f..d41fd5dae46 100644 --- a/homeassistant/components/select/reproduce_state.py +++ b/homeassistant/components/select/reproduce_state.py @@ -23,9 +23,7 @@ async def _async_reproduce_state( reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" - cur_state = hass.states.get(state.entity_id) - - if cur_state is None: + if (cur_state := hass.states.get(state.entity_id)) is None: _LOGGER.warning("Unable to find entity %s", state.entity_id) return diff --git a/homeassistant/components/sighthound/image_processing.py b/homeassistant/components/sighthound/image_processing.py index e31b30f1174..77d9e33b043 100644 --- a/homeassistant/components/sighthound/image_processing.py +++ b/homeassistant/components/sighthound/image_processing.py @@ -55,8 +55,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.error("Sighthound error %s setup aborted", exc) return - save_file_folder = config.get(CONF_SAVE_FILE_FOLDER) - if save_file_folder: + if save_file_folder := config.get(CONF_SAVE_FILE_FOLDER): save_file_folder = Path(save_file_folder) entities = [] diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index 4f5997b2fa1..3988775ad2b 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -58,9 +58,7 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Tradfri component.""" - conf = config.get(DOMAIN) - - if conf is None: + if (conf := config.get(DOMAIN)) is None: return True configured_hosts = [ diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index b3f73cebc82..53309e144ff 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -53,10 +53,8 @@ async def async_setup_entry( if lights: async_add_entities(TradfriLight(light, api, gateway_id) for light in lights) - if config_entry.data[CONF_IMPORT_GROUPS]: - groups = tradfri_data[GROUPS] - if groups: - async_add_entities(TradfriGroup(group, api, gateway_id) for group in groups) + if config_entry.data[CONF_IMPORT_GROUPS] and (groups := tradfri_data[GROUPS]): + async_add_entities(TradfriGroup(group, api, gateway_id) for group in groups) class TradfriGroup(TradfriBaseClass, LightEntity): diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index 2822d048152..a6aa757e124 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -262,8 +262,7 @@ class ViCareClimate(ClimateEntity): def set_temperature(self, **kwargs): """Set new target temperatures.""" - temp = kwargs.get(ATTR_TEMPERATURE) - if temp is not None: + if (temp := kwargs.get(ATTR_TEMPERATURE)) is not None: self._api.setProgramTemperature(self._current_program, temp) self._target_temperature = temp diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index af373c6ee6e..557d5257427 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -124,8 +124,7 @@ class ViCareWater(WaterHeaterEntity): def set_temperature(self, **kwargs): """Set new target temperatures.""" - temp = kwargs.get(ATTR_TEMPERATURE) - if temp is not None: + if (temp := kwargs.get(ATTR_TEMPERATURE)) is not None: self._api.setDomesticHotWaterTemperature(temp) self._target_temperature = temp diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py index 9f07329a1dd..02c1cec668e 100644 --- a/homeassistant/components/wallbox/__init__.py +++ b/homeassistant/components/wallbox/__init__.py @@ -58,8 +58,7 @@ class WallboxHub: filtered_data = {k: data[k] for k in CONF_SENSOR_TYPES if k in data} for key, value in filtered_data.items(): - sensor_round = CONF_SENSOR_TYPES[key][CONF_ROUND] - if sensor_round: + if sensor_round := CONF_SENSOR_TYPES[key][CONF_ROUND]: try: filtered_data[key] = round(value, sensor_round) except TypeError: From f149bef9f3033bd668502e3f00d019b11a7d0462 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 18 Oct 2021 18:36:35 +0200 Subject: [PATCH 0501/1038] Use assignment expressions 18 (#57967) --- homeassistant/components/cast/__init__.py | 4 +-- homeassistant/components/cast/media_player.py | 10 +++----- .../components/conversation/__init__.py | 3 +-- .../components/conversation/default_agent.py | 4 +-- homeassistant/components/insteon/api/aldb.py | 25 ++++++------------- .../components/insteon/api/device.py | 6 ++--- .../components/insteon/api/properties.py | 15 ++++------- homeassistant/components/insteon/schemas.py | 3 +-- .../components/openuv/binary_sensor.py | 4 +-- homeassistant/components/openuv/sensor.py | 4 +-- homeassistant/components/sms/__init__.py | 3 +-- homeassistant/components/sql/sensor.py | 3 +-- .../components/webostv/media_player.py | 6 ++--- homeassistant/components/wilight/fan.py | 3 +-- 14 files changed, 29 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index 43b6b77ebd2..9ccac6e4f6c 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -18,9 +18,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup(hass, config): """Set up the Cast component.""" - conf = config.get(DOMAIN) - - if conf is not None: + if (conf := config.get(DOMAIN)) is not None: media_player_config_validated = [] media_player_config = conf.get("media_player", {}) if not isinstance(media_player_config, list): diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 74c90f43372..57983808cbd 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -565,9 +565,7 @@ class CastDevice(MediaPlayerEntity): @property def state(self): """Return the state of the player.""" - media_status = self._media_status()[0] - - if media_status is None: + if (media_status := self._media_status()[0]) is None: return None if media_status.player_is_playing: return STATE_PLAYING @@ -588,8 +586,7 @@ class CastDevice(MediaPlayerEntity): @property def media_content_type(self): """Content type of current playing media.""" - media_status = self._media_status()[0] - if media_status is None: + if (media_status := self._media_status()[0]) is None: return None if media_status.media_is_tvshow: return MEDIA_TYPE_TVSHOW @@ -608,8 +605,7 @@ class CastDevice(MediaPlayerEntity): @property def media_image_url(self): """Image url of current playing media.""" - media_status = self._media_status()[0] - if media_status is None: + if (media_status := self._media_status()[0]) is None: return None images = media_status.images diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index 4d3297d8c65..401d240957e 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -154,8 +154,7 @@ class ConversationProcessView(http.HomeAssistantView): async def _get_agent(hass: core.HomeAssistant) -> AbstractConversationAgent: """Get the active conversation agent.""" - agent = hass.data.get(DATA_AGENT) - if agent is None: + if (agent := hass.data.get(DATA_AGENT)) is None: agent = hass.data[DATA_AGENT] = DefaultAgent(hass) await agent.async_initialize(hass.data.get(DATA_CONFIG)) return agent diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 1773ca46cb5..d957eb8e0b2 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -66,9 +66,7 @@ class DefaultAgent(AbstractConversationAgent): intents = self.hass.data.setdefault(DOMAIN, {}) for intent_type, utterances in config.get("intents", {}).items(): - conf = intents.get(intent_type) - - if conf is None: + if (conf := intents.get(intent_type)) is None: conf = intents[intent_type] = [] conf.extend(create_matcher(utterance) for utterance in utterances) diff --git a/homeassistant/components/insteon/api/aldb.py b/homeassistant/components/insteon/api/aldb.py index 881cb0bb8c7..a3132570ccc 100644 --- a/homeassistant/components/insteon/api/aldb.py +++ b/homeassistant/components/insteon/api/aldb.py @@ -73,8 +73,7 @@ async def websocket_get_aldb( msg: dict, ) -> None: """Get the All-Link Database for an Insteon device.""" - device = devices[msg[DEVICE_ADDRESS]] - if not device: + if not (device := devices[msg[DEVICE_ADDRESS]]): notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) return @@ -110,8 +109,7 @@ async def websocket_change_aldb_record( msg: dict, ) -> None: """Change an All-Link Database record for an Insteon device.""" - device = devices[msg[DEVICE_ADDRESS]] - if not device: + if not (device := devices[msg[DEVICE_ADDRESS]]): notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) return @@ -144,8 +142,7 @@ async def websocket_create_aldb_record( msg: dict, ) -> None: """Create an All-Link Database record for an Insteon device.""" - device = devices[msg[DEVICE_ADDRESS]] - if not device: + if not (device := devices[msg[DEVICE_ADDRESS]]): notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) return @@ -175,8 +172,7 @@ async def websocket_write_aldb( msg: dict, ) -> None: """Create an All-Link Database record for an Insteon device.""" - device = devices[msg[DEVICE_ADDRESS]] - if not device: + if not (device := devices[msg[DEVICE_ADDRESS]]): notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) return @@ -199,8 +195,7 @@ async def websocket_load_aldb( msg: dict, ) -> None: """Create an All-Link Database record for an Insteon device.""" - device = devices[msg[DEVICE_ADDRESS]] - if not device: + if not (device := devices[msg[DEVICE_ADDRESS]]): notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) return @@ -222,8 +217,7 @@ async def websocket_reset_aldb( msg: dict, ) -> None: """Create an All-Link Database record for an Insteon device.""" - device = devices[msg[DEVICE_ADDRESS]] - if not device: + if not (device := devices[msg[DEVICE_ADDRESS]]): notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) return @@ -245,8 +239,7 @@ async def websocket_add_default_links( msg: dict, ) -> None: """Add the default All-Link Database records for an Insteon device.""" - device = devices[msg[DEVICE_ADDRESS]] - if not device: + if not (device := devices[msg[DEVICE_ADDRESS]]): notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) return @@ -270,9 +263,7 @@ async def websocket_notify_on_aldb_status( msg: dict, ) -> None: """Tell Insteon a new ALDB record was added.""" - - device = devices[msg[DEVICE_ADDRESS]] - if not device: + if not (device := devices[msg[DEVICE_ADDRESS]]): notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) return diff --git a/homeassistant/components/insteon/api/device.py b/homeassistant/components/insteon/api/device.py index 9d77e8b765c..6815ec43031 100644 --- a/homeassistant/components/insteon/api/device.py +++ b/homeassistant/components/insteon/api/device.py @@ -35,8 +35,7 @@ async def async_device_name(dev_registry, address): identifiers={(DOMAIN, str(address))}, connections=set() ) if not ha_device: - device = devices[address] - if device: + if device := devices[address]: return f"{device.description} ({device.model})" return "" return compute_device_name(ha_device) @@ -61,8 +60,7 @@ async def websocket_get_device( ) -> None: """Get an Insteon device.""" dev_registry = await hass.helpers.device_registry.async_get_registry() - ha_device = dev_registry.async_get(msg[DEVICE_ID]) - if not ha_device: + if not (ha_device := dev_registry.async_get(msg[DEVICE_ID])): notify_device_not_found(connection, msg, HA_DEVICE_NOT_FOUND) return device = get_insteon_device_from_ha_device(ha_device) diff --git a/homeassistant/components/insteon/api/properties.py b/homeassistant/components/insteon/api/properties.py index 0b3b643b617..6ec12a5fd89 100644 --- a/homeassistant/components/insteon/api/properties.py +++ b/homeassistant/components/insteon/api/properties.py @@ -295,8 +295,7 @@ async def websocket_get_properties( msg: dict, ) -> None: """Add the default All-Link Database records for an Insteon device.""" - device = devices[msg[DEVICE_ADDRESS]] - if not device: + if not (device := devices[msg[DEVICE_ADDRESS]]): notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) return @@ -321,8 +320,7 @@ async def websocket_change_properties_record( msg: dict, ) -> None: """Add the default All-Link Database records for an Insteon device.""" - device = devices[msg[DEVICE_ADDRESS]] - if not device: + if not (device := devices[msg[DEVICE_ADDRESS]]): notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) return @@ -344,8 +342,7 @@ async def websocket_write_properties( msg: dict, ) -> None: """Add the default All-Link Database records for an Insteon device.""" - device = devices[msg[DEVICE_ADDRESS]] - if not device: + if not (device := devices[msg[DEVICE_ADDRESS]]): notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) return @@ -376,8 +373,7 @@ async def websocket_load_properties( msg: dict, ) -> None: """Add the default All-Link Database records for an Insteon device.""" - device = devices[msg[DEVICE_ADDRESS]] - if not device: + if not (device := devices[msg[DEVICE_ADDRESS]]): notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) return @@ -408,8 +404,7 @@ async def websocket_reset_properties( msg: dict, ) -> None: """Add the default All-Link Database records for an Insteon device.""" - device = devices[msg[DEVICE_ADDRESS]] - if not device: + if not (device := devices[msg[DEVICE_ADDRESS]]): notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) return diff --git a/homeassistant/components/insteon/schemas.py b/homeassistant/components/insteon/schemas.py index 626dc7dde4b..09315919052 100644 --- a/homeassistant/components/insteon/schemas.py +++ b/homeassistant/components/insteon/schemas.py @@ -57,8 +57,7 @@ def set_default_port(schema: dict) -> dict: """Set the default port based on the Hub version.""" # If the ip_port is found do nothing # If it is not found the set the default - ip_port = schema.get(CONF_IP_PORT) - if not ip_port: + if not schema.get(CONF_IP_PORT): hub_version = schema.get(CONF_HUB_VERSION) # Found hub_version but not ip_port schema[CONF_IP_PORT] = PORT_HUB_V1 if hub_version == 1 else PORT_HUB_V2 diff --git a/homeassistant/components/openuv/binary_sensor.py b/homeassistant/components/openuv/binary_sensor.py index 4d10aa53a39..913d844a7c3 100644 --- a/homeassistant/components/openuv/binary_sensor.py +++ b/homeassistant/components/openuv/binary_sensor.py @@ -45,9 +45,7 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorEntity): @callback def update_from_latest_data(self) -> None: """Update the state.""" - data = self.openuv.data[DATA_PROTECTION_WINDOW] - - if not data: + if not (data := self.openuv.data[DATA_PROTECTION_WINDOW]): self._attr_available = False return diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py index 2eac6ba41b2..f1d0ba9e0b1 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -134,9 +134,7 @@ class OpenUvSensor(OpenUvEntity, SensorEntity): @callback def update_from_latest_data(self) -> None: """Update the state.""" - data = self.openuv.data[DATA_UV].get("result") - - if not data: + if not (data := self.openuv.data[DATA_UV].get("result")): self._attr_available = False return diff --git a/homeassistant/components/sms/__init__.py b/homeassistant/components/sms/__init__.py index e2904825bbb..358f608be58 100644 --- a/homeassistant/components/sms/__init__.py +++ b/homeassistant/components/sms/__init__.py @@ -21,8 +21,7 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Configure Gammu state machine.""" hass.data.setdefault(DOMAIN, {}) - sms_config = config.get(DOMAIN, {}) - if not sms_config: + if not (sms_config := config.get(DOMAIN, {})): return True hass.async_create_task( diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index 1b0ae5a9076..bcf7aea8e14 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -51,8 +51,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the SQL sensor platform.""" - db_url = config.get(CONF_DB_URL) - if not db_url: + if not (db_url := config.get(CONF_DB_URL)): db_url = DEFAULT_URL.format(hass_config_path=hass.config.path(DEFAULT_DB_FILE)) sess = None diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 7380f15b983..36480e90f12 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -143,8 +143,7 @@ class LgWebOSMediaPlayerEntity(MediaPlayerEntity): async def async_signal_handler(self, data): """Handle domain-specific signal by calling appropriate method.""" - entity_ids = data[ATTR_ENTITY_ID] - if entity_ids == ENTITY_MATCH_NONE: + if (entity_ids := data[ATTR_ENTITY_ID]) == ENTITY_MATCH_NONE: return if entity_ids == ENTITY_MATCH_ALL or self.entity_id in entity_ids: @@ -368,8 +367,7 @@ class LgWebOSMediaPlayerEntity(MediaPlayerEntity): @cmd async def async_select_source(self, source): """Select input source.""" - source_dict = self._source_list.get(source) - if source_dict is None: + if (source_dict := self._source_list.get(source)) is None: _LOGGER.warning("Source %s not found for %s", source, self.name) return if source_dict.get("title"): diff --git a/homeassistant/components/wilight/fan.py b/homeassistant/components/wilight/fan.py index e55413926ac..b549eb2f813 100644 --- a/homeassistant/components/wilight/fan.py +++ b/homeassistant/components/wilight/fan.py @@ -86,8 +86,7 @@ class WiLightFan(WiLightDevice, FanEntity): ): return 0 - wl_speed = self._status.get("speed") - if wl_speed is None: + if (wl_speed := self._status.get("speed")) is None: return None return ordered_list_item_to_percentage(ORDERED_NAMED_FAN_SPEEDS, wl_speed) From 47fdf078e49bf3583b4904348adb5821b9f5d3f6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 18 Oct 2021 18:41:18 +0200 Subject: [PATCH 0502/1038] Bump hatasmota to 0.2.21 (#57966) --- .../components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tasmota/test_sensor.py | 77 +++++++++++++++++++ 4 files changed, 80 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index ae918c7fe44..592f833fd12 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.2.20"], + "requirements": ["hatasmota==0.2.21"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/requirements_all.txt b/requirements_all.txt index cd8b959479f..f8991853848 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -783,7 +783,7 @@ hass-nabucasa==0.50.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.2.20 +hatasmota==0.2.21 # homeassistant.components.jewish_calendar hdate==0.10.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f5d64ac91be..b6b72bdc0a6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -476,7 +476,7 @@ hangups==0.4.14 hass-nabucasa==0.50.0 # homeassistant.components.tasmota -hatasmota==0.2.20 +hatasmota==0.2.21 # homeassistant.components.jewish_calendar hdate==0.10.2 diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index adb73dcf334..26699763a6c 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -79,6 +79,33 @@ INDEXED_SENSOR_CONFIG = { } } +INDEXED_SENSOR_CONFIG_2 = { + "sn": { + "Time": "2020-09-25T12:47:15", + "ENERGY": { + "TotalStartTime": "2018-11-23T15:33:47", + "Total": [0.000, 0.017], + "TotalTariff": [0.000, 0.017], + "Yesterday": 0.000, + "Today": 0.002, + "ExportActive": 0.000, + "ExportTariff": [0.000, 0.000], + "Period": 0.00, + "Power": 0.00, + "ApparentPower": 7.84, + "ReactivePower": -7.21, + "Factor": 0.39, + "Frequency": 50.0, + "Voltage": 234.31, + "Current": 0.039, + "ImportActive": 12.580, + "ImportReactive": 0.002, + "ExportReactive": 39.131, + "PhaseAngle": 290.45, + }, + } +} + NESTED_SENSOR_CONFIG = { "sn": { @@ -283,6 +310,56 @@ async def test_indexed_sensor_state_via_mqtt2(hass, mqtt_mock, setup_tasmota): assert state.state == "5.6" +async def test_indexed_sensor_state_via_mqtt3(hass, mqtt_mock, setup_tasmota): + """Test state update via MQTT for indexed sensor with last_reset property.""" + config = copy.deepcopy(DEFAULT_CONFIG) + sensor_config = copy.deepcopy(INDEXED_SENSOR_CONFIG_2) + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/sensors", + json.dumps(sensor_config), + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.tasmota_energy_total_1") + assert state.state == "unavailable" + assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert ( + state.attributes[sensor.ATTR_STATE_CLASS] == sensor.STATE_CLASS_TOTAL_INCREASING + ) + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + state = hass.states.get("sensor.tasmota_energy_total_1") + assert state.state == STATE_UNKNOWN + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + # Test periodic state update + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/tele/SENSOR", + '{"ENERGY":{"Total":[1.2, 3.4],"TotalStartTime":"2018-11-23T15:33:47"}}', + ) + state = hass.states.get("sensor.tasmota_energy_total_1") + assert state.state == "3.4" + + # Test polled state update + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/stat/STATUS10", + '{"StatusSNS":{"ENERGY":{"Total":[5.6,7.8],"TotalStartTime":"2018-11-23T16:33:47"}}}', + ) + state = hass.states.get("sensor.tasmota_energy_total_1") + assert state.state == "7.8" + + async def test_bad_indexed_sensor_state_via_mqtt(hass, mqtt_mock, setup_tasmota): """Test state update via MQTT where sensor is not matching configuration.""" config = copy.deepcopy(DEFAULT_CONFIG) From 4f25b2ca08dbdc7a28ef6cba74a654d37b337366 Mon Sep 17 00:00:00 2001 From: David Le Brun Date: Mon, 18 Oct 2021 18:45:26 +0200 Subject: [PATCH 0503/1038] Support device and state classes for WAQI sensor (#57762) --- homeassistant/components/waqi/sensor.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/waqi/sensor.py b/homeassistant/components/waqi/sensor.py index ed6013daa74..98ee8db4e89 100644 --- a/homeassistant/components/waqi/sensor.py +++ b/homeassistant/components/waqi/sensor.py @@ -7,12 +7,13 @@ import aiohttp import voluptuous as vol from waqiasync import WaqiClient -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_TEMPERATURE, ATTR_TIME, CONF_TOKEN, + DEVICE_CLASS_AQI, ) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -43,6 +44,9 @@ KEY_TO_ATTR = { ATTRIBUTION = "Data provided by the World Air Quality Index project" +ATTR_ICON = "mdi:cloud" +ATTR_UNIT = "AQI" + CONF_LOCATIONS = "locations" CONF_STATIONS = "stations" @@ -96,6 +100,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class WaqiSensor(SensorEntity): """Implementation of a WAQI sensor.""" + _attr_icon = ATTR_ICON + _attr_native_unit_of_measurement = ATTR_UNIT + _attr_device_class = DEVICE_CLASS_AQI + _attr_state_class = STATE_CLASS_MEASUREMENT + def __init__(self, client, station): """Initialize the sensor.""" self._client = client @@ -123,11 +132,6 @@ class WaqiSensor(SensorEntity): return f"WAQI {self.station_name}" return f"WAQI {self.url if self.url else self.uid}" - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return "mdi:cloud" - @property def native_value(self): """Return the state of the device.""" @@ -145,11 +149,6 @@ class WaqiSensor(SensorEntity): """Return unique ID.""" return self.uid - @property - def native_unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return "AQI" - @property def extra_state_attributes(self): """Return the state attributes of the last update.""" From 698f3ca96c17fd4f140678ae4fc7b22dd06cd0c7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 18 Oct 2021 06:52:17 -1000 Subject: [PATCH 0504/1038] Bump flux_led to 0.24.8 (#57934) --- homeassistant/components/flux_led/manifest.json | 6 +++++- homeassistant/generated/dhcp.py | 4 ++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index b2ebbbb3a3a..88bcaabece2 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,8 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.24.6"], + "requirements": ["flux_led==0.24.8"], + "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", "dhcp": [ @@ -33,6 +34,9 @@ { "hostname": "zengge_07_*" }, + { + "hostname": "zengge_21_*" + }, { "hostname": "zengge_33_*" }, diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index ecfbfe657df..5f861f1a56e 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -104,6 +104,10 @@ DHCP = [ "domain": "flux_led", "hostname": "zengge_07_*" }, + { + "domain": "flux_led", + "hostname": "zengge_21_*" + }, { "domain": "flux_led", "hostname": "zengge_33_*" diff --git a/requirements_all.txt b/requirements_all.txt index f8991853848..14feee4753b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -652,7 +652,7 @@ fjaraskupan==1.0.1 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.6 +flux_led==0.24.8 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b6b72bdc0a6..d89d60206ec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -384,7 +384,7 @@ fjaraskupan==1.0.1 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.6 +flux_led==0.24.8 # homeassistant.components.homekit fnvhash==0.1.0 From 3d8e802141570de8645f905544660451acbeebeb Mon Sep 17 00:00:00 2001 From: Peter Nijssen Date: Mon, 18 Oct 2021 18:56:10 +0200 Subject: [PATCH 0505/1038] Mark spider YAML configuration as deprecated (#57974) --- homeassistant/components/spider/__init__.py | 25 ++++++++++++--------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/spider/__init__.py b/homeassistant/components/spider/__init__.py index 887f6471cca..be1a5bd29c6 100644 --- a/homeassistant/components/spider/__init__.py +++ b/homeassistant/components/spider/__init__.py @@ -14,17 +14,20 @@ from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional( - CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL - ): cv.time_period, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL + ): cv.time_period, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) From 24737d4d1df0a5b40117e2a4a73cee723ccc0b69 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 18 Oct 2021 19:16:53 +0200 Subject: [PATCH 0506/1038] Use pytest fixtures on Onewire tests (#57973) * Add pytest fixtures * Add sysbus fixtures * Adjust parameter name Co-authored-by: epenet --- tests/components/onewire/__init__.py | 100 +------------ tests/components/onewire/conftest.py | 95 ++++++++++++ .../components/onewire/test_binary_sensor.py | 39 ++--- tests/components/onewire/test_config_flow.py | 57 ++++---- tests/components/onewire/test_init.py | 137 +++++++----------- tests/components/onewire/test_sensor.py | 70 ++++----- tests/components/onewire/test_switch.py | 39 ++--- 7 files changed, 262 insertions(+), 275 deletions(-) create mode 100644 tests/components/onewire/conftest.py diff --git a/tests/components/onewire/__init__.py b/tests/components/onewire/__init__.py index 0ca9b55c41a..27091b895bf 100644 --- a/tests/components/onewire/__init__.py +++ b/tests/components/onewire/__init__.py @@ -2,102 +2,18 @@ from __future__ import annotations from typing import Any -from unittest.mock import patch +from unittest.mock import MagicMock from pyownet.protocol import ProtocolError -from homeassistant.components.onewire.const import ( - CONF_MOUNT_DIR, - CONF_NAMES, - CONF_TYPE_OWSERVER, - CONF_TYPE_SYSBUS, - DEFAULT_SYSBUS_MOUNT_DIR, - DOMAIN, -) -from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE +from homeassistant.components.onewire.const import DEFAULT_SYSBUS_MOUNT_DIR from .const import MOCK_OWPROXY_DEVICES, MOCK_SYSBUS_DEVICES -from tests.common import MockConfigEntry - -async def setup_onewire_sysbus_integration(hass): - """Create the 1-Wire integration.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - source=SOURCE_USER, - data={ - CONF_TYPE: CONF_TYPE_SYSBUS, - CONF_MOUNT_DIR: DEFAULT_SYSBUS_MOUNT_DIR, - "names": { - "10-111111111111": "My DS18B20", - }, - }, - unique_id=f"{CONF_TYPE_SYSBUS}:{DEFAULT_SYSBUS_MOUNT_DIR}", - options={}, - entry_id="1", - ) - config_entry.add_to_hass(hass) - - with patch( - "homeassistant.components.onewire.onewirehub.os.path.isdir", return_value=True - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - return config_entry - - -async def setup_onewire_owserver_integration(hass): - """Create the 1-Wire integration.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - source=SOURCE_USER, - data={ - CONF_TYPE: CONF_TYPE_OWSERVER, - CONF_HOST: "1.2.3.4", - CONF_PORT: 1234, - }, - options={}, - entry_id="2", - ) - config_entry.add_to_hass(hass) - - with patch( - "homeassistant.components.onewire.onewirehub.protocol.proxy", - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - return config_entry - - -async def setup_onewire_patched_owserver_integration(hass): - """Create the 1-Wire integration.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - source=SOURCE_USER, - data={ - CONF_TYPE: CONF_TYPE_OWSERVER, - CONF_HOST: "1.2.3.4", - CONF_PORT: 1234, - CONF_NAMES: { - "10.111111111111": "My DS18B20", - }, - }, - options={}, - entry_id="2", - ) - config_entry.add_to_hass(hass) - - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - return config_entry - - -def setup_owproxy_mock_devices(owproxy, domain, device_ids) -> None: +def setup_owproxy_mock_devices( + owproxy: MagicMock, platform: str, device_ids: list(str) +) -> None: """Set up mock for owproxy.""" dir_return_value = [] main_read_side_effect = [] @@ -115,7 +31,7 @@ def setup_owproxy_mock_devices(owproxy, domain, device_ids) -> None: main_read_side_effect += mock_device["inject_reads"] # Setup sub-device reads - device_sensors = mock_device.get(domain, []) + device_sensors = mock_device.get(platform, []) for expected_sensor in device_sensors: sub_read_side_effect.append(expected_sensor["injected_value"]) @@ -130,7 +46,7 @@ def setup_owproxy_mock_devices(owproxy, domain, device_ids) -> None: def setup_sysbus_mock_devices( - domain: str, device_ids: list[str] + platform: str, device_ids: list[str] ) -> tuple[list[str], list[Any]]: """Set up mock for sysbus.""" glob_result = [] @@ -143,7 +59,7 @@ def setup_sysbus_mock_devices( glob_result += [f"/{DEFAULT_SYSBUS_MOUNT_DIR}/{device_id}"] # Setup sub-device reads - device_sensors = mock_device.get(domain, []) + device_sensors = mock_device.get(platform, []) for expected_sensor in device_sensors: if isinstance(expected_sensor["injected_value"], list): read_side_effect += expected_sensor["injected_value"] diff --git a/tests/components/onewire/conftest.py b/tests/components/onewire/conftest.py new file mode 100644 index 00000000000..455602c348f --- /dev/null +++ b/tests/components/onewire/conftest.py @@ -0,0 +1,95 @@ +"""Provide common 1-Wire fixtures.""" +from unittest.mock import MagicMock, patch + +from pyownet.protocol import ConnError +import pytest + +from homeassistant.components.onewire.const import ( + CONF_MOUNT_DIR, + CONF_NAMES, + CONF_TYPE_OWSERVER, + CONF_TYPE_SYSBUS, + DEFAULT_SYSBUS_MOUNT_DIR, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_USER, ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE +from homeassistant.core import HomeAssistant + +from .const import MOCK_OWPROXY_DEVICES + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="device_id", params=MOCK_OWPROXY_DEVICES.keys()) +def get_device_id(request: pytest.FixtureRequest) -> str: + """Parametrize device id.""" + return request.param + + +@pytest.fixture(name="config_entry") +def get_config_entry(hass: HomeAssistant) -> ConfigEntry: + """Create and register mock config entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data={ + CONF_TYPE: CONF_TYPE_OWSERVER, + CONF_HOST: "1.2.3.4", + CONF_PORT: 1234, + CONF_NAMES: { + "10.111111111111": "My DS18B20", + }, + }, + options={}, + entry_id="2", + ) + config_entry.add_to_hass(hass) + return config_entry + + +@pytest.fixture(name="sysbus_config_entry") +def get_sysbus_config_entry(hass: HomeAssistant) -> ConfigEntry: + """Create and register mock config entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data={ + CONF_TYPE: CONF_TYPE_SYSBUS, + CONF_MOUNT_DIR: DEFAULT_SYSBUS_MOUNT_DIR, + "names": { + "10-111111111111": "My DS18B20", + }, + }, + unique_id=f"{CONF_TYPE_SYSBUS}:{DEFAULT_SYSBUS_MOUNT_DIR}", + options={}, + entry_id="3", + ) + config_entry.add_to_hass(hass) + return config_entry + + +@pytest.fixture(name="owproxy") +def get_owproxy() -> MagicMock: + """Mock owproxy.""" + with patch("homeassistant.components.onewire.onewirehub.protocol.proxy") as owproxy: + yield owproxy + + +@pytest.fixture(name="owproxy_with_connerror") +def get_owproxy_with_connerror() -> MagicMock: + """Mock owproxy.""" + with patch( + "homeassistant.components.onewire.onewirehub.protocol.proxy", + side_effect=ConnError, + ) as owproxy: + yield owproxy + + +@pytest.fixture(name="sysbus") +def get_sysbus() -> MagicMock: + """Mock sysbus.""" + with patch( + "homeassistant.components.onewire.onewirehub.os.path.isdir", return_value=True + ): + yield diff --git a/tests/components/onewire/test_binary_sensor.py b/tests/components/onewire/test_binary_sensor.py index 7a34fe44a94..752f73d0304 100644 --- a/tests/components/onewire/test_binary_sensor.py +++ b/tests/components/onewire/test_binary_sensor.py @@ -1,51 +1,52 @@ """Tests for 1-Wire devices connected on OWServer.""" import copy -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.onewire.binary_sensor import DEVICE_BINARY_SENSORS +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant -from . import setup_onewire_patched_owserver_integration, setup_owproxy_mock_devices +from . import setup_owproxy_mock_devices from .const import MOCK_OWPROXY_DEVICES from tests.common import mock_registry -MOCK_BINARY_SENSORS = { - key: value - for (key, value) in MOCK_OWPROXY_DEVICES.items() - if BINARY_SENSOR_DOMAIN in value -} + +@pytest.fixture(autouse=True) +def override_platforms(): + """Override PLATFORMS.""" + with patch("homeassistant.components.onewire.PLATFORMS", [BINARY_SENSOR_DOMAIN]): + yield -@pytest.mark.parametrize("device_id", MOCK_BINARY_SENSORS.keys()) -@patch("homeassistant.components.onewire.onewirehub.protocol.proxy") -async def test_owserver_binary_sensor(owproxy, hass, device_id): +async def test_owserver_binary_sensor( + hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock, device_id: str +): """Test for 1-Wire binary sensor. This test forces all entities to be enabled. """ - entity_registry = mock_registry(hass) setup_owproxy_mock_devices(owproxy, BINARY_SENSOR_DOMAIN, [device_id]) - mock_device = MOCK_BINARY_SENSORS[device_id] - expected_entities = mock_device[BINARY_SENSOR_DOMAIN] + mock_device = MOCK_OWPROXY_DEVICES[device_id] + expected_entities = mock_device.get(BINARY_SENSOR_DOMAIN, []) # Force enable binary sensors patch_device_binary_sensors = copy.deepcopy(DEVICE_BINARY_SENSORS) - for item in patch_device_binary_sensors[device_id[0:2]]: - item.entity_registry_enabled_default = True + if device_binary_sensor := patch_device_binary_sensors.get(device_id[0:2]): + for item in device_binary_sensor: + item.entity_registry_enabled_default = True - with patch( - "homeassistant.components.onewire.PLATFORMS", [BINARY_SENSOR_DOMAIN] - ), patch.dict( + with patch.dict( "homeassistant.components.onewire.binary_sensor.DEVICE_BINARY_SENSORS", patch_device_binary_sensors, ): - await setup_onewire_patched_owserver_integration(hass) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert len(entity_registry.entities) == len(expected_entities) diff --git a/tests/components/onewire/test_config_flow.py b/tests/components/onewire/test_config_flow.py index d83e9203270..7bf7b58ebfa 100644 --- a/tests/components/onewire/test_config_flow.py +++ b/tests/components/onewire/test_config_flow.py @@ -1,7 +1,8 @@ """Tests for 1-Wire config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from pyownet import protocol +import pytest from homeassistant.components.onewire.const import ( CONF_MOUNT_DIR, @@ -10,18 +11,26 @@ from homeassistant.components.onewire.const import ( DEFAULT_SYSBUS_MOUNT_DIR, DOMAIN, ) -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_USER, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE +from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM, ) -from . import setup_onewire_owserver_integration, setup_onewire_sysbus_integration + +@pytest.fixture(autouse=True, name="mock_setup_entry") +def override_async_setup_entry() -> AsyncMock: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.onewire.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry -async def test_user_owserver(hass): +async def test_user_owserver(hass: HomeAssistant, mock_setup_entry: AsyncMock): """Test OWServer user flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -53,10 +62,9 @@ async def test_user_owserver(hass): assert result["errors"] == {"base": "cannot_connect"} # Valid server - with patch("homeassistant.components.onewire.onewirehub.protocol.proxy",), patch( - "homeassistant.components.onewire.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + with patch( + "homeassistant.components.onewire.onewirehub.protocol.proxy", + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 1234}, @@ -73,14 +81,13 @@ async def test_user_owserver(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_user_owserver_duplicate(hass): +async def test_user_owserver_duplicate( + hass: HomeAssistant, config_entry: ConfigEntry, mock_setup_entry: AsyncMock +): """Test OWServer flow.""" - with patch( - "homeassistant.components.onewire.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - await setup_onewire_owserver_integration(hass) - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -108,7 +115,7 @@ async def test_user_owserver_duplicate(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_user_sysbus(hass): +async def test_user_sysbus(hass: HomeAssistant, mock_setup_entry: AsyncMock): """Test SysBus flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -143,10 +150,7 @@ async def test_user_sysbus(hass): with patch( "homeassistant.components.onewire.onewirehub.os.path.isdir", return_value=True, - ), patch( - "homeassistant.components.onewire.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_MOUNT_DIR: "/sys/bus/directory"}, @@ -162,14 +166,13 @@ async def test_user_sysbus(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_user_sysbus_duplicate(hass): +async def test_user_sysbus_duplicate( + hass: HomeAssistant, sysbus_config_entry: ConfigEntry, mock_setup_entry: AsyncMock +): """Test SysBus duplicate flow.""" - with patch( - "homeassistant.components.onewire.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - await setup_onewire_sysbus_integration(hass) - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + await hass.config_entries.async_setup(sysbus_config_entry.entry_id) + await hass.async_block_till_done() + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} diff --git a/tests/components/onewire/test_init.py b/tests/components/onewire/test_init.py index 38f63cda09a..1bf95ee5c0c 100644 --- a/tests/components/onewire/test_init.py +++ b/tests/components/onewire/test_init.py @@ -1,99 +1,72 @@ """Tests for 1-Wire config flow.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch -from pyownet.protocol import ConnError, OwnetError +import pytest -from homeassistant.components.onewire.const import CONF_TYPE_OWSERVER, DOMAIN +from homeassistant.components.onewire.const import DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.config_entries import SOURCE_USER, ConfigEntryState -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE +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 . import ( - setup_onewire_owserver_integration, - setup_onewire_patched_owserver_integration, - setup_onewire_sysbus_integration, - setup_owproxy_mock_devices, -) +from . import setup_owproxy_mock_devices -from tests.common import MockConfigEntry, mock_device_registry, mock_registry +from tests.common import mock_device_registry, mock_registry -async def test_owserver_connect_failure(hass): +@pytest.mark.usefixtures("owproxy_with_connerror") +async def test_owserver_connect_failure(hass: HomeAssistant, config_entry: ConfigEntry): """Test connection failure raises ConfigEntryNotReady.""" - config_entry_owserver = MockConfigEntry( - domain=DOMAIN, - source=SOURCE_USER, - data={ - CONF_TYPE: CONF_TYPE_OWSERVER, - CONF_HOST: "1.2.3.4", - CONF_PORT: "1234", - }, - options={}, - entry_id="2", - ) - config_entry_owserver.add_to_hass(hass) - - with patch( - "homeassistant.components.onewire.onewirehub.protocol.proxy", - side_effect=ConnError, - ): - await hass.config_entries.async_setup(config_entry_owserver.entry_id) - await hass.async_block_till_done() - - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert config_entry_owserver.state is ConfigEntryState.SETUP_RETRY - assert not hass.data.get(DOMAIN) - - -async def test_failed_owserver_listing(hass): - """Create the 1-Wire integration.""" - config_entry_owserver = MockConfigEntry( - domain=DOMAIN, - source=SOURCE_USER, - data={ - CONF_TYPE: CONF_TYPE_OWSERVER, - CONF_HOST: "1.2.3.4", - CONF_PORT: "1234", - }, - options={}, - entry_id="2", - ) - config_entry_owserver.add_to_hass(hass) - - with patch("homeassistant.components.onewire.onewirehub.protocol.proxy") as owproxy: - owproxy.return_value.dir.side_effect = OwnetError - await hass.config_entries.async_setup(config_entry_owserver.entry_id) - await hass.async_block_till_done() - - return config_entry_owserver - - -async def test_unload_entry(hass): - """Test being able to unload an entry.""" - config_entry_owserver = await setup_onewire_owserver_integration(hass) - config_entry_sysbus = await setup_onewire_sysbus_integration(hass) - - assert len(hass.config_entries.async_entries(DOMAIN)) == 2 - assert config_entry_owserver.state is ConfigEntryState.LOADED - assert config_entry_sysbus.state is ConfigEntryState.LOADED - - assert await hass.config_entries.async_unload(config_entry_owserver.entry_id) - assert await hass.config_entries.async_unload(config_entry_sysbus.entry_id) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry_owserver.state is ConfigEntryState.NOT_LOADED - assert config_entry_sysbus.state is ConfigEntryState.NOT_LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert config_entry.state is ConfigEntryState.SETUP_RETRY assert not hass.data.get(DOMAIN) -@patch("homeassistant.components.onewire.onewirehub.protocol.proxy") -async def test_registry_cleanup(owproxy, hass): +@pytest.mark.usefixtures("owproxy") +async def test_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): + """Test being able to unload an entry.""" + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert config_entry.state is ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.NOT_LOADED + assert not hass.data.get(DOMAIN) + + +@pytest.mark.usefixtures("sysbus") +async def test_unload_sysbus_entry( + hass: HomeAssistant, sysbus_config_entry: ConfigEntry +): + """Test being able to unload an entry.""" + await hass.config_entries.async_setup(sysbus_config_entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert sysbus_config_entry.state is ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(sysbus_config_entry.entry_id) + await hass.async_block_till_done() + + assert sysbus_config_entry.state is ConfigEntryState.NOT_LOADED + assert not hass.data.get(DOMAIN) + + +@patch("homeassistant.components.onewire.PLATFORMS", [SENSOR_DOMAIN]) +async def test_registry_cleanup( + hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock +): """Test for 1-Wire device. As they would be on a clean setup: all binary-sensors and switches disabled. """ - entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -101,9 +74,8 @@ async def test_registry_cleanup(owproxy, hass): setup_owproxy_mock_devices( owproxy, SENSOR_DOMAIN, ["10.111111111111", "28.111111111111"] ) - with patch("homeassistant.components.onewire.PLATFORMS", [SENSOR_DOMAIN]): - await setup_onewire_patched_owserver_integration(hass) - await hass.async_block_till_done() + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() assert len(dr.async_entries_for_config_entry(device_registry, "2")) == 2 assert len(er.async_entries_for_config_entry(entity_registry, "2")) == 2 @@ -117,9 +89,8 @@ async def test_registry_cleanup(owproxy, hass): assert len(dr.async_entries_for_config_entry(device_registry, "2")) == 2 # Second item has disappeared from bus, and was removed manually from the front-end - with patch("homeassistant.components.onewire.PLATFORMS", [SENSOR_DOMAIN]): - await hass.config_entries.async_reload("2") - await hass.async_block_till_done() + await hass.config_entries.async_reload("2") + await hass.async_block_till_done() assert len(er.async_entries_for_config_entry(entity_registry, "2")) == 1 assert len(dr.async_entries_for_config_entry(device_registry, "2")) == 1 diff --git a/tests/components/onewire/test_sensor.py b/tests/components/onewire/test_sensor.py index f70d2912da3..8e8ee40e725 100644 --- a/tests/components/onewire/test_sensor.py +++ b/tests/components/onewire/test_sensor.py @@ -1,15 +1,12 @@ """Tests for 1-Wire sensor platform.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch from pyownet.protocol import Error as ProtocolError import pytest -from homeassistant.components.onewire.const import ( - DEFAULT_SYSBUS_MOUNT_DIR, - DOMAIN, - PLATFORMS, -) +from homeassistant.components.onewire.const import DEFAULT_SYSBUS_MOUNT_DIR, DOMAIN from homeassistant.components.sensor import ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_MANUFACTURER, @@ -17,14 +14,10 @@ from homeassistant.const import ( ATTR_NAME, ATTR_UNIT_OF_MEASUREMENT, ) +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from . import ( - setup_onewire_patched_owserver_integration, - setup_onewire_sysbus_integration, - setup_owproxy_mock_devices, - setup_sysbus_mock_devices, -) +from . import setup_owproxy_mock_devices, setup_sysbus_mock_devices from .const import MOCK_OWPROXY_DEVICES, MOCK_SYSBUS_DEVICES from tests.common import assert_setup_component, mock_device_registry, mock_registry @@ -34,7 +27,14 @@ MOCK_COUPLERS = { } -async def test_setup_minimum(hass): +@pytest.fixture(autouse=True) +def override_platforms(): + """Override PLATFORMS.""" + with patch("homeassistant.components.onewire.PLATFORMS", [SENSOR_DOMAIN]): + yield + + +async def test_setup_minimum(hass: HomeAssistant): """Test old platform setup with minimum configuration.""" config = {"sensor": {"platform": "onewire"}} with assert_setup_component(1, "sensor"): @@ -42,7 +42,7 @@ async def test_setup_minimum(hass): await hass.async_block_till_done() -async def test_setup_sysbus(hass): +async def test_setup_sysbus(hass: HomeAssistant): """Test old platform setup with SysBus configuration.""" config = { "sensor": { @@ -55,7 +55,7 @@ async def test_setup_sysbus(hass): await hass.async_block_till_done() -async def test_setup_owserver(hass): +async def test_setup_owserver(hass: HomeAssistant): """Test old platform setup with OWServer configuration.""" config = {"sensor": {"platform": "onewire", "host": "localhost"}} with assert_setup_component(1, "sensor"): @@ -63,7 +63,7 @@ async def test_setup_owserver(hass): await hass.async_block_till_done() -async def test_setup_owserver_with_port(hass): +async def test_setup_owserver_with_port(hass: HomeAssistant): """Test old platform setup with OWServer configuration.""" config = {"sensor": {"platform": "onewire", "host": "localhost", "port": "1234"}} with assert_setup_component(1, "sensor"): @@ -71,9 +71,10 @@ async def test_setup_owserver_with_port(hass): await hass.async_block_till_done() -@pytest.mark.parametrize("device_id", ["1F.111111111111"]) -@patch("homeassistant.components.onewire.onewirehub.protocol.proxy") -async def test_sensors_on_owserver_coupler(owproxy, hass, device_id): +@pytest.mark.parametrize("device_id", ["1F.111111111111"], indirect=True) +async def test_sensors_on_owserver_coupler( + hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock, device_id: str +): """Test for 1-Wire sensors connected to DS2409 coupler.""" entity_registry = mock_registry(hass) @@ -111,9 +112,8 @@ async def test_sensors_on_owserver_coupler(owproxy, hass, device_id): owproxy.return_value.dir.side_effect = dir_side_effect owproxy.return_value.read.side_effect = read_side_effect - with patch("homeassistant.components.onewire.PLATFORMS", [SENSOR_DOMAIN]): - await setup_onewire_patched_owserver_integration(hass) - await hass.async_block_till_done() + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() assert len(entity_registry.entities) == len(expected_sensors) @@ -130,26 +130,23 @@ async def test_sensors_on_owserver_coupler(owproxy, hass, device_id): assert state.attributes["device_file"] == expected_sensor["device_file"] -@pytest.mark.parametrize("device_id", MOCK_OWPROXY_DEVICES.keys()) -@pytest.mark.parametrize("platform", PLATFORMS) -@patch("homeassistant.components.onewire.onewirehub.protocol.proxy") -async def test_owserver_setup_valid_device(owproxy, hass, device_id, platform): +async def test_owserver_setup_valid_device( + hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock, device_id: str +): """Test for 1-Wire device. As they would be on a clean setup: all binary-sensors and switches disabled. """ - entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) - setup_owproxy_mock_devices(owproxy, platform, [device_id]) + setup_owproxy_mock_devices(owproxy, SENSOR_DOMAIN, [device_id]) mock_device = MOCK_OWPROXY_DEVICES[device_id] - expected_entities = mock_device.get(platform, []) + expected_entities = mock_device.get(SENSOR_DOMAIN, []) - with patch("homeassistant.components.onewire.PLATFORMS", [platform]): - await setup_onewire_patched_owserver_integration(hass) - await hass.async_block_till_done() + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() assert len(entity_registry.entities) == len(expected_entities) @@ -181,8 +178,11 @@ async def test_owserver_setup_valid_device(owproxy, hass, device_id, platform): ) -@pytest.mark.parametrize("device_id", MOCK_SYSBUS_DEVICES.keys()) -async def test_onewiredirect_setup_valid_device(hass, device_id): +@pytest.mark.usefixtures("sysbus") +@pytest.mark.parametrize("device_id", MOCK_SYSBUS_DEVICES.keys(), indirect=True) +async def test_onewiredirect_setup_valid_device( + hass: HomeAssistant, sysbus_config_entry: ConfigEntry, device_id: str +): """Test that sysbus config entry works correctly.""" entity_registry = mock_registry(hass) @@ -199,7 +199,7 @@ async def test_onewiredirect_setup_valid_device(hass, device_id): "pi1wire.OneWire.get_temperature", side_effect=read_side_effect, ): - assert await setup_onewire_sysbus_integration(hass) + await hass.config_entries.async_setup(sysbus_config_entry.entry_id) await hass.async_block_till_done() assert len(entity_registry.entities) == len(expected_entities) diff --git a/tests/components/onewire/test_switch.py b/tests/components/onewire/test_switch.py index 7abb8a5e9bd..04a30092db3 100644 --- a/tests/components/onewire/test_switch.py +++ b/tests/components/onewire/test_switch.py @@ -1,51 +1,52 @@ """Tests for 1-Wire devices connected on OWServer.""" import copy -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest from homeassistant.components.onewire.switch import DEVICE_SWITCHES from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TOGGLE, STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant -from . import setup_onewire_patched_owserver_integration, setup_owproxy_mock_devices +from . import setup_owproxy_mock_devices from .const import MOCK_OWPROXY_DEVICES from tests.common import mock_registry -MOCK_SWITCHES = { - key: value - for (key, value) in MOCK_OWPROXY_DEVICES.items() - if SWITCH_DOMAIN in value -} + +@pytest.fixture(autouse=True) +def override_platforms(): + """Override PLATFORMS.""" + with patch("homeassistant.components.onewire.PLATFORMS", [SWITCH_DOMAIN]): + yield -@pytest.mark.parametrize("device_id", MOCK_SWITCHES.keys()) -@patch("homeassistant.components.onewire.onewirehub.protocol.proxy") -async def test_owserver_switch(owproxy, hass, device_id): +async def test_owserver_switch( + hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock, device_id: str +): """Test for 1-Wire switch. This test forces all entities to be enabled. """ - entity_registry = mock_registry(hass) setup_owproxy_mock_devices(owproxy, SWITCH_DOMAIN, [device_id]) - mock_device = MOCK_SWITCHES[device_id] - expected_entities = mock_device[SWITCH_DOMAIN] + mock_device = MOCK_OWPROXY_DEVICES[device_id] + expected_entities = mock_device.get(SWITCH_DOMAIN, []) # Force enable switches patch_device_switches = copy.deepcopy(DEVICE_SWITCHES) - for item in patch_device_switches[device_id[0:2]]: - item.entity_registry_enabled_default = True + if device_switch := patch_device_switches.get(device_id[0:2]): + for item in device_switch: + item.entity_registry_enabled_default = True - with patch( - "homeassistant.components.onewire.PLATFORMS", [SWITCH_DOMAIN] - ), patch.dict( + with patch.dict( "homeassistant.components.onewire.switch.DEVICE_SWITCHES", patch_device_switches ): - await setup_onewire_patched_owserver_integration(hass) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert len(entity_registry.entities) == len(expected_entities) From b01170d91775b04d196c7daa9e542cd6b67be89d Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Mon, 18 Oct 2021 19:17:10 +0200 Subject: [PATCH 0507/1038] Add Nut missing tests for config flow (#57964) --- tests/components/nut/test_config_flow.py | 271 +++++++++++++++++------ tests/components/nut/test_sensor.py | 16 +- 2 files changed, 213 insertions(+), 74 deletions(-) diff --git a/tests/components/nut/test_config_flow.py b/tests/components/nut/test_config_flow.py index 0799248384b..68b5356a32c 100644 --- a/tests/components/nut/test_config_flow.py +++ b/tests/components/nut/test_config_flow.py @@ -2,19 +2,30 @@ from unittest.mock import patch -from homeassistant import config_entries, data_entry_flow +from pynut2.nut2 import PyNUTError + +from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.nut.const import DOMAIN -from homeassistant.const import CONF_RESOURCES, CONF_SCAN_INTERVAL +from homeassistant.const import ( + CONF_ALIAS, + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_RESOURCES, + CONF_SCAN_INTERVAL, + CONF_USERNAME, +) from .util import _get_mock_pynutclient from tests.common import MockConfigEntry VALID_CONFIG = { - "host": "localhost", - "port": 123, - "name": "name", - "resources": ["battery.charge"], + CONF_HOST: "localhost", + CONF_PORT: 123, + CONF_NAME: "name", + CONF_RESOURCES: ["battery.charge"], } @@ -23,7 +34,7 @@ async def test_form_zeroconf(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data={"host": "192.168.1.5", "port": 1234}, + data={CONF_HOST: "192.168.1.5", CONF_PORT: 1234}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -39,11 +50,11 @@ async def test_form_zeroconf(hass): ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {"username": "test-username", "password": "test-password"}, + {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, ) assert result2["step_id"] == "resources" - assert result2["type"] == "form" + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM with patch( "homeassistant.components.nut.PyNUTClient", @@ -54,18 +65,18 @@ async def test_form_zeroconf(hass): ) as mock_setup_entry: result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], - {"resources": ["battery.voltage", "ups.status", "ups.status.display"]}, + {CONF_RESOURCES: ["battery.voltage", "ups.status", "ups.status.display"]}, ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result3["title"] == "192.168.1.5:1234" assert result3["data"] == { - "host": "192.168.1.5", - "password": "test-password", - "port": 1234, - "resources": ["battery.voltage", "ups.status", "ups.status.display"], - "username": "test-username", + CONF_HOST: "192.168.1.5", + CONF_PASSWORD: "test-password", + CONF_PORT: 1234, + CONF_RESOURCES: ["battery.voltage", "ups.status", "ups.status.display"], + CONF_USERNAME: "test-username", } assert result3["result"].unique_id is None assert len(mock_setup_entry.mock_calls) == 1 @@ -73,11 +84,11 @@ async def test_form_zeroconf(hass): async def test_form_user_one_ups(hass): """Test we get the form.""" - + await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} mock_pynut = _get_mock_pynutclient( @@ -91,15 +102,15 @@ async def test_form_user_one_ups(hass): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "host": "1.1.1.1", - "username": "test-username", - "password": "test-password", - "port": 2222, + CONF_HOST: "1.1.1.1", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_PORT: 2222, }, ) assert result2["step_id"] == "resources" - assert result2["type"] == "form" + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM with patch( "homeassistant.components.nut.PyNUTClient", @@ -110,36 +121,36 @@ async def test_form_user_one_ups(hass): ) as mock_setup_entry: result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], - {"resources": ["battery.voltage", "ups.status", "ups.status.display"]}, + {CONF_RESOURCES: ["battery.voltage", "ups.status", "ups.status.display"]}, ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result3["title"] == "1.1.1.1:2222" assert result3["data"] == { - "host": "1.1.1.1", - "password": "test-password", - "port": 2222, - "resources": ["battery.voltage", "ups.status", "ups.status.display"], - "username": "test-username", + CONF_HOST: "1.1.1.1", + CONF_PASSWORD: "test-password", + CONF_PORT: 2222, + CONF_RESOURCES: ["battery.voltage", "ups.status", "ups.status.display"], + CONF_USERNAME: "test-username", } assert len(mock_setup_entry.mock_calls) == 1 async def test_form_user_multiple_ups(hass): """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) config_entry = MockConfigEntry( domain=DOMAIN, - data={"host": "2.2.2.2", "port": 123, "resources": ["battery.charge"]}, - options={CONF_RESOURCES: ["battery.charge"]}, + data={CONF_HOST: "2.2.2.2", CONF_PORT: 123, CONF_RESOURCES: ["battery.charge"]}, ) config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} mock_pynut = _get_mock_pynutclient( @@ -154,15 +165,15 @@ async def test_form_user_multiple_ups(hass): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "host": "1.1.1.1", - "username": "test-username", - "password": "test-password", - "port": 2222, + CONF_HOST: "1.1.1.1", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_PORT: 2222, }, ) assert result2["step_id"] == "ups" - assert result2["type"] == "form" + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM with patch( "homeassistant.components.nut.PyNUTClient", @@ -170,11 +181,11 @@ async def test_form_user_multiple_ups(hass): ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], - {"alias": "ups2"}, + {CONF_ALIAS: "ups2"}, ) assert result3["step_id"] == "resources" - assert result3["type"] == "form" + assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM with patch( "homeassistant.components.nut.PyNUTClient", @@ -185,19 +196,19 @@ async def test_form_user_multiple_ups(hass): ) as mock_setup_entry: result4 = await hass.config_entries.flow.async_configure( result3["flow_id"], - {"resources": ["battery.voltage"]}, + {CONF_RESOURCES: ["battery.voltage"]}, ) await hass.async_block_till_done() - assert result4["type"] == "create_entry" + assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result4["title"] == "ups2@1.1.1.1:2222" assert result4["data"] == { - "host": "1.1.1.1", - "password": "test-password", - "alias": "ups2", - "port": 2222, - "resources": ["battery.voltage"], - "username": "test-username", + CONF_HOST: "1.1.1.1", + CONF_PASSWORD: "test-password", + CONF_ALIAS: "ups2", + CONF_PORT: 2222, + CONF_RESOURCES: ["battery.voltage"], + CONF_USERNAME: "test-username", } assert len(mock_setup_entry.mock_calls) == 2 @@ -209,10 +220,11 @@ async def test_form_user_one_ups_with_ignored_entry(hass): ) ignored_entry.add_to_hass(hass) + await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} mock_pynut = _get_mock_pynutclient( @@ -226,15 +238,15 @@ async def test_form_user_one_ups_with_ignored_entry(hass): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "host": "1.1.1.1", - "username": "test-username", - "password": "test-password", - "port": 2222, + CONF_HOST: "1.1.1.1", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_PORT: 2222, }, ) assert result2["step_id"] == "resources" - assert result2["type"] == "form" + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM with patch( "homeassistant.components.nut.PyNUTClient", @@ -245,18 +257,18 @@ async def test_form_user_one_ups_with_ignored_entry(hass): ) as mock_setup_entry: result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], - {"resources": ["battery.voltage", "ups.status", "ups.status.display"]}, + {CONF_RESOURCES: ["battery.voltage", "ups.status", "ups.status.display"]}, ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result3["title"] == "1.1.1.1:2222" assert result3["data"] == { - "host": "1.1.1.1", - "password": "test-password", - "port": 2222, - "resources": ["battery.voltage", "ups.status", "ups.status.display"], - "username": "test-username", + CONF_HOST: "1.1.1.1", + CONF_PASSWORD: "test-password", + CONF_PORT: 2222, + CONF_RESOURCES: ["battery.voltage", "ups.status", "ups.status.display"], + CONF_USERNAME: "test-username", } assert len(mock_setup_entry.mock_calls) == 1 @@ -276,16 +288,143 @@ async def test_form_cannot_connect(hass): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "host": "1.1.1.1", - "username": "test-username", - "password": "test-password", - "port": 2222, + CONF_HOST: "1.1.1.1", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_PORT: 2222, }, ) - assert result2["type"] == "form" + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["errors"] == {"base": "cannot_connect"} + with patch( + "homeassistant.components.nut.PyNUTClient.list_ups", + side_effect=PyNUTError, + ), patch( + "homeassistant.components.nut.PyNUTClient.list_vars", + side_effect=PyNUTError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_PORT: 2222, + }, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + with patch( + "homeassistant.components.nut.PyNUTClient.list_ups", + return_value=["ups1"], + ), patch( + "homeassistant.components.nut.PyNUTClient.list_vars", + side_effect=TypeError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_PORT: 2222, + }, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_abort_if_already_setup(hass): + """Test we abort if component is already setup.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 123, + CONF_RESOURCES: ["battery.voltage"], + }, + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_pynut = _get_mock_pynutclient( + list_vars={"battery.voltage": "voltage"}, + list_ups={"ups1": "UPS 1"}, + ) + + with patch( + "homeassistant.components.nut.PyNUTClient", + return_value=mock_pynut, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_PORT: 123, + }, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "already_configured" + + +async def test_abort_if_already_setup_alias(hass): + """Test we abort if component is already setup with same alias.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 123, + CONF_RESOURCES: ["battery.voltage"], + CONF_ALIAS: "ups1", + }, + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_pynut = _get_mock_pynutclient( + list_vars={"battery.voltage": "voltage"}, + list_ups={"ups1": "UPS 1", "ups2": "UPS 2"}, + ) + + with patch( + "homeassistant.components.nut.PyNUTClient", + return_value=mock_pynut, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_PORT: 123, + }, + ) + + assert result2["step_id"] == "ups" + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + + with patch( + "homeassistant.components.nut.PyNUTClient", + return_value=mock_pynut, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {CONF_ALIAS: "ups1"}, + ) + + assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["reason"] == "already_configured" + async def test_options_flow(hass): """Test config flow options.""" diff --git a/tests/components/nut/test_sensor.py b/tests/components/nut/test_sensor.py index 4b1e1cc8a9a..5afc662251c 100644 --- a/tests/components/nut/test_sensor.py +++ b/tests/components/nut/test_sensor.py @@ -37,7 +37,7 @@ async def test_pr3000rt2u(hass): # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes + state.attributes[key] == attr for key, attr in expected_attributes.items() ) @@ -62,7 +62,7 @@ async def test_cp1350c(hass): # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes + state.attributes[key] == attr for key, attr in expected_attributes.items() ) @@ -86,7 +86,7 @@ async def test_5e850i(hass): # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes + state.attributes[key] == attr for key, attr in expected_attributes.items() ) @@ -110,7 +110,7 @@ async def test_5e650i(hass): # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes + state.attributes[key] == attr for key, attr in expected_attributes.items() ) @@ -137,7 +137,7 @@ async def test_backupsses600m1(hass): # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes + state.attributes[key] == attr for key, attr in expected_attributes.items() ) @@ -163,7 +163,7 @@ async def test_cp1500pfclcd(hass): # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes + state.attributes[key] == attr for key, attr in expected_attributes.items() ) @@ -187,7 +187,7 @@ async def test_dl650elcd(hass): # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes + state.attributes[key] == attr for key, attr in expected_attributes.items() ) @@ -211,7 +211,7 @@ async def test_blazer_usb(hass): # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all( - state.attributes[key] == expected_attributes[key] for key in expected_attributes + state.attributes[key] == attr for key, attr in expected_attributes.items() ) From be73b21f810b5787f2fec90cce81ffaf3ca9d2e2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 18 Oct 2021 20:35:01 +0200 Subject: [PATCH 0508/1038] Refactor Tuya light platform (#57980) --- homeassistant/components/tuya/base.py | 28 +- homeassistant/components/tuya/const.py | 27 +- homeassistant/components/tuya/light.py | 696 +++++++++++++++---------- 3 files changed, 473 insertions(+), 278 deletions(-) diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index 66b497c2f4e..f0bcb8e537f 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -22,9 +22,9 @@ class IntegerTypeData: min: int max: int - unit: str scale: float step: float + unit: str | None = None @property def max_scaled(self) -> float: @@ -45,6 +45,32 @@ class IntegerTypeData: """Scale a value.""" return value * 1.0 / (10 ** self.scale) + def remap_value_to( + self, + value: float, + to_min: float | int = 0, + to_max: float | int = 255, + reverse: bool = False, + ) -> float: + """Remap a value from this range to a new range.""" + if reverse: + value = self.max - value + self.min + return ((value - self.min) / (self.max - self.min)) * (to_max - to_min) + to_min + + def remap_value_from( + self, + value: float, + from_min: float | int = 0, + from_max: float | int = 255, + reverse: bool = False, + ) -> float: + """Remap a value from its current range to this range.""" + if reverse: + value = from_max - value + from_min + return ((value - from_min) / (from_max - from_min)) * ( + self.max - self.min + ) + self.min + @classmethod def from_json(cls, data: str) -> IntegerTypeData: """Load JSON string and return a IntegerTypeData object.""" diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 0bc5dcfa575..259fbc73b9e 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -93,8 +93,10 @@ TUYA_SUPPORTED_PRODUCT_CATEGORIES = ( "dj", # Light "dlq", # Breaker "fs", # Fan - "fs", # Fan + "fsd", # Ceiling Fan Light + "fwd", # Ambient Light "fwl", # Ambient light + "gyd", # Motion Sensor Light "jsq", # Humidifier's light "kfj", # Coffee maker "kg", # Switch @@ -108,6 +110,8 @@ TUYA_SUPPORTED_PRODUCT_CATEGORIES = ( "sgbj", # Siren Alarm "sos", # SOS Button "sp", # Smart Camera + "tgq", # Dimmer + "tyndj", # Solar Light "wk", # Thermostat "xdd", # Ceiling Light "xxj", # Diffuser @@ -132,6 +136,15 @@ PLATFORMS = [ ] +class WorkMode(str, Enum): + """Work modes.""" + + COLOUR = "colour" + MUSIC = "music" + SCENE = "scene" + WHITE = "white" + + class DPCode(str, Enum): """Device Property Codes used by Tuya. @@ -144,11 +157,16 @@ class DPCode(str, Enum): ANION = "anion" # Ionizer unit BATTERY_PERCENTAGE = "battery_percentage" # Battery percentage BATTERY_STATE = "battery_state" # Battery state + BRIGHT_CONTROLLER = "bright_controller" BRIGHT_STATE = "bright_state" # Brightness status BRIGHT_VALUE = "bright_value" # Brightness + BRIGHT_VALUE_1 = "bright_value_1" + BRIGHT_VALUE_2 = "bright_value_2" + BRIGHT_VALUE_V2 = "bright_value_v2" C_F = "c_f" # Temperature unit switching CHILD_LOCK = "child_lock" # Child lock CO2_VALUE = "co2_value" # CO2 concentration + COLOR_DATA_V2 = "color_data_v2" COLOUR_DATA = "colour_data" # Colored light mode COLOUR_DATA_V2 = "colour_data_v2" # Colored light mode CONCENTRATION_SET = "concentration_set" # Concentration setting @@ -176,9 +194,9 @@ class DPCode(str, Enum): RECORD_SWITCH = "record_switch" # Recording switch SENSITIVITY = "sensitivity" # Sensitivity SHAKE = "shake" # Oscillating + SHOCK_STATE = "shock_state" # Vibration status SOS = "sos" # Emergency State SOS_STATE = "sos_state" # Emergency mode - SHOCK_STATE = "shock_state" # Vibration status SPEED = "speed" # Speed level START = "start" # Start SWING = "swing" # Swing mode @@ -190,8 +208,11 @@ class DPCode(str, Enum): SWITCH_5 = "switch_5" # Switch 5 SWITCH_6 = "switch_6" # Switch 6 SWITCH_BACKLIGHT = "switch_backlight" # Backlight switch + SWITCH_CONTROLLER = "switch_controller" SWITCH_HORIZONTAL = "switch_horizontal" # Horizontal swing flap switch SWITCH_LED = "switch_led" # Switch + SWITCH_LED_1 = "switch_led_1" + SWITCH_LED_2 = "switch_led_2" SWITCH_SPRAY = "switch_spray" # Spraying switch SWITCH_USB1 = "switch_usb1" # USB 1 SWITCH_USB2 = "switch_usb2" # USB 2 @@ -201,12 +222,14 @@ class DPCode(str, Enum): SWITCH_USB6 = "switch_usb6" # USB 6 SWITCH_VERTICAL = "switch_vertical" # Vertical swing flap switch SWITCH_VOICE = "switch_voice" # Voice switch + TEMP_CONTROLLER = "temp_controller" TEMP_CURRENT = "temp_current" # Current temperature in °C TEMP_CURRENT_F = "temp_current_f" # Current temperature in °F TEMP_SET = "temp_set" # Set the temperature in °C TEMP_SET_F = "temp_set_f" # Set the temperature in °F TEMP_UNIT_CONVERT = "temp_unit_convert" # Temperature unit switching TEMP_VALUE = "temp_value" # Color temperature + TEMP_VALUE_V2 = "temp_value_v2" TEMPER_ALARM = "temper_alarm" # Tamper alarm UV = "uv" # UV sterilization WARM = "warm" # Heat preservation diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 9758fdefc80..ba0710c390c 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -1,8 +1,8 @@ """Support for the Tuya lights.""" from __future__ import annotations +from dataclasses import dataclass import json -import logging from typing import Any from tuya_iot import TuyaDevice, TuyaDeviceManager @@ -16,6 +16,7 @@ from homeassistant.components.light import ( COLOR_MODE_HS, COLOR_MODE_ONOFF, LightEntity, + LightEntityDescription, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -23,45 +24,179 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantTuyaData -from .base import TuyaEntity -from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode +from .base import IntegerTypeData, TuyaEntity +from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode, WorkMode -_LOGGER = logging.getLogger(__name__) -MIREDS_MAX = 500 -MIREDS_MIN = 153 +@dataclass +class TuyaLightEntityDescription(LightEntityDescription): + """Describe an Tuya light entity.""" -HSV_HA_HUE_MIN = 0 -HSV_HA_HUE_MAX = 360 -HSV_HA_SATURATION_MIN = 0 -HSV_HA_SATURATION_MAX = 100 + color_mode: DPCode | None = None + brightness: DPCode | tuple[DPCode, ...] | None = None + color_temp: DPCode | tuple[DPCode, ...] | None = None + color_data: DPCode | tuple[DPCode, ...] | None = None -WORK_MODE_WHITE = "white" -WORK_MODE_COLOUR = "colour" -# https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq -TUYA_SUPPORT_TYPE = { - "dj", # Light - "dd", # Light strip - "fwl", # Ambient light - "dc", # Light string - "jsq", # Humidifier's light - "xdd", # Ceiling Light - "xxj", # Diffuser's light - "fs", # Fan +LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { + # String Lights + # https://developer.tuya.com/en/docs/iot/dc?id=Kaof7taxmvadu + "dc": ( + TuyaLightEntityDescription( + key=DPCode.SWITCH_LED, + color_mode=DPCode.WORK_MODE, + brightness=DPCode.BRIGHT_VALUE, + color_temp=DPCode.TEMP_VALUE, + color_data=DPCode.COLOUR_DATA, + ), + ), + # Strip Lights + # https://developer.tuya.com/en/docs/iot/dd?id=Kaof804aibg2l + "dd": ( + TuyaLightEntityDescription( + key=DPCode.SWITCH_LED, + color_mode=DPCode.WORK_MODE, + brightness=DPCode.BRIGHT_VALUE, + color_temp=DPCode.TEMP_VALUE, + color_data=DPCode.COLOUR_DATA, + ), + ), + # Light + # https://developer.tuya.com/en/docs/iot/categorydj?id=Kaiuyzy3eheyy + "dj": ( + TuyaLightEntityDescription( + key=DPCode.SWITCH_LED, + color_mode=DPCode.WORK_MODE, + brightness=(DPCode.BRIGHT_VALUE_V2, DPCode.BRIGHT_VALUE), + color_temp=(DPCode.TEMP_VALUE_V2, DPCode.TEMP_VALUE), + color_data=(DPCode.COLOUR_DATA_V2, DPCode.COLOUR_DATA), + ), + ), + # Ceiling Fan Light + # https://developer.tuya.com/en/docs/iot/fsd?id=Kaof8eiei4c2v + "fsd": ( + TuyaLightEntityDescription( + key=DPCode.SWITCH_LED, + color_mode=DPCode.WORK_MODE, + brightness=DPCode.BRIGHT_VALUE, + color_temp=DPCode.TEMP_VALUE, + color_data=DPCode.COLOUR_DATA, + ), + ), + # Ambient Light + # https://developer.tuya.com/en/docs/iot/ambient-light?id=Kaiuz06amhe6g + "fwd": ( + TuyaLightEntityDescription( + key=DPCode.SWITCH_LED, + color_mode=DPCode.WORK_MODE, + brightness=DPCode.BRIGHT_VALUE, + color_temp=DPCode.TEMP_VALUE, + color_data=DPCode.COLOUR_DATA, + ), + ), + # Motion Sensor Light + # https://developer.tuya.com/en/docs/iot/gyd?id=Kaof8a8hycfmy + "gyd": ( + TuyaLightEntityDescription( + key=DPCode.SWITCH_LED, + color_mode=DPCode.WORK_MODE, + brightness=DPCode.BRIGHT_VALUE, + color_temp=DPCode.TEMP_VALUE, + color_data=DPCode.COLOUR_DATA, + ), + ), + # Dimmer + # https://developer.tuya.com/en/docs/iot/tgq?id=Kaof8ke9il4k4 + "tgq": ( + TuyaLightEntityDescription( + key=DPCode.SWITCH_LED_1, + name="Light", + brightness=DPCode.BRIGHT_VALUE_1, + ), + TuyaLightEntityDescription( + key=DPCode.SWITCH_LED_2, + name="Light 2", + brightness=DPCode.BRIGHT_VALUE_2, + ), + ), + # Solar Light + # https://developer.tuya.com/en/docs/iot/tynd?id=Kaof8j02e1t98 + "tyndj": ( + TuyaLightEntityDescription( + key=DPCode.SWITCH_LED, + color_mode=DPCode.WORK_MODE, + brightness=DPCode.BRIGHT_VALUE, + color_temp=DPCode.TEMP_VALUE, + color_data=DPCode.COLOUR_DATA, + ), + ), + # Ceiling Light + # https://developer.tuya.com/en/docs/iot/ceiling-light?id=Kaiuz03xxfc4r + "xdd": ( + TuyaLightEntityDescription( + key=DPCode.SWITCH_LED, + color_mode=DPCode.WORK_MODE, + brightness=DPCode.BRIGHT_VALUE, + color_temp=DPCode.TEMP_VALUE, + color_data=DPCode.COLOUR_DATA, + ), + ), + # Remote Control + # https://developer.tuya.com/en/docs/iot/ykq?id=Kaof8ljn81aov + "ykq": ( + TuyaLightEntityDescription( + key=DPCode.SWITCH_CONTROLLER, + color_mode=DPCode.WORK_MODE, + brightness=DPCode.BRIGHT_CONTROLLER, + color_temp=DPCode.TEMP_CONTROLLER, + ), + ), } -DEFAULT_HSV = { - "h": {"min": 1, "scale": 0, "unit": "", "max": 360, "step": 1}, - "s": {"min": 1, "scale": 0, "unit": "", "max": 255, "step": 1}, - "v": {"min": 1, "scale": 0, "unit": "", "max": 255, "step": 1}, -} -DEFAULT_HSV_V2 = { - "h": {"min": 1, "scale": 0, "unit": "", "max": 360, "step": 1}, - "s": {"min": 1, "scale": 0, "unit": "", "max": 1000, "step": 1}, - "v": {"min": 1, "scale": 0, "unit": "", "max": 1000, "step": 1}, -} +@dataclass +class ColorTypeData: + """Color Type Data.""" + + h_type: IntegerTypeData + s_type: IntegerTypeData + v_type: IntegerTypeData + + +DEFAULT_COLOR_TYPE_DATA = ColorTypeData( + h_type=IntegerTypeData(min=1, scale=0, max=360, step=1), + s_type=IntegerTypeData(min=1, scale=0, max=255, step=1), + v_type=IntegerTypeData(min=1, scale=0, max=255, step=1), +) + +DEFAULT_COLOR_TYPE_DATA_V2 = ColorTypeData( + h_type=IntegerTypeData(min=1, scale=0, max=360, step=1), + s_type=IntegerTypeData(min=1, scale=0, max=1000, step=1), + v_type=IntegerTypeData(min=1, scale=0, max=1000, step=1), +) + + +@dataclass +class ColorData: + """Color Data.""" + + type_data: ColorTypeData + h_value: int + s_value: int + v_value: int + + @property + def hs_color(self) -> tuple[float, float]: + """Get the HS value from this color data.""" + return ( + self.type_data.h_type.remap_value_to(self.h_value, 0, 360), + self.type_data.s_type.remap_value_to(self.s_value, 0, 100), + ) + + @property + def brightness(self) -> int: + """Get the brightness value from this color data.""" + return round(self.type_data.v_type.remap_value_to(self.v_value, 0, 255)) async def async_setup_entry( @@ -76,8 +211,18 @@ async def async_setup_entry( entities: list[TuyaLightEntity] = [] for device_id in device_ids: device = hass_data.device_manager.device_map[device_id] - if device and device.category in TUYA_SUPPORT_TYPE: - entities.append(TuyaLightEntity(device, hass_data.device_manager)) + if descriptions := LIGHTS.get(device.category): + for description in descriptions: + if ( + description.key in device.function + or description.key in device.status + ): + entities.append( + TuyaLightEntity( + device, hass_data.device_manager, description + ) + ) + async_add_entities(entities) async_discover_device([*hass_data.device_manager.device_map]) @@ -90,278 +235,279 @@ async def async_setup_entry( class TuyaLightEntity(TuyaEntity, LightEntity): """Tuya light device.""" - def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> None: + entity_description: TuyaLightEntityDescription + _brightness_dpcode: DPCode | None = None + _brightness_type: IntegerTypeData | None = None + _color_data_dpcode: DPCode | None = None + _color_data_type: ColorTypeData | None = None + _color_temp_dpcode: DPCode | None = None + _color_temp_type: IntegerTypeData | None = None + + def __init__( + self, + device: TuyaDevice, + device_manager: TuyaDeviceManager, + description: TuyaLightEntityDescription, + ) -> None: """Init TuyaHaLight.""" - self.dp_code_bright = DPCode.BRIGHT_VALUE - self.dp_code_temp = DPCode.TEMP_VALUE - self.dp_code_colour = DPCode.COLOUR_DATA - - for key in device.function: - if key.startswith(DPCode.BRIGHT_VALUE): - self.dp_code_bright = key - elif key.startswith(DPCode.TEMP_VALUE): - self.dp_code_temp = key - elif key.startswith(DPCode.COLOUR_DATA): - self.dp_code_colour = key - super().__init__(device, device_manager) + self.entity_description = description + self._attr_unique_id = f"{super().unique_id}{description.key}" + self._attr_supported_color_modes = {COLOR_MODE_ONOFF} + + # Determine brightness DPCodes + if ( + isinstance(description.brightness, DPCode) + and description.brightness in device.function + ): + self._brightness_dpcode = description.brightness + elif isinstance(description.brightness, tuple): + self._brightness_dpcode = next( + ( + dpcode + for dpcode in description.brightness + if dpcode in device.function + ), + None, + ) + + # Determine DPCodes for color temperature + if ( + isinstance(description.color_temp, DPCode) + and description.color_temp in device.function + ): + self._color_temp_dpcode = description.color_temp + elif isinstance(description.color_temp, tuple): + self._color_temp_dpcode = next( + ( + dpcode + for dpcode in description.color_temp + if dpcode in device.function + ), + None, + ) + + # Determine DPCodes for color data + if ( + isinstance(description.color_data, DPCode) + and description.color_data in device.function + ): + self._color_data_dpcode = description.color_data + elif isinstance(description.color_data, tuple): + self._color_data_dpcode = next( + ( + dpcode + for dpcode in description.color_data + if dpcode in device.function + ), + None, + ) + + # Update internals based on found brightness dpcode + if self._brightness_dpcode: + self._attr_supported_color_modes.add(COLOR_MODE_BRIGHTNESS) + self._brightness_type = IntegerTypeData.from_json( + device.status_range[self._brightness_dpcode].values + ) + + # Update internals based on found color temperature dpcode + if self._color_temp_dpcode: + self._attr_supported_color_modes.add(COLOR_MODE_COLOR_TEMP) + self._color_temp_type = IntegerTypeData.from_json( + device.status_range[self._color_temp_dpcode].values + ) + + # Update internals based on found color data dpcode + if self._color_data_dpcode: + self._attr_supported_color_modes.add(COLOR_MODE_HS) + # Fetch color data type information + if function_data := json.loads( + self.device.function[self._color_data_dpcode].values + ): + self._color_data_type = ColorTypeData( + h_type=IntegerTypeData(**function_data["h"]), + s_type=IntegerTypeData(**function_data["s"]), + v_type=IntegerTypeData(**function_data["v"]), + ) + else: + # If no type is found, use a default one + self._color_data_type = DEFAULT_COLOR_TYPE_DATA + if self._color_data_dpcode == DPCode.COLOUR_DATA_V2 or ( + self._brightness_type and self._brightness_type.max > 255 + ): + self._color_data_type = DEFAULT_COLOR_TYPE_DATA_V2 @property def is_on(self) -> bool: """Return true if light is on.""" - return self.device.status.get(DPCode.SWITCH_LED, False) + return self.device.status.get(self.entity_description.key, False) def turn_on(self, **kwargs: Any) -> None: """Turn on or control the light.""" - commands = [] - work_mode = self._work_mode() - _LOGGER.debug("light kwargs-> %s; work_mode %s", kwargs, work_mode) + commands = [{"code": self.entity_description.key, "value": True}] + + if self._color_data_type and ( + ATTR_HS_COLOR in kwargs + or (ATTR_BRIGHTNESS in kwargs and self.color_mode == COLOR_MODE_HS) + ): + if color_mode_dpcode := self.entity_description.color_mode: + commands += [ + { + "code": color_mode_dpcode, + "value": WorkMode.COLOUR, + }, + ] + + if not (brightness := kwargs.get(ATTR_BRIGHTNESS)): + brightness = self.brightness or 0 + + if not (color := kwargs.get(ATTR_HS_COLOR)): + color = self.hs_color or (0, 0) + + commands += [ + { + "code": self._color_data_dpcode, + "value": json.dumps( + { + "h": round( + self._color_data_type.h_type.remap_value_from( + color[0], 0, 360 + ) + ), + "s": round( + self._color_data_type.s_type.remap_value_from( + color[1], 0, 100 + ) + ), + "v": round( + self._color_data_type.v_type.remap_value_from( + brightness + ) + ), + } + ), + }, + ] + + elif ATTR_COLOR_TEMP in kwargs and self._color_temp_type: + if color_mode_dpcode := self.entity_description.color_mode: + commands += [ + { + "code": color_mode_dpcode, + "value": WorkMode.WHITE, + }, + ] + + commands += [ + { + "code": self._color_temp_dpcode, + "value": round( + self._color_temp_type.remap_value_from( + kwargs[ATTR_COLOR_TEMP], + self.min_mireds, + self.max_mireds, + reverse=True, + ) + ), + }, + ] if ( - DPCode.LIGHT in self.device.status - and DPCode.SWITCH_LED not in self.device.status + ATTR_BRIGHTNESS in kwargs + and self.color_mode != COLOR_MODE_HS + and self._brightness_type ): - commands += [{"code": DPCode.LIGHT, "value": True}] - else: - commands += [{"code": DPCode.SWITCH_LED, "value": True}] - - colour_data = self._get_hsv() - v_range = self._tuya_hsv_v_range() - send_colour_data = False - - if ATTR_HS_COLOR in kwargs: - # hsv h - colour_data["h"] = int(kwargs[ATTR_HS_COLOR][0]) - # hsv s - ha_s = kwargs[ATTR_HS_COLOR][1] - s_range = self._tuya_hsv_s_range() - colour_data["s"] = int( - self.remap( - ha_s, - HSV_HA_SATURATION_MIN, - HSV_HA_SATURATION_MAX, - s_range[0], - s_range[1], - ) - ) - # hsv v - ha_v = self.brightness - colour_data["v"] = int(self.remap(ha_v, 0, 255, v_range[0], v_range[1])) - commands += [ - {"code": self.dp_code_colour, "value": json.dumps(colour_data)} - ] - if work_mode != WORK_MODE_COLOUR: - work_mode = WORK_MODE_COLOUR - commands += [{"code": DPCode.WORK_MODE, "value": work_mode}] - - elif ATTR_COLOR_TEMP in kwargs: - # temp color - new_range = self._tuya_temp_range() - color_temp = self.remap( - self.max_mireds - kwargs[ATTR_COLOR_TEMP] + self.min_mireds, - self.min_mireds, - self.max_mireds, - new_range[0], - new_range[1], - ) - commands += [{"code": self.dp_code_temp, "value": int(color_temp)}] - - # brightness - ha_brightness = self.brightness - new_range = self._tuya_brightness_range() - tuya_brightness = self.remap( - ha_brightness, 0, 255, new_range[0], new_range[1] - ) - commands += [{"code": self.dp_code_bright, "value": int(tuya_brightness)}] - - if work_mode != WORK_MODE_WHITE: - work_mode = WORK_MODE_WHITE - commands += [{"code": DPCode.WORK_MODE, "value": WORK_MODE_WHITE}] - - if ATTR_BRIGHTNESS in kwargs: - if work_mode == WORK_MODE_COLOUR: - colour_data["v"] = int( - self.remap(kwargs[ATTR_BRIGHTNESS], 0, 255, v_range[0], v_range[1]) - ) - send_colour_data = True - elif work_mode == WORK_MODE_WHITE: - new_range = self._tuya_brightness_range() - tuya_brightness = int( - self.remap( - kwargs[ATTR_BRIGHTNESS], 0, 255, new_range[0], new_range[1] - ) - ) - commands += [{"code": self.dp_code_bright, "value": tuya_brightness}] - - if send_colour_data: - commands += [ - {"code": self.dp_code_colour, "value": json.dumps(colour_data)} + { + "code": self._brightness_dpcode, + "value": round( + self._brightness_type.remap_value_from(kwargs[ATTR_BRIGHTNESS]) + ), + }, ] self._send_command(commands) def turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" - if ( - DPCode.LIGHT in self.device.status - and DPCode.SWITCH_LED not in self.device.status - ): - commands = [{"code": DPCode.LIGHT, "value": False}] - else: - commands = [{"code": DPCode.SWITCH_LED, "value": False}] - self._send_command(commands) + self._send_command([{"code": self.entity_description.key, "value": False}]) @property def brightness(self) -> int | None: """Return the brightness of the light.""" - old_range = self._tuya_brightness_range() - brightness = self.device.status.get(self.dp_code_bright, 0) + # If the light is currently in color mode, extract the brightness from the color data + if self.color_mode == COLOR_MODE_HS and (color_data := self._get_color_data()): + return color_data.brightness - if self._work_mode().startswith(WORK_MODE_COLOUR): - colour_json = self.device.status.get(self.dp_code_colour) - if not colour_json: - return None - colour_data = json.loads(colour_json) - v_range = self._tuya_hsv_v_range() - hsv_v = colour_data.get("v", 0) - return int(self.remap(hsv_v, v_range[0], v_range[1], 0, 255)) + if not self._brightness_dpcode or not self._brightness_type: + return None - return int(self.remap(brightness, old_range[0], old_range[1], 0, 255)) + brightness = self.device.status.get(self._brightness_dpcode) + if brightness is None: + return None - def _tuya_brightness_range(self) -> tuple[int, int]: - if self.dp_code_bright not in self.device.status: - return 0, 255 - bright_item = self.device.function.get(self.dp_code_bright) - if not bright_item: - return 0, 255 - bright_value = json.loads(bright_item.values) - return bright_value.get("min", 0), bright_value.get("max", 255) + return round(self._brightness_type.remap_value_to(brightness)) @property - def color_mode(self) -> str: - """Return the color_mode of the light.""" - work_mode = self._work_mode() - if work_mode == WORK_MODE_WHITE: - return COLOR_MODE_COLOR_TEMP - return COLOR_MODE_HS + def color_temp(self) -> int | None: + """Return the color_temp of the light.""" + if not self._color_temp_dpcode or not self._color_temp_type: + return None + + temperature = self.device.status.get(self._color_temp_dpcode) + if temperature is None: + return None + + return round( + self._color_temp_type.remap_value_to( + temperature, self.min_mireds, self.max_mireds, reverse=True + ) + ) @property def hs_color(self) -> tuple[float, float] | None: """Return the hs_color of the light.""" - colour_json = self.device.status.get(self.dp_code_colour) - if not colour_json: + if self._color_data_dpcode is None or not ( + color_data := self._get_color_data() + ): return None - colour_data = json.loads(colour_json) - s_range = self._tuya_hsv_s_range() - return colour_data.get("h", 0), self.remap( - colour_data.get("s", 0), - s_range[0], - s_range[1], - HSV_HA_SATURATION_MIN, - HSV_HA_SATURATION_MAX, + return color_data.hs_color + + @property + def color_mode(self) -> str: + """Return the color_mode of the light.""" + # We consider it to be in HS color mode, when work mode is anything + # else than "white". + if ( + self.entity_description.color_mode + and self.device.status.get(self.entity_description.color_mode) + != WorkMode.WHITE + ): + return COLOR_MODE_HS + if self._color_temp_dpcode: + return COLOR_MODE_COLOR_TEMP + if self._brightness_dpcode: + return COLOR_MODE_BRIGHTNESS + return COLOR_MODE_ONOFF + + def _get_color_data(self) -> ColorData | None: + """Get current color data from device.""" + if ( + self._color_data_type is None + or self._color_data_dpcode is None + or self._color_data_dpcode not in self.device.status + ): + return None + + if not (status_data := self.device.status[self._color_data_dpcode]): + return None + + if not (status := json.loads(status_data)): + return None + + return ColorData( + type_data=self._color_data_type, + h_value=status["h"], + s_value=status["s"], + v_value=status["v"], ) - - @property - def color_temp(self) -> int: - """Return the color_temp of the light.""" - new_range = self._tuya_temp_range() - tuya_color_temp = self.device.status.get(self.dp_code_temp, 0) - return ( - self.max_mireds - - self.remap( - tuya_color_temp, - new_range[0], - new_range[1], - self.min_mireds, - self.max_mireds, - ) - + self.min_mireds - ) - - @property - def min_mireds(self) -> int: - """Return color temperature min mireds.""" - return MIREDS_MIN - - @property - def max_mireds(self) -> int: - """Return color temperature max mireds.""" - return MIREDS_MAX - - def _tuya_temp_range(self) -> tuple[int, int]: - temp_item = self.device.function.get(self.dp_code_temp) - if not temp_item: - return 0, 255 - temp_value = json.loads(temp_item.values) - return temp_value.get("min", 0), temp_value.get("max", 255) - - def _tuya_hsv_s_range(self) -> tuple[int, int]: - hsv_data_range = self._tuya_hsv_function() - if hsv_data_range is not None: - hsv_s = hsv_data_range.get("s", {"min": 0, "max": 255}) - return hsv_s.get("min", 0), hsv_s.get("max", 255) - return 0, 255 - - def _tuya_hsv_v_range(self) -> tuple[int, int]: - hsv_data_range = self._tuya_hsv_function() - if hsv_data_range is not None: - hsv_v = hsv_data_range.get("v", {"min": 0, "max": 255}) - return hsv_v.get("min", 0), hsv_v.get("max", 255) - - return 0, 255 - - def _tuya_hsv_function(self) -> dict[str, dict] | None: - hsv_item = self.device.function.get(self.dp_code_colour) - if not hsv_item: - return None - hsv_data = json.loads(hsv_item.values) - if hsv_data: - return hsv_data - colour_json = self.device.status.get(self.dp_code_colour) - if not colour_json: - return None - colour_data = json.loads(colour_json) - if ( - self.dp_code_colour == DPCode.COLOUR_DATA_V2 - or colour_data.get("v", 0) > 255 - or colour_data.get("s", 0) > 255 - ): - return DEFAULT_HSV_V2 - return DEFAULT_HSV - - def _work_mode(self) -> str: - return self.device.status.get(DPCode.WORK_MODE, "") - - def _get_hsv(self) -> dict[str, int]: - if ( - self.dp_code_colour not in self.device.status - or len(self.device.status[self.dp_code_colour]) == 0 - ): - return {"h": 0, "s": 0, "v": 0} - - return json.loads(self.device.status[self.dp_code_colour]) - - @property - def supported_color_modes(self) -> set[str] | None: - """Flag supported color modes.""" - color_modes = [COLOR_MODE_ONOFF] - if self.dp_code_bright in self.device.status: - color_modes.append(COLOR_MODE_BRIGHTNESS) - - if self.dp_code_temp in self.device.status: - color_modes.append(COLOR_MODE_COLOR_TEMP) - - if ( - self.dp_code_colour in self.device.status - and len(self.device.status[self.dp_code_colour]) > 0 - ): - color_modes.append(COLOR_MODE_HS) - return set(color_modes) - - @staticmethod - def remap(old_value, old_min, old_max, new_min, new_max): - """Remap old_value to new_value.""" - return ((old_value - old_min) / (old_max - old_min)) * ( - new_max - new_min - ) + new_min From 8b21a36e37a31e74423f6fd5534de85f8c2e32e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klomp?= Date: Mon, 18 Oct 2021 20:59:37 +0200 Subject: [PATCH 0509/1038] Bump pysma to 0.6.7 (#57978) --- 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 da24627d268..13e29c8227c 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.6"], + "requirements": ["pysma==0.6.7"], "codeowners": ["@kellerza", "@rklomp"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 14feee4753b..908640308b1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1793,7 +1793,7 @@ pysignalclirestapi==0.3.4 pyskyqhub==0.1.3 # homeassistant.components.sma -pysma==0.6.6 +pysma==0.6.7 # homeassistant.components.smappee pysmappee==0.2.27 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d89d60206ec..0e0b69d3fb0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1070,7 +1070,7 @@ pysiaalarm==3.0.2 pysignalclirestapi==0.3.4 # homeassistant.components.sma -pysma==0.6.6 +pysma==0.6.7 # homeassistant.components.smappee pysmappee==0.2.27 From fe8b9caf9992d7973a49b01cfc430769de0d52b9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 18 Oct 2021 21:48:50 +0200 Subject: [PATCH 0510/1038] Bump pychromecast to 9.3.0 (#57991) --- homeassistant/components/cast/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 092d122d5cf..89c13cf04bd 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==9.2.1"], + "requirements": ["pychromecast==9.3.0"], "after_dependencies": [ "cloud", "http", diff --git a/requirements_all.txt b/requirements_all.txt index 908640308b1..a480c3909bb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1384,7 +1384,7 @@ pycfdns==1.2.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==9.2.1 +pychromecast==9.3.0 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0e0b69d3fb0..6e601c3fe3a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -823,7 +823,7 @@ pybotvac==0.0.22 pycfdns==1.2.1 # homeassistant.components.cast -pychromecast==9.2.1 +pychromecast==9.3.0 # homeassistant.components.climacell pyclimacell==0.18.2 From bc5422d73707bca5cd88f5759ac92d52d7eecf13 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 18 Oct 2021 22:23:06 +0200 Subject: [PATCH 0511/1038] Bump fjaraskupan to 1.0.2 (#57992) --- .../components/fjaraskupan/__init__.py | 21 +++++++++++-------- .../components/fjaraskupan/config_flow.py | 6 ++++-- .../components/fjaraskupan/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index 40113744977..b6eda82ea10 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -9,7 +9,7 @@ import logging from bleak import BleakScanner from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData -from fjaraskupan import Device, State, device_filter +from fjaraskupan import UUID_SERVICE, Device, State, device_filter from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -48,7 +48,7 @@ class EntryState: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Fjäråskupan from a config entry.""" - scanner = BleakScanner() + scanner = BleakScanner(filters={"UUIDs": [str(UUID_SERVICE)]}) state = EntryState(scanner, {}) hass.data.setdefault(DOMAIN, {}) @@ -57,17 +57,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def detection_callback( ble_device: BLEDevice, advertisement_data: AdvertisementData ) -> None: - if not device_filter(ble_device, advertisement_data): - return - - _LOGGER.debug( - "Detection: %s %s - %s", ble_device.name, ble_device, advertisement_data - ) - if data := state.devices.get(ble_device.address): + _LOGGER.debug( + "Update: %s %s - %s", ble_device.name, ble_device, advertisement_data + ) + data.device.detection_callback(ble_device, advertisement_data) data.coordinator.async_set_updated_data(data.device.state) else: + if not device_filter(ble_device, advertisement_data): + return + + _LOGGER.debug( + "Detected: %s %s - %s", ble_device.name, ble_device, advertisement_data + ) device = Device(ble_device) device.detection_callback(ble_device, advertisement_data) diff --git a/homeassistant/components/fjaraskupan/config_flow.py b/homeassistant/components/fjaraskupan/config_flow.py index 9b82ae1199b..4d4d1882dcd 100644 --- a/homeassistant/components/fjaraskupan/config_flow.py +++ b/homeassistant/components/fjaraskupan/config_flow.py @@ -7,7 +7,7 @@ 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 fjaraskupan import UUID_SERVICE, device_filter from homeassistant.helpers.config_entry_flow import register_discovery_flow @@ -25,7 +25,9 @@ async def _async_has_devices(hass) -> bool: if device_filter(device, advertisement_data): event.set() - async with BleakScanner(detection_callback=detection): + async with BleakScanner( + detection_callback=detection, filters={"UUIDs": [str(UUID_SERVICE)]} + ): try: async with async_timeout.timeout(CONST_WAIT_TIME): await event.wait() diff --git a/homeassistant/components/fjaraskupan/manifest.json b/homeassistant/components/fjaraskupan/manifest.json index d9cd5640848..fb27d8b803f 100644 --- a/homeassistant/components/fjaraskupan/manifest.json +++ b/homeassistant/components/fjaraskupan/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/fjaraskupan", "requirements": [ - "fjaraskupan==1.0.1" + "fjaraskupan==1.0.2" ], "codeowners": [ "@elupus" diff --git a/requirements_all.txt b/requirements_all.txt index a480c3909bb..b7a2bd80a4a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -646,7 +646,7 @@ fitbit==0.3.1 fixerio==1.0.0a0 # homeassistant.components.fjaraskupan -fjaraskupan==1.0.1 +fjaraskupan==1.0.2 # homeassistant.components.flipr flipr-api==1.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6e601c3fe3a..1780c0d54aa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -378,7 +378,7 @@ faadelays==0.0.7 feedparser==6.0.2 # homeassistant.components.fjaraskupan -fjaraskupan==1.0.1 +fjaraskupan==1.0.2 # homeassistant.components.flipr flipr-api==1.4.1 From ae660d93665999fe7f8f497da8c46c6be64435c7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 18 Oct 2021 10:27:33 -1000 Subject: [PATCH 0512/1038] Pickup codeowner for bond (#57995) --- CODEOWNERS | 2 +- homeassistant/components/bond/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 5687b8100da..d25abecb8ed 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -75,7 +75,7 @@ homeassistant/components/blink/* @fronzbot homeassistant/components/blueprint/* @home-assistant/core homeassistant/components/bmp280/* @belidzs homeassistant/components/bmw_connected_drive/* @gerard33 @rikroe -homeassistant/components/bond/* @prystupa @joshs85 +homeassistant/components/bond/* @bdraco @prystupa @joshs85 homeassistant/components/bosch_shc/* @tschamm homeassistant/components/braviatv/* @bieniu @Drafteed homeassistant/components/broadlink/* @danielhiversen @felipediel diff --git a/homeassistant/components/bond/manifest.json b/homeassistant/components/bond/manifest.json index 6f11b8c66e3..a8395b68d60 100644 --- a/homeassistant/components/bond/manifest.json +++ b/homeassistant/components/bond/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/bond", "requirements": ["bond-api==0.1.14"], "zeroconf": ["_bond._tcp.local."], - "codeowners": ["@prystupa", "@joshs85"], + "codeowners": ["@bdraco", "@prystupa", "@joshs85"], "quality_scale": "platinum", "iot_class": "local_push" } From 174eaefe61a2cc234981e496c3ccc1eab107c2ad Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 18 Oct 2021 23:30:26 +0200 Subject: [PATCH 0513/1038] Add vacuum platform to Tuya (#57996) --- .coveragerc | 1 + homeassistant/components/tuya/const.py | 9 ++ homeassistant/components/tuya/vacuum.py | 167 ++++++++++++++++++++++++ 3 files changed, 177 insertions(+) create mode 100644 homeassistant/components/tuya/vacuum.py diff --git a/.coveragerc b/.coveragerc index 5198d8c34b2..64d9c8e9b76 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1123,6 +1123,7 @@ omit = homeassistant/components/tuya/sensor.py homeassistant/components/tuya/siren.py homeassistant/components/tuya/switch.py + homeassistant/components/tuya/vacuum.py homeassistant/components/twentemilieu/const.py homeassistant/components/twentemilieu/sensor.py homeassistant/components/twilio_call/notify.py diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 259fbc73b9e..9c0c1ce2105 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -107,6 +107,7 @@ TUYA_SUPPORTED_PRODUCT_CATEGORIES = ( "pc", # Power Strip "pir", # PIR Detector "qn", # Heater + "sd", # Robot vacuum "sgbj", # Siren Alarm "sos", # SOS Button "sp", # Smart Camera @@ -133,6 +134,7 @@ PLATFORMS = [ "sensor", "siren", "switch", + "vacuum", ] @@ -175,6 +177,7 @@ class DPCode(str, Enum): CUR_POWER = "cur_power" # Actual power CUR_VOLTAGE = "cur_voltage" # Actual voltage DOORCONTACT_STATE = "doorcontact_state" # Status of door window sensor + ELECTRICITY_LEFT = "electricity_left" FAN_DIRECTION = "fan_direction" # Fan direction FAN_SPEED_ENUM = "fan_speed_enum" # Speed mode FAN_SPEED_PERCENT = "fan_speed_percent" # Stepless speed @@ -188,10 +191,13 @@ class DPCode(str, Enum): MODE = "mode" # Working mode / Mode MOTION_SWITCH = "motion_switch" # Motion switch MUFFLING = "muffling" # Muffling + PAUSE = "pause" PIR = "pir" # Motion sensor POWDER_SET = "powder_set" # Powder + POWER_GO = "power_go" PUMP_RESET = "pump_reset" # Water pump reset RECORD_SWITCH = "record_switch" # Recording switch + SEEK = "seek" SENSITIVITY = "sensitivity" # Sensitivity SHAKE = "shake" # Oscillating SHOCK_STATE = "shock_state" # Vibration status @@ -199,6 +205,8 @@ class DPCode(str, Enum): SOS_STATE = "sos_state" # Emergency mode SPEED = "speed" # Speed level START = "start" # Start + STATUS = "status" + SUCTION = "suction" SWING = "swing" # Swing mode SWITCH = "switch" # Switch SWITCH_1 = "switch_1" # Switch 1 @@ -208,6 +216,7 @@ class DPCode(str, Enum): SWITCH_5 = "switch_5" # Switch 5 SWITCH_6 = "switch_6" # Switch 6 SWITCH_BACKLIGHT = "switch_backlight" # Backlight switch + SWITCH_CHARGE = "switch_charge" SWITCH_CONTROLLER = "switch_controller" SWITCH_HORIZONTAL = "switch_horizontal" # Horizontal swing flap switch SWITCH_LED = "switch_led" # Switch diff --git a/homeassistant/components/tuya/vacuum.py b/homeassistant/components/tuya/vacuum.py new file mode 100644 index 00000000000..25596da01b5 --- /dev/null +++ b/homeassistant/components/tuya/vacuum.py @@ -0,0 +1,167 @@ +"""Support for Tuya Vacuums.""" +from __future__ import annotations + +from typing import Any + +from tuya_iot import TuyaDevice, TuyaDeviceManager + +from homeassistant.components.vacuum import ( + STATE_CLEANING, + STATE_DOCKED, + STATE_IDLE, + STATE_PAUSED, + STATE_RETURNING, + SUPPORT_BATTERY, + SUPPORT_FAN_SPEED, + SUPPORT_PAUSE, + SUPPORT_RETURN_HOME, + SUPPORT_START, + SUPPORT_STATE, + SUPPORT_STATUS, + SUPPORT_STOP, + StateVacuumEntity, +) +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 . import HomeAssistantTuyaData +from .base import EnumTypeData, IntegerTypeData, TuyaEntity +from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode + +TUYA_STATUS_TO_HA = { + "charge_done": STATE_DOCKED, + "chargecompleted": STATE_DOCKED, + "charging": STATE_DOCKED, + "cleaning": STATE_CLEANING, + "docking": STATE_RETURNING, + "goto_charge": STATE_RETURNING, + "goto_pos": STATE_CLEANING, + "mop_clean": STATE_CLEANING, + "part_clean": STATE_CLEANING, + "paused": STATE_PAUSED, + "pick_zone_clean": STATE_CLEANING, + "pos_arrived": STATE_CLEANING, + "pos_unarrive": STATE_CLEANING, + "sleep": STATE_IDLE, + "smart_clean": STATE_CLEANING, + "spot_clean": STATE_CLEANING, + "standby": STATE_IDLE, + "wall_clean": STATE_CLEANING, + "zone_clean": STATE_CLEANING, +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Tuya vacuum dynamically through Tuya discovery.""" + hass_data: HomeAssistantTuyaData = hass.data[DOMAIN][entry.entry_id] + + @callback + def async_discover_device(device_ids: list[str]) -> None: + """Discover and add a discovered Tuya vacuum.""" + entities: list[TuyaVacuumEntity] = [] + for device_id in device_ids: + device = hass_data.device_manager.device_map[device_id] + if device.category == "sd": + entities.append(TuyaVacuumEntity(device, hass_data.device_manager)) + async_add_entities(entities) + + async_discover_device([*hass_data.device_manager.device_map]) + + entry.async_on_unload( + async_dispatcher_connect(hass, TUYA_DISCOVERY_NEW, async_discover_device) + ) + + +class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity): + """Tuya Vacuum Device.""" + + _fan_speed_type: EnumTypeData | None = None + _battery_level_type: IntegerTypeData | None = None + _supported_features = 0 + + def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> None: + """Init Tuya vacuum.""" + super().__init__(device, device_manager) + + if DPCode.PAUSE in self.device.status: + self._supported_features |= SUPPORT_PAUSE + + if DPCode.SWITCH_CHARGE in self.device.status: + self._supported_features |= SUPPORT_RETURN_HOME + + if DPCode.STATUS in self.device.status: + self._supported_features |= SUPPORT_STATE | SUPPORT_STATUS + + if DPCode.POWER_GO in self.device.status: + self._supported_features |= SUPPORT_STOP | SUPPORT_START + + if function := device.function.get(DPCode.SUCTION): + self._supported_features |= SUPPORT_FAN_SPEED + self._fan_speed_type = EnumTypeData.from_json(function.values) + + if function := device.function.get(DPCode.ELECTRICITY_LEFT): + self._supported_features |= SUPPORT_BATTERY + self._battery_level_type = IntegerTypeData.from_json(function.values) + + @property + def battery_level(self) -> int | None: + """Return Tuya device state.""" + if self._battery_level_type is None or not ( + status := self.device.status.get(DPCode.ELECTRICITY_LEFT) + ): + return None + return round(self._battery_level_type.scale_value(status)) + + @property + def fan_speed(self) -> str | None: + """Return the fan speed of the vacuum cleaner.""" + return self.device.status.get(DPCode.SUCTION) + + @property + def fan_speed_list(self) -> list[str]: + """Get the list of available fan speed steps of the vacuum cleaner.""" + if self._fan_speed_type is None: + return [] + return self._fan_speed_type.range + + @property + def state(self) -> str | None: + """Return Tuya vacuum device state.""" + if self.device.status.get(DPCode.PAUSE): + return STATE_PAUSED + if not (status := self.device.status.get(DPCode.STATUS)): + return None + return TUYA_STATUS_TO_HA.get(status) + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return self._supported_features + + def start(self, **kwargs: Any) -> None: + """Turn the device on.""" + self._send_command([{"code": DPCode.POWER_GO, "value": True}]) + + def stop(self, **kwargs: Any) -> None: + """Turn the device off.""" + self._send_command([{"code": DPCode.POWER_GO, "value": False}]) + + def pause(self, **kwargs: Any) -> None: + """Pause the device.""" + self._send_command([{"code": DPCode.POWER_GO, "value": True}]) + + def return_to_base(self, **kwargs: Any) -> None: + """Return device to dock.""" + self._send_command([{"code": DPCode.MODE, "value": "chargego"}]) + + def locate(self, **kwargs: Any) -> None: + """Return device to dock.""" + self._send_command([{"code": DPCode.SEEK, "value": True}]) + + def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: + """Set fan speed.""" + self._send_command([{"code": DPCode.SUCTION, "value": fan_speed}]) From 9bd2115a2010b5f45740b84202fa9d86796249bc Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 18 Oct 2021 23:34:51 +0200 Subject: [PATCH 0514/1038] Motion blinds add interface and wait_for_push options (#50067) Co-authored-by: Martin Hjelmare --- .../components/motion_blinds/__init__.py | 39 ++++- .../components/motion_blinds/config_flow.py | 113 ++++++++++++-- .../components/motion_blinds/const.py | 5 + .../components/motion_blinds/manifest.json | 1 + .../components/motion_blinds/strings.json | 17 ++- .../motion_blinds/translations/en.json | 17 ++- .../motion_blinds/test_config_flow.py | 141 ++++++++++++++++-- 7 files changed, 300 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py index a4fb003b546..14bdeae817b 100644 --- a/homeassistant/components/motion_blinds/__init__.py +++ b/homeassistant/components/motion_blinds/__init__.py @@ -3,7 +3,7 @@ from datetime import timedelta import logging from socket import timeout -from motionblinds import MotionMulticast, ParseException +from motionblinds import AsyncMotionMulticast, ParseException from homeassistant import config_entries, core from homeassistant.const import CONF_API_KEY, CONF_HOST, EVENT_HOMEASSISTANT_STOP @@ -13,6 +13,10 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( ATTR_AVAILABLE, + CONF_INTERFACE, + CONF_WAIT_FOR_PUSH, + DEFAULT_INTERFACE, + DEFAULT_WAIT_FOR_PUSH, DOMAIN, KEY_COORDINATOR, KEY_GATEWAY, @@ -34,7 +38,7 @@ class DataUpdateCoordinatorMotionBlinds(DataUpdateCoordinator): self, hass, logger, - gateway, + coordinator_info, *, name, update_interval=None, @@ -49,7 +53,8 @@ class DataUpdateCoordinatorMotionBlinds(DataUpdateCoordinator): update_interval=update_interval, ) - self._gateway = gateway + self._gateway = coordinator_info[KEY_GATEWAY] + self._wait_for_push = coordinator_info[CONF_WAIT_FOR_PUSH] def update_gateway(self): """Call all updates using one async_add_executor_job.""" @@ -66,7 +71,10 @@ class DataUpdateCoordinatorMotionBlinds(DataUpdateCoordinator): for blind in self._gateway.device_list.values(): try: - blind.Update() + if self._wait_for_push: + blind.Update() + else: + blind.Update_trigger() except (timeout, ParseException): # let the error be logged and handled by the motionblinds library data[blind.mac] = {ATTR_AVAILABLE: False} @@ -95,13 +103,17 @@ async def async_setup_entry( hass.data.setdefault(DOMAIN, {}) host = entry.data[CONF_HOST] key = entry.data[CONF_API_KEY] + 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 if KEY_MULTICAST_LISTENER not in hass.data[DOMAIN]: - multicast = MotionMulticast() + multicast = AsyncMotionMulticast(interface=multicast_interface) hass.data[DOMAIN][KEY_MULTICAST_LISTENER] = multicast # start listening for local pushes (only once) - await hass.async_add_executor_job(multicast.Start_listen) + await multicast.Start_listen() # register stop callback to shutdown listening for local pushes def stop_motion_multicast(event): @@ -117,11 +129,15 @@ async def async_setup_entry( if not await connect_gateway_class.async_connect_gateway(host, key): raise ConfigEntryNotReady motion_gateway = connect_gateway_class.gateway_device + coordinator_info = { + KEY_GATEWAY: motion_gateway, + CONF_WAIT_FOR_PUSH: wait_for_push, + } coordinator = DataUpdateCoordinatorMotionBlinds( hass, _LOGGER, - motion_gateway, + coordinator_info, # Name of the data. For logging purposes. name=entry.title, # Polling interval. Will only be polled if there are subscribers. @@ -172,6 +188,13 @@ async def async_unload_entry( # No motion gateways left, stop Motion multicast _LOGGER.debug("Shutting down Motion Listener") multicast = hass.data[DOMAIN].pop(KEY_MULTICAST_LISTENER) - await hass.async_add_executor_job(multicast.Stop_listen) + multicast.Stop_listen() return unload_ok + + +async def update_listener( + hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry +) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(config_entry.entry_id) diff --git a/homeassistant/components/motion_blinds/config_flow.py b/homeassistant/components/motion_blinds/config_flow.py index 796911cef6e..12d4a3440ec 100644 --- a/homeassistant/components/motion_blinds/config_flow.py +++ b/homeassistant/components/motion_blinds/config_flow.py @@ -1,11 +1,22 @@ """Config flow to configure Motion Blinds using their WLAN API.""" -from motionblinds import MotionDiscovery +from socket import gaierror + +from motionblinds import AsyncMotionMulticast, MotionDiscovery import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import network from homeassistant.const import CONF_API_KEY, CONF_HOST +from homeassistant.core import callback -from .const import DEFAULT_GATEWAY_NAME, DOMAIN +from .const import ( + CONF_INTERFACE, + CONF_WAIT_FOR_PUSH, + DEFAULT_GATEWAY_NAME, + DEFAULT_INTERFACE, + DEFAULT_WAIT_FOR_PUSH, + DOMAIN, +) from .gateway import ConnectMotionGateway CONFIG_SCHEMA = vol.Schema( @@ -14,11 +25,34 @@ CONFIG_SCHEMA = vol.Schema( } ) -CONFIG_SETTINGS = vol.Schema( - { - vol.Required(CONF_API_KEY): vol.All(str, vol.Length(min=16, max=16)), - } -) + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Options for the component.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Init object.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the options.""" + errors = {} + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + settings_schema = vol.Schema( + { + vol.Optional( + CONF_WAIT_FOR_PUSH, + default=self.config_entry.options.get( + CONF_WAIT_FOR_PUSH, DEFAULT_WAIT_FOR_PUSH + ), + ): bool, + } + ) + + return self.async_show_form( + step_id="init", data_schema=settings_schema, errors=errors + ) class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -30,6 +64,13 @@ class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Initialize the Motion Blinds flow.""" self._host = None self._ips = [] + self._config_settings = None + + @staticmethod + @callback + def async_get_options_flow(config_entry) -> OptionsFlowHandler: + """Get the options flow.""" + return OptionsFlowHandler(config_entry) async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" @@ -70,8 +111,24 @@ class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_connect(self, user_input=None): """Connect to the Motion Gateway.""" + errors = {} if user_input is not None: key = user_input[CONF_API_KEY] + multicast_interface = user_input[CONF_INTERFACE] + + # check socket interface + if multicast_interface != DEFAULT_INTERFACE: + motion_multicast = AsyncMotionMulticast(interface=multicast_interface) + try: + await motion_multicast.Start_listen() + motion_multicast.Stop_listen() + except gaierror: + errors[CONF_INTERFACE] = "invalid_interface" + return self.async_show_form( + step_id="connect", + data_schema=self._config_settings, + errors=errors, + ) connect_gateway_class = ConnectMotionGateway(self.hass, multicast=None) if not await connect_gateway_class.async_connect_gateway(self._host, key): @@ -85,7 +142,45 @@ class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry( title=DEFAULT_GATEWAY_NAME, - data={CONF_HOST: self._host, CONF_API_KEY: key}, + data={ + CONF_HOST: self._host, + CONF_API_KEY: key, + CONF_INTERFACE: multicast_interface, + }, ) - return self.async_show_form(step_id="connect", data_schema=CONFIG_SETTINGS) + (interfaces, default_interface) = await self.async_get_interfaces() + + self._config_settings = vol.Schema( + { + vol.Required(CONF_API_KEY): vol.All(str, vol.Length(min=16, max=16)), + vol.Optional(CONF_INTERFACE, default=default_interface): vol.In( + interfaces + ), + } + ) + + return self.async_show_form( + step_id="connect", data_schema=self._config_settings, errors=errors + ) + + async def async_get_interfaces(self): + """Get list of interface to use.""" + interfaces = [DEFAULT_INTERFACE] + enabled_interfaces = [] + default_interface = DEFAULT_INTERFACE + + adapters = await network.async_get_adapters(self.hass) + for adapter in adapters: + if ipv4s := adapter["ipv4"]: + ip4 = ipv4s[0]["address"] + interfaces.append(ip4) + if adapter["enabled"]: + enabled_interfaces.append(ip4) + if adapter["default"]: + default_interface = ip4 + + if len(enabled_interfaces) == 1: + default_interface = enabled_interfaces[0] + + return (interfaces, default_interface) diff --git a/homeassistant/components/motion_blinds/const.py b/homeassistant/components/motion_blinds/const.py index 52c6e39b096..fca6b694fad 100644 --- a/homeassistant/components/motion_blinds/const.py +++ b/homeassistant/components/motion_blinds/const.py @@ -5,6 +5,11 @@ DEFAULT_GATEWAY_NAME = "Motion Blinds Gateway" PLATFORMS = ["cover", "sensor"] +CONF_WAIT_FOR_PUSH = "wait_for_push" +CONF_INTERFACE = "interface" +DEFAULT_WAIT_FOR_PUSH = False +DEFAULT_INTERFACE = "any" + KEY_GATEWAY = "gateway" KEY_COORDINATOR = "coordinator" KEY_MULTICAST_LISTENER = "multicast_listener" diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index 346729925e9..7ff87c1b58b 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -4,6 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/motion_blinds", "requirements": ["motionblinds==0.5.7"], + "dependencies": ["network"], "codeowners": ["@starkillerOG"], "iot_class": "local_push" } diff --git a/homeassistant/components/motion_blinds/strings.json b/homeassistant/components/motion_blinds/strings.json index 4511b316cd6..e5c86c2a45e 100644 --- a/homeassistant/components/motion_blinds/strings.json +++ b/homeassistant/components/motion_blinds/strings.json @@ -12,7 +12,8 @@ "title": "Motion Blinds", "description": "You will need the 16 character API Key, see https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instructions", "data": { - "api_key": "[%key:common::config_flow::data::api_key%]" + "api_key": "[%key:common::config_flow::data::api_key%]", + "interface": "The network interface to use" } }, "select": { @@ -24,13 +25,25 @@ } }, "error": { - "discovery_error": "Failed to discover a Motion Gateway" + "discovery_error": "Failed to discover a Motion Gateway", + "invalid_interface": "Invalid network interface" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "connection_error": "[%key:common::config_flow::error::cannot_connect%]" } + }, + "options": { + "step": { + "init": { + "title": "Motion Blinds", + "description": "Specify optional settings", + "data": { + "wait_for_push": "Wait for multicast push on update" + } + } + } } } diff --git a/homeassistant/components/motion_blinds/translations/en.json b/homeassistant/components/motion_blinds/translations/en.json index 3a968bc6491..233e019e255 100644 --- a/homeassistant/components/motion_blinds/translations/en.json +++ b/homeassistant/components/motion_blinds/translations/en.json @@ -6,13 +6,15 @@ "connection_error": "Failed to connect" }, "error": { - "discovery_error": "Failed to discover a Motion Gateway" + "discovery_error": "Failed to discover a Motion Gateway", + "invalid_interface": "Invalid network interface" }, "flow_title": "Motion Blinds", "step": { "connect": { "data": { - "api_key": "API Key" + "api_key": "API Key", + "interface": "The network interface to use" }, "description": "You will need the 16 character API Key, see https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instructions", "title": "Motion Blinds" @@ -33,5 +35,16 @@ "title": "Motion Blinds" } } + }, + "options": { + "step": { + "init": { + "title": "Motion Blinds", + "description": "Specify optional settings", + "data": { + "wait_for_push": "Wait for multicast push on update" + } + } + } } } \ No newline at end of file diff --git a/tests/components/motion_blinds/test_config_flow.py b/tests/components/motion_blinds/test_config_flow.py index 18592421249..b5e2f8fb717 100644 --- a/tests/components/motion_blinds/test_config_flow.py +++ b/tests/components/motion_blinds/test_config_flow.py @@ -4,13 +4,16 @@ from unittest.mock import Mock, patch import pytest -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.motion_blinds import const from homeassistant.components.motion_blinds.config_flow import DEFAULT_GATEWAY_NAME -from homeassistant.components.motion_blinds.const import DOMAIN from homeassistant.const import CONF_API_KEY, CONF_HOST +from tests.common import MockConfigEntry + TEST_HOST = "1.2.3.4" TEST_HOST2 = "5.6.7.8" +TEST_HOST_HA = "9.10.11.12" TEST_API_KEY = "12ab345c-d67e-8f" TEST_MAC = "ab:cd:ef:gh" TEST_MAC2 = "ij:kl:mn:op" @@ -56,9 +59,13 @@ TEST_DISCOVERY_2 = { }, } +TEST_INTERFACES = [ + {"enabled": True, "default": True, "ipv4": [{"address": TEST_HOST_HA}]} +] + @pytest.fixture(name="motion_blinds_connect", autouse=True) -def motion_blinds_connect_fixture(): +def motion_blinds_connect_fixture(mock_get_source_ip): """Mock motion blinds connection and entry setup.""" with patch( "homeassistant.components.motion_blinds.gateway.MotionGateway.GetDeviceList", @@ -72,6 +79,15 @@ def motion_blinds_connect_fixture(): ), patch( "homeassistant.components.motion_blinds.config_flow.MotionDiscovery.discover", return_value=TEST_DISCOVERY_1, + ), patch( + "homeassistant.components.motion_blinds.config_flow.AsyncMotionMulticast.Start_listen", + return_value=True, + ), patch( + "homeassistant.components.motion_blinds.config_flow.AsyncMotionMulticast.Stop_listen", + return_value=True, + ), patch( + "homeassistant.components.motion_blinds.config_flow.network.async_get_adapters", + return_value=TEST_INTERFACES, ), patch( "homeassistant.components.motion_blinds.async_setup_entry", return_value=True ): @@ -81,7 +97,7 @@ def motion_blinds_connect_fixture(): async def test_config_flow_manual_host_success(hass): """Successful flow manually initialized by the user.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" @@ -95,7 +111,7 @@ async def test_config_flow_manual_host_success(hass): assert result["type"] == "form" assert result["step_id"] == "connect" - assert result["errors"] is None + assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -107,13 +123,14 @@ async def test_config_flow_manual_host_success(hass): assert result["data"] == { CONF_HOST: TEST_HOST, CONF_API_KEY: TEST_API_KEY, + const.CONF_INTERFACE: TEST_HOST_HA, } async def test_config_flow_discovery_1_success(hass): """Successful flow with 1 gateway discovered.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" @@ -127,7 +144,7 @@ async def test_config_flow_discovery_1_success(hass): assert result["type"] == "form" assert result["step_id"] == "connect" - assert result["errors"] is None + assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -139,13 +156,14 @@ async def test_config_flow_discovery_1_success(hass): assert result["data"] == { CONF_HOST: TEST_HOST, CONF_API_KEY: TEST_API_KEY, + const.CONF_INTERFACE: TEST_HOST_HA, } async def test_config_flow_discovery_2_success(hass): """Successful flow with 2 gateway discovered.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" @@ -176,7 +194,7 @@ async def test_config_flow_discovery_2_success(hass): assert result["type"] == "form" assert result["step_id"] == "connect" - assert result["errors"] is None + assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -188,13 +206,14 @@ async def test_config_flow_discovery_2_success(hass): assert result["data"] == { CONF_HOST: TEST_HOST2, CONF_API_KEY: TEST_API_KEY, + const.CONF_INTERFACE: TEST_HOST_HA, } async def test_config_flow_connection_error(hass): """Failed flow manually initialized by the user with connection timeout.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" @@ -208,7 +227,7 @@ async def test_config_flow_connection_error(hass): assert result["type"] == "form" assert result["step_id"] == "connect" - assert result["errors"] is None + assert result["errors"] == {} with patch( "homeassistant.components.motion_blinds.gateway.MotionGateway.GetDeviceList", @@ -226,7 +245,7 @@ async def test_config_flow_connection_error(hass): async def test_config_flow_discovery_fail(hass): """Failed flow with no gateways discovered.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == "form" @@ -245,3 +264,101 @@ async def test_config_flow_discovery_fail(hass): assert result["type"] == "form" assert result["step_id"] == "user" assert result["errors"] == {"base": "discovery_error"} + + +async def test_config_flow_interface(hass): + """Successful flow manually initialized by the user with interface specified.""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: TEST_HOST}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "connect" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: TEST_API_KEY, const.CONF_INTERFACE: TEST_HOST_HA}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == DEFAULT_GATEWAY_NAME + assert result["data"] == { + CONF_HOST: TEST_HOST, + CONF_API_KEY: TEST_API_KEY, + const.CONF_INTERFACE: TEST_HOST_HA, + } + + +async def test_config_flow_invalid_interface(hass): + """Failed flow manually initialized by the user with invalid interface.""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: TEST_HOST}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "connect" + assert result["errors"] == {} + + with patch( + "homeassistant.components.motion_blinds.config_flow.AsyncMotionMulticast.Start_listen", + side_effect=socket.gaierror, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: TEST_API_KEY, const.CONF_INTERFACE: TEST_HOST_HA}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "connect" + assert result["errors"] == {const.CONF_INTERFACE: "invalid_interface"} + + +async def test_options_flow(hass): + """Test specifying non default settings using options flow.""" + config_entry = MockConfigEntry( + domain=const.DOMAIN, + unique_id=TEST_MAC, + data={ + CONF_HOST: TEST_HOST, + CONF_API_KEY: TEST_API_KEY, + }, + title=DEFAULT_GATEWAY_NAME, + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={const.CONF_WAIT_FOR_PUSH: False}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + const.CONF_WAIT_FOR_PUSH: False, + } From 6f0a7d29216d37b7b1b5c3f84ce83f8379a40834 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Mon, 18 Oct 2021 18:38:51 -0300 Subject: [PATCH 0515/1038] Add to the Broadlink integration support for voltage, current, overload and total consumption sensors (#53628) --- homeassistant/components/broadlink/sensor.py | 42 ++++++- tests/components/broadlink/__init__.py | 10 ++ tests/components/broadlink/test_sensors.py | 110 ++++++++++++++++++- 3 files changed, 160 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index 676edb53b9a..8f739deeaa8 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -6,16 +6,28 @@ import logging import voluptuous as vol from homeassistant.components.sensor import ( + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, PLATFORM_SCHEMA, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, SensorEntityDescription, ) -from homeassistant.const import CONF_HOST, PERCENTAGE, POWER_WATT, TEMP_CELSIUS +from homeassistant.const import ( + CONF_HOST, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, + PERCENTAGE, + POWER_WATT, + TEMP_CELSIUS, +) from homeassistant.helpers import config_validation as cv from .const import DOMAIN @@ -59,6 +71,34 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), + SensorEntityDescription( + key="volt", + name="Voltage", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="current", + name="Current", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="overload", + name="Overload", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="totalconsum", + name="Total consumption", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), ) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/tests/components/broadlink/__init__.py b/tests/components/broadlink/__init__.py index c65870add96..a323fd5e276 100644 --- a/tests/components/broadlink/__init__.py +++ b/tests/components/broadlink/__init__.py @@ -58,6 +58,16 @@ BROADLINK_DEVICES = { 20025, 5, ), + "Dining room": ( + "192.168.0.16", + "34ea34b4fd1c", + "SCB1E", + "Broadlink", + "SP4B", + 0x5115, + 57, + 5, + ), "Kitchen": ( # Not supported. "192.168.0.64", "34ea34b61d2c", diff --git a/tests/components/broadlink/test_sensors.py b/tests/components/broadlink/test_sensors.py index 1f8f913cfe4..b7fccd2e2ff 100644 --- a/tests/components/broadlink/test_sensors.py +++ b/tests/components/broadlink/test_sensors.py @@ -1,10 +1,14 @@ """Tests for Broadlink sensors.""" +from datetime import timedelta + from homeassistant.components.broadlink.const import DOMAIN, SENSOR_DOMAIN +from homeassistant.components.broadlink.updater import BroadlinkSP4UpdateManager from homeassistant.helpers.entity_registry import async_entries_for_device +from homeassistant.util import dt from . import get_device -from tests.common import mock_device_registry, mock_registry +from tests.common import async_fire_time_changed, mock_device_registry, mock_registry async def test_a1_sensor_setup(hass): @@ -286,3 +290,107 @@ async def test_rm4_pro_no_sensor(hass): entries = async_entries_for_device(entity_registry, device_entry.id) sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} assert len(sensors) == 0 + + +async def test_scb1e_sensor_setup(hass): + """Test a successful SCB1E sensor setup.""" + device = get_device("Dining room") + mock_api = device.get_mock_api() + mock_api.get_state.return_value = { + "pwr": 1, + "indicator": 1, + "maxworktime": 0, + "power": 255.57, + "volt": 121.7, + "current": 2.1, + "overload": 0, + "totalconsum": 1.7, + "childlock": 0, + } + + device_registry = mock_device_registry(hass) + entity_registry = mock_registry(hass) + + mock_setup = await device.setup_entry(hass, mock_api=mock_api) + + assert mock_api.get_state.call_count == 1 + device_entry = device_registry.async_get_device( + {(DOMAIN, mock_setup.entry.unique_id)} + ) + entries = async_entries_for_device(entity_registry, device_entry.id) + sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] + assert len(sensors) == 5 + + sensors_and_states = { + (sensor.original_name, hass.states.get(sensor.entity_id).state) + for sensor in sensors + } + assert sensors_and_states == { + (f"{device.name} Current power", "255.57"), + (f"{device.name} Voltage", "121.7"), + (f"{device.name} Current", "2.1"), + (f"{device.name} Overload", "0"), + (f"{device.name} Total consumption", "1.7"), + } + + +async def test_scb1e_sensor_update(hass): + """Test a successful SCB1E sensor update.""" + device = get_device("Dining room") + mock_api = device.get_mock_api() + mock_api.get_state.return_value = { + "pwr": 1, + "indicator": 1, + "maxworktime": 0, + "power": 255.6, + "volt": 121.7, + "current": 2.1, + "overload": 0, + "totalconsum": 1.7, + "childlock": 0, + } + + device_registry = mock_device_registry(hass) + entity_registry = mock_registry(hass) + + target_time = ( + dt.utcnow() + BroadlinkSP4UpdateManager.SCAN_INTERVAL * 3 + timedelta(seconds=1) + ) + + mock_setup = await device.setup_entry(hass, mock_api=mock_api) + + device_entry = device_registry.async_get_device( + {(DOMAIN, mock_setup.entry.unique_id)} + ) + entries = async_entries_for_device(entity_registry, device_entry.id) + sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] + assert len(sensors) == 5 + + mock_setup.api.get_state.return_value = { + "pwr": 1, + "indicator": 1, + "maxworktime": 0, + "power": 291.8, + "volt": 121.6, + "current": 2.4, + "overload": 0, + "totalconsum": 0.5, + "childlock": 0, + } + + async_fire_time_changed(hass, target_time) + await hass.async_block_till_done() + + assert mock_setup.api.get_state.call_count == 2 + + sensors_and_states = { + (sensor.original_name, hass.states.get(sensor.entity_id).state) + for sensor in sensors + } + assert sensors_and_states == { + (f"{device.name} Current power", "291.8"), + (f"{device.name} Voltage", "121.6"), + (f"{device.name} Current", "2.4"), + (f"{device.name} Overload", "0"), + (f"{device.name} Total consumption", "0.5"), + } From 334e3062a6ce9ff528b4079e87437f58cb8858fc Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 19 Oct 2021 11:51:29 +1300 Subject: [PATCH 0516/1038] Add configuration url to Juicenet (#57999) --- homeassistant/components/juicenet/entity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/juicenet/entity.py b/homeassistant/components/juicenet/entity.py index 9b1def3b678..2ad6bd9b774 100644 --- a/homeassistant/components/juicenet/entity.py +++ b/homeassistant/components/juicenet/entity.py @@ -26,4 +26,5 @@ class JuiceNetDevice(CoordinatorEntity): "identifiers": {(DOMAIN, self.device.id)}, "name": self.device.name, "manufacturer": "JuiceNet", + "configuration_url": f"https://home.juice.net/Portal/Details?unitID={self.device.id}", } From 3855bb43ec42212a24f50016fff2a70edd3d4893 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 19 Oct 2021 00:12:08 +0000 Subject: [PATCH 0517/1038] [ci skip] Translation update --- .../components/abode/translations/bg.json | 3 ++ .../alarmdecoder/translations/bg.json | 9 +++++ .../binary_sensor/translations/hu.json | 4 +++ .../binary_sensor/translations/it.json | 4 +++ .../components/bsblan/translations/bg.json | 3 ++ .../components/canary/translations/bg.json | 13 +++++++ .../coolmaster/translations/bg.json | 1 + .../components/deconz/translations/bg.json | 3 +- .../components/dexcom/translations/bg.json | 7 ++++ .../dialogflow/translations/bg.json | 3 ++ .../components/dlna_dmr/translations/bg.json | 3 ++ .../components/dsmr/translations/bg.json | 1 + .../components/elgato/translations/bg.json | 6 ++++ .../components/geofency/translations/bg.json | 3 ++ .../components/goalzero/translations/bg.json | 15 +++++++- .../components/gpslogger/translations/bg.json | 3 ++ .../homekit_controller/translations/bg.json | 3 +- .../components/ifttt/translations/bg.json | 3 ++ .../components/insteon/translations/bg.json | 9 +++-- .../islamic_prayer_times/translations/bg.json | 7 ++++ .../components/kodi/translations/bg.json | 5 ++- .../components/locative/translations/bg.json | 3 ++ .../components/mailgun/translations/bg.json | 3 ++ .../motion_blinds/translations/en.json | 6 ++-- .../motion_blinds/translations/it.json | 17 +++++++-- .../nightscout/translations/bg.json | 10 +++++- .../components/nzbget/translations/bg.json | 11 +++++- .../components/omnilogic/translations/bg.json | 15 +++++++- .../openweathermap/translations/bg.json | 26 ++++++++++++++ .../components/owntracks/translations/bg.json | 3 ++ .../components/ozw/translations/bg.json | 7 ++++ .../components/plaato/translations/bg.json | 3 ++ .../components/plex/translations/bg.json | 1 + .../progettihwsw/translations/bg.json | 7 +++- .../components/remote/translations/bg.json | 10 ++++++ .../components/rfxtrx/translations/bg.json | 7 ++++ .../components/risco/translations/bg.json | 9 +++++ .../components/rpi_power/translations/bg.json | 12 +++++++ .../components/sharkiq/translations/bg.json | 22 ++++++++++-- .../components/shelly/translations/bg.json | 22 +++++++++++- .../components/soma/translations/it.json | 8 ++--- .../components/somfy/translations/bg.json | 3 +- .../components/sonarr/translations/bg.json | 6 ++++ .../speedtestdotnet/translations/bg.json | 7 ++++ .../components/spotify/translations/bg.json | 9 +++++ .../components/traccar/translations/bg.json | 3 ++ .../components/twilio/translations/bg.json | 3 ++ .../uptimerobot/translations/it.json | 4 +-- .../components/velbus/translations/bg.json | 4 +++ .../vlc_telnet/translations/hu.json | 8 ++++- .../vlc_telnet/translations/it.json | 36 +++++++++++++++++++ .../components/wilight/translations/bg.json | 8 +++++ .../xiaomi_miio/translations/it.json | 3 +- .../components/yeelight/translations/bg.json | 26 ++++++++++++++ .../zodiac/translations/sensor.bg.json | 18 ++++++++++ .../zoneminder/translations/bg.json | 15 ++++++++ 56 files changed, 436 insertions(+), 27 deletions(-) create mode 100644 homeassistant/components/dexcom/translations/bg.json create mode 100644 homeassistant/components/islamic_prayer_times/translations/bg.json create mode 100644 homeassistant/components/ozw/translations/bg.json create mode 100644 homeassistant/components/rpi_power/translations/bg.json create mode 100644 homeassistant/components/speedtestdotnet/translations/bg.json create mode 100644 homeassistant/components/spotify/translations/bg.json create mode 100644 homeassistant/components/vlc_telnet/translations/it.json create mode 100644 homeassistant/components/wilight/translations/bg.json create mode 100644 homeassistant/components/zodiac/translations/sensor.bg.json diff --git a/homeassistant/components/abode/translations/bg.json b/homeassistant/components/abode/translations/bg.json index 285bf18d330..7ed0fae081a 100644 --- a/homeassistant/components/abode/translations/bg.json +++ b/homeassistant/components/abode/translations/bg.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 Abode." }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/alarmdecoder/translations/bg.json b/homeassistant/components/alarmdecoder/translations/bg.json index 3d12f7d19a5..b918c0c7710 100644 --- a/homeassistant/components/alarmdecoder/translations/bg.json +++ b/homeassistant/components/alarmdecoder/translations/bg.json @@ -1,10 +1,19 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "step": { "protocol": { "data": { + "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" } + }, + "user": { + "data": { + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b" + } } } } diff --git a/homeassistant/components/binary_sensor/translations/hu.json b/homeassistant/components/binary_sensor/translations/hu.json index d8befd7ae35..f9de016d9c8 100644 --- a/homeassistant/components/binary_sensor/translations/hu.json +++ b/homeassistant/components/binary_sensor/translations/hu.json @@ -31,6 +31,7 @@ "is_not_plugged_in": "{entity_name} nincs csatlakoztatva", "is_not_powered": "{entity_name} nincs fesz\u00fcts\u00e9g alatt", "is_not_present": "{entity_name} nincs jelen", + "is_not_tampered": "{entity_name} nem \u00e9szlel manipul\u00e1l\u00e1st", "is_not_unsafe": "{entity_name} biztons\u00e1gos", "is_occupied": "{entity_name} foglalt", "is_off": "{entity_name} ki van kapcsolva", @@ -42,6 +43,7 @@ "is_problem": "{entity_name} probl\u00e9m\u00e1t \u00e9szlel", "is_smoke": "{entity_name} f\u00fcst\u00f6t \u00e9rz\u00e9kel", "is_sound": "{entity_name} hangot \u00e9rz\u00e9kel", + "is_tampered": "{entity_name} manipul\u00e1l\u00e1st \u00e9szlel", "is_unsafe": "{entity_name} nem biztons\u00e1gos", "is_update": "{entity_name} egy friss\u00edt\u00e9s \u00e1ll rendelkez\u00e9sre", "is_vibration": "{entity_name} rezg\u00e9st \u00e9rz\u00e9kel" @@ -52,6 +54,8 @@ "connected": "{entity_name} csatlakozik", "gas": "{entity_name} g\u00e1zt \u00e9rz\u00e9kel", "hot": "{entity_name} felforr\u00f3sodik", + "is_not_tampered": "{entity_name} nem \u00e9szlelt manipul\u00e1l\u00e1st", + "is_tampered": "{entity_name} manipul\u00e1l\u00e1st \u00e9szlelt", "light": "{entity_name} f\u00e9nyt \u00e9rz\u00e9kel", "locked": "{entity_name} be lett z\u00e1rva", "moist": "{entity_name} nedves lett", diff --git a/homeassistant/components/binary_sensor/translations/it.json b/homeassistant/components/binary_sensor/translations/it.json index b6301ed8f62..ef16af64af7 100644 --- a/homeassistant/components/binary_sensor/translations/it.json +++ b/homeassistant/components/binary_sensor/translations/it.json @@ -31,6 +31,7 @@ "is_not_plugged_in": "{entity_name} \u00e8 collegato", "is_not_powered": "{entity_name} non \u00e8 alimentato", "is_not_present": "{entity_name} non \u00e8 presente", + "is_not_tampered": "{entity_name} non rileva manomissioni", "is_not_unsafe": "{entity_name} \u00e8 sicuro", "is_occupied": "{entity_name} \u00e8 occupato", "is_off": "{entity_name} \u00e8 spento", @@ -42,6 +43,7 @@ "is_problem": "{entity_name} sta rilevando un problema", "is_smoke": "{entity_name} sta rilevando il fumo", "is_sound": "{entity_name} sta rilevando il suono", + "is_tampered": "{entity_name} rileva manomissioni", "is_unsafe": "{entity_name} non \u00e8 sicuro", "is_update": "{entity_name} ha un aggiornamento disponibile", "is_vibration": "{entity_name} sta rilevando la vibrazione" @@ -52,6 +54,8 @@ "connected": "{entity_name} connesso", "gas": "{entity_name} ha iniziato a rilevare il gas", "hot": "{entity_name} \u00e8 diventato caldo", + "is_not_tampered": "{entity_name} ha smesso di rilevare manomissioni", + "is_tampered": "{entity_name} ha iniziato a rilevare manomissioni", "light": "{entity_name} ha iniziato a rilevare la luce", "locked": "{entity_name} bloccato", "moist": "{entity_name} diventato umido", diff --git a/homeassistant/components/bsblan/translations/bg.json b/homeassistant/components/bsblan/translations/bg.json index 76be712c2d5..6b82cd374e7 100644 --- a/homeassistant/components/bsblan/translations/bg.json +++ b/homeassistant/components/bsblan/translations/bg.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "flow_title": "{name}", "step": { "user": { diff --git a/homeassistant/components/canary/translations/bg.json b/homeassistant/components/canary/translations/bg.json index 2ac8a444100..337f1384446 100644 --- a/homeassistant/components/canary/translations/bg.json +++ b/homeassistant/components/canary/translations/bg.json @@ -1,7 +1,20 @@ { "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/coolmaster/translations/bg.json b/homeassistant/components/coolmaster/translations/bg.json index 079082c01cf..72b8df6634d 100644 --- a/homeassistant/components/coolmaster/translations/bg.json +++ b/homeassistant/components/coolmaster/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "no_units": "\u041d\u0435 \u0431\u044f\u0445\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u043a\u043b\u0438\u043c\u0430\u0442\u0438\u0447\u043d\u0438/\u0432\u0435\u043d\u0442\u0438\u043b\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0441\u0438\u0441\u0442\u0435\u043c\u0438 \u043d\u0430 \u0437\u0430\u0434\u0430\u0434\u0435\u043d\u0438\u044f CoolMasterNet \u0430\u0434\u0440\u0435\u0441." }, "step": { diff --git a/homeassistant/components/deconz/translations/bg.json b/homeassistant/components/deconz/translations/bg.json index 2f339bf73a7..3fe700efd3e 100644 --- a/homeassistant/components/deconz/translations/bg.json +++ b/homeassistant/components/deconz/translations/bg.json @@ -79,7 +79,8 @@ "deconz_devices": { "data": { "allow_clip_sensor": "\u0420\u0430\u0437\u0440\u0435\u0448\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 deCONZ CLIP \u0441\u0435\u043d\u0437\u043e\u0440\u0438", - "allow_deconz_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438 deCONZ \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u043d\u0438 \u0433\u0440\u0443\u043f\u0438" + "allow_deconz_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438 deCONZ \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u043d\u0438 \u0433\u0440\u0443\u043f\u0438", + "allow_new_devices": "\u0420\u0430\u0437\u0440\u0435\u0448\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u044f\u043d\u0435 \u043d\u0430 \u043d\u043e\u0432\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" }, "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0439\u0442\u0435 \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u0442\u0430 \u043d\u0430 \u0442\u0438\u043f\u043e\u0432\u0435\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 deCONZ" } diff --git a/homeassistant/components/dexcom/translations/bg.json b/homeassistant/components/dexcom/translations/bg.json new file mode 100644 index 00000000000..2ac8a444100 --- /dev/null +++ b/homeassistant/components/dexcom/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dialogflow/translations/bg.json b/homeassistant/components/dialogflow/translations/bg.json index cc8faa1f0fd..d27bddfcd05 100644 --- a/homeassistant/components/dialogflow/translations/bg.json +++ b/homeassistant/components/dialogflow/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, "create_entry": { "default": "\u0417\u0430 \u0434\u0430 \u0438\u0437\u043f\u0440\u0430\u0449\u0430\u0442\u0435 \u0441\u044a\u0431\u0438\u0442\u0438\u044f \u0434\u043e Home Assistant, \u0449\u0435 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 [\u0444\u0443\u043d\u043a\u0446\u0438\u044f\u0442\u0430 webhook \u0432 Dialogflow]({dialogflow_url}). \n\n \u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0441\u043b\u0435\u0434\u043d\u0430\u0442\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f: \n\n - URL: ` {webhook_url} ` \n - Method: POST\n - Content Type: application/json\n\n \u0412\u0438\u0436\u0442\u0435 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430]({docs_url}) \u0437\u0430 \u043f\u043e\u0432\u0435\u0447\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438." }, diff --git a/homeassistant/components/dlna_dmr/translations/bg.json b/homeassistant/components/dlna_dmr/translations/bg.json index 9f4eca6ca56..3c266fff82b 100644 --- a/homeassistant/components/dlna_dmr/translations/bg.json +++ b/homeassistant/components/dlna_dmr/translations/bg.json @@ -10,6 +10,9 @@ }, "flow_title": "{name}", "step": { + "confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0442\u0430?" + }, "user": { "data": { "url": "URL" diff --git a/homeassistant/components/dsmr/translations/bg.json b/homeassistant/components/dsmr/translations/bg.json index fafad0e471c..73e82d9f048 100644 --- a/homeassistant/components/dsmr/translations/bg.json +++ b/homeassistant/components/dsmr/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "cannot_communicate": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430 \u043a\u043e\u043c\u0443\u043d\u0438\u043a\u0430\u0446\u0438\u044f" }, "error": { diff --git a/homeassistant/components/elgato/translations/bg.json b/homeassistant/components/elgato/translations/bg.json index 4983c9a14b2..d00cbb30191 100644 --- a/homeassistant/components/elgato/translations/bg.json +++ b/homeassistant/components/elgato/translations/bg.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/geofency/translations/bg.json b/homeassistant/components/geofency/translations/bg.json index de2e8af5d97..916336f37a4 100644 --- a/homeassistant/components/geofency/translations/bg.json +++ b/homeassistant/components/geofency/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, "create_entry": { "default": "\u0417\u0430 \u0434\u0430 \u0438\u0437\u043f\u0440\u0430\u0449\u0430\u0442\u0435 \u0441\u044a\u0431\u0438\u0442\u0438\u044f \u0434\u043e Home Assistant, \u0449\u0435 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u044f\u0442\u0430 webhook \u0432 Geofency. \n\n \u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0441\u043b\u0435\u0434\u043d\u0430\u0442\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n\n \u0412\u0438\u0436\u0442\u0435 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430]({docs_url}) \u0437\u0430 \u043f\u043e\u0432\u0435\u0447\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438." }, diff --git a/homeassistant/components/goalzero/translations/bg.json b/homeassistant/components/goalzero/translations/bg.json index 2ac8a444100..7a50cb7d9cc 100644 --- a/homeassistant/components/goalzero/translations/bg.json +++ b/homeassistant/components/goalzero/translations/bg.json @@ -1,7 +1,20 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_host": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0438\u043c\u0435 \u043d\u0430 \u0445\u043e\u0441\u0442 \u0438\u043b\u0438 IP \u0430\u0434\u0440\u0435\u0441", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u0418\u043c\u0435" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/gpslogger/translations/bg.json b/homeassistant/components/gpslogger/translations/bg.json index 895cf27ee5b..8e1049d859e 100644 --- a/homeassistant/components/gpslogger/translations/bg.json +++ b/homeassistant/components/gpslogger/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, "create_entry": { "default": "\u0417\u0430 \u0434\u0430 \u0438\u0437\u043f\u0440\u0430\u0449\u0430\u0442\u0435 \u0441\u044a\u0431\u0438\u0442\u0438\u044f \u0434\u043e Home Assistant, \u0449\u0435 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u044f\u0442\u0430 webhook \u0432 GPSLogger. \n\n \u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0441\u043b\u0435\u0434\u043d\u0430\u0442\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n\n \u0412\u0438\u0436\u0442\u0435 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430]({docs_url}) \u0437\u0430 \u043f\u043e\u0432\u0435\u0447\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438." }, diff --git a/homeassistant/components/homekit_controller/translations/bg.json b/homeassistant/components/homekit_controller/translations/bg.json index 01439889734..4bedc7bcb27 100644 --- a/homeassistant/components/homekit_controller/translations/bg.json +++ b/homeassistant/components/homekit_controller/translations/bg.json @@ -45,7 +45,8 @@ "button6": "\u0411\u0443\u0442\u043e\u043d 6", "button7": "\u0411\u0443\u0442\u043e\u043d 7", "button8": "\u0411\u0443\u0442\u043e\u043d 8", - "button9": "\u0411\u0443\u0442\u043e\u043d 9" + "button9": "\u0411\u0443\u0442\u043e\u043d 9", + "doorbell": "\u0417\u0432\u044a\u043d\u0435\u0446 \u043d\u0430 \u0432\u0440\u0430\u0442\u0430\u0442\u0430" } }, "title": "HomeKit \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" diff --git a/homeassistant/components/ifttt/translations/bg.json b/homeassistant/components/ifttt/translations/bg.json index 98f24af2997..3a23d735f76 100644 --- a/homeassistant/components/ifttt/translations/bg.json +++ b/homeassistant/components/ifttt/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, "create_entry": { "default": "\u0417\u0430 \u0434\u0430 \u0438\u0437\u043f\u0440\u0430\u0449\u0430\u0442\u0435 \u0441\u044a\u0431\u0438\u0442\u0438\u044f \u0434\u043e Home Assistant, \u0449\u0435 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \"Make a web request\" \u043e\u0442 [IFTTT Webhook \u0430\u043f\u043b\u0435\u0442]({applet_url}). \n\n\u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0441\u043b\u0435\u0434\u043d\u0430\u0442\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json\n\n \u0412\u0438\u0436\u0442\u0435 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430]({docs_url}) \u0437\u0430 \u0442\u043e\u0432\u0430 \u043a\u0430\u043a \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438\u0442\u0435 \u0437\u0430 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u043d\u0430 \u0432\u0445\u043e\u0434\u044f\u0449\u0438 \u0434\u0430\u043d\u043d\u0438." }, diff --git a/homeassistant/components/insteon/translations/bg.json b/homeassistant/components/insteon/translations/bg.json index 923715c0559..3f29fc43cbd 100644 --- a/homeassistant/components/insteon/translations/bg.json +++ b/homeassistant/components/insteon/translations/bg.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" @@ -9,12 +10,16 @@ "step": { "hubv1": { "data": { + "host": "IP \u0430\u0434\u0440\u0435\u0441", "port": "\u041f\u043e\u0440\u0442" } }, "hubv2": { "data": { - "port": "\u041f\u043e\u0440\u0442" + "host": "IP \u0430\u0434\u0440\u0435\u0441", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } } } diff --git a/homeassistant/components/islamic_prayer_times/translations/bg.json b/homeassistant/components/islamic_prayer_times/translations/bg.json new file mode 100644 index 00000000000..1c6120581b0 --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/bg.json b/homeassistant/components/kodi/translations/bg.json index 0eb3c94dd0d..2d54c793bb5 100644 --- a/homeassistant/components/kodi/translations/bg.json +++ b/homeassistant/components/kodi/translations/bg.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" @@ -9,6 +11,7 @@ "step": { "user": { "data": { + "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" } }, diff --git a/homeassistant/components/locative/translations/bg.json b/homeassistant/components/locative/translations/bg.json index b75490ab4f8..9c79f86d4f7 100644 --- a/homeassistant/components/locative/translations/bg.json +++ b/homeassistant/components/locative/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, "create_entry": { "default": "\u0417\u0430 \u0434\u0430 \u0438\u0437\u043f\u0440\u0430\u0449\u0430\u0442\u0435 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043a\u044a\u043c Home Assistant, \u0449\u0435 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u044f\u0442\u0430 webhook \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e Locative. \n\n \u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0441\u043b\u0435\u0434\u043d\u0430\u0442\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f: \n\n - URL: ` {webhook_url} ` \n - \u041c\u0435\u0442\u043e\u0434: POST \n\n \u0412\u0438\u0436\u0442\u0435 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430]({docs_url}) \u0437\u0430 \u043f\u043e\u0432\u0435\u0447\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438." }, diff --git a/homeassistant/components/mailgun/translations/bg.json b/homeassistant/components/mailgun/translations/bg.json index 19eaa6facf3..d9bcdd38dfd 100644 --- a/homeassistant/components/mailgun/translations/bg.json +++ b/homeassistant/components/mailgun/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, "create_entry": { "default": "\u0417\u0430 \u0434\u0430 \u0438\u0437\u043f\u0440\u0430\u0449\u0430\u0442\u0435 \u0441\u044a\u0431\u0438\u0442\u0438\u044f \u0434\u043e Home Assistant, \u0449\u0435 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 [Webhooks \u0441 Mailgun]({mailgun_url}). \n\n\u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0441\u043b\u0435\u0434\u043d\u0430\u0442\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json\n\n \u0412\u0438\u0436\u0442\u0435 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430]({docs_url}) \u0437\u0430 \u0442\u043e\u0432\u0430 \u043a\u0430\u043a \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438\u0442\u0435 \u0437\u0430 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u043d\u0430 \u0432\u0445\u043e\u0434\u044f\u0449\u0438 \u0434\u0430\u043d\u043d\u0438." }, diff --git a/homeassistant/components/motion_blinds/translations/en.json b/homeassistant/components/motion_blinds/translations/en.json index 233e019e255..8f0d21addce 100644 --- a/homeassistant/components/motion_blinds/translations/en.json +++ b/homeassistant/components/motion_blinds/translations/en.json @@ -39,11 +39,11 @@ "options": { "step": { "init": { - "title": "Motion Blinds", - "description": "Specify optional settings", "data": { "wait_for_push": "Wait for multicast push on update" - } + }, + "description": "Specify optional settings", + "title": "Motion Blinds" } } } diff --git a/homeassistant/components/motion_blinds/translations/it.json b/homeassistant/components/motion_blinds/translations/it.json index 1d79ae28ee5..29b7ac8e950 100644 --- a/homeassistant/components/motion_blinds/translations/it.json +++ b/homeassistant/components/motion_blinds/translations/it.json @@ -6,13 +6,15 @@ "connection_error": "Impossibile connettersi" }, "error": { - "discovery_error": "Impossibile rilevare un Motion Gateway" + "discovery_error": "Impossibile rilevare un Motion Gateway", + "invalid_interface": "Interfaccia di rete non valida" }, "flow_title": "Tende Motion", "step": { "connect": { "data": { - "api_key": "Chiave API" + "api_key": "Chiave API", + "interface": "L'interfaccia di rete da utilizzare" }, "description": "Avrai bisogno della chiave API di 16 caratteri, consulta https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key per le istruzioni", "title": "Motion Blinds" @@ -33,5 +35,16 @@ "title": "Tende Motion" } } + }, + "options": { + "step": { + "init": { + "data": { + "wait_for_push": "Attendi il push multicast all'aggiornamento" + }, + "description": "Specificare le impostazioni opzionali", + "title": "Motion Blinds" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/bg.json b/homeassistant/components/nightscout/translations/bg.json index 2ac8a444100..93e70be433e 100644 --- a/homeassistant/components/nightscout/translations/bg.json +++ b/homeassistant/components/nightscout/translations/bg.json @@ -1,7 +1,15 @@ { "config": { "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "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" + }, + "step": { + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/nzbget/translations/bg.json b/homeassistant/components/nzbget/translations/bg.json index a610a1f2a64..544917ac15e 100644 --- a/homeassistant/components/nzbget/translations/bg.json +++ b/homeassistant/components/nzbget/translations/bg.json @@ -1,12 +1,21 @@ { "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, + "flow_title": "{name}", "step": { "user": { "data": { - "port": "\u041f\u043e\u0440\u0442" + "host": "\u0425\u043e\u0441\u0442", + "name": "\u0418\u043c\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } } } diff --git a/homeassistant/components/omnilogic/translations/bg.json b/homeassistant/components/omnilogic/translations/bg.json index 2ac8a444100..10a7388a24c 100644 --- a/homeassistant/components/omnilogic/translations/bg.json +++ b/homeassistant/components/omnilogic/translations/bg.json @@ -1,7 +1,20 @@ { "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/openweathermap/translations/bg.json b/homeassistant/components/openweathermap/translations/bg.json index 2ac8a444100..463ddf48132 100644 --- a/homeassistant/components/openweathermap/translations/bg.json +++ b/homeassistant/components/openweathermap/translations/bg.json @@ -1,7 +1,33 @@ { "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" + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447", + "language": "\u0415\u0437\u0438\u043a", + "latitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0448\u0438\u0440\u0438\u043d\u0430", + "longitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0434\u044a\u043b\u0436\u0438\u043d\u0430", + "mode": "\u0420\u0435\u0436\u0438\u043c", + "name": "\u0418\u043c\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, + "title": "OpenWeatherMap" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "\u0415\u0437\u0438\u043a", + "mode": "\u0420\u0435\u0436\u0438\u043c" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/owntracks/translations/bg.json b/homeassistant/components/owntracks/translations/bg.json index 1e6cc45107a..b69f7ada2a2 100644 --- a/homeassistant/components/owntracks/translations/bg.json +++ b/homeassistant/components/owntracks/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, "create_entry": { "default": "\n\n \u0412 Android \u043e\u0442\u0432\u043e\u0440\u0435\u0442\u0435 [\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e OwnTracks]({android_url}), \u043f\u0440\u0435\u043c\u0438\u043d\u0435\u0442\u0435 \u043a\u044a\u043c Preferences - > Connection. \u041f\u0440\u043e\u043c\u0435\u043d\u0435\u0442\u0435 \u0441\u043b\u0435\u0434\u043d\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438: \n - Mode: Private HTTP\n - Host: {webhook_url} \n - Identification: \n - Username: ` ` \n - Device ID: ` ` \n\n \u0412 iOS \u043e\u0442\u0432\u043e\u0440\u0435\u0442\u0435 [\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e OwnTracks]({ios_url}), \u0434\u043e\u043a\u043e\u0441\u043d\u0435\u0442\u0435 (i) \u0438\u043a\u043e\u043d\u0430\u0442\u0430 \u0432 \u0433\u043e\u0440\u043d\u0438\u044f \u043b\u044f\u0432 \u044a\u0433\u044a\u043b - > Settings. \u041f\u0440\u043e\u043c\u0435\u043d\u0435\u0442\u0435 \u0441\u043b\u0435\u0434\u043d\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438: \n - Mode: HTTP \n - URL: {webhook_url} \n - Turn on authentication \n - UserID: ` ` \n\n {secret} \n \n \u0412\u0438\u0436\u0442\u0435 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430]({docs_url}) \u0437\u0430 \u043f\u043e\u0432\u0435\u0447\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f." }, diff --git a/homeassistant/components/ozw/translations/bg.json b/homeassistant/components/ozw/translations/bg.json new file mode 100644 index 00000000000..1c6120581b0 --- /dev/null +++ b/homeassistant/components/ozw/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/translations/bg.json b/homeassistant/components/plaato/translations/bg.json index dc86bb69256..d9dd41dbb80 100644 --- a/homeassistant/components/plaato/translations/bg.json +++ b/homeassistant/components/plaato/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, "create_entry": { "default": "\u0417\u0430 \u0434\u0430 \u0438\u0437\u043f\u0440\u0430\u0449\u0430\u0442\u0435 \u0441\u044a\u0431\u0438\u0442\u0438\u044f \u0434\u043e Home Assistant, \u0449\u0435 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u044f\u0442\u0430 webhook \u0432 Plaato Airlock. \n\n \u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0441\u043b\u0435\u0434\u043d\u0430\u0442\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n\n \u0412\u0438\u0436\u0442\u0435 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430]({docs_url}) \u0437\u0430 \u043f\u043e\u0432\u0435\u0447\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438." }, diff --git a/homeassistant/components/plex/translations/bg.json b/homeassistant/components/plex/translations/bg.json index 4f1fdbe98c5..d1b1867c246 100644 --- a/homeassistant/components/plex/translations/bg.json +++ b/homeassistant/components/plex/translations/bg.json @@ -4,6 +4,7 @@ "all_configured": "\u0412\u0441\u0438\u0447\u043a\u0438 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u0438 \u0441\u044a\u0440\u0432\u044a\u0440\u0438 \u0432\u0435\u0447\u0435 \u0441\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0438", "already_configured": "\u0422\u043e\u0437\u0438 Plex \u0441\u044a\u0440\u0432\u044a\u0440 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "already_in_progress": "Plex \u0441\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430", + "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", "token_request_timeout": "\u0418\u0437\u0442\u0435\u0447\u0435 \u0432\u0440\u0435\u043c\u0435\u0442\u043e \u0437\u0430 \u043f\u043e\u043b\u0443\u0447\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u043a\u043e\u0434 \u0437\u0430 \u0434\u043e\u0441\u0442\u044a\u043f", "unknown": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0440\u0430\u0434\u0438 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, diff --git a/homeassistant/components/progettihwsw/translations/bg.json b/homeassistant/components/progettihwsw/translations/bg.json index a610a1f2a64..db7bc985ab5 100644 --- a/homeassistant/components/progettihwsw/translations/bg.json +++ b/homeassistant/components/progettihwsw/translations/bg.json @@ -1,11 +1,16 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { "user": { "data": { + "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" } } diff --git a/homeassistant/components/remote/translations/bg.json b/homeassistant/components/remote/translations/bg.json index 9f9c5a5782a..a236d387cb5 100644 --- a/homeassistant/components/remote/translations/bg.json +++ b/homeassistant/components/remote/translations/bg.json @@ -1,4 +1,14 @@ { + "device_automation": { + "condition_type": { + "is_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "is_on": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + }, + "trigger_type": { + "turned_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "turned_on": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + } + }, "state": { "_": { "off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0435\u043d", diff --git a/homeassistant/components/rfxtrx/translations/bg.json b/homeassistant/components/rfxtrx/translations/bg.json index f7406217eed..e35eaf15bd2 100644 --- a/homeassistant/components/rfxtrx/translations/bg.json +++ b/homeassistant/components/rfxtrx/translations/bg.json @@ -9,8 +9,15 @@ "step": { "setup_network": { "data": { + "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" } + }, + "setup_serial": { + "data": { + "device": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "title": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" } } } diff --git a/homeassistant/components/risco/translations/bg.json b/homeassistant/components/risco/translations/bg.json index 2ac8a444100..ac95bcfebf8 100644 --- a/homeassistant/components/risco/translations/bg.json +++ b/homeassistant/components/risco/translations/bg.json @@ -2,6 +2,15 @@ "config": { "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "pin": "\u041f\u0418\u041d \u043a\u043e\u0434", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/rpi_power/translations/bg.json b/homeassistant/components/rpi_power/translations/bg.json new file mode 100644 index 00000000000..cc9ca7efe6c --- /dev/null +++ b/homeassistant/components/rpi_power/translations/bg.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, + "step": { + "confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0442\u0430?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/bg.json b/homeassistant/components/sharkiq/translations/bg.json index 7b79ce3a72c..339d2106051 100644 --- a/homeassistant/components/sharkiq/translations/bg.json +++ b/homeassistant/components/sharkiq/translations/bg.json @@ -2,10 +2,28 @@ "config": { "abort": { "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "reauth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/bg.json b/homeassistant/components/shelly/translations/bg.json index 6b8902ebfcc..f7cb7f9c48e 100644 --- a/homeassistant/components/shelly/translations/bg.json +++ b/homeassistant/components/shelly/translations/bg.json @@ -1,7 +1,27 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "unsupported_firmware": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u043d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0432\u0435\u0440\u0441\u0438\u044f \u043d\u0430 \u0444\u044a\u0440\u043c\u0443\u0435\u0440\u0430." + }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "flow_title": "{name}", + "step": { + "credentials": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } } }, "device_automation": { diff --git a/homeassistant/components/soma/translations/it.json b/homeassistant/components/soma/translations/it.json index 237ce347cb0..c3cd51e6351 100644 --- a/homeassistant/components/soma/translations/it.json +++ b/homeassistant/components/soma/translations/it.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "already_setup": "\u00c8 possibile configurare un solo account Soma.", - "authorize_url_timeout": "Tempo scaduto nella generazione dell'URL di autorizzazione.", - "connection_error": "Impossibile connettersi a SOMA Connect.", + "already_setup": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", + "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", + "connection_error": "Impossibile connettersi", "missing_configuration": "Il componente Soma non \u00e8 configurato. Si prega di seguire la documentazione.", "result_error": "SOMA Connect ha risposto con stato di errore." }, "create_entry": { - "default": "Autenticato con successo con Soma." + "default": "Autenticazione riuscita" }, "step": { "user": { diff --git a/homeassistant/components/somfy/translations/bg.json b/homeassistant/components/somfy/translations/bg.json index 4ac8389ad42..62905ef389e 100644 --- a/homeassistant/components/somfy/translations/bg.json +++ b/homeassistant/components/somfy/translations/bg.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0434\u0440\u0435\u0441 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0432 \u0441\u0440\u043e\u043a.", - "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044a\u0442 Somfy \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." + "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044a\u0442 Somfy \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.", + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." }, "create_entry": { "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u043d\u0435 \u0441\u044a\u0441 Somfy." diff --git a/homeassistant/components/sonarr/translations/bg.json b/homeassistant/components/sonarr/translations/bg.json index 4983c9a14b2..f370ff8a2fd 100644 --- a/homeassistant/components/sonarr/translations/bg.json +++ b/homeassistant/components/sonarr/translations/bg.json @@ -1,6 +1,12 @@ { "config": { + "abort": { + "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" + }, "step": { + "reauth_confirm": { + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, "user": { "data": { "port": "\u041f\u043e\u0440\u0442" diff --git a/homeassistant/components/speedtestdotnet/translations/bg.json b/homeassistant/components/speedtestdotnet/translations/bg.json new file mode 100644 index 00000000000..1c6120581b0 --- /dev/null +++ b/homeassistant/components/speedtestdotnet/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/bg.json b/homeassistant/components/spotify/translations/bg.json new file mode 100644 index 00000000000..982756c0d85 --- /dev/null +++ b/homeassistant/components/spotify/translations/bg.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/translations/bg.json b/homeassistant/components/traccar/translations/bg.json index 5b9d2ae0e6d..3859cbda430 100644 --- a/homeassistant/components/traccar/translations/bg.json +++ b/homeassistant/components/traccar/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, "create_entry": { "default": "\u0417\u0430 \u0434\u0430 \u0438\u0437\u043f\u0440\u0430\u0449\u0430\u0442\u0435 \u0441\u044a\u0431\u0438\u0442\u0438\u044f \u0434\u043e Home Assistant, \u0449\u0435 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u044f\u0442\u0430 webhook \u0432 Traccar. \n\n \u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u043d\u0438\u044f \u0430\u0434\u0440\u0435\u0441: ` {webhook_url} ` \n\n \u0412\u0438\u0436\u0442\u0435 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430]({docs_url}) \u0437\u0430 \u043f\u043e\u0432\u0435\u0447\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438." }, diff --git a/homeassistant/components/twilio/translations/bg.json b/homeassistant/components/twilio/translations/bg.json index 4ea3d470281..c3defd52d71 100644 --- a/homeassistant/components/twilio/translations/bg.json +++ b/homeassistant/components/twilio/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, "create_entry": { "default": "\u0417\u0430 \u0434\u0430 \u0438\u0437\u043f\u0440\u0430\u0449\u0430\u0442\u0435 \u0441\u044a\u0431\u0438\u0442\u0438\u044f \u0434\u043e Home Assistant, \u0449\u0435 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 [Webhooks \u0441 Twilio]({twilio_url}). \n\n\u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0441\u043b\u0435\u0434\u043d\u0430\u0442\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/x-www-form-urlencoded\n\n\u0412\u0438\u0436\u0442\u0435 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430]({docs_url}) \u0437\u0430 \u0442\u043e\u0432\u0430 \u043a\u0430\u043a \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438\u0442\u0435 \u0437\u0430 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u043d\u0430 \u0432\u0445\u043e\u0434\u044f\u0449\u0438 \u0434\u0430\u043d\u043d\u0438." }, diff --git a/homeassistant/components/uptimerobot/translations/it.json b/homeassistant/components/uptimerobot/translations/it.json index 517bbf6463f..521f2417125 100644 --- a/homeassistant/components/uptimerobot/translations/it.json +++ b/homeassistant/components/uptimerobot/translations/it.json @@ -17,14 +17,14 @@ "data": { "api_key": "Chiave API" }, - "description": "Devi fornire una nuova chiave API di sola lettura da Uptime Robot", + "description": "Devi fornire una nuova chiave API di sola lettura da UptimeRobot", "title": "Autenticare nuovamente l'integrazione" }, "user": { "data": { "api_key": "Chiave API" }, - "description": "Devi fornire una chiave API di sola lettura da Uptime Robot" + "description": "Devi fornire una chiave API di sola lettura da UptimeRobot" } } } diff --git a/homeassistant/components/velbus/translations/bg.json b/homeassistant/components/velbus/translations/bg.json index 62b75d4bd81..f42bd9dfcbc 100644 --- a/homeassistant/components/velbus/translations/bg.json +++ b/homeassistant/components/velbus/translations/bg.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "error": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "step": { diff --git a/homeassistant/components/vlc_telnet/translations/hu.json b/homeassistant/components/vlc_telnet/translations/hu.json index 639dc690c13..1680d29b087 100644 --- a/homeassistant/components/vlc_telnet/translations/hu.json +++ b/homeassistant/components/vlc_telnet/translations/hu.json @@ -2,7 +2,10 @@ "config": { "abort": { "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -11,6 +14,9 @@ }, "flow_title": "{host}", "step": { + "hassio_confirm": { + "description": "Szeretne csatlakozni {addon} kieg\u00e9sz\u00edt\u0151h\u00f6z?" + }, "reauth_confirm": { "data": { "password": "Jelsz\u00f3" diff --git a/homeassistant/components/vlc_telnet/translations/it.json b/homeassistant/components/vlc_telnet/translations/it.json new file mode 100644 index 00000000000..7f2abfdf211 --- /dev/null +++ b/homeassistant/components/vlc_telnet/translations/it.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "unknown": "Errore imprevisto" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "flow_title": "{host}", + "step": { + "hassio_confirm": { + "description": "Vuoi connetterti al componente aggiuntivo {addon}?" + }, + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "Inserisci la password corretta per l'host: {host}" + }, + "user": { + "data": { + "host": "Host", + "name": "Nome", + "password": "Password", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wilight/translations/bg.json b/homeassistant/components/wilight/translations/bg.json new file mode 100644 index 00000000000..331c0e79971 --- /dev/null +++ b/homeassistant/components/wilight/translations/bg.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/it.json b/homeassistant/components/xiaomi_miio/translations/it.json index ba53ea170fe..df69b9a2736 100644 --- a/homeassistant/components/xiaomi_miio/translations/it.json +++ b/homeassistant/components/xiaomi_miio/translations/it.json @@ -13,7 +13,8 @@ "cloud_login_error": "Impossibile accedere a Xioami Miio Cloud, controlla le credenziali.", "cloud_no_devices": "Nessun dispositivo trovato in questo account cloud Xiaomi Miio.", "no_device_selected": "Nessun dispositivo selezionato, selezionare un dispositivo.", - "unknown_device": "Il modello del dispositivo non \u00e8 noto, non \u00e8 possibile configurare il dispositivo utilizzando il flusso di configurazione." + "unknown_device": "Il modello del dispositivo non \u00e8 noto, non \u00e8 possibile configurare il dispositivo utilizzando il flusso di configurazione.", + "wrong_token": "Errore di checksum, token errato" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/yeelight/translations/bg.json b/homeassistant/components/yeelight/translations/bg.json index 2ac8a444100..d68ec8b933c 100644 --- a/homeassistant/components/yeelight/translations/bg.json +++ b/homeassistant/components/yeelight/translations/bg.json @@ -1,7 +1,33 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "no_devices_found": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430" + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "pick_device": { + "data": { + "device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + } + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "model": "\u041c\u043e\u0434\u0435\u043b (\u043f\u043e \u0438\u0437\u0431\u043e\u0440)" + }, + "description": "\u0410\u043a\u043e \u043e\u0441\u0442\u0430\u0432\u0438\u0442\u0435 \u043c\u043e\u0434\u0435\u043b\u0430 \u043f\u0440\u0430\u0437\u0435\u043d, \u0442\u043e\u0439 \u0449\u0435 \u0431\u044a\u0434\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0440\u0430\u0437\u043f\u043e\u0437\u043d\u0430\u0442." + } } } } \ No newline at end of file diff --git a/homeassistant/components/zodiac/translations/sensor.bg.json b/homeassistant/components/zodiac/translations/sensor.bg.json new file mode 100644 index 00000000000..991a9d6e9aa --- /dev/null +++ b/homeassistant/components/zodiac/translations/sensor.bg.json @@ -0,0 +1,18 @@ +{ + "state": { + "zodiac__sign": { + "aquarius": "\u0412\u043e\u0434\u043e\u043b\u0435\u0439", + "aries": "\u041e\u0432\u0435\u043d", + "cancer": "\u0420\u0430\u043a", + "capricorn": "\u041a\u043e\u0437\u0438\u0440\u043e\u0433", + "gemini": "\u0411\u043b\u0438\u0437\u043d\u0430\u0446\u0438", + "leo": "\u041b\u044a\u0432", + "libra": "\u0412\u0435\u0437\u043d\u0438", + "pisces": "\u0420\u0438\u0431\u0438", + "sagittarius": "\u0421\u0442\u0440\u0435\u043b\u0435\u0446", + "scorpio": "\u0421\u043a\u043e\u0440\u043f\u0438\u043e\u043d", + "taurus": "\u0422\u0435\u043b\u0435\u0446", + "virgo": "\u0414\u0435\u0432\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/bg.json b/homeassistant/components/zoneminder/translations/bg.json index 946b62a8690..a19fe59b023 100644 --- a/homeassistant/components/zoneminder/translations/bg.json +++ b/homeassistant/components/zoneminder/translations/bg.json @@ -1,7 +1,22 @@ { "config": { + "abort": { + "auth_fail": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e\u0442\u043e \u0438\u043c\u0435 \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0441\u0430 \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u0438.", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "error": { + "auth_fail": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e\u0442\u043e \u0438\u043c\u0435 \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0441\u0430 \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u0438.", + "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" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442 \u0438 \u043f\u043e\u0440\u0442 (\u043f\u0440. 10.10.0.4:8010)", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } } } } \ No newline at end of file From 5138efcd92f8754f254da7dbf7062c4c088e6d1d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 18 Oct 2021 16:27:26 -1000 Subject: [PATCH 0518/1038] Bump flux_led to 0.24.9 (#58006) - Adds missing description for 0x04 models --- 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 88bcaabece2..3019f07a1be 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.24.8"], + "requirements": ["flux_led==0.24.9"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index b7a2bd80a4a..f13f61a2db8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -652,7 +652,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.8 +flux_led==0.24.9 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1780c0d54aa..6caea8aa266 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -384,7 +384,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.8 +flux_led==0.24.9 # homeassistant.components.homekit fnvhash==0.1.0 From 58417f509b9508d557282753ef17001f4d75c403 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Tue, 19 Oct 2021 04:28:19 +0200 Subject: [PATCH 0519/1038] BMW: Fix check_control_message short description (#57998) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/binary_sensor.py | 2 +- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index 358cdb67970..d2d2aa9d42f 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -137,7 +137,7 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity): if has_check_control_messages: cbs_list = [] for message in check_control_messages: - cbs_list.append(message["ccmDescriptionShort"]) + cbs_list.append(message.description_short) result["check_control_messages"] = cbs_list else: result["check_control_messages"] = "OK" diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 8a1e7e2c826..110f8295c8a 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -2,7 +2,7 @@ "domain": "bmw_connected_drive", "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", - "requirements": ["bimmer_connected==0.7.20"], + "requirements": ["bimmer_connected==0.7.21"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index f13f61a2db8..9467dfde3a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -384,7 +384,7 @@ beautifulsoup4==4.10.0 bellows==0.28.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.7.20 +bimmer_connected==0.7.21 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6caea8aa266..a3311d132af 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -248,7 +248,7 @@ base36==0.1.1 bellows==0.28.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.7.20 +bimmer_connected==0.7.21 # homeassistant.components.blebox blebox_uniapi==1.3.3 From 1904019b5f1c6cce6ccd6047777d000f6b9d6fc8 Mon Sep 17 00:00:00 2001 From: Zac West <74188+zacwest@users.noreply.github.com> Date: Mon, 18 Oct 2021 19:29:13 -0700 Subject: [PATCH 0520/1038] Include webhook_id in mobile_app's notify registration_info (#58007) --- homeassistant/components/mobile_app/const.py | 1 + homeassistant/components/mobile_app/notify.py | 2 ++ tests/components/mobile_app/test_notify.py | 1 + 3 files changed, 4 insertions(+) diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index e375ec55ff2..48fed416c33 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -49,6 +49,7 @@ ATTR_VERTICAL_ACCURACY = "vertical_accuracy" ATTR_WEBHOOK_DATA = "data" ATTR_WEBHOOK_ENCRYPTED = "encrypted" ATTR_WEBHOOK_ENCRYPTED_DATA = "encrypted_data" +ATTR_WEBHOOK_ID = "webhook_id" ATTR_WEBHOOK_TYPE = "type" ERR_ENCRYPTION_ALREADY_ENABLED = "encryption_already_enabled" diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py index 025880d8107..36afbac71c8 100644 --- a/homeassistant/components/mobile_app/notify.py +++ b/homeassistant/components/mobile_app/notify.py @@ -36,6 +36,7 @@ from .const import ( ATTR_PUSH_RATE_LIMITS_SUCCESSFUL, ATTR_PUSH_TOKEN, ATTR_PUSH_URL, + ATTR_WEBHOOK_ID, DATA_CONFIG_ENTRIES, DATA_NOTIFY, DATA_PUSH_CHANNEL, @@ -147,6 +148,7 @@ class MobileAppNotificationService(BaseNotificationService): reg_info = { ATTR_APP_ID: entry_data[ATTR_APP_ID], ATTR_APP_VERSION: entry_data[ATTR_APP_VERSION], + ATTR_WEBHOOK_ID: target, } if ATTR_OS_VERSION in entry_data: reg_info[ATTR_OS_VERSION] = entry_data[ATTR_OS_VERSION] diff --git a/tests/components/mobile_app/test_notify.py b/tests/components/mobile_app/test_notify.py index c0e1b4c2a85..86cc9f0ae67 100644 --- a/tests/components/mobile_app/test_notify.py +++ b/tests/components/mobile_app/test_notify.py @@ -118,6 +118,7 @@ async def test_notify_works(hass, aioclient_mock, setup_push_receiver): assert call_json["message"] == "Hello world" assert call_json["registration_info"]["app_id"] == "io.homeassistant.mobile_app" assert call_json["registration_info"]["app_version"] == "1.0" + assert call_json["registration_info"]["webhook_id"] == "mock-webhook_id" async def test_notify_ws_works( From 1a978662ec24919b8913624ba4d2955fe0e59894 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 19 Oct 2021 04:30:45 +0200 Subject: [PATCH 0521/1038] Add configuration_url and entity_category to Fritz (#58004) --- homeassistant/components/fritz/binary_sensor.py | 4 ++++ homeassistant/components/fritz/common.py | 1 + homeassistant/components/fritz/sensor.py | 6 ++++++ homeassistant/components/fritz/switch.py | 4 ++++ 4 files changed, 15 insertions(+) diff --git a/homeassistant/components/fritz/binary_sensor.py b/homeassistant/components/fritz/binary_sensor.py index edde8c0c22a..994c7ff656e 100644 --- a/homeassistant/components/fritz/binary_sensor.py +++ b/homeassistant/components/fritz/binary_sensor.py @@ -11,6 +11,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -25,16 +26,19 @@ SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( key="is_connected", name="Connection", device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), BinarySensorEntityDescription( key="is_linked", name="Link", device_class=DEVICE_CLASS_PLUG, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), BinarySensorEntityDescription( key="firmware_update", name="Firmware Update", device_class=DEVICE_CLASS_UPDATE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 61cff890a93..053b0e117d9 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -487,4 +487,5 @@ class FritzBoxBaseEntity: "manufacturer": "AVM", "model": self._fritzbox_tools.model, "sw_version": self._fritzbox_tools.current_firmware, + "configuration_url": f"http://{self._fritzbox_tools.host}", } diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index fd82d245b9a..42a682cf1e0 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -27,6 +27,8 @@ from homeassistant.const import ( DATA_RATE_KILOBITS_PER_SECOND, DATA_RATE_KILOBYTES_PER_SECOND, DEVICE_CLASS_TIMESTAMP, + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, SIGNAL_STRENGTH_DECIBELS, ) from homeassistant.core import HomeAssistant @@ -165,12 +167,14 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = ( key="device_uptime", name="Device Uptime", device_class=DEVICE_CLASS_TIMESTAMP, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, value_fn=_retrieve_device_uptime_state, ), FritzSensorEntityDescription( key="connection_uptime", name="Connection Uptime", device_class=DEVICE_CLASS_TIMESTAMP, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, value_fn=_retrieve_connection_uptime_state, ), FritzSensorEntityDescription( @@ -194,6 +198,7 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = ( name="Max Connection Upload Throughput", native_unit_of_measurement=DATA_RATE_KILOBITS_PER_SECOND, icon="mdi:upload", + entity_category=ENTITY_CATEGORY_CONFIG, value_fn=_retrieve_max_kb_s_sent_state, ), FritzSensorEntityDescription( @@ -201,6 +206,7 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = ( name="Max Connection Download Throughput", native_unit_of_measurement=DATA_RATE_KILOBITS_PER_SECOND, icon="mdi:download", + entity_category=ENTITY_CATEGORY_CONFIG, value_fn=_retrieve_max_kb_s_received_state, ), FritzSensorEntityDescription( diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index a53d0867a3c..072cca6a1b0 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -18,6 +18,7 @@ import xmltodict from homeassistant.components.network import async_get_source_ip from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity @@ -441,6 +442,7 @@ class FritzBoxPortSwitch(FritzBoxBaseSwitch, SwitchEntity): self.connection_type = connection_type self.port_mapping = port_mapping # dict in the format as it comes from fritzconnection. eg: {'NewRemoteHost': '0.0.0.0', 'NewExternalPort': 22, 'NewProtocol': 'TCP', 'NewInternalPort': 22, 'NewInternalClient': '192.168.178.31', 'NewEnabled': True, 'NewPortMappingDescription': 'Beast SSH ', 'NewLeaseDuration': 0} self._idx = idx # needed for update routine + self._attr_entity_category = ENTITY_CATEGORY_CONFIG if port_mapping is None: return @@ -519,6 +521,7 @@ class FritzBoxDeflectionSwitch(FritzBoxBaseSwitch, SwitchEntity): self.dict_of_deflection = dict_of_deflection self._attributes = {} self.id = int(self.dict_of_deflection["DeflectionId"]) + self._attr_entity_category = ENTITY_CATEGORY_CONFIG switch_info = SwitchInfo( description=f"Call deflection {self.id}", @@ -588,6 +591,7 @@ class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity): self._attr_is_on: bool = False self._name = f"{device.hostname} Internet Access" self._attr_unique_id = f"{self._mac}_internet_access" + self._attr_entity_category = ENTITY_CATEGORY_CONFIG async def async_process_update(self) -> None: """Update device.""" From 2bae1137480c756b755a0f01122513c02a0e4486 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 19 Oct 2021 04:33:26 +0200 Subject: [PATCH 0522/1038] Mark Tasmota status sensors as diagnostic sensors (#57958) * Mark Tasmota status sensors as diagnostic sensors * Disable IP and firmware version sensors by default --- homeassistant/components/tasmota/sensor.py | 17 +++++++- tests/components/tasmota/test_sensor.py | 47 +++++++++++++++++++++- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index 39ee97d1648..348ff741c9b 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -32,6 +32,7 @@ from homeassistant.const import ( ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, + ENTITY_CATEGORY_DIAGNOSTIC, FREQUENCY_HERTZ, LENGTH_CENTIMETERS, LIGHT_LUX, @@ -229,11 +230,23 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): ) return class_or_icon.get(STATE_CLASS) + @property + def entity_category(self) -> str | None: + """Return the category of the entity, if any.""" + if self._tasmota_entity.quantity in status_sensor.SENSORS: + return ENTITY_CATEGORY_DIAGNOSTIC + return None + @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" - # Hide status sensors to not overwhelm users - if self._tasmota_entity.quantity in status_sensor.SENSORS: + # Hide fast changing status sensors + if self._tasmota_entity.quantity in ( + hc.SENSOR_STATUS_IP, + hc.SENSOR_STATUS_RSSI, + hc.SENSOR_STATUS_SIGNAL, + hc.SENSOR_STATUS_VERSION, + ): return False return True diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index 26699763a6c..c6f27f0193c 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -148,6 +148,12 @@ async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota): assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) + entity_reg = er.async_get(hass) + entry = entity_reg.async_get("sensor.tasmota_dht11_temperature") + assert entry.disabled is False + assert entry.disabled_by is None + assert entry.entity_category is None + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") state = hass.states.get("sensor.tasmota_dht11_temperature") assert state.state == STATE_UNKNOWN @@ -769,6 +775,45 @@ async def test_indexed_sensor_attributes(hass, mqtt_mock, setup_tasmota): assert state.attributes.get("unit_of_measurement") == "ppm" +@pytest.mark.parametrize("status_sensor_disabled", [False]) +@pytest.mark.parametrize( + "sensor_name, disabled, disabled_by", + [ + ("tasmota_firmware_version", True, er.DISABLED_INTEGRATION), + ("tasmota_ip", True, er.DISABLED_INTEGRATION), + ("tasmota_last_restart_time", False, None), + ("tasmota_mqtt_connect_count", False, None), + ("tasmota_rssi", True, er.DISABLED_INTEGRATION), + ("tasmota_signal", True, er.DISABLED_INTEGRATION), + ("tasmota_ssid", False, None), + ("tasmota_wifi_connect_count", False, None), + ], +) +async def test_diagnostic_sensors( + hass, mqtt_mock, setup_tasmota, sensor_name, disabled, disabled_by +): + """Test properties of diagnostic sensors.""" + entity_reg = er.async_get(hass) + + config = copy.deepcopy(DEFAULT_CONFIG) + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get(f"sensor.{sensor_name}") + assert bool(state) != disabled + entry = entity_reg.async_get(f"sensor.{sensor_name}") + assert entry.disabled == disabled + assert entry.disabled_by == disabled_by + assert entry.entity_category == "diagnostic" + + @pytest.mark.parametrize("status_sensor_disabled", [False]) async def test_enable_status_sensor(hass, mqtt_mock, setup_tasmota): """Test enabling status sensor.""" @@ -791,7 +836,7 @@ async def test_enable_status_sensor(hass, mqtt_mock, setup_tasmota): assert entry.disabled assert entry.disabled_by == er.DISABLED_INTEGRATION - # Enable the status sensor + # Enable the signal level status sensor updated_entry = entity_reg.async_update_entity( "sensor.tasmota_signal", disabled_by=None ) From 9561c51276399d5f5746152d11eaaf5fd8fff4d7 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 19 Oct 2021 04:36:35 +0200 Subject: [PATCH 0523/1038] Use assignment expressions 16 (#57962) --- .../components/doods/image_processing.py | 6 ++---- homeassistant/components/gpmdp/media_player.py | 3 +-- .../components/hisense_aehw4a1/climate.py | 3 +-- homeassistant/components/lifx/light.py | 15 +++++---------- homeassistant/components/lightwave/__init__.py | 9 +++------ .../components/lutron_caseta/__init__.py | 3 +-- .../components/lutron_caseta/device_trigger.py | 7 ++----- .../components/mediaroom/media_player.py | 12 +++++------- homeassistant/components/nuki/__init__.py | 3 +-- homeassistant/components/roon/media_browser.py | 6 ++---- homeassistant/components/roon/media_player.py | 3 +-- .../components/somfy_mylink/config_flow.py | 3 +-- homeassistant/components/sonos/__init__.py | 3 +-- homeassistant/components/steam_online/sensor.py | 10 +++------- 14 files changed, 29 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py index 3e41c1871bf..e4398a756ec 100644 --- a/homeassistant/components/doods/image_processing.py +++ b/homeassistant/components/doods/image_processing.py @@ -154,8 +154,7 @@ class Doods(ImageProcessingEntity): continue # If label confidence is not specified, use global confidence - label_confidence = label.get(CONF_CONFIDENCE) - if not label_confidence: + if not (label_confidence := label.get(CONF_CONFIDENCE)): label_confidence = confidence if label_name not in dconfig or dconfig[label_name] > label_confidence: dconfig[label_name] = label_confidence @@ -187,8 +186,7 @@ class Doods(ImageProcessingEntity): # Handle global detection area self._area = [0, 0, 1, 1] self._covers = True - area_config = config.get(CONF_AREA) - if area_config: + if area_config := config.get(CONF_AREA): self._area = [ area_config[CONF_TOP], area_config[CONF_LEFT], diff --git a/homeassistant/components/gpmdp/media_player.py b/homeassistant/components/gpmdp/media_player.py index 0a26a514323..649e0283f5a 100644 --- a/homeassistant/components/gpmdp/media_player.py +++ b/homeassistant/components/gpmdp/media_player.py @@ -109,8 +109,7 @@ def request_configuration(hass, config, url, add_entities_callback): "the desktop player and try again" ) break - code = tmpmsg["payload"] - if code == "CODE_REQUIRED": + if (code := tmpmsg["payload"]) == "CODE_REQUIRED": continue setup_gpmdp(hass, config, code, add_entities_callback) save_json(hass.config.path(GPMDP_CONFIG_FILE), {"CODE": code}) diff --git a/homeassistant/components/hisense_aehw4a1/climate.py b/homeassistant/components/hisense_aehw4a1/climate.py index 23a3a0c1416..096b39bdbca 100644 --- a/homeassistant/components/hisense_aehw4a1/climate.py +++ b/homeassistant/components/hisense_aehw4a1/climate.py @@ -325,8 +325,7 @@ class ClimateAehW4a1(ClimateEntity): "AC at %s is off, could not set temperature", self._unique_id ) return - temp = kwargs.get(ATTR_TEMPERATURE) - if temp is not None: + if (temp := kwargs.get(ATTR_TEMPERATURE)) is not None: _LOGGER.debug("Setting temp of %s to %s", self._unique_id, temp) if self._preset_mode != PRESET_NONE: await self.async_set_preset_mode(PRESET_NONE) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 48896cea8a5..b30315ac77d 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -166,8 +166,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up LIFX from a config entry.""" # Priority 1: manual config - interfaces = hass.data[LIFX_DOMAIN].get(DOMAIN) - if not interfaces: + if not (interfaces := hass.data[LIFX_DOMAIN].get(DOMAIN)): # Priority 2: scanned interfaces lifx_ip_addresses = await aiolifx().LifxScan(hass.loop).scan() interfaces = [{CONF_SERVER: ip} for ip in lifx_ip_addresses] @@ -251,17 +250,14 @@ class LIFXManager: def start_discovery(self, interface): """Start discovery on a network interface.""" kwargs = {"discovery_interval": DISCOVERY_INTERVAL} - broadcast_ip = interface.get(CONF_BROADCAST) - if broadcast_ip: + if broadcast_ip := interface.get(CONF_BROADCAST): kwargs["broadcast_ip"] = broadcast_ip lifx_discovery = aiolifx().LifxDiscovery(self.hass.loop, self, **kwargs) kwargs = {} - listen_ip = interface.get(CONF_SERVER) - if listen_ip: + if listen_ip := interface.get(CONF_SERVER): kwargs["listen_ip"] = listen_ip - listen_port = interface.get(CONF_PORT) - if listen_port: + if listen_port := interface.get(CONF_PORT): kwargs["listen_port"] = listen_port lifx_discovery.start(**kwargs) @@ -692,8 +688,7 @@ class LIFXStrip(LIFXColor): bulb = self.bulb num_zones = len(bulb.color_zones) - zones = kwargs.get(ATTR_ZONES) - if zones is None: + if (zones := kwargs.get(ATTR_ZONES)) is None: # Fast track: setting all zones to the same brightness and color # can be treated as a single-zone bulb. if hsbk[2] is not None and hsbk[3] is not None: diff --git a/homeassistant/components/lightwave/__init__.py b/homeassistant/components/lightwave/__init__.py index 73002908ff3..9cd73d8ffea 100644 --- a/homeassistant/components/lightwave/__init__.py +++ b/homeassistant/components/lightwave/__init__.py @@ -64,20 +64,17 @@ async def async_setup(hass, config): lwlink = LWLink(host) hass.data[LIGHTWAVE_LINK] = lwlink - lights = config[DOMAIN][CONF_LIGHTS] - if lights: + if lights := config[DOMAIN][CONF_LIGHTS]: hass.async_create_task( async_load_platform(hass, "light", DOMAIN, lights, config) ) - switches = config[DOMAIN][CONF_SWITCHES] - if switches: + if switches := config[DOMAIN][CONF_SWITCHES]: hass.async_create_task( async_load_platform(hass, "switch", DOMAIN, switches, config) ) - trv = config[DOMAIN][CONF_TRV] - if trv: + if trv := config[DOMAIN][CONF_TRV]: trvs = trv[CONF_TRVS] proxy_ip = trv[CONF_PROXY_IP] proxy_port = trv[CONF_PROXY_PORT] diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 44e6e8761b1..4aefb2fb50e 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -193,8 +193,7 @@ def _async_merge_lip_leap_data(lip_devices, bridge): if leap_device_data is None: continue for key in ("type", "model", "serial"): - val = leap_device_data.get(key) - if val is not None: + if (val := leap_device_data.get(key)) is not None: device[key] = val _LOGGER.debug("Button Devices: %s", button_devices_by_id) diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index 4e378942bd8..3d1d179eac1 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -220,9 +220,7 @@ async def async_validate_trigger_config(hass: HomeAssistant, config: ConfigType) if not device: return config - schema = DEVICE_TYPE_SCHEMA_MAP.get(device["type"]) - - if not schema: + if not (schema := DEVICE_TYPE_SCHEMA_MAP.get(device["type"])): raise InvalidDeviceAutomationConfig( f"Device type {device['type']} not supported: {config[CONF_DEVICE_ID]}" ) @@ -290,8 +288,7 @@ def get_button_device_by_dr_id(hass: HomeAssistant, device_id: str): for config_entry in hass.data[DOMAIN]: button_devices = hass.data[DOMAIN][config_entry][BUTTON_DEVICES] - device = button_devices.get(device_id) - if device: + if device := button_devices.get(device_id): return device return None diff --git a/homeassistant/components/mediaroom/media_player.py b/homeassistant/components/mediaroom/media_player.py index 9c0812af6e5..d85aee89463 100644 --- a/homeassistant/components/mediaroom/media_player.py +++ b/homeassistant/components/mediaroom/media_player.py @@ -75,11 +75,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Mediaroom platform.""" - known_hosts = hass.data.get(DATA_MEDIAROOM) - if known_hosts is None: + if (known_hosts := hass.data.get(DATA_MEDIAROOM)) is None: known_hosts = hass.data[DATA_MEDIAROOM] = [] - host = config.get(CONF_HOST) - if host: + if host := config.get(CONF_HOST): async_add_entities( [ MediaroomDevice( @@ -90,18 +88,18 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) ] ) - hass.data[DATA_MEDIAROOM].append(host) + known_hosts.append(host) _LOGGER.debug("Trying to discover Mediaroom STB") def callback_notify(notify): """Process NOTIFY message from STB.""" - if notify.ip_address in hass.data[DATA_MEDIAROOM]: + if notify.ip_address in known_hosts: dispatcher_send(hass, SIGNAL_STB_NOTIFY, notify) return _LOGGER.debug("Discovered new stb %s", notify.ip_address) - hass.data[DATA_MEDIAROOM].append(notify.ip_address) + known_hosts.append(notify.ip_address) new_stb = MediaroomDevice( host=notify.ip_address, device_id=notify.device_uuid, optimistic=False ) diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index ea224612d82..99def8d4117 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -55,8 +55,7 @@ async def async_setup(hass, config): hass.data.setdefault(DOMAIN, {}) for platform in PLATFORMS: - confs = config.get(platform) - if confs is None: + if (confs := config.get(platform)) is None: continue for conf in confs: diff --git a/homeassistant/components/roon/media_browser.py b/homeassistant/components/roon/media_browser.py index 0d744befb5c..2f132ee9d23 100644 --- a/homeassistant/components/roon/media_browser.py +++ b/homeassistant/components/roon/media_browser.py @@ -55,8 +55,7 @@ def item_payload(roon_server, item, list_image_id): """Create response payload for a single media item.""" title = item["title"] - subtitle = item.get("subtitle") - if subtitle is None: + if (subtitle := item.get("subtitle")) is None: display_title = title else: display_title = f"{title} ({subtitle})" @@ -123,8 +122,7 @@ def library_payload(roon_server, zone_id, media_content_id): header = result_header["list"] title = header.get("title") - subtitle = header.get("subtitle") - if subtitle is None: + if (subtitle := header.get("subtitle")) is None: list_title = title else: list_title = f"{title} ({subtitle})" diff --git a/homeassistant/components/roon/media_player.py b/homeassistant/components/roon/media_player.py index dd8d9e83c2d..0d7f736961f 100644 --- a/homeassistant/components/roon/media_player.py +++ b/homeassistant/components/roon/media_player.py @@ -552,8 +552,7 @@ class RoonDevice(MediaPlayerEntity): if output["display_name"] != self.name } - transfer_id = zone_ids.get(name) - if transfer_id is None: + if (transfer_id := zone_ids.get(name)) is None: _LOGGER.error( "Can't transfer from %s to %s because destination is not known %s", self.name, diff --git a/homeassistant/components/somfy_mylink/config_flow.py b/homeassistant/components/somfy_mylink/config_flow.py index ecbd9abd402..79fbf028b16 100644 --- a/homeassistant/components/somfy_mylink/config_flow.py +++ b/homeassistant/components/somfy_mylink/config_flow.py @@ -149,8 +149,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return self.async_abort(reason="cannot_connect") if user_input is not None: - target_id = user_input.get(CONF_TARGET_ID) - if target_id: + if target_id := user_input.get(CONF_TARGET_ID): return await self.async_step_target_config(None, target_id) return self.async_create_entry(title="", data=self.options) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index aafcba744ea..5e02832b05a 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -121,8 +121,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hosts = config.get(CONF_HOSTS, []) _LOGGER.debug("Reached async_setup_entry, config=%s", config) - advertise_addr = config.get(CONF_ADVERTISE_ADDR) - if advertise_addr: + if advertise_addr := config.get(CONF_ADVERTISE_ADDR): soco_config.EVENT_ADVERTISE_IP = advertise_addr if deprecated_address := config.get(CONF_INTERFACE_ADDR): diff --git a/homeassistant/components/steam_online/sensor.py b/homeassistant/components/steam_online/sensor.py index 18f7c6cc447..71f25afe69f 100644 --- a/homeassistant/components/steam_online/sensor.py +++ b/homeassistant/components/steam_online/sensor.py @@ -145,13 +145,10 @@ class SteamSensor(SensorEntity): def _get_current_game(self): """Gather current game name from APP ID.""" - game_id = self._profile.current_game[0] - game_extra_info = self._profile.current_game[2] - - if game_extra_info: + if game_extra_info := self._profile.current_game[2]: return game_extra_info - if not game_id: + if not (game_id := self._profile.current_game[0]): return None app_list = self.hass.data[APP_LIST_KEY] @@ -174,8 +171,7 @@ class SteamSensor(SensorEntity): return repr(game_id) def _get_game_info(self): - game_id = self._profile.current_game[0] - if game_id is not None: + if (game_id := self._profile.current_game[0]) is not None: for game in self._owned_games["response"]["games"]: if game["appid"] == game_id: From f92fe38bbd73469ef0b91a279e3dc823fa406d57 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 19 Oct 2021 04:38:33 +0200 Subject: [PATCH 0524/1038] Change warning to info when modbus is ready (#57953) * Change warning to info. * Make level info implicit. --- homeassistant/components/modbus/modbus.py | 2 +- tests/components/modbus/test_init.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index d53556b392a..ee5e8306696 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -354,7 +354,7 @@ class ModbusHub: return False else: message = f"modbus {self.name} communication open" - _LOGGER.warning(message) + _LOGGER.info(message) return True def _pymodbus_call( diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 08b9540d507..a303e116307 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -770,6 +770,7 @@ async def test_shutdown(hass, caplog, mock_pymodbus, mock_modbus_with_pymodbus): async def test_stop_restart(hass, caplog, mock_modbus): """Run test for service stop.""" + caplog.set_level(logging.INFO) entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}" assert hass.states.get(entity_id).state == STATE_UNKNOWN hass.states.async_set(entity_id, 17) From 6576225c48cea893eae0c68aff92a566e5b8a33c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 18 Oct 2021 17:07:51 -1000 Subject: [PATCH 0525/1038] Log unhandled loop exception traces when asyncio debug is on (#57602) --- homeassistant/runner.py | 13 ++++++++++--- tests/ignore_uncaught_exceptions.py | 6 ++++++ tests/test_runner.py | 20 ++++++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/homeassistant/runner.py b/homeassistant/runner.py index 60f278c4efe..dcf39485531 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -5,6 +5,7 @@ import asyncio import dataclasses import logging import threading +import traceback from typing import Any from homeassistant import bootstrap @@ -86,9 +87,15 @@ def _async_loop_exception_handler(_: Any, context: dict[str, Any]) -> None: if exception := context.get("exception"): kwargs["exc_info"] = (type(exception), exception, exception.__traceback__) - logging.getLogger(__package__).error( - "Error doing job: %s", context["message"], **kwargs # type: ignore - ) + logger = logging.getLogger(__package__) + if source_traceback := context.get("source_traceback"): + stack_summary = "".join(traceback.format_list(source_traceback)) + logger.error( + "Error doing job: %s: %s", context["message"], stack_summary, **kwargs # type: ignore + ) + return + + logger.error("Error doing job: %s", context["message"], **kwargs) # type: ignore async def setup_and_run_hass(runtime_config: RuntimeConfig) -> int: diff --git a/tests/ignore_uncaught_exceptions.py b/tests/ignore_uncaught_exceptions.py index 5f991d15bbf..e9327c0255a 100644 --- a/tests/ignore_uncaught_exceptions.py +++ b/tests/ignore_uncaught_exceptions.py @@ -1,5 +1,11 @@ """List of tests that have uncaught exceptions today. Will be shrunk over time.""" IGNORE_UNCAUGHT_EXCEPTIONS = [ + ( + # This test explicitly throws an uncaught exception + # and should not be removed. + "tests.test_runner", + "test_unhandled_exception_traceback", + ), ( "test_homeassistant_bridge", "test_homeassistant_bridge_fan_setup", diff --git a/tests/test_runner.py b/tests/test_runner.py index 0e38cef0fff..20136275b74 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -117,3 +117,23 @@ def test_run_does_not_block_forever_with_shielded_task(hass, tmpdir, caplog): assert ( "Task could not be canceled and was still running after shutdown" in caplog.text ) + + +async def test_unhandled_exception_traceback(hass, caplog): + """Test an unhandled exception gets a traceback in debug mode.""" + + async def _unhandled_exception(): + raise Exception("This is unhandled") + + try: + hass.loop.set_debug(True) + asyncio.create_task(_unhandled_exception()) + finally: + hass.loop.set_debug(False) + + await asyncio.sleep(0) + await asyncio.sleep(0) + + assert "Task exception was never retrieved" in caplog.text + assert "This is unhandled" in caplog.text + assert "_unhandled_exception" in caplog.text From 9a26a8cfd810d5029c7a0d59c1b6d6b6fbd9c326 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 19 Oct 2021 08:29:23 +0200 Subject: [PATCH 0526/1038] Add support for daily and monthly statistics (#57576) * Add support for daily and monthly statistics * Remove debug code * Format code * Don't use dateutil package * Remove 2 TODOs * Remove TODO * Add comments --- .../components/recorder/statistics.py | 126 ++++++++++++++-- tests/components/sensor/test_recorder.py | 134 +++++++++++++++++- 2 files changed, 243 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 402668e50d8..175a7e33fb0 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -7,6 +7,7 @@ import dataclasses from datetime import datetime, timedelta from itertools import chain, groupby import logging +from statistics import mean from typing import TYPE_CHECKING, Any, Literal from sqlalchemy import bindparam, func @@ -583,13 +584,107 @@ def _statistics_during_period_query( return baked_query # type: ignore[no-any-return] +def _reduce_statistics( + stats: dict[str, list[dict[str, Any]]], + same_period: Callable[[datetime, datetime], bool], + period_start_end: Callable[[datetime], tuple[datetime, datetime]], + period: timedelta, +) -> dict[str, list[dict[str, Any]]]: + """Reduce hourly statistics to daily or monthly statistics.""" + result: dict[str, list[dict[str, Any]]] = defaultdict(list) + for statistic_id, stat_list in stats.items(): + max_values: list[float] = [] + mean_values: list[float] = [] + min_values: list[float] = [] + prev_stat: dict[str, Any] = stat_list[0] + + # Loop over the hourly statistics + a fake entry to end the period + for statistic in chain( + stat_list, ({"start": stat_list[-1]["start"] + period},) + ): + if not same_period(prev_stat["start"], statistic["start"]): + start, end = period_start_end(prev_stat["start"]) + # The previous statistic was the last entry of the period + result[statistic_id].append( + { + "statistic_id": statistic_id, + "start": start.isoformat(), + "end": end.isoformat(), + "mean": mean(mean_values) if mean_values else None, + "min": min(min_values) if min_values else None, + "max": max(max_values) if max_values else None, + "last_reset": prev_stat["last_reset"], + "state": prev_stat["state"], + "sum": prev_stat["sum"], + } + ) + max_values = [] + mean_values = [] + min_values = [] + if statistic.get("max") is not None: + max_values.append(statistic["max"]) + if statistic.get("mean") is not None: + mean_values.append(statistic["mean"]) + if statistic.get("min") is not None: + min_values.append(statistic["min"]) + prev_stat = statistic + + return result + + +def _reduce_statistics_per_day( + stats: dict[str, list[dict[str, Any]]] +) -> dict[str, list[dict[str, Any]]]: + """Reduce hourly statistics to daily statistics.""" + + def same_period(time1: datetime, time2: datetime) -> bool: + """Return True if time1 and time2 are in the same date.""" + date1 = dt_util.as_local(time1).date() + date2 = dt_util.as_local(time2).date() + return date1 == date2 + + def period_start_end(time: datetime) -> tuple[datetime, datetime]: + """Return the start and end of the period (day) time is within.""" + start = dt_util.as_utc( + dt_util.as_local(time).replace(hour=0, minute=0, second=0, microsecond=0) + ) + end = start + timedelta(days=1) + return (start, end) + + return _reduce_statistics(stats, same_period, period_start_end, timedelta(days=1)) + + +def _reduce_statistics_per_month( + stats: dict[str, list[dict[str, Any]]] +) -> dict[str, list[dict[str, Any]]]: + """Reduce hourly statistics to monthly statistics.""" + + def same_period(time1: datetime, time2: datetime) -> bool: + """Return True if time1 and time2 are in the same year and month.""" + date1 = dt_util.as_local(time1).date() + date2 = dt_util.as_local(time2).date() + return (date1.year, date1.month) == (date2.year, date2.month) + + def period_start_end(time: datetime) -> tuple[datetime, datetime]: + """Return the start and end of the period (month) time is within.""" + start = dt_util.as_utc( + dt_util.as_local(time).replace( + day=1, hour=0, minute=0, second=0, microsecond=0 + ) + ) + end = (start + timedelta(days=31)).replace(day=1) + return (start, end) + + return _reduce_statistics(stats, same_period, period_start_end, timedelta(days=31)) + + def statistics_during_period( hass: HomeAssistant, start_time: datetime, end_time: datetime | None = None, statistic_ids: list[str] | None = None, - period: Literal["hour"] | Literal["5minute"] = "hour", -) -> dict[str, list[dict[str, str]]]: + period: Literal["5minute", "day", "hour", "month"] = "hour", +) -> dict[str, list[dict[str, Any]]]: """Return statistics during UTC period start_time - end_time for the statistic_ids. If end_time is omitted, returns statistics newer than or equal to start_time. @@ -606,14 +701,14 @@ def statistics_during_period( if statistic_ids is not None: metadata_ids = [metadata_id for metadata_id, _ in metadata.values()] - if period == "hour": - bakery = STATISTICS_BAKERY - base_query = QUERY_STATISTICS - table = Statistics - else: + if period == "5minute": bakery = STATISTICS_SHORT_TERM_BAKERY base_query = QUERY_STATISTICS_SHORT_TERM table = StatisticsShortTerm + else: + bakery = STATISTICS_BAKERY + base_query = QUERY_STATISTICS + table = Statistics baked_query = _statistics_during_period_query( hass, end_time, statistic_ids, bakery, base_query, table @@ -627,10 +722,20 @@ def statistics_during_period( if not stats: return {} # Return statistics combined with metadata - return _sorted_statistics_to_dict( - hass, session, stats, statistic_ids, metadata, True, table, start_time + if period not in ("day", "month"): + return _sorted_statistics_to_dict( + hass, session, stats, statistic_ids, metadata, True, table, start_time + ) + + result = _sorted_statistics_to_dict( + hass, session, stats, statistic_ids, metadata, True, table, start_time, True ) + if period == "day": + return _reduce_statistics_per_day(result) + + return _reduce_statistics_per_month(result) + def get_last_statistics( hass: HomeAssistant, number_of_stats: int, statistic_id: str, convert_units: bool @@ -718,6 +823,7 @@ def _sorted_statistics_to_dict( convert_units: bool, table: type[Statistics | StatisticsShortTerm], start_time: datetime | None, + start_time_as_datetime: bool = False, ) -> dict[str, list[dict]]: """Convert SQL results into JSON friendly data structure.""" result: dict = defaultdict(list) @@ -765,7 +871,7 @@ def _sorted_statistics_to_dict( ent_results.append( { "statistic_id": statistic_id, - "start": start.isoformat(), + "start": start if start_time_as_datetime else start.isoformat(), "end": end.isoformat(), "mean": convert(db_state.mean, units), "min": convert(db_state.min, units), diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 032ff85561c..19dea9a8466 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -2017,16 +2017,19 @@ def test_compile_hourly_statistics_changing_statistics( "db_supports_row_number,in_log,not_in_log", [(True, "row_number", None), (False, None, "row_number")], ) -def test_compile_statistics_hourly_summary( +def test_compile_statistics_hourly_daily_monthly_summary( hass_recorder, caplog, db_supports_row_number, in_log, not_in_log ): - """Test compiling hourly statistics.""" + """Test compiling hourly statistics + monthly and daily summary.""" zero = dt_util.utcnow() - zero = zero.replace(minute=0, second=0, microsecond=0) - # Travel to the future, recorder gets confused otherwise because states are added - # before the start of the recorder_run - zero += timedelta(hours=1) - hass = hass_recorder() + # August 31st, 23:00 local time + zero = zero.replace( + year=2021, month=9, day=1, hour=5, minute=0, second=0, microsecond=0 + ) + with patch( + "homeassistant.components.recorder.models.dt_util.utcnow", return_value=zero + ): + hass = hass_recorder() recorder = hass.data[DATA_INSTANCE] recorder._db_supports_row_number = db_supports_row_number setup_component(hass, "sensor", {}) @@ -2265,6 +2268,123 @@ def test_compile_statistics_hourly_summary( start += timedelta(hours=1) end += timedelta(hours=1) assert stats == expected_stats + + stats = statistics_during_period(hass, zero, period="day") + expected_stats = { + "sensor.test1": [], + "sensor.test2": [], + "sensor.test3": [], + "sensor.test4": [], + } + start = dt_util.parse_datetime("2021-08-31T06:00:00+00:00") + end = start + timedelta(days=1) + for i in range(2): + for entity_id in [ + "sensor.test1", + "sensor.test2", + "sensor.test3", + "sensor.test4", + ]: + expected_average = ( + mean(expected_averages[entity_id][i * 12 : (i + 1) * 12]) + if entity_id in expected_averages + else None + ) + expected_minimum = ( + min(expected_minima[entity_id][i * 12 : (i + 1) * 12]) + if entity_id in expected_minima + else None + ) + expected_maximum = ( + max(expected_maxima[entity_id][i * 12 : (i + 1) * 12]) + if entity_id in expected_maxima + else None + ) + expected_state = ( + expected_states[entity_id][(i + 1) * 12 - 1] + if entity_id in expected_states + else None + ) + expected_sum = ( + expected_sums[entity_id][(i + 1) * 12 - 1] + if entity_id in expected_sums + else None + ) + expected_stats[entity_id].append( + { + "statistic_id": entity_id, + "start": process_timestamp_to_utc_isoformat(start), + "end": process_timestamp_to_utc_isoformat(end), + "mean": approx(expected_average), + "min": approx(expected_minimum), + "max": approx(expected_maximum), + "last_reset": None, + "state": expected_state, + "sum": expected_sum, + } + ) + start += timedelta(days=1) + end += timedelta(days=1) + assert stats == expected_stats + + stats = statistics_during_period(hass, zero, period="month") + expected_stats = { + "sensor.test1": [], + "sensor.test2": [], + "sensor.test3": [], + "sensor.test4": [], + } + start = dt_util.parse_datetime("2021-08-01T06:00:00+00:00") + end = dt_util.parse_datetime("2021-09-01T06:00:00+00:00") + for i in range(2): + for entity_id in [ + "sensor.test1", + "sensor.test2", + "sensor.test3", + "sensor.test4", + ]: + expected_average = ( + mean(expected_averages[entity_id][i * 12 : (i + 1) * 12]) + if entity_id in expected_averages + else None + ) + expected_minimum = ( + min(expected_minima[entity_id][i * 12 : (i + 1) * 12]) + if entity_id in expected_minima + else None + ) + expected_maximum = ( + max(expected_maxima[entity_id][i * 12 : (i + 1) * 12]) + if entity_id in expected_maxima + else None + ) + expected_state = ( + expected_states[entity_id][(i + 1) * 12 - 1] + if entity_id in expected_states + else None + ) + expected_sum = ( + expected_sums[entity_id][(i + 1) * 12 - 1] + if entity_id in expected_sums + else None + ) + expected_stats[entity_id].append( + { + "statistic_id": entity_id, + "start": process_timestamp_to_utc_isoformat(start), + "end": process_timestamp_to_utc_isoformat(end), + "mean": approx(expected_average), + "min": approx(expected_minimum), + "max": approx(expected_maximum), + "last_reset": None, + "state": expected_state, + "sum": expected_sum, + } + ) + start = (start + timedelta(days=31)).replace(day=1) + end = (end + timedelta(days=31)).replace(day=1) + assert stats == expected_stats + assert "Error while processing event StatisticsTask" not in caplog.text if in_log: assert in_log in caplog.text From 708f2ae089bd2148239de645caa43117148569bc Mon Sep 17 00:00:00 2001 From: Brian Egge Date: Tue, 19 Oct 2021 02:36:25 -0400 Subject: [PATCH 0527/1038] Fix issue parsing color effect None in flux_led (#57979) Co-authored-by: J. Nick Koston --- homeassistant/components/flux_led/light.py | 5 +- tests/components/flux_led/test_light.py | 65 +++++++++++++++++++--- 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 4298a4f11e6..655b868a4f5 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -214,6 +214,9 @@ async def async_setup_platform( host, ) custom_effects = device_config.get(CONF_CUSTOM_EFFECT, {}) + custom_effect_colors = None + if CONF_COLORS in custom_effects: + custom_effect_colors = str(custom_effects[CONF_COLORS]) hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, @@ -224,7 +227,7 @@ async def async_setup_platform( CONF_NAME: device_config[CONF_NAME], CONF_PROTOCOL: device_config.get(CONF_PROTOCOL), CONF_MODE: device_config.get(ATTR_MODE, MODE_AUTO), - CONF_CUSTOM_EFFECT_COLORS: str(custom_effects.get(CONF_COLORS)), + CONF_CUSTOM_EFFECT_COLORS: custom_effect_colors, CONF_CUSTOM_EFFECT_SPEED_PCT: custom_effects.get( CONF_SPEED_PCT, DEFAULT_EFFECT_SPEED ), diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index afca4956055..a3f381b0c1d 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -741,18 +741,23 @@ async def test_rgb_light_custom_effects(hass: HomeAssistant) -> None: assert attributes[ATTR_EFFECT] == "custom" -async def test_rgb_light_custom_effects_invalid_colors(hass: HomeAssistant) -> None: +@pytest.mark.parametrize("effect_colors", [":: CANNOT BE PARSED ::", None]) +async def test_rgb_light_custom_effects_invalid_colors( + hass: HomeAssistant, effect_colors: str +) -> None: """Test an rgb light with a invalid effect.""" + options = { + CONF_MODE: MODE_AUTO, + CONF_CUSTOM_EFFECT_SPEED_PCT: 88, + CONF_CUSTOM_EFFECT_TRANSITION: TRANSITION_JUMP, + } + if effect_colors: + options[CONF_CUSTOM_EFFECT_COLORS] = effect_colors config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + options=options, unique_id=MAC_ADDRESS, - options={ - CONF_MODE: MODE_AUTO, - CONF_CUSTOM_EFFECT_COLORS: ":: CANNOT BE PARSED ::", - CONF_CUSTOM_EFFECT_SPEED_PCT: 88, - CONF_CUSTOM_EFFECT_TRANSITION: TRANSITION_JUMP, - }, ) config_entry.add_to_hass(hass) bulb = _mocked_bulb() @@ -827,7 +832,7 @@ async def test_rgb_light_custom_effect_via_service( bulb.async_set_custom_pattern.reset_mock() -async def test_migrate_from_yaml(hass: HomeAssistant) -> None: +async def test_migrate_from_yaml_with_custom_effect(hass: HomeAssistant) -> None: """Test migrate from yaml.""" config = { LIGHT_DOMAIN: [ @@ -876,6 +881,50 @@ async def test_migrate_from_yaml(hass: HomeAssistant) -> None: } +async def test_migrate_from_yaml_no_custom_effect(hass: HomeAssistant) -> None: + """Test migrate from yaml.""" + config = { + LIGHT_DOMAIN: [ + { + CONF_PLATFORM: DOMAIN, + CONF_DEVICES: { + IP_ADDRESS: { + CONF_NAME: "flux_lamppost", + CONF_PROTOCOL: "ledenet", + } + }, + } + ], + } + with _patch_discovery(), _patch_wifibulb(): + await async_setup_component(hass, LIGHT_DOMAIN, config) + await hass.async_block_till_done() + await hass.async_block_till_done() + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + + migrated_entry = None + for entry in entries: + if entry.unique_id == MAC_ADDRESS: + migrated_entry = entry + break + + assert migrated_entry is not None + assert migrated_entry.data == { + CONF_HOST: IP_ADDRESS, + CONF_NAME: "flux_lamppost", + CONF_PROTOCOL: "ledenet", + } + assert migrated_entry.options == { + CONF_MODE: "auto", + CONF_CUSTOM_EFFECT_COLORS: None, + CONF_CUSTOM_EFFECT_SPEED_PCT: 50, + CONF_CUSTOM_EFFECT_TRANSITION: "gradual", + } + + async def test_addressable_light(hass: HomeAssistant) -> None: """Test an addressable light.""" config_entry = MockConfigEntry( From 8debb7c784b026054bad9f5d525cd6ba35ebbe34 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Tue, 19 Oct 2021 08:46:04 +0200 Subject: [PATCH 0528/1038] Add service to stop air conditioning to bmw_connected_drive (#57772) Co-authored-by: rikroe --- .../bmw_connected_drive/__init__.py | 1 + .../bmw_connected_drive/services.yaml | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index f82a04f9355..843fd3a2437 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -74,6 +74,7 @@ _SERVICE_MAP = { "light_flash": "trigger_remote_light_flash", "sound_horn": "trigger_remote_horn", "activate_air_conditioning": "trigger_remote_air_conditioning", + "deactivate_air_conditioning": "trigger_remote_air_conditioning_stop", "find_vehicle": "trigger_remote_vehicle_finder", } diff --git a/homeassistant/components/bmw_connected_drive/services.yaml b/homeassistant/components/bmw_connected_drive/services.yaml index 964fb8ab39b..3f5ff76bdd3 100644 --- a/homeassistant/components/bmw_connected_drive/services.yaml +++ b/homeassistant/components/bmw_connected_drive/services.yaml @@ -68,6 +68,28 @@ activate_air_conditioning: selector: text: +deactivate_air_conditioning: + name: Deactivate air conditioning + description: > + Stops the air conditioning of the vehicle. This only works on newer vehicles if you also + have the option in the MyBMW app. The vehicle is identified either via its + device entry or the VIN. If a VIN is specified, the device entry will be ignored. + fields: + device_id: + name: Car + description: The BMW Connected Drive device + selector: + device: + integration: bmw_connected_drive + vin: + name: VIN + description: The vehicle identification number (VIN) of the vehicle, 17 characters + advanced: true + required: false + example: WBANXXXXXX1234567 + selector: + text: + find_vehicle: name: Find vehicle description: > From 4625a057063811c112170c73bf33369b55df84a6 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 19 Oct 2021 02:53:30 -0400 Subject: [PATCH 0529/1038] Add init tests for agent dvr (#57022) * Add init tests for agent dvr * ci --- .coveragerc | 2 - .../components/agent_dvr/config_flow.py | 4 +- tests/components/agent_dvr/__init__.py | 29 ++++++---- .../components/agent_dvr/test_config_flow.py | 8 ++- tests/components/agent_dvr/test_init.py | 56 +++++++++++++++++++ 5 files changed, 80 insertions(+), 19 deletions(-) create mode 100644 tests/components/agent_dvr/test_init.py diff --git a/.coveragerc b/.coveragerc index 64d9c8e9b76..c0425c1bb81 100644 --- a/.coveragerc +++ b/.coveragerc @@ -29,10 +29,8 @@ omit = homeassistant/components/ads/* homeassistant/components/aemet/weather_update_coordinator.py homeassistant/components/aftership/* - homeassistant/components/agent_dvr/__init__.py homeassistant/components/agent_dvr/alarm_control_panel.py homeassistant/components/agent_dvr/camera.py - homeassistant/components/agent_dvr/const.py homeassistant/components/agent_dvr/helpers.py homeassistant/components/airnow/__init__.py homeassistant/components/airnow/sensor.py diff --git a/homeassistant/components/agent_dvr/config_flow.py b/homeassistant/components/agent_dvr/config_flow.py index a21e6855337..7dd3c7d5bc3 100644 --- a/homeassistant/components/agent_dvr/config_flow.py +++ b/homeassistant/components/agent_dvr/config_flow.py @@ -33,9 +33,7 @@ class AgentFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): try: await agent_client.update() - except AgentConnectionError: - pass - except AgentError: + except (AgentConnectionError, AgentError): pass await agent_client.close() diff --git a/tests/components/agent_dvr/__init__.py b/tests/components/agent_dvr/__init__.py index ec35b521a17..3f2fc82101a 100644 --- a/tests/components/agent_dvr/__init__.py +++ b/tests/components/agent_dvr/__init__.py @@ -7,6 +7,23 @@ from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture from tests.test_util.aiohttp import AiohttpClientMocker +CONF_DATA = { + CONF_HOST: "example.local", + CONF_PORT: 8090, + SERVER_URL: "http://example.local:8090/", +} + + +def create_entry(hass: HomeAssistant): + """Add config entry in Home Assistant.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="c0715bba-c2d0-48ef-9e3e-bc81c9ea4447", + data=CONF_DATA, + ) + entry.add_to_hass(hass) + return entry + async def init_integration( hass: HomeAssistant, @@ -25,17 +42,7 @@ async def init_integration( text=load_fixture("agent_dvr/objects.json"), headers={"Content-Type": CONTENT_TYPE_JSON}, ) - entry = MockConfigEntry( - domain=DOMAIN, - unique_id="c0715bba-c2d0-48ef-9e3e-bc81c9ea4447", - data={ - CONF_HOST: "example.local", - CONF_PORT: 8090, - SERVER_URL: "http://example.local:8090/", - }, - ) - - entry.add_to_hass(hass) + entry = create_entry(hass) if not skip_setup: await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/agent_dvr/test_config_flow.py b/tests/components/agent_dvr/test_config_flow.py index 3e11aae054a..01cb31b3f19 100644 --- a/tests/components/agent_dvr/test_config_flow.py +++ b/tests/components/agent_dvr/test_config_flow.py @@ -38,7 +38,9 @@ async def test_user_device_exists_abort( assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT -async def test_connection_error(hass: HomeAssistant, aioclient_mock) -> None: +async def test_connection_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test we show user form on Agent connection error.""" aioclient_mock.get("http://example.local:8090/command.cgi?cmd=getStatus", text="") @@ -49,13 +51,13 @@ async def test_connection_error(hass: HomeAssistant, aioclient_mock) -> None: data={CONF_HOST: "example.local", CONF_PORT: 8090}, ) - assert result["errors"] == {"base": "cannot_connect"} + assert result["errors"]["base"] == "cannot_connect" assert result["step_id"] == "user" assert result["type"] == data_entry_flow.RESULT_TYPE_FORM async def test_full_user_flow_implementation( - hass: HomeAssistant, aioclient_mock + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the full manual user flow from start to finish.""" aioclient_mock.get( diff --git a/tests/components/agent_dvr/test_init.py b/tests/components/agent_dvr/test_init.py new file mode 100644 index 00000000000..27b3652a446 --- /dev/null +++ b/tests/components/agent_dvr/test_init.py @@ -0,0 +1,56 @@ +"""Test Agent DVR integration.""" +from unittest.mock import AsyncMock, patch + +from agent import AgentError + +from homeassistant.components.agent_dvr.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from . import CONF_DATA, create_entry + +from tests.components.agent_dvr import init_integration +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def _create_mocked_agent(available: bool = True): + mocked_agent = AsyncMock() + mocked_agent.is_available = available + return mocked_agent + + +def _patch_init_agent(mocked_agent): + return patch( + "homeassistant.components.agent_dvr.Agent", + return_value=mocked_agent, + ) + + +async def test_setup_config_and_unload( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +): + """Test setup and unload.""" + entry = await init_integration(hass, aioclient_mock) + assert entry.state == ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.data == CONF_DATA + + 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.get(DOMAIN) + + +async def test_async_setup_entry_not_ready(hass: HomeAssistant): + """Test that it throws ConfigEntryNotReady when exception occurs during setup.""" + entry = create_entry(hass) + with patch( + "homeassistant.components.agent_dvr.Agent.update", + side_effect=AgentError, + ): + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ConfigEntryState.SETUP_RETRY + with _patch_init_agent(await _create_mocked_agent(available=False)): + await hass.config_entries.async_reload(entry.entry_id) + assert entry.state == ConfigEntryState.SETUP_RETRY From db474707a09e8bbeb48ed6f223036d5e9a8a039c Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Tue, 19 Oct 2021 08:57:48 +0200 Subject: [PATCH 0530/1038] Preventing working with incomplete discoveries from user config flow in upnp (#57994) --- homeassistant/components/upnp/config_flow.py | 29 +++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index d1c2c4b3c0f..80c126edbec 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -37,6 +37,16 @@ def _friendly_name_from_discovery(discovery_info: Mapping[str, Any]) -> str: ) +def _is_complete_discovery(discovery_info: Mapping[str, Any]) -> bool: + """Test if discovery is complete and usable.""" + return ( + ssdp.ATTR_UPNP_UDN in discovery_info + and ssdp.ATTR_SSDP_ST in discovery_info + and ssdp.ATTR_SSDP_LOCATION in discovery_info + and ssdp.ATTR_SSDP_USN in discovery_info + ) + + async def _async_wait_for_discoveries(hass: HomeAssistant) -> bool: """Wait for a device to be discovered.""" device_discovered_event = asyncio.Event() @@ -133,7 +143,10 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._discoveries = [ discovery for discovery in discoveries - if discovery[ssdp.ATTR_SSDP_USN] not in current_unique_ids + if ( + _is_complete_discovery(discovery) + and discovery[ssdp.ATTR_SSDP_USN] not in current_unique_ids + ) ] # Ensure anything to add. @@ -183,12 +196,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Ensure complete discovery. discovery = discoveries[0] - if ( - ssdp.ATTR_UPNP_UDN not in discovery - or ssdp.ATTR_SSDP_ST not in discovery - or ssdp.ATTR_SSDP_LOCATION not in discovery - or ssdp.ATTR_SSDP_USN not in discovery - ): + if not _is_complete_discovery(discovery): LOGGER.debug("Incomplete discovery, ignoring") return self.async_abort(reason="incomplete_discovery") @@ -207,12 +215,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): LOGGER.debug("async_step_ssdp: discovery_info: %s", discovery_info) # Ensure complete discovery. - if ( - ssdp.ATTR_UPNP_UDN not in discovery_info - or ssdp.ATTR_SSDP_ST not in discovery_info - or ssdp.ATTR_SSDP_LOCATION not in discovery_info - or ssdp.ATTR_SSDP_USN not in discovery_info - ): + if not _is_complete_discovery(discovery_info): LOGGER.debug("Incomplete discovery, ignoring") return self.async_abort(reason="incomplete_discovery") From a3cae17d88f230bb05b47ca0f0a4d92fd74f3548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 19 Oct 2021 09:05:55 +0200 Subject: [PATCH 0531/1038] Open garage sensor (#57976) --- .coveragerc | 2 + .../components/opengarage/__init__.py | 2 +- homeassistant/components/opengarage/const.py | 4 -- homeassistant/components/opengarage/cover.py | 53 ++------------- homeassistant/components/opengarage/entity.py | 43 ++++++++++++ homeassistant/components/opengarage/sensor.py | 65 +++++++++++++++++++ 6 files changed, 118 insertions(+), 51 deletions(-) create mode 100644 homeassistant/components/opengarage/entity.py create mode 100644 homeassistant/components/opengarage/sensor.py diff --git a/.coveragerc b/.coveragerc index c0425c1bb81..ab1e7642836 100644 --- a/.coveragerc +++ b/.coveragerc @@ -759,6 +759,8 @@ omit = homeassistant/components/openexchangerates/sensor.py homeassistant/components/opengarage/__init__.py homeassistant/components/opengarage/cover.py + homeassistant/components/opengarage/entity.py + homeassistant/components/opengarage/sensor.py homeassistant/components/openhome/__init__.py homeassistant/components/openhome/media_player.py homeassistant/components/openhome/const.py diff --git a/homeassistant/components/opengarage/__init__.py b/homeassistant/components/opengarage/__init__.py index d64a608a3ef..4f890d07f9a 100644 --- a/homeassistant/components/opengarage/__init__.py +++ b/homeassistant/components/opengarage/__init__.py @@ -16,7 +16,7 @@ from .const import CONF_DEVICE_KEY, DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["cover"] +PLATFORMS = ["cover", "sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/opengarage/const.py b/homeassistant/components/opengarage/const.py index 7cf9287e182..93f3179b1a9 100644 --- a/homeassistant/components/opengarage/const.py +++ b/homeassistant/components/opengarage/const.py @@ -1,9 +1,5 @@ """Constants for the OpenGarage integration.""" -ATTR_DISTANCE_SENSOR = "distance_sensor" -ATTR_DOOR_STATE = "door_state" -ATTR_SIGNAL_STRENGTH = "wifi_signal" - CONF_DEVICE_KEY = "device_key" DEFAULT_NAME = "OpenGarage" diff --git a/homeassistant/components/opengarage/cover.py b/homeassistant/components/opengarage/cover.py index 6952e4bff24..8737a0499fb 100644 --- a/homeassistant/components/opengarage/cover.py +++ b/homeassistant/components/opengarage/cover.py @@ -25,17 +25,9 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ( - ATTR_DISTANCE_SENSOR, - ATTR_DOOR_STATE, - ATTR_SIGNAL_STRENGTH, - CONF_DEVICE_KEY, - DEFAULT_PORT, - DOMAIN, -) +from .const import CONF_DEVICE_KEY, DEFAULT_PORT, DOMAIN +from .entity import OpenGarageEntity _LOGGER = logging.getLogger(__name__) @@ -81,7 +73,7 @@ async def async_setup_entry(hass, entry, async_add_entities): ) -class OpenGarageCover(CoordinatorEntity, CoverEntity): +class OpenGarageCover(OpenGarageEntity, CoverEntity): """Representation of a OpenGarage cover.""" _attr_device_class = DEVICE_CLASS_GARAGE @@ -89,14 +81,10 @@ class OpenGarageCover(CoordinatorEntity, CoverEntity): def __init__(self, open_garage_data_coordinator, device_id): """Initialize the cover.""" - super().__init__(open_garage_data_coordinator) - self._state = None self._state_before_move = None - self._attr_extra_state_attributes = {} - self._attr_unique_id = self._device_id = device_id - self._device_name = None - self._update_attr() + + super().__init__(open_garage_data_coordinator, device_id) @property def is_closed(self): @@ -138,12 +126,9 @@ class OpenGarageCover(CoordinatorEntity, CoverEntity): @callback def _update_attr(self) -> None: """Update the state and attributes.""" - if (status := self.coordinator.data) is None: - _LOGGER.error("Unable to connect to OpenGarage device") - self._attr_available = False - return + status = self.coordinator.data - self._device_name = self._attr_name = status["name"] + self._attr_name = status["name"] state = STATES_MAP.get(status.get("door")) if self._state_before_move is not None: if self._state_before_move != state: @@ -152,20 +137,6 @@ class OpenGarageCover(CoordinatorEntity, CoverEntity): else: self._state = state - _LOGGER.debug("%s status: %s", self.name, self._state) - if status.get("rssi") is not None: - self._attr_extra_state_attributes[ATTR_SIGNAL_STRENGTH] = status.get("rssi") - if status.get("dist") is not None: - self._attr_extra_state_attributes[ATTR_DISTANCE_SENSOR] = status.get("dist") - if self._state is not None: - self._attr_extra_state_attributes[ATTR_DOOR_STATE] = self._state - - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - self._update_attr() - self.async_write_ha_state() - async def _push_button(self): """Send commands to API.""" result = await self.coordinator.open_garage_connection.push_button() @@ -181,13 +152,3 @@ class OpenGarageCover(CoordinatorEntity, CoverEntity): self._state = self._state_before_move self._state_before_move = None - - @property - def device_info(self): - """Return the device_info of the device.""" - device_info = DeviceInfo( - identifiers={(DOMAIN, self._device_id)}, - name=self._device_name, - manufacturer="Open Garage", - ) - return device_info diff --git a/homeassistant/components/opengarage/entity.py b/homeassistant/components/opengarage/entity.py new file mode 100644 index 00000000000..7c6d169935a --- /dev/null +++ b/homeassistant/components/opengarage/entity.py @@ -0,0 +1,43 @@ +"""Entity for the opengarage.io component.""" + +from homeassistant.components.opengarage import DOMAIN +from homeassistant.core import callback +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + + +class OpenGarageEntity(CoordinatorEntity): + """Representation of a OpenGarage entity.""" + + def __init__(self, open_garage_data_coordinator, device_id, description=None): + """Initialize the entity.""" + super().__init__(open_garage_data_coordinator) + + if description is not None: + self.entity_description = description + self._attr_unique_id = f"{device_id}_{description.key}" + else: + self._attr_unique_id = device_id + + self._device_id = device_id + self._update_attr() + + @callback + def _update_attr(self) -> None: + """Update the state and attributes.""" + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_attr() + self.async_write_ha_state() + + @property + def device_info(self): + """Return the device_info of the device.""" + device_info = DeviceInfo( + identifiers={(DOMAIN, self._device_id)}, + name=self.coordinator.data["name"], + manufacturer="Open Garage", + ) + return device_info diff --git a/homeassistant/components/opengarage/sensor.py b/homeassistant/components/opengarage/sensor.py new file mode 100644 index 00000000000..e6b6a73c0c6 --- /dev/null +++ b/homeassistant/components/opengarage/sensor.py @@ -0,0 +1,65 @@ +"""Platform for the opengarage.io sensor component.""" +from __future__ import annotations + +import logging + +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.const import ( + DEVICE_CLASS_SIGNAL_STRENGTH, + ENTITY_CATEGORY_DIAGNOSTIC, + LENGTH_CENTIMETERS, + SIGNAL_STRENGTH_DECIBELS, +) +from homeassistant.core import callback + +from .const import DOMAIN +from .entity import OpenGarageEntity + +_LOGGER = logging.getLogger(__name__) + +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="dist", + native_unit_of_measurement=LENGTH_CENTIMETERS, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="rssi", + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + state_class=STATE_CLASS_MEASUREMENT, + ), +) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the OpenGarage sensors.""" + open_garage_data_coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + [ + OpenGarageSensor( + open_garage_data_coordinator, + entry.unique_id, + description, + ) + for description in SENSOR_TYPES + ], + ) + + +class OpenGarageSensor(OpenGarageEntity, SensorEntity): + """Representation of a OpenGarage sensor.""" + + @callback + def _update_attr(self) -> None: + """Handle updated data from the coordinator.""" + self._attr_name = ( + f'{self.coordinator.data["name"]} {self.entity_description.key}' + ) + self._attr_native_value = self.coordinator.data.get(self.entity_description.key) From 29c062fcc44f01a37f0a6c26a2bc13b84e9ca7f4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 19 Oct 2021 09:10:26 +0200 Subject: [PATCH 0532/1038] Rework Onewire tests to enable disabled entities (#58014) Co-authored-by: epenet --- tests/components/onewire/const.py | 97 ++++++++++--------- .../components/onewire/test_binary_sensor.py | 34 +++---- tests/components/onewire/test_sensor.py | 35 ++++--- tests/components/onewire/test_switch.py | 33 ++++--- 4 files changed, 104 insertions(+), 95 deletions(-) diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index 7a39e70d4bc..9652b6d89a7 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -1,5 +1,4 @@ """Constants for 1-Wire integration.""" - from pi1wire import InvalidCRCException, UnsupportResponseException from pyownet.protocol import Error as ProtocolError @@ -35,6 +34,8 @@ from homeassistant.const import ( TEMP_CELSIUS, ) +ATTR_DEFAULT_DISABLED = "default_disabled" + MANUFACTURER = "Maxim Integrated" MOCK_OWPROXY_DEVICES = { @@ -62,7 +63,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_ON, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, ], }, @@ -106,7 +107,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_ON, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, { "entity_id": "binary_sensor.12_111111111111_sensed_b", @@ -115,7 +116,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_OFF, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, ], SENSOR_DOMAIN: [ @@ -126,7 +127,7 @@ MOCK_OWPROXY_DEVICES = { "result": "25.1", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, { @@ -136,7 +137,7 @@ MOCK_OWPROXY_DEVICES = { "result": "1025.1", ATTR_UNIT_OF_MEASUREMENT: PRESSURE_MBAR, ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ], @@ -148,7 +149,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_ON, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, { "entity_id": "switch.12_111111111111_pio_b", @@ -157,7 +158,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_OFF, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, { "entity_id": "switch.12_111111111111_latch_a", @@ -166,7 +167,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_ON, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, { "entity_id": "switch.12_111111111111_latch_b", @@ -175,7 +176,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_OFF, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, ], }, @@ -308,7 +309,7 @@ MOCK_OWPROXY_DEVICES = { "result": "72.8", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, { @@ -318,7 +319,7 @@ MOCK_OWPROXY_DEVICES = { "result": "73.8", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, { @@ -328,7 +329,7 @@ MOCK_OWPROXY_DEVICES = { "result": "74.8", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, { @@ -338,7 +339,7 @@ MOCK_OWPROXY_DEVICES = { "result": "75.8", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, { @@ -348,7 +349,7 @@ MOCK_OWPROXY_DEVICES = { "result": "unknown", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, { @@ -358,7 +359,7 @@ MOCK_OWPROXY_DEVICES = { "result": "969.3", ATTR_UNIT_OF_MEASUREMENT: PRESSURE_MBAR, ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, { @@ -368,7 +369,7 @@ MOCK_OWPROXY_DEVICES = { "result": "65.9", ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX, ATTR_DEVICE_CLASS: DEVICE_CLASS_ILLUMINANCE, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, { @@ -378,7 +379,7 @@ MOCK_OWPROXY_DEVICES = { "result": "3.0", ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, ATTR_DEVICE_CLASS: DEVICE_CLASS_VOLTAGE, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, { @@ -388,7 +389,7 @@ MOCK_OWPROXY_DEVICES = { "result": "4.7", ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, ATTR_DEVICE_CLASS: DEVICE_CLASS_VOLTAGE, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, { @@ -398,7 +399,7 @@ MOCK_OWPROXY_DEVICES = { "result": "1.0", ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, ATTR_DEVICE_CLASS: DEVICE_CLASS_CURRENT, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, ], @@ -443,7 +444,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_ON, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, { "entity_id": "binary_sensor.29_111111111111_sensed_1", @@ -452,7 +453,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_OFF, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, { "entity_id": "binary_sensor.29_111111111111_sensed_2", @@ -461,7 +462,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_OFF, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, { "entity_id": "binary_sensor.29_111111111111_sensed_3", @@ -470,7 +471,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_OFF, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, { "entity_id": "binary_sensor.29_111111111111_sensed_4", @@ -479,7 +480,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_OFF, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, { "entity_id": "binary_sensor.29_111111111111_sensed_5", @@ -488,7 +489,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_OFF, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, { "entity_id": "binary_sensor.29_111111111111_sensed_6", @@ -497,7 +498,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_OFF, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, { "entity_id": "binary_sensor.29_111111111111_sensed_7", @@ -506,7 +507,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_OFF, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, ], SWITCH_DOMAIN: [ @@ -517,7 +518,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_ON, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, { "entity_id": "switch.29_111111111111_pio_1", @@ -526,7 +527,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_OFF, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, { "entity_id": "switch.29_111111111111_pio_2", @@ -535,7 +536,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_ON, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, { "entity_id": "switch.29_111111111111_pio_3", @@ -544,7 +545,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_OFF, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, { "entity_id": "switch.29_111111111111_pio_4", @@ -553,7 +554,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_ON, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, { "entity_id": "switch.29_111111111111_pio_5", @@ -562,7 +563,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_OFF, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, { "entity_id": "switch.29_111111111111_pio_6", @@ -571,7 +572,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_ON, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, { "entity_id": "switch.29_111111111111_pio_7", @@ -580,7 +581,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_OFF, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, { "entity_id": "switch.29_111111111111_latch_0", @@ -589,7 +590,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_ON, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, { "entity_id": "switch.29_111111111111_latch_1", @@ -598,7 +599,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_OFF, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, { "entity_id": "switch.29_111111111111_latch_2", @@ -607,7 +608,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_ON, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, { "entity_id": "switch.29_111111111111_latch_3", @@ -616,7 +617,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_OFF, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, { "entity_id": "switch.29_111111111111_latch_4", @@ -625,7 +626,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_ON, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, { "entity_id": "switch.29_111111111111_latch_5", @@ -634,7 +635,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_OFF, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, { "entity_id": "switch.29_111111111111_latch_6", @@ -643,7 +644,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_ON, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, { "entity_id": "switch.29_111111111111_latch_7", @@ -652,7 +653,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_OFF, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, ], }, @@ -674,7 +675,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_ON, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, { "entity_id": "binary_sensor.3a_111111111111_sensed_b", @@ -683,7 +684,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_OFF, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, ], SWITCH_DOMAIN: [ @@ -694,7 +695,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_ON, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, { "entity_id": "switch.3a_111111111111_pio_b", @@ -703,7 +704,7 @@ MOCK_OWPROXY_DEVICES = { "result": STATE_OFF, ATTR_UNIT_OF_MEASUREMENT: None, ATTR_DEVICE_CLASS: None, - "disabled": True, + ATTR_DEFAULT_DISABLED: True, }, ], }, diff --git a/tests/components/onewire/test_binary_sensor.py b/tests/components/onewire/test_binary_sensor.py index 752f73d0304..eabe96481ea 100644 --- a/tests/components/onewire/test_binary_sensor.py +++ b/tests/components/onewire/test_binary_sensor.py @@ -1,16 +1,14 @@ """Tests for 1-Wire devices connected on OWServer.""" -import copy from unittest.mock import MagicMock, patch import pytest from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.onewire.binary_sensor import DEVICE_BINARY_SENSORS from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from . import setup_owproxy_mock_devices -from .const import MOCK_OWPROXY_DEVICES +from .const import ATTR_DEFAULT_DISABLED, MOCK_OWPROXY_DEVICES from tests.common import mock_registry @@ -31,26 +29,28 @@ async def test_owserver_binary_sensor( """ entity_registry = mock_registry(hass) - setup_owproxy_mock_devices(owproxy, BINARY_SENSOR_DOMAIN, [device_id]) - mock_device = MOCK_OWPROXY_DEVICES[device_id] expected_entities = mock_device.get(BINARY_SENSOR_DOMAIN, []) - # Force enable binary sensors - patch_device_binary_sensors = copy.deepcopy(DEVICE_BINARY_SENSORS) - if device_binary_sensor := patch_device_binary_sensors.get(device_id[0:2]): - for item in device_binary_sensor: - item.entity_registry_enabled_default = True - - with patch.dict( - "homeassistant.components.onewire.binary_sensor.DEVICE_BINARY_SENSORS", - patch_device_binary_sensors, - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + setup_owproxy_mock_devices(owproxy, BINARY_SENSOR_DOMAIN, [device_id]) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() assert len(entity_registry.entities) == len(expected_entities) + # Ensure all entities are enabled + for expected_entity in expected_entities: + if expected_entity.get(ATTR_DEFAULT_DISABLED): + entity_id = expected_entity["entity_id"] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry.disabled + assert registry_entry.disabled_by == "integration" + entity_registry.async_update_entity(entity_id, **{"disabled_by": None}) + + setup_owproxy_mock_devices(owproxy, BINARY_SENSOR_DOMAIN, [device_id]) + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + for expected_entity in expected_entities: entity_id = expected_entity["entity_id"] registry_entry = entity_registry.entities.get(entity_id) diff --git a/tests/components/onewire/test_sensor.py b/tests/components/onewire/test_sensor.py index 8e8ee40e725..1b4c53b0a63 100644 --- a/tests/components/onewire/test_sensor.py +++ b/tests/components/onewire/test_sensor.py @@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from . import setup_owproxy_mock_devices, setup_sysbus_mock_devices -from .const import MOCK_OWPROXY_DEVICES, MOCK_SYSBUS_DEVICES +from .const import ATTR_DEFAULT_DISABLED, MOCK_OWPROXY_DEVICES, MOCK_SYSBUS_DEVICES from tests.common import assert_setup_component, mock_device_registry, mock_registry @@ -122,7 +122,6 @@ async def test_sensors_on_owserver_coupler( registry_entry = entity_registry.entities.get(entity_id) assert registry_entry is not None assert registry_entry.unique_id == expected_sensor["unique_id"] - assert registry_entry.disabled == expected_sensor.get("disabled", False) state = hass.states.get(entity_id) assert state.state == expected_sensor["result"] for attr in (ATTR_DEVICE_CLASS, ATTR_STATE_CLASS, ATTR_UNIT_OF_MEASUREMENT): @@ -140,16 +139,28 @@ async def test_owserver_setup_valid_device( entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) - setup_owproxy_mock_devices(owproxy, SENSOR_DOMAIN, [device_id]) - mock_device = MOCK_OWPROXY_DEVICES[device_id] expected_entities = mock_device.get(SENSOR_DOMAIN, []) + setup_owproxy_mock_devices(owproxy, SENSOR_DOMAIN, [device_id]) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert len(entity_registry.entities) == len(expected_entities) + # Ensure all entities are enabled + for expected_entity in expected_entities: + if expected_entity.get(ATTR_DEFAULT_DISABLED): + entity_id = expected_entity["entity_id"] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry.disabled + assert registry_entry.disabled_by == "integration" + entity_registry.async_update_entity(entity_id, **{"disabled_by": None}) + + setup_owproxy_mock_devices(owproxy, SENSOR_DOMAIN, [device_id]) + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + if len(expected_entities) > 0: device_info = mock_device["device_info"] assert len(device_registry.devices) == 1 @@ -165,17 +176,13 @@ async def test_owserver_setup_valid_device( registry_entry = entity_registry.entities.get(entity_id) assert registry_entry is not None assert registry_entry.unique_id == expected_entity["unique_id"] - assert registry_entry.disabled == expected_entity.get("disabled", False) state = hass.states.get(entity_id) - if registry_entry.disabled: - assert state is None - else: - assert state.state == expected_entity["result"] - for attr in (ATTR_DEVICE_CLASS, ATTR_STATE_CLASS, ATTR_UNIT_OF_MEASUREMENT): - assert state.attributes.get(attr) == expected_entity[attr] - assert state.attributes["device_file"] == expected_entity.get( - "device_file", registry_entry.unique_id - ) + assert state.state == expected_entity["result"] + for attr in (ATTR_DEVICE_CLASS, ATTR_STATE_CLASS, ATTR_UNIT_OF_MEASUREMENT): + assert state.attributes.get(attr) == expected_entity[attr] + assert state.attributes["device_file"] == expected_entity.get( + "device_file", registry_entry.unique_id + ) @pytest.mark.usefixtures("sysbus") diff --git a/tests/components/onewire/test_switch.py b/tests/components/onewire/test_switch.py index 04a30092db3..aa5230fbb20 100644 --- a/tests/components/onewire/test_switch.py +++ b/tests/components/onewire/test_switch.py @@ -1,17 +1,15 @@ """Tests for 1-Wire devices connected on OWServer.""" -import copy from unittest.mock import MagicMock, patch import pytest -from homeassistant.components.onewire.switch import DEVICE_SWITCHES from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TOGGLE, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from . import setup_owproxy_mock_devices -from .const import MOCK_OWPROXY_DEVICES +from .const import ATTR_DEFAULT_DISABLED, MOCK_OWPROXY_DEVICES from tests.common import mock_registry @@ -32,25 +30,28 @@ async def test_owserver_switch( """ entity_registry = mock_registry(hass) - setup_owproxy_mock_devices(owproxy, SWITCH_DOMAIN, [device_id]) - mock_device = MOCK_OWPROXY_DEVICES[device_id] expected_entities = mock_device.get(SWITCH_DOMAIN, []) - # Force enable switches - patch_device_switches = copy.deepcopy(DEVICE_SWITCHES) - if device_switch := patch_device_switches.get(device_id[0:2]): - for item in device_switch: - item.entity_registry_enabled_default = True - - with patch.dict( - "homeassistant.components.onewire.switch.DEVICE_SWITCHES", patch_device_switches - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + setup_owproxy_mock_devices(owproxy, SWITCH_DOMAIN, [device_id]) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() assert len(entity_registry.entities) == len(expected_entities) + # Ensure all entities are enabled + for expected_entity in expected_entities: + if expected_entity.get(ATTR_DEFAULT_DISABLED): + entity_id = expected_entity["entity_id"] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry.disabled + assert registry_entry.disabled_by == "integration" + entity_registry.async_update_entity(entity_id, **{"disabled_by": None}) + + setup_owproxy_mock_devices(owproxy, SWITCH_DOMAIN, [device_id]) + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + for expected_entity in expected_entities: entity_id = expected_entity["entity_id"] registry_entry = entity_registry.entities.get(entity_id) From 961ee717ef893df991f924f77c55de7a6aa24ed5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 19 Oct 2021 10:23:23 +0200 Subject: [PATCH 0533/1038] Store automation and script traces (#56894) * Store automation and script traces * Pylint * Deduplicate code * Fix issues when no stored traces are available * Store serialized data for restored traces * Update WS API * Update test * Restore context * Improve tests * Add new test files * Rename restore_traces to async_restore_traces * Refactor trace.websocket_api * Defer loading stored traces * Lint * Revert refactoring which is no longer needed * Correct order when restoring traces * Apply suggestion from code review * Improve test coverage * Apply suggestions from code review --- .../components/automation/__init__.py | 1 - homeassistant/components/automation/trace.py | 10 +- homeassistant/components/script/trace.py | 15 +- homeassistant/components/trace/__init__.py | 230 ++++++++- homeassistant/components/trace/const.py | 2 + .../components/trace/websocket_api.py | 75 +-- homeassistant/helpers/trace.py | 19 +- tests/components/script/test_init.py | 128 ++--- tests/components/trace/test_websocket_api.py | 316 +++++++++++- .../trace/automation_saved_traces.json | 486 ++++++++++++++++++ tests/fixtures/trace/script_saved_traces.json | 165 ++++++ 11 files changed, 1256 insertions(+), 191 deletions(-) create mode 100644 tests/fixtures/trace/automation_saved_traces.json create mode 100644 tests/fixtures/trace/script_saved_traces.json diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index b6635d54d2e..92fbd0e8b04 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -228,7 +228,6 @@ def areas_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: async def async_setup(hass, config): """Set up all automations.""" - # Local import to avoid circular import hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass) # To register the automation blueprints diff --git a/homeassistant/components/automation/trace.py b/homeassistant/components/automation/trace.py index 1fbc7e5cbc9..f76dd57e4ed 100644 --- a/homeassistant/components/automation/trace.py +++ b/homeassistant/components/automation/trace.py @@ -8,6 +8,8 @@ from homeassistant.components.trace import ActionTrace, async_store_trace from homeassistant.components.trace.const import CONF_STORED_TRACES from homeassistant.core import Context +from .const import DOMAIN + # mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any @@ -15,6 +17,8 @@ from homeassistant.core import Context class AutomationTrace(ActionTrace): """Container for automation trace.""" + _domain = DOMAIN + def __init__( self, item_id: str, @@ -23,8 +27,7 @@ class AutomationTrace(ActionTrace): context: Context, ) -> None: """Container for automation trace.""" - key = ("automation", item_id) - super().__init__(key, config, blueprint_inputs, context) + super().__init__(item_id, config, blueprint_inputs, context) self._trigger_description: str | None = None def set_trigger_description(self, trigger: str) -> None: @@ -33,6 +36,9 @@ class AutomationTrace(ActionTrace): def as_short_dict(self) -> dict[str, Any]: """Return a brief dictionary version of this AutomationTrace.""" + if self._short_dict: + return self._short_dict + result = super().as_short_dict() result["trigger"] = self._trigger_description return result diff --git a/homeassistant/components/script/trace.py b/homeassistant/components/script/trace.py index afabd68d986..27cb1514448 100644 --- a/homeassistant/components/script/trace.py +++ b/homeassistant/components/script/trace.py @@ -9,20 +9,13 @@ from homeassistant.components.trace import ActionTrace, async_store_trace from homeassistant.components.trace.const import CONF_STORED_TRACES from homeassistant.core import Context, HomeAssistant +from .const import DOMAIN + class ScriptTrace(ActionTrace): - """Container for automation trace.""" + """Container for script trace.""" - def __init__( - self, - item_id: str, - config: dict[str, Any], - blueprint_inputs: dict[str, Any], - context: Context, - ) -> None: - """Container for automation trace.""" - key = ("script", item_id) - super().__init__(key, config, blueprint_inputs, context) + _domain = DOMAIN @contextmanager diff --git a/homeassistant/components/trace/__init__.py b/homeassistant/components/trace/__init__.py index 0ecdb610698..2f41365cb2f 100644 --- a/homeassistant/components/trace/__init__.py +++ b/homeassistant/components/trace/__init__.py @@ -1,15 +1,20 @@ """Support for script and automation tracing and debugging.""" from __future__ import annotations +import abc from collections import deque import datetime as dt -from itertools import count +import logging from typing import Any import voluptuous as vol +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import Context +from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.json import ExtendedJSONEncoder +from homeassistant.helpers.storage import Store from homeassistant.helpers.trace import ( TraceElement, script_execution_get, @@ -18,13 +23,25 @@ from homeassistant.helpers.trace import ( trace_set_child_id, ) import homeassistant.util.dt as dt_util +import homeassistant.util.uuid as uuid_util from . import websocket_api -from .const import CONF_STORED_TRACES, DATA_TRACE, DEFAULT_STORED_TRACES +from .const import ( + CONF_STORED_TRACES, + DATA_TRACE, + DATA_TRACE_STORE, + DATA_TRACES_RESTORED, + DEFAULT_STORED_TRACES, +) from .utils import LimitedSizeDict +_LOGGER = logging.getLogger(__name__) + DOMAIN = "trace" +STORAGE_KEY = "trace.saved_traces" +STORAGE_VERSION = 1 + TRACE_CONFIG_SCHEMA = { vol.Optional(CONF_STORED_TRACES, default=DEFAULT_STORED_TRACES): cv.positive_int } @@ -34,13 +51,89 @@ async def async_setup(hass, config): """Initialize the trace integration.""" hass.data[DATA_TRACE] = {} websocket_api.async_setup(hass) + store = Store(hass, STORAGE_VERSION, STORAGE_KEY, encoder=ExtendedJSONEncoder) + hass.data[DATA_TRACE_STORE] = store + + async def _async_store_traces_at_stop(*_) -> None: + """Save traces to storage.""" + _LOGGER.debug("Storing traces") + try: + await store.async_save( + { + key: list(traces.values()) + for key, traces in hass.data[DATA_TRACE].items() + } + ) + except HomeAssistantError as exc: + _LOGGER.error("Error storing traces", exc_info=exc) + + # Store traces when stopping hass + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_store_traces_at_stop) + return True +async def async_get_trace(hass, key, run_id): + """Return the requested trace.""" + # Restore saved traces if not done + await async_restore_traces(hass) + + return hass.data[DATA_TRACE][key][run_id].as_extended_dict() + + +async def async_list_contexts(hass, key): + """List contexts for which we have traces.""" + # Restore saved traces if not done + await async_restore_traces(hass) + + if key is not None: + values = {key: hass.data[DATA_TRACE].get(key, {})} + else: + values = hass.data[DATA_TRACE] + + def _trace_id(run_id, key) -> dict: + """Make trace_id for the response.""" + domain, item_id = key.split(".", 1) + return {"run_id": run_id, "domain": domain, "item_id": item_id} + + return { + trace.context.id: _trace_id(trace.run_id, key) + for key, traces in values.items() + for trace in traces.values() + } + + +def _get_debug_traces(hass, key): + """Return a serializable list of debug traces for a script or automation.""" + traces = [] + + for trace in hass.data[DATA_TRACE].get(key, {}).values(): + traces.append(trace.as_short_dict()) + + return traces + + +async def async_list_traces(hass, wanted_domain, wanted_key): + """List traces for a domain.""" + # Restore saved traces if not done already + await async_restore_traces(hass) + + if not wanted_key: + traces = [] + for key in hass.data[DATA_TRACE]: + domain = key.split(".", 1)[0] + if domain == wanted_domain: + traces.extend(_get_debug_traces(hass, key)) + else: + traces = _get_debug_traces(hass, wanted_key) + + return traces + + def async_store_trace(hass, trace, stored_traces): - """Store a trace if its item_id is valid.""" + """Store a trace if its key is valid.""" key = trace.key - if key[1]: + if key: traces = hass.data[DATA_TRACE] if key not in traces: traces[key] = LimitedSizeDict(size_limit=stored_traces) @@ -49,14 +142,79 @@ def async_store_trace(hass, trace, stored_traces): traces[key][trace.run_id] = trace -class ActionTrace: +def _async_store_restored_trace(hass, trace): + """Store a restored trace and move it to the end of the LimitedSizeDict.""" + key = trace.key + traces = hass.data[DATA_TRACE] + if key not in traces: + traces[key] = LimitedSizeDict() + traces[key][trace.run_id] = trace + traces[key].move_to_end(trace.run_id, last=False) + + +async def async_restore_traces(hass): + """Restore saved traces.""" + if DATA_TRACES_RESTORED in hass.data: + return + + hass.data[DATA_TRACES_RESTORED] = True + + store = hass.data[DATA_TRACE_STORE] + try: + restored_traces = await store.async_load() or {} + except HomeAssistantError: + _LOGGER.exception("Error loading traces") + restored_traces = {} + + for key, traces in restored_traces.items(): + # Add stored traces in reversed order to priorize the newest traces + for json_trace in reversed(traces): + if ( + (stored_traces := hass.data[DATA_TRACE].get(key)) + and stored_traces.size_limit is not None + and len(stored_traces) >= stored_traces.size_limit + ): + break + + try: + trace = RestoredTrace(json_trace) + # Catch any exception to not blow up if the stored trace is invalid + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Failed to restore trace") + continue + _async_store_restored_trace(hass, trace) + + +class BaseTrace(abc.ABC): """Base container for a script or automation trace.""" - _run_ids = count(0) + context: Context + key: str + + def as_dict(self) -> dict[str, Any]: + """Return an dictionary version of this ActionTrace for saving.""" + return { + "extended_dict": self.as_extended_dict(), + "short_dict": self.as_short_dict(), + } + + @abc.abstractmethod + def as_extended_dict(self) -> dict[str, Any]: + """Return an extended dictionary version of this ActionTrace.""" + + @abc.abstractmethod + def as_short_dict(self) -> dict[str, Any]: + """Return a brief dictionary version of this ActionTrace.""" + + +class ActionTrace(BaseTrace): + """Base container for a script or automation trace.""" + + _domain: str | None = None def __init__( self, - key: tuple[str, str], + item_id: str, config: dict[str, Any], blueprint_inputs: dict[str, Any], context: Context, @@ -69,16 +227,18 @@ class ActionTrace: self._error: Exception | None = None self._state: str = "running" self._script_execution: str | None = None - self.run_id: str = str(next(self._run_ids)) + self.run_id: str = uuid_util.random_uuid_hex() self._timestamp_finish: dt.datetime | None = None self._timestamp_start: dt.datetime = dt_util.utcnow() - self.key: tuple[str, str] = key + self.key = f"{self._domain}.{item_id}" + self._dict: dict[str, Any] | None = None + self._short_dict: dict[str, Any] | None = None if trace_id_get(): trace_set_child_id(self.key, self.run_id) - trace_id_set((key, self.run_id)) + trace_id_set((self.key, self.run_id)) def set_trace(self, trace: dict[str, deque[TraceElement]]) -> None: - """Set trace.""" + """Set action trace.""" self._trace = trace def set_error(self, ex: Exception) -> None: @@ -91,10 +251,12 @@ class ActionTrace: self._state = "stopped" self._script_execution = script_execution_get() - def as_dict(self) -> dict[str, Any]: - """Return dictionary version of this ActionTrace.""" + def as_extended_dict(self) -> dict[str, Any]: + """Return an extended dictionary version of this ActionTrace.""" + if self._dict: + return self._dict - result = self.as_short_dict() + result = dict(self.as_short_dict()) traces = {} if self._trace: @@ -110,15 +272,21 @@ class ActionTrace: } ) + if self._state == "stopped": + # Execution has stopped, save the result + self._dict = result return result def as_short_dict(self) -> dict[str, Any]: """Return a brief dictionary version of this ActionTrace.""" + if self._short_dict: + return self._short_dict last_step = None if self._trace: last_step = list(self._trace)[-1] + domain, item_id = self.key.split(".", 1) result = { "last_step": last_step, @@ -129,10 +297,40 @@ class ActionTrace: "start": self._timestamp_start, "finish": self._timestamp_finish, }, - "domain": self.key[0], - "item_id": self.key[1], + "domain": domain, + "item_id": item_id, } if self._error is not None: result["error"] = str(self._error) + if self._state == "stopped": + # Execution has stopped, save the result + self._short_dict = result return result + + +class RestoredTrace(BaseTrace): + """Container for a restored script or automation trace.""" + + def __init__(self, data: dict[str, Any]) -> None: + """Restore from dict.""" + extended_dict = data["extended_dict"] + short_dict = data["short_dict"] + context = Context( + user_id=extended_dict["context"]["user_id"], + parent_id=extended_dict["context"]["parent_id"], + id=extended_dict["context"]["id"], + ) + self.context = context + self.key = f"{extended_dict['domain']}.{extended_dict['item_id']}" + self.run_id = extended_dict["run_id"] + self._dict = extended_dict + self._short_dict = short_dict + + def as_extended_dict(self) -> dict[str, Any]: + """Return an extended dictionary version of this RestoredTrace.""" + return self._dict + + def as_short_dict(self) -> dict[str, Any]: + """Return a brief dictionary version of this RestoredTrace.""" + return self._short_dict diff --git a/homeassistant/components/trace/const.py b/homeassistant/components/trace/const.py index f64bf4e3f38..f17328325c6 100644 --- a/homeassistant/components/trace/const.py +++ b/homeassistant/components/trace/const.py @@ -2,4 +2,6 @@ CONF_STORED_TRACES = "stored_traces" DATA_TRACE = "trace" +DATA_TRACE_STORE = "trace_store" +DATA_TRACES_RESTORED = "trace_traces_restored" DEFAULT_STORED_TRACES = 5 # Stored traces per script or automation diff --git a/homeassistant/components/trace/websocket_api.py b/homeassistant/components/trace/websocket_api.py index 59d8c58635e..d45265c2989 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 websocket_api +from homeassistant.components import trace, websocket_api from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import ( @@ -24,8 +24,6 @@ from homeassistant.helpers.script import ( debug_stop, ) -from .const import DATA_TRACE - # mypy: allow-untyped-calls, allow-untyped-defs TRACE_DOMAINS = ("automation", "script") @@ -46,7 +44,6 @@ def async_setup(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, websocket_subscribe_breakpoint_events) -@callback @websocket_api.require_admin @websocket_api.websocket_command( { @@ -56,37 +53,27 @@ def async_setup(hass: HomeAssistant) -> None: vol.Required("run_id"): str, } ) -def websocket_trace_get(hass, connection, msg): +@websocket_api.async_response +async def websocket_trace_get(hass, connection, msg): """Get a script or automation trace.""" - key = (msg["domain"], msg["item_id"]) + key = f"{msg['domain']}.{msg['item_id']}" run_id = msg["run_id"] try: - trace = hass.data[DATA_TRACE][key][run_id] + requested_trace = await trace.async_get_trace(hass, key, run_id) except KeyError: connection.send_error( msg["id"], websocket_api.ERR_NOT_FOUND, "The trace could not be found" ) return - message = websocket_api.messages.result_message(msg["id"], trace) + message = websocket_api.messages.result_message(msg["id"], requested_trace) connection.send_message( json.dumps(message, cls=ExtendedJSONEncoder, allow_nan=False) ) -def get_debug_traces(hass, key): - """Return a serializable list of debug traces for a script or automation.""" - traces = [] - - for trace in hass.data[DATA_TRACE].get(key, {}).values(): - traces.append(trace.as_short_dict()) - - return traces - - -@callback @websocket_api.require_admin @websocket_api.websocket_command( { @@ -95,23 +82,17 @@ def get_debug_traces(hass, key): vol.Optional("item_id", "id"): str, } ) -def websocket_trace_list(hass, connection, msg): +@websocket_api.async_response +async def websocket_trace_list(hass, connection, msg): """Summarize script and automation traces.""" - domain = msg["domain"] - key = (domain, msg["item_id"]) if "item_id" in msg else None + wanted_domain = msg["domain"] + key = f"{msg['domain']}.{msg['item_id']}" if "item_id" in msg else None - if not key: - traces = [] - for key in hass.data[DATA_TRACE]: - if key[0] == domain: - traces.extend(get_debug_traces(hass, key)) - else: - traces = get_debug_traces(hass, key) + traces = await trace.async_list_traces(hass, wanted_domain, key) connection.send_result(msg["id"], traces) -@callback @websocket_api.require_admin @websocket_api.websocket_command( { @@ -120,20 +101,12 @@ def websocket_trace_list(hass, connection, msg): vol.Inclusive("item_id", "id"): str, } ) -def websocket_trace_contexts(hass, connection, msg): +@websocket_api.async_response +async def websocket_trace_contexts(hass, connection, msg): """Retrieve contexts we have traces for.""" - key = (msg["domain"], msg["item_id"]) if "item_id" in msg else None + key = f"{msg['domain']}.{msg['item_id']}" if "item_id" in msg else None - if key is not None: - values = {key: hass.data[DATA_TRACE].get(key, {})} - else: - values = hass.data[DATA_TRACE] - - contexts = { - trace.context.id: {"run_id": trace.run_id, "domain": key[0], "item_id": key[1]} - for key, traces in values.items() - for trace in traces.values() - } + contexts = await trace.async_list_contexts(hass, key) connection.send_result(msg["id"], contexts) @@ -151,7 +124,7 @@ def websocket_trace_contexts(hass, connection, msg): ) def websocket_breakpoint_set(hass, connection, msg): """Set breakpoint.""" - key = (msg["domain"], msg["item_id"]) + key = f"{msg['domain']}.{msg['item_id']}" node = msg["node"] run_id = msg.get("run_id") @@ -178,7 +151,7 @@ def websocket_breakpoint_set(hass, connection, msg): ) def websocket_breakpoint_clear(hass, connection, msg): """Clear breakpoint.""" - key = (msg["domain"], msg["item_id"]) + key = f"{msg['domain']}.{msg['item_id']}" node = msg["node"] run_id = msg.get("run_id") @@ -194,7 +167,8 @@ def websocket_breakpoint_list(hass, connection, msg): """List breakpoints.""" breakpoints = breakpoint_list(hass) for _breakpoint in breakpoints: - _breakpoint["domain"], _breakpoint["item_id"] = _breakpoint.pop("key") + key = _breakpoint.pop("key") + _breakpoint["domain"], _breakpoint["item_id"] = key.split(".", 1) connection.send_result(msg["id"], breakpoints) @@ -210,12 +184,13 @@ def websocket_subscribe_breakpoint_events(hass, connection, msg): @callback def breakpoint_hit(key, run_id, node): """Forward events to websocket.""" + domain, item_id = key.split(".", 1) connection.send_message( websocket_api.event_message( msg["id"], { - "domain": key[0], - "item_id": key[1], + "domain": domain, + "item_id": item_id, "run_id": run_id, "node": node, }, @@ -254,7 +229,7 @@ def websocket_subscribe_breakpoint_events(hass, connection, msg): ) def websocket_debug_continue(hass, connection, msg): """Resume execution of halted script or automation.""" - key = (msg["domain"], msg["item_id"]) + key = f"{msg['domain']}.{msg['item_id']}" run_id = msg["run_id"] result = debug_continue(hass, key, run_id) @@ -274,7 +249,7 @@ def websocket_debug_continue(hass, connection, msg): ) def websocket_debug_step(hass, connection, msg): """Single step a halted script or automation.""" - key = (msg["domain"], msg["item_id"]) + key = f"{msg['domain']}.{msg['item_id']}" run_id = msg["run_id"] result = debug_step(hass, key, run_id) @@ -294,7 +269,7 @@ def websocket_debug_step(hass, connection, msg): ) def websocket_debug_stop(hass, connection, msg): """Stop a halted script or automation.""" - key = (msg["domain"], msg["item_id"]) + key = f"{msg['domain']}.{msg['item_id']}" run_id = msg["run_id"] result = debug_stop(hass, key, run_id) diff --git a/homeassistant/helpers/trace.py b/homeassistant/helpers/trace.py index f779ccb84c1..8848be00e79 100644 --- a/homeassistant/helpers/trace.py +++ b/homeassistant/helpers/trace.py @@ -17,7 +17,7 @@ class TraceElement: def __init__(self, variables: TemplateVarsType, path: str) -> None: """Container for trace data.""" - self._child_key: tuple[str, str] | None = None + self._child_key: str | None = None self._child_run_id: str | None = None self._error: Exception | None = None self.path: str = path @@ -40,7 +40,7 @@ class TraceElement: """Container for trace data.""" return str(self.as_dict()) - def set_child_id(self, child_key: tuple[str, str], child_run_id: str) -> None: + def set_child_id(self, child_key: str, child_run_id: str) -> None: """Set trace id of a nested script run.""" self._child_key = child_key self._child_run_id = child_run_id @@ -62,9 +62,10 @@ class TraceElement: """Return dictionary version of this TraceElement.""" result: dict[str, Any] = {"path": self.path, "timestamp": self._timestamp} if self._child_key is not None: + domain, item_id = self._child_key.split(".", 1) result["child_id"] = { - "domain": self._child_key[0], - "item_id": self._child_key[1], + "domain": domain, + "item_id": item_id, "run_id": str(self._child_run_id), } if self._variables: @@ -91,8 +92,8 @@ trace_path_stack_cv: ContextVar[list[str] | None] = ContextVar( ) # Copy of last variables variables_cv: ContextVar[Any | None] = ContextVar("variables_cv", default=None) -# (domain, item_id) + Run ID -trace_id_cv: ContextVar[tuple[tuple[str, str], str] | None] = ContextVar( +# (domain.item_id, Run ID) +trace_id_cv: ContextVar[tuple[str, str] | None] = ContextVar( "trace_id_cv", default=None ) # Reason for stopped script execution @@ -101,12 +102,12 @@ script_execution_cv: ContextVar[StopReason | None] = ContextVar( ) -def trace_id_set(trace_id: tuple[tuple[str, str], str]) -> None: +def trace_id_set(trace_id: tuple[str, str]) -> None: """Set id of the current trace.""" trace_id_cv.set(trace_id) -def trace_id_get() -> tuple[tuple[str, str], str] | None: +def trace_id_get() -> tuple[str, str] | None: """Get id if the current trace.""" return trace_id_cv.get() @@ -182,7 +183,7 @@ def trace_clear() -> None: script_execution_cv.set(StopReason()) -def trace_set_child_id(child_key: tuple[str, str], child_run_id: str) -> None: +def trace_set_child_id(child_key: str, child_run_id: str) -> None: """Set child trace_id of TraceElement at the top of the stack.""" node = cast(TraceElement, trace_stack_top(trace_stack_cv)) if node: diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index 9190b033f44..a6923c88aa2 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -1,7 +1,6 @@ """The tests for the Script component.""" # pylint: disable=protected-access import asyncio -import unittest from unittest.mock import Mock, patch import pytest @@ -29,113 +28,62 @@ from homeassistant.exceptions import ServiceNotFound from homeassistant.helpers import template from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.service import async_get_all_descriptions -from homeassistant.loader import bind_hass -from homeassistant.setup import async_setup_component, setup_component +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import async_mock_service, get_test_home_assistant, mock_restore_cache +from tests.common import async_mock_service, mock_restore_cache from tests.components.logbook.test_init import MockLazyEventPartialState ENTITY_ID = "script.test" -@bind_hass -def turn_on(hass, entity_id, variables=None, context=None): - """Turn script on. +async def test_passing_variables(hass): + """Test different ways of passing in variables.""" + mock_restore_cache(hass, ()) + calls = [] + context = Context() - This is a legacy helper method. Do not use it for new tests. - """ - _, object_id = split_entity_id(entity_id) + @callback + def record_call(service): + """Add recorded event to set.""" + calls.append(service) - hass.services.call(DOMAIN, object_id, variables, context=context) + hass.services.async_register("test", "script", record_call) - -@bind_hass -def turn_off(hass, entity_id): - """Turn script on. - - This is a legacy helper method. Do not use it for new tests. - """ - hass.services.call(DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}) - - -@bind_hass -def toggle(hass, entity_id): - """Toggle the script. - - This is a legacy helper method. Do not use it for new tests. - """ - hass.services.call(DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id}) - - -@bind_hass -def reload(hass): - """Reload script component. - - This is a legacy helper method. Do not use it for new tests. - """ - hass.services.call(DOMAIN, SERVICE_RELOAD) - - -class TestScriptComponent(unittest.TestCase): - """Test the Script component.""" - - # pylint: disable=invalid-name - def setUp(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - - self.addCleanup(self.tear_down_cleanup) - - def tear_down_cleanup(self): - """Stop down everything that was started.""" - self.hass.stop() - - def test_passing_variables(self): - """Test different ways of passing in variables.""" - mock_restore_cache(self.hass, ()) - calls = [] - context = Context() - - @callback - def record_call(service): - """Add recorded event to set.""" - calls.append(service) - - self.hass.services.register("test", "script", record_call) - - assert setup_component( - self.hass, - "script", - { - "script": { - "test": { - "sequence": { - "service": "test.script", - "data_template": {"hello": "{{ greeting }}"}, - } + assert await async_setup_component( + hass, + "script", + { + "script": { + "test": { + "sequence": { + "service": "test.script", + "data_template": {"hello": "{{ greeting }}"}, } } - }, - ) + } + }, + ) - turn_on(self.hass, ENTITY_ID, {"greeting": "world"}, context=context) + await hass.services.async_call( + DOMAIN, "test", {"greeting": "world"}, context=context + ) - self.hass.block_till_done() + await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].context is context - assert calls[0].data["hello"] == "world" + assert len(calls) == 1 + assert calls[0].context is context + assert calls[0].data["hello"] == "world" - self.hass.services.call( - "script", "test", {"greeting": "universe"}, context=context - ) + await hass.services.async_call( + "script", "test", {"greeting": "universe"}, context=context + ) - self.hass.block_till_done() + await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].context is context - assert calls[1].data["hello"] == "universe" + assert len(calls) == 2 + assert calls[1].context is context + assert calls[1].data["hello"] == "universe" @pytest.mark.parametrize("toggle", [False, True]) diff --git a/tests/components/trace/test_websocket_api.py b/tests/components/trace/test_websocket_api.py index 2660fa86879..f55999a1e48 100644 --- a/tests/components/trace/test_websocket_api.py +++ b/tests/components/trace/test_websocket_api.py @@ -1,14 +1,19 @@ """Test Trace websocket API.""" import asyncio +import json +from typing import DefaultDict +from unittest.mock import patch import pytest from homeassistant.bootstrap import async_setup_component from homeassistant.components.trace.const import DEFAULT_STORED_TRACES -from homeassistant.core import Context, callback +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import Context, CoreState, callback from homeassistant.helpers.typing import UNDEFINED +from homeassistant.util.uuid import random_uuid_hex -from tests.common import assert_lists_same +from tests.common import assert_lists_same, load_fixture def _find_run_id(traces, trace_type, item_id): @@ -70,8 +75,12 @@ def _assert_raw_config(domain, config, trace): assert trace["config"] == config -async def _assert_contexts(client, next_id, contexts): - await client.send_json({"id": next_id(), "type": "trace/contexts"}) +async def _assert_contexts(client, next_id, contexts, domain=None, item_id=None): + request = {"id": next_id(), "type": "trace/contexts"} + if domain is not None: + request["domain"] = domain + request["item_id"] = item_id + await client.send_json(request) response = await client.receive_json() assert response["success"] assert response["result"] == contexts @@ -101,6 +110,7 @@ async def _assert_contexts(client, next_id, contexts): ) async def test_get_trace( hass, + hass_storage, hass_ws_client, domain, prefix, @@ -152,6 +162,8 @@ async def test_get_trace( client = await hass_ws_client() contexts = {} + contexts_sun = {} + contexts_moon = {} # Trigger "sun" automation / run "sun" script context = Context() @@ -195,6 +207,11 @@ async def test_get_trace( "domain": domain, "item_id": trace["item_id"], } + contexts_sun[trace["context"]["id"]] = { + "run_id": trace["run_id"], + "domain": domain, + "item_id": trace["item_id"], + } # Trigger "moon" automation, with passing condition / run "moon" script await _run_automation_or_script(hass, domain, moon_config, "test_event2", context) @@ -244,10 +261,17 @@ async def test_get_trace( "domain": domain, "item_id": trace["item_id"], } + contexts_moon[trace["context"]["id"]] = { + "run_id": trace["run_id"], + "domain": domain, + "item_id": trace["item_id"], + } if len(extra_trace_keys) <= 2: # Check contexts await _assert_contexts(client, next_id, contexts) + await _assert_contexts(client, next_id, contexts_moon, domain, "moon") + await _assert_contexts(client, next_id, contexts_sun, domain, "sun") return # Trigger "moon" automation with failing condition @@ -291,6 +315,11 @@ async def test_get_trace( "domain": domain, "item_id": trace["item_id"], } + contexts_moon[trace["context"]["id"]] = { + "run_id": trace["run_id"], + "domain": domain, + "item_id": trace["item_id"], + } # Trigger "moon" automation with passing condition hass.bus.async_fire("test_event2") @@ -336,9 +365,119 @@ async def test_get_trace( "domain": domain, "item_id": trace["item_id"], } + contexts_moon[trace["context"]["id"]] = { + "run_id": trace["run_id"], + "domain": domain, + "item_id": trace["item_id"], + } # Check contexts await _assert_contexts(client, next_id, contexts) + await _assert_contexts(client, next_id, contexts_moon, domain, "moon") + await _assert_contexts(client, next_id, contexts_sun, domain, "sun") + + # List traces + await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain}) + response = await client.receive_json() + assert response["success"] + trace_list = response["result"] + + # Get all traces and generate expected stored traces + traces = DefaultDict(list) + for trace in trace_list: + item_id = trace["item_id"] + run_id = trace["run_id"] + await client.send_json( + { + "id": next_id(), + "type": "trace/get", + "domain": domain, + "item_id": item_id, + "run_id": run_id, + } + ) + response = await client.receive_json() + assert response["success"] + traces[f"{domain}.{item_id}"].append( + {"short_dict": trace, "extended_dict": response["result"]} + ) + + # Fake stop + assert "trace.saved_traces" not in hass_storage + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + # Check that saved data is same as the serialized traces + assert "trace.saved_traces" in hass_storage + assert hass_storage["trace.saved_traces"]["data"] == traces + + +@pytest.mark.parametrize("domain", ["automation", "script"]) +async def test_restore_traces(hass, hass_storage, hass_ws_client, domain): + """Test restored traces.""" + hass.state = CoreState.not_running + id = 1 + + def next_id(): + nonlocal id + id += 1 + return id + + saved_traces = json.loads(load_fixture(f"trace/{domain}_saved_traces.json")) + hass_storage["trace.saved_traces"] = saved_traces + await _setup_automation_or_script(hass, domain, []) + await hass.async_start() + await hass.async_block_till_done() + + client = await hass_ws_client() + + # List traces + await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain}) + response = await client.receive_json() + assert response["success"] + trace_list = response["result"] + + # Get all traces and generate expected stored traces + traces = DefaultDict(list) + contexts = {} + for trace in trace_list: + item_id = trace["item_id"] + run_id = trace["run_id"] + await client.send_json( + { + "id": next_id(), + "type": "trace/get", + "domain": domain, + "item_id": item_id, + "run_id": run_id, + } + ) + response = await client.receive_json() + assert response["success"] + traces[f"{domain}.{item_id}"].append( + {"short_dict": trace, "extended_dict": response["result"]} + ) + contexts[response["result"]["context"]["id"]] = { + "run_id": trace["run_id"], + "domain": domain, + "item_id": trace["item_id"], + } + + # Check that loaded data is same as the serialized traces + assert hass_storage["trace.saved_traces"]["data"] == traces + + # Check restored contexts + await _assert_contexts(client, next_id, contexts) + + # Fake stop + hass_storage.pop("trace.saved_traces") + assert "trace.saved_traces" not in hass_storage + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + # Check that saved data is same as the serialized traces + assert "trace.saved_traces" in hass_storage + assert hass_storage["trace.saved_traces"] == saved_traces @pytest.mark.parametrize("domain", ["automation", "script"]) @@ -368,6 +507,13 @@ async def test_trace_overflow(hass, hass_ws_client, domain, stored_traces): """Test the number of stored traces per script or automation is limited.""" id = 1 + trace_uuids = [] + + def mock_random_uuid_hex(): + nonlocal trace_uuids + trace_uuids.append(random_uuid_hex()) + return trace_uuids[-1] + def next_id(): nonlocal id id += 1 @@ -404,13 +550,16 @@ async def test_trace_overflow(hass, hass_ws_client, domain, stored_traces): response = await client.receive_json() assert response["success"] assert len(_find_traces(response["result"], domain, "moon")) == 1 - moon_run_id = _find_run_id(response["result"], domain, "moon") assert len(_find_traces(response["result"], domain, "sun")) == 1 # Trigger "moon" enough times to overflow the max number of stored traces - for _ in range(stored_traces or DEFAULT_STORED_TRACES): - await _run_automation_or_script(hass, domain, moon_config, "test_event2") - await hass.async_block_till_done() + with patch( + "homeassistant.components.trace.uuid_util.random_uuid_hex", + wraps=mock_random_uuid_hex, + ): + for _ in range(stored_traces or DEFAULT_STORED_TRACES): + await _run_automation_or_script(hass, domain, moon_config, "test_event2") + await hass.async_block_till_done() await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain}) response = await client.receive_json() @@ -418,10 +567,153 @@ async def test_trace_overflow(hass, hass_ws_client, domain, stored_traces): moon_traces = _find_traces(response["result"], domain, "moon") assert len(moon_traces) == stored_traces or DEFAULT_STORED_TRACES assert moon_traces[0] - assert int(moon_traces[0]["run_id"]) == int(moon_run_id) + 1 - assert int(moon_traces[-1]["run_id"]) == int(moon_run_id) + ( - stored_traces or DEFAULT_STORED_TRACES - ) + assert moon_traces[0]["run_id"] == trace_uuids[0] + assert moon_traces[-1]["run_id"] == trace_uuids[-1] + assert len(_find_traces(response["result"], domain, "sun")) == 1 + + +@pytest.mark.parametrize( + "domain,num_restored_moon_traces", [("automation", 3), ("script", 1)] +) +async def test_restore_traces_overflow( + hass, hass_storage, hass_ws_client, domain, num_restored_moon_traces +): + """Test restored traces are evicted first.""" + hass.state = CoreState.not_running + id = 1 + + trace_uuids = [] + + def mock_random_uuid_hex(): + nonlocal trace_uuids + trace_uuids.append(random_uuid_hex()) + return trace_uuids[-1] + + def next_id(): + nonlocal id + id += 1 + return id + + saved_traces = json.loads(load_fixture(f"trace/{domain}_saved_traces.json")) + hass_storage["trace.saved_traces"] = saved_traces + sun_config = { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": {"event": "some_event"}, + } + moon_config = { + "id": "moon", + "trigger": {"platform": "event", "event_type": "test_event2"}, + "action": {"event": "another_event"}, + } + await _setup_automation_or_script(hass, domain, [sun_config, moon_config]) + await hass.async_start() + await hass.async_block_till_done() + + client = await hass_ws_client() + + # Traces should not yet be restored + assert "trace_traces_restored" not in hass.data + + # List traces + await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain}) + response = await client.receive_json() + assert response["success"] + restored_moon_traces = _find_traces(response["result"], domain, "moon") + assert len(restored_moon_traces) == num_restored_moon_traces + assert len(_find_traces(response["result"], domain, "sun")) == 1 + + # Traces should be restored + assert "trace_traces_restored" in hass.data + + # Trigger "moon" enough times to overflow the max number of stored traces + with patch( + "homeassistant.components.trace.uuid_util.random_uuid_hex", + wraps=mock_random_uuid_hex, + ): + for _ in range(DEFAULT_STORED_TRACES - num_restored_moon_traces + 1): + await _run_automation_or_script(hass, domain, moon_config, "test_event2") + await hass.async_block_till_done() + + await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain}) + response = await client.receive_json() + assert response["success"] + moon_traces = _find_traces(response["result"], domain, "moon") + assert len(moon_traces) == DEFAULT_STORED_TRACES + if num_restored_moon_traces > 1: + assert moon_traces[0]["run_id"] == restored_moon_traces[1]["run_id"] + assert moon_traces[num_restored_moon_traces - 1]["run_id"] == trace_uuids[0] + assert moon_traces[-1]["run_id"] == trace_uuids[-1] + assert len(_find_traces(response["result"], domain, "sun")) == 1 + + +@pytest.mark.parametrize( + "domain,num_restored_moon_traces,restored_run_id", + [("automation", 3, "e2c97432afe9b8a42d7983588ed5e6ef"), ("script", 1, "")], +) +async def test_restore_traces_late_overflow( + hass, + hass_storage, + hass_ws_client, + domain, + num_restored_moon_traces, + restored_run_id, +): + """Test restored traces are evicted first.""" + hass.state = CoreState.not_running + id = 1 + + trace_uuids = [] + + def mock_random_uuid_hex(): + nonlocal trace_uuids + trace_uuids.append(random_uuid_hex()) + return trace_uuids[-1] + + def next_id(): + nonlocal id + id += 1 + return id + + saved_traces = json.loads(load_fixture(f"trace/{domain}_saved_traces.json")) + hass_storage["trace.saved_traces"] = saved_traces + sun_config = { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": {"event": "some_event"}, + } + moon_config = { + "id": "moon", + "trigger": {"platform": "event", "event_type": "test_event2"}, + "action": {"event": "another_event"}, + } + await _setup_automation_or_script(hass, domain, [sun_config, moon_config]) + await hass.async_start() + await hass.async_block_till_done() + + client = await hass_ws_client() + + # Traces should not yet be restored + assert "trace_traces_restored" not in hass.data + + # Trigger "moon" enough times to overflow the max number of stored traces + with patch( + "homeassistant.components.trace.uuid_util.random_uuid_hex", + wraps=mock_random_uuid_hex, + ): + for _ in range(DEFAULT_STORED_TRACES - num_restored_moon_traces + 1): + await _run_automation_or_script(hass, domain, moon_config, "test_event2") + await hass.async_block_till_done() + + await client.send_json({"id": next_id(), "type": "trace/list", "domain": domain}) + response = await client.receive_json() + assert response["success"] + moon_traces = _find_traces(response["result"], domain, "moon") + assert len(moon_traces) == DEFAULT_STORED_TRACES + if num_restored_moon_traces > 1: + assert moon_traces[0]["run_id"] == restored_run_id + assert moon_traces[num_restored_moon_traces - 1]["run_id"] == trace_uuids[0] + assert moon_traces[-1]["run_id"] == trace_uuids[-1] assert len(_find_traces(response["result"], domain, "sun")) == 1 diff --git a/tests/fixtures/trace/automation_saved_traces.json b/tests/fixtures/trace/automation_saved_traces.json new file mode 100644 index 00000000000..45bcfffc157 --- /dev/null +++ b/tests/fixtures/trace/automation_saved_traces.json @@ -0,0 +1,486 @@ +{ + "version": 1, + "key": "trace.saved_traces", + "data": { + "automation.sun": [ + { + "extended_dict": { + "last_step": "action/0", + "run_id": "d09f46a4007732c53fa69f434acc1c02", + "state": "stopped", + "script_execution": "error", + "timestamp": { + "start": "2021-10-14T06:43:39.540977+00:00", + "finish": "2021-10-14T06:43:39.542744+00:00" + }, + "domain": "automation", + "item_id": "sun", + "error": "Unable to find service test.automation", + "trigger": "event 'test_event'", + "trace": { + "trigger/0": [ + { + "path": "trigger/0", + "timestamp": "2021-10-14T06:43:39.541024+00:00", + "changed_variables": { + "this": { + "entity_id": "automation.automation_0", + "state": "on", + "attributes": { + "last_triggered": null, + "mode": "single", + "current": 0, + "id": "sun", + "friendly_name": "automation 0" + }, + "last_changed": "2021-10-14T06:43:39.368423+00:00", + "last_updated": "2021-10-14T06:43:39.368423+00:00", + "context": { + "id": "c62f6b3f975b4f9bd479b10a4d7425db", + "parent_id": null, + "user_id": null + } + }, + "trigger": { + "id": "0", + "idx": "0", + "platform": "event", + "event": { + "event_type": "test_event", + "data": {}, + "origin": "LOCAL", + "time_fired": "2021-10-14T06:43:39.540382+00:00", + "context": { + "id": "66934a357e691e845d7f00ee953c0f0f", + "parent_id": null, + "user_id": null + } + }, + "description": "event 'test_event'" + } + } + } + ], + "action/0": [ + { + "path": "action/0", + "timestamp": "2021-10-14T06:43:39.541738+00:00", + "changed_variables": { + "context": { + "id": "4438e85e335bd05e6474d2846d7001cc", + "parent_id": "66934a357e691e845d7f00ee953c0f0f", + "user_id": null + } + }, + "error": "Unable to find service test.automation", + "result": { + "params": { + "domain": "test", + "service": "automation", + "service_data": {}, + "target": {} + }, + "running_script": false, + "limit": 10 + } + } + ] + }, + "config": { + "id": "sun", + "trigger": { + "platform": "event", + "event_type": "test_event" + }, + "action": { + "service": "test.automation" + } + }, + "blueprint_inputs": null, + "context": { + "id": "4438e85e335bd05e6474d2846d7001cc", + "parent_id": "66934a357e691e845d7f00ee953c0f0f", + "user_id": null + } + }, + "short_dict": { + "last_step": "action/0", + "run_id": "d09f46a4007732c53fa69f434acc1c02", + "state": "stopped", + "script_execution": "error", + "timestamp": { + "start": "2021-10-14T06:43:39.540977+00:00", + "finish": "2021-10-14T06:43:39.542744+00:00" + }, + "domain": "automation", + "item_id": "sun", + "error": "Unable to find service test.automation", + "trigger": "event 'test_event'" + } + } + ], + "automation.moon": [ + { + "extended_dict": { + "last_step": "action/0", + "run_id": "511d210ac62aa04668ab418063b57e2c", + "state": "stopped", + "script_execution": "finished", + "timestamp": { + "start": "2021-10-14T06:43:39.545290+00:00", + "finish": "2021-10-14T06:43:39.546962+00:00" + }, + "domain": "automation", + "item_id": "moon", + "trigger": "event 'test_event2'", + "trace": { + "trigger/0": [ + { + "path": "trigger/0", + "timestamp": "2021-10-14T06:43:39.545313+00:00", + "changed_variables": { + "this": { + "entity_id": "automation.automation_1", + "state": "on", + "attributes": { + "last_triggered": null, + "mode": "single", + "current": 0, + "id": "moon", + "friendly_name": "automation 1" + }, + "last_changed": "2021-10-14T06:43:39.369282+00:00", + "last_updated": "2021-10-14T06:43:39.369282+00:00", + "context": { + "id": "c914e818f5b234c0fc0dfddf75e98b0e", + "parent_id": null, + "user_id": null + } + }, + "trigger": { + "id": "0", + "idx": "0", + "platform": "event", + "event": { + "event_type": "test_event2", + "data": {}, + "origin": "LOCAL", + "time_fired": "2021-10-14T06:43:39.545003+00:00", + "context": { + "id": "66934a357e691e845d7f00ee953c0f0f", + "parent_id": null, + "user_id": null + } + }, + "description": "event 'test_event2'" + } + } + } + ], + "condition/0": [ + { + "path": "condition/0", + "timestamp": "2021-10-14T06:43:39.545336+00:00", + "result": { + "result": true, + "entities": [] + } + } + ], + "action/0": [ + { + "path": "action/0", + "timestamp": "2021-10-14T06:43:39.546378+00:00", + "changed_variables": { + "context": { + "id": "8948898e0074ecaa98be2e041256c81b", + "parent_id": "66934a357e691e845d7f00ee953c0f0f", + "user_id": null + } + }, + "result": { + "event": "another_event", + "event_data": {} + } + } + ] + }, + "config": { + "id": "moon", + "trigger": [ + { + "platform": "event", + "event_type": "test_event2" + }, + { + "platform": "event", + "event_type": "test_event3" + } + ], + "condition": { + "condition": "template", + "value_template": "{{ trigger.event.event_type=='test_event2' }}" + }, + "action": { + "event": "another_event" + } + }, + "blueprint_inputs": null, + "context": { + "id": "8948898e0074ecaa98be2e041256c81b", + "parent_id": "66934a357e691e845d7f00ee953c0f0f", + "user_id": null + } + }, + "short_dict": { + "last_step": "action/0", + "run_id": "511d210ac62aa04668ab418063b57e2c", + "state": "stopped", + "script_execution": "finished", + "timestamp": { + "start": "2021-10-14T06:43:39.545290+00:00", + "finish": "2021-10-14T06:43:39.546962+00:00" + }, + "domain": "automation", + "item_id": "moon", + "trigger": "event 'test_event2'" + } + }, + { + "extended_dict": { + "last_step": "condition/0", + "run_id": "e2c97432afe9b8a42d7983588ed5e6ef", + "state": "stopped", + "script_execution": "failed_conditions", + "timestamp": { + "start": "2021-10-14T06:43:39.549081+00:00", + "finish": "2021-10-14T06:43:39.549468+00:00" + }, + "domain": "automation", + "item_id": "moon", + "trigger": "event 'test_event3'", + "trace": { + "trigger/1": [ + { + "path": "trigger/1", + "timestamp": "2021-10-14T06:43:39.549115+00:00", + "changed_variables": { + "this": { + "entity_id": "automation.automation_1", + "state": "on", + "attributes": { + "last_triggered": "2021-10-14T06:43:39.545943+00:00", + "mode": "single", + "current": 0, + "id": "moon", + "friendly_name": "automation 1" + }, + "last_changed": "2021-10-14T06:43:39.369282+00:00", + "last_updated": "2021-10-14T06:43:39.546662+00:00", + "context": { + "id": "8948898e0074ecaa98be2e041256c81b", + "parent_id": "66934a357e691e845d7f00ee953c0f0f", + "user_id": null + } + }, + "trigger": { + "id": "1", + "idx": "1", + "platform": "event", + "event": { + "event_type": "test_event3", + "data": {}, + "origin": "LOCAL", + "time_fired": "2021-10-14T06:43:39.548788+00:00", + "context": { + "id": "5f5113a378b3c06fe146ead2908f6f44", + "parent_id": null, + "user_id": null + } + }, + "description": "event 'test_event3'" + } + } + } + ], + "condition/0": [ + { + "path": "condition/0", + "timestamp": "2021-10-14T06:43:39.549136+00:00", + "result": { + "result": false, + "entities": [] + } + } + ] + }, + "config": { + "id": "moon", + "trigger": [ + { + "platform": "event", + "event_type": "test_event2" + }, + { + "platform": "event", + "event_type": "test_event3" + } + ], + "condition": { + "condition": "template", + "value_template": "{{ trigger.event.event_type=='test_event2' }}" + }, + "action": { + "event": "another_event" + } + }, + "blueprint_inputs": null, + "context": { + "id": "77d041c4e0ecc91ab5e707239c983faf", + "parent_id": "5f5113a378b3c06fe146ead2908f6f44", + "user_id": null + } + }, + "short_dict": { + "last_step": "condition/0", + "run_id": "e2c97432afe9b8a42d7983588ed5e6ef", + "state": "stopped", + "script_execution": "failed_conditions", + "timestamp": { + "start": "2021-10-14T06:43:39.549081+00:00", + "finish": "2021-10-14T06:43:39.549468+00:00" + }, + "domain": "automation", + "item_id": "moon", + "trigger": "event 'test_event3'" + } + }, + { + "extended_dict": { + "last_step": "action/0", + "run_id": "f71d7fa261d361ed999c1dda0a846c99", + "state": "stopped", + "script_execution": "finished", + "timestamp": { + "start": "2021-10-14T06:43:39.551485+00:00", + "finish": "2021-10-14T06:43:39.552822+00:00" + }, + "domain": "automation", + "item_id": "moon", + "trigger": "event 'test_event2'", + "trace": { + "trigger/0": [ + { + "path": "trigger/0", + "timestamp": "2021-10-14T06:43:39.551503+00:00", + "changed_variables": { + "this": { + "entity_id": "automation.automation_1", + "state": "on", + "attributes": { + "last_triggered": "2021-10-14T06:43:39.545943+00:00", + "mode": "single", + "current": 0, + "id": "moon", + "friendly_name": "automation 1" + }, + "last_changed": "2021-10-14T06:43:39.369282+00:00", + "last_updated": "2021-10-14T06:43:39.546662+00:00", + "context": { + "id": "8948898e0074ecaa98be2e041256c81b", + "parent_id": "66934a357e691e845d7f00ee953c0f0f", + "user_id": null + } + }, + "trigger": { + "id": "0", + "idx": "0", + "platform": "event", + "event": { + "event_type": "test_event2", + "data": {}, + "origin": "LOCAL", + "time_fired": "2021-10-14T06:43:39.551202+00:00", + "context": { + "id": "66a59f97502785c544724fdb46bcb94d", + "parent_id": null, + "user_id": null + } + }, + "description": "event 'test_event2'" + } + } + } + ], + "condition/0": [ + { + "path": "condition/0", + "timestamp": "2021-10-14T06:43:39.551524+00:00", + "result": { + "result": true, + "entities": [] + } + } + ], + "action/0": [ + { + "path": "action/0", + "timestamp": "2021-10-14T06:43:39.552236+00:00", + "changed_variables": { + "context": { + "id": "3128b5fa3494cb17cfb485176ef2cee3", + "parent_id": "66a59f97502785c544724fdb46bcb94d", + "user_id": null + } + }, + "result": { + "event": "another_event", + "event_data": {} + } + } + ] + }, + "config": { + "id": "moon", + "trigger": [ + { + "platform": "event", + "event_type": "test_event2" + }, + { + "platform": "event", + "event_type": "test_event3" + } + ], + "condition": { + "condition": "template", + "value_template": "{{ trigger.event.event_type=='test_event2' }}" + }, + "action": { + "event": "another_event" + } + }, + "blueprint_inputs": null, + "context": { + "id": "3128b5fa3494cb17cfb485176ef2cee3", + "parent_id": "66a59f97502785c544724fdb46bcb94d", + "user_id": null + } + }, + "short_dict": { + "last_step": "action/0", + "run_id": "f71d7fa261d361ed999c1dda0a846c99", + "state": "stopped", + "script_execution": "finished", + "timestamp": { + "start": "2021-10-14T06:43:39.551485+00:00", + "finish": "2021-10-14T06:43:39.552822+00:00" + }, + "domain": "automation", + "item_id": "moon", + "trigger": "event 'test_event2'" + } + } + ] + } +} \ No newline at end of file diff --git a/tests/fixtures/trace/script_saved_traces.json b/tests/fixtures/trace/script_saved_traces.json new file mode 100644 index 00000000000..91677b2a47e --- /dev/null +++ b/tests/fixtures/trace/script_saved_traces.json @@ -0,0 +1,165 @@ +{ + "version": 1, + "key": "trace.saved_traces", + "data": { + "script.sun": [ + { + "extended_dict": { + "last_step": "sequence/0", + "run_id": "6bd24c3b715333fd2192c9501b77664a", + "state": "stopped", + "script_execution": "error", + "timestamp": { + "start": "2021-10-14T06:48:18.037973+00:00", + "finish": "2021-10-14T06:48:18.039367+00:00" + }, + "domain": "script", + "item_id": "sun", + "error": "Unable to find service test.automation", + "trace": { + "sequence/0": [ + { + "path": "sequence/0", + "timestamp": "2021-10-14T06:48:18.038692+00:00", + "changed_variables": { + "this": { + "entity_id": "script.sun", + "state": "off", + "attributes": { + "last_triggered": null, + "mode": "single", + "current": 0, + "friendly_name": "sun" + }, + "last_changed": "2021-10-14T06:48:18.023069+00:00", + "last_updated": "2021-10-14T06:48:18.023069+00:00", + "context": { + "id": "0c28537a7a55a0c43360fda5c86fb63a", + "parent_id": null, + "user_id": null + } + }, + "context": { + "id": "436e5cbeb27415fae813d302e2acb168", + "parent_id": null, + "user_id": null + } + }, + "error": "Unable to find service test.automation", + "result": { + "params": { + "domain": "test", + "service": "automation", + "service_data": {}, + "target": {} + }, + "running_script": false, + "limit": 10 + } + } + ] + }, + "config": { + "sequence": { + "service": "test.automation" + } + }, + "blueprint_inputs": null, + "context": { + "id": "436e5cbeb27415fae813d302e2acb168", + "parent_id": null, + "user_id": null + } + }, + "short_dict": { + "last_step": "sequence/0", + "run_id": "6bd24c3b715333fd2192c9501b77664a", + "state": "stopped", + "script_execution": "error", + "timestamp": { + "start": "2021-10-14T06:48:18.037973+00:00", + "finish": "2021-10-14T06:48:18.039367+00:00" + }, + "domain": "script", + "item_id": "sun", + "error": "Unable to find service test.automation" + } + } + ], + "script.moon": [ + { + "extended_dict": { + "last_step": "sequence/0", + "run_id": "76912f5a7f5e7be2300f92523fd3edf7", + "state": "stopped", + "script_execution": "finished", + "timestamp": { + "start": "2021-10-14T06:48:18.045937+00:00", + "finish": "2021-10-14T06:48:18.047293+00:00" + }, + "domain": "script", + "item_id": "moon", + "trace": { + "sequence/0": [ + { + "path": "sequence/0", + "timestamp": "2021-10-14T06:48:18.046659+00:00", + "changed_variables": { + "this": { + "entity_id": "script.moon", + "state": "off", + "attributes": { + "last_triggered": null, + "mode": "single", + "current": 0, + "friendly_name": "moon" + }, + "last_changed": "2021-10-14T06:48:18.023671+00:00", + "last_updated": "2021-10-14T06:48:18.023671+00:00", + "context": { + "id": "3dcdb3daa596e44bfd10b407f3078ec0", + "parent_id": null, + "user_id": null + } + }, + "context": { + "id": "436e5cbeb27415fae813d302e2acb168", + "parent_id": null, + "user_id": null + } + }, + "result": { + "event": "another_event", + "event_data": {} + } + } + ] + }, + "config": { + "sequence": { + "event": "another_event" + } + }, + "blueprint_inputs": null, + "context": { + "id": "436e5cbeb27415fae813d302e2acb168", + "parent_id": null, + "user_id": null + } + }, + "short_dict": { + "last_step": "sequence/0", + "run_id": "76912f5a7f5e7be2300f92523fd3edf7", + "state": "stopped", + "script_execution": "finished", + "timestamp": { + "start": "2021-10-14T06:48:18.045937+00:00", + "finish": "2021-10-14T06:48:18.047293+00:00" + }, + "domain": "script", + "item_id": "moon" + } + } + ] + } +} From 4f2d313a4a4b89070e1bfaabfc0bc386615a3c29 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 19 Oct 2021 10:56:37 +0200 Subject: [PATCH 0534/1038] Remove device category filtering from Tuya init (#58019) --- homeassistant/components/tuya/__init__.py | 25 +++++++--------- homeassistant/components/tuya/const.py | 35 ----------------------- 2 files changed, 11 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index b5ff3ec19ad..d1e661fbe42 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -33,7 +33,6 @@ from .const import ( PLATFORMS, TUYA_DISCOVERY_NEW, TUYA_HA_SIGNAL_UPDATE_ENTITY, - TUYA_SUPPORTED_PRODUCT_CATEGORIES, ) _LOGGER = logging.getLogger(__name__) @@ -118,8 +117,7 @@ async def _init_tuya_sdk(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Register known device IDs for device in device_manager.device_map.values(): - if device.category in TUYA_SUPPORTED_PRODUCT_CATEGORIES: - device_ids.add(device.id) + device_ids.add(device.id) hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -178,20 +176,19 @@ class DeviceListener(TuyaDeviceListener): def add_device(self, device: TuyaDevice) -> None: """Add device added listener.""" - if device.category in TUYA_SUPPORTED_PRODUCT_CATEGORIES: - # Ensure the device isn't present stale - self.hass.add_job(self.async_remove_device, device.id) + # Ensure the device isn't present stale + self.hass.add_job(self.async_remove_device, device.id) - self.device_ids.add(device.id) - dispatcher_send(self.hass, TUYA_DISCOVERY_NEW, [device.id]) + self.device_ids.add(device.id) + dispatcher_send(self.hass, TUYA_DISCOVERY_NEW, [device.id]) - device_manager = self.device_manager - device_manager.mq.stop() - tuya_mq = TuyaOpenMQ(device_manager.api) - tuya_mq.start() + device_manager = self.device_manager + device_manager.mq.stop() + tuya_mq = TuyaOpenMQ(device_manager.api) + tuya_mq.start() - device_manager.mq = tuya_mq - tuya_mq.add_message_listener(device_manager.on_message) + device_manager.mq = tuya_mq + tuya_mq.add_message_listener(device_manager.on_message) def remove_device(self, device_id: str) -> None: """Add device removed listener.""" diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 9c0c1ce2105..bc1bbd49d98 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -84,41 +84,6 @@ TUYA_RESPONSE_MSG = "msg" TUYA_RESPONSE_SUCCESS = "success" TUYA_RESPONSE_PLATFORM_URL = "platform_url" -TUYA_SUPPORTED_PRODUCT_CATEGORIES = ( - "bh", # Smart Kettle - "cwysj", # Pet Water Feeder - "cz", # Socket - "dc", # Light string - "dd", # Light strip - "dj", # Light - "dlq", # Breaker - "fs", # Fan - "fsd", # Ceiling Fan Light - "fwd", # Ambient Light - "fwl", # Ambient light - "gyd", # Motion Sensor Light - "jsq", # Humidifier's light - "kfj", # Coffee maker - "kg", # Switch - "kj", # Air Purifier - "kt", # Air conditioner - "ldcg", # Luminance Sensor - "mcs", # Door Window Sensor - "pc", # Power Strip - "pir", # PIR Detector - "qn", # Heater - "sd", # Robot vacuum - "sgbj", # Siren Alarm - "sos", # SOS Button - "sp", # Smart Camera - "tgq", # Dimmer - "tyndj", # Solar Light - "wk", # Thermostat - "xdd", # Ceiling Light - "xxj", # Diffuser - "zd", # Vibration Sensor -) - TUYA_SMART_APP = "tuyaSmart" SMARTLIFE_APP = "smartlife" From 58569a58a93c644e7e391c030d9170df9d58e5a1 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 19 Oct 2021 12:07:38 +0200 Subject: [PATCH 0535/1038] MQTT Alarm control panel - Enable remote code validation (#57764) * Enable remote code validation * Update homeassistant/components/mqtt/alarm_control_panel.py Co-authored-by: Erik Montnemery Co-authored-by: Erik Montnemery --- .../components/mqtt/alarm_control_panel.py | 13 +- .../mqtt/test_alarm_control_panel.py | 117 ++++++++++++++++++ 2 files changed, 128 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 0966497c024..3d632baf0f7 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -67,6 +67,10 @@ DEFAULT_ARM_HOME = "ARM_HOME" DEFAULT_ARM_CUSTOM_BYPASS = "ARM_CUSTOM_BYPASS" DEFAULT_DISARM = "DISARM" DEFAULT_NAME = "MQTT Alarm" + +REMOTE_CODE = "REMOTE_CODE" +REMOTE_CODE_TEXT = "REMOTE_CODE_TEXT" + PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_CODE): cv.string, @@ -204,7 +208,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): code = self._config.get(CONF_CODE) if code is None: return None - if isinstance(code, str) and re.search("^\\d+$", code): + if code == REMOTE_CODE or (isinstance(code, str) and re.search("^\\d+$", code)): return alarm.FORMAT_NUMBER return alarm.FORMAT_TEXT @@ -296,7 +300,12 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): def _validate_code(self, code, state): """Validate given code.""" conf_code = self._config.get(CONF_CODE) - check = conf_code is None or code == conf_code + check = ( + conf_code is None + or code == conf_code + or (conf_code == REMOTE_CODE and code) + or (conf_code == REMOTE_CODE_TEXT and code) + ) if not check: _LOGGER.warning("Wrong code entered for %s", state) return check diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index e01b246b8af..7b4b8d22168 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -83,6 +83,28 @@ DEFAULT_CONFIG_CODE = { } } +DEFAULT_CONFIG_REMOTE_CODE = { + alarm_control_panel.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "alarm/state", + "command_topic": "alarm/command", + "code": "REMOTE_CODE", + "code_arm_required": True, + } +} + +DEFAULT_CONFIG_REMOTE_CODE_TEXT = { + alarm_control_panel.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "alarm/state", + "command_topic": "alarm/command", + "code": "REMOTE_CODE_TEXT", + "code_arm_required": True, + } +} + async def test_fail_setup_without_state_topic(hass, mqtt_mock): """Test for failing with no state topic.""" @@ -240,6 +262,86 @@ async def test_publish_mqtt_with_code(hass, mqtt_mock, service, payload): mqtt_mock.async_publish.assert_called_once_with("alarm/command", payload, 0, False) +@pytest.mark.parametrize( + "service,payload", + [ + (SERVICE_ALARM_ARM_HOME, "ARM_HOME"), + (SERVICE_ALARM_ARM_AWAY, "ARM_AWAY"), + (SERVICE_ALARM_ARM_NIGHT, "ARM_NIGHT"), + (SERVICE_ALARM_ARM_VACATION, "ARM_VACATION"), + (SERVICE_ALARM_ARM_CUSTOM_BYPASS, "ARM_CUSTOM_BYPASS"), + (SERVICE_ALARM_DISARM, "DISARM"), + ], +) +async def test_publish_mqtt_with_remote_code(hass, mqtt_mock, service, payload): + """Test publishing of MQTT messages when remode code is configured.""" + assert await async_setup_component( + hass, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG_REMOTE_CODE, + ) + await hass.async_block_till_done() + call_count = mqtt_mock.async_publish.call_count + + # No code provided, should not publish + await hass.services.async_call( + alarm_control_panel.DOMAIN, + service, + {ATTR_ENTITY_ID: "alarm_control_panel.test"}, + blocking=True, + ) + assert mqtt_mock.async_publish.call_count == call_count + + # Any code numbered provided, should publish + await hass.services.async_call( + alarm_control_panel.DOMAIN, + service, + {ATTR_ENTITY_ID: "alarm_control_panel.test", ATTR_CODE: "1234"}, + blocking=True, + ) + mqtt_mock.async_publish.assert_called_once_with("alarm/command", payload, 0, False) + + +@pytest.mark.parametrize( + "service,payload", + [ + (SERVICE_ALARM_ARM_HOME, "ARM_HOME"), + (SERVICE_ALARM_ARM_AWAY, "ARM_AWAY"), + (SERVICE_ALARM_ARM_NIGHT, "ARM_NIGHT"), + (SERVICE_ALARM_ARM_VACATION, "ARM_VACATION"), + (SERVICE_ALARM_ARM_CUSTOM_BYPASS, "ARM_CUSTOM_BYPASS"), + (SERVICE_ALARM_DISARM, "DISARM"), + ], +) +async def test_publish_mqtt_with_remote_code_text(hass, mqtt_mock, service, payload): + """Test publishing of MQTT messages when remote text code is configured.""" + assert await async_setup_component( + hass, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG_REMOTE_CODE_TEXT, + ) + await hass.async_block_till_done() + call_count = mqtt_mock.async_publish.call_count + + # No code provided, should not publish + await hass.services.async_call( + alarm_control_panel.DOMAIN, + service, + {ATTR_ENTITY_ID: "alarm_control_panel.test"}, + blocking=True, + ) + assert mqtt_mock.async_publish.call_count == call_count + + # Any code numbered provided, should publish + await hass.services.async_call( + alarm_control_panel.DOMAIN, + service, + {ATTR_ENTITY_ID: "alarm_control_panel.test", ATTR_CODE: "any_code"}, + blocking=True, + ) + mqtt_mock.async_publish.assert_called_once_with("alarm/command", payload, 0, False) + + @pytest.mark.parametrize( "service,payload,disable_code", [ @@ -367,6 +469,21 @@ async def test_attributes_code_number(hass, mqtt_mock): ) +async def test_attributes_remote_code_number(hass, mqtt_mock): + """Test attributes which are not supported by the vacuum.""" + config = copy.deepcopy(DEFAULT_CONFIG_REMOTE_CODE) + config[alarm_control_panel.DOMAIN]["code"] = "REMOTE_CODE" + + assert await async_setup_component(hass, alarm_control_panel.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("alarm_control_panel.test") + assert ( + state.attributes.get(alarm_control_panel.ATTR_CODE_FORMAT) + == alarm_control_panel.FORMAT_NUMBER + ) + + async def test_attributes_code_text(hass, mqtt_mock): """Test attributes which are not supported by the vacuum.""" config = copy.deepcopy(DEFAULT_CONFIG) From 9a5f16d85c99b92c21330cc284579754f8843d32 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 19 Oct 2021 12:07:57 +0200 Subject: [PATCH 0536/1038] Deprecate OpenZWave in manifest (#57987) --- homeassistant/components/ozw/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ozw/manifest.json b/homeassistant/components/ozw/manifest.json index e2adce13339..bf54f217f24 100644 --- a/homeassistant/components/ozw/manifest.json +++ b/homeassistant/components/ozw/manifest.json @@ -1,6 +1,6 @@ { "domain": "ozw", - "name": "OpenZWave (beta)", + "name": "OpenZWave (deprecated)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ozw", "requirements": ["python-openzwave-mqtt[mqtt-client]==1.4.0"], From d8a354fa8f762dc003d34b3e4731b8d03aa9f770 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 19 Oct 2021 12:10:05 +0200 Subject: [PATCH 0537/1038] Add humidifier platform to Tuya (#58025) --- .coveragerc | 1 + homeassistant/components/tuya/const.py | 2 + homeassistant/components/tuya/humidifier.py | 174 ++++++++++++++++++++ 3 files changed, 177 insertions(+) create mode 100644 homeassistant/components/tuya/humidifier.py diff --git a/.coveragerc b/.coveragerc index ab1e7642836..aa6f63ca915 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1116,6 +1116,7 @@ omit = homeassistant/components/tuya/climate.py homeassistant/components/tuya/const.py homeassistant/components/tuya/fan.py + homeassistant/components/tuya/humidifier.py homeassistant/components/tuya/light.py homeassistant/components/tuya/number.py homeassistant/components/tuya/scene.py diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index bc1bbd49d98..b6564ab9024 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -92,6 +92,7 @@ PLATFORMS = [ "camera", "climate", "fan", + "humidifier", "light", "number", "scene", @@ -141,6 +142,7 @@ class DPCode(str, Enum): CUR_CURRENT = "cur_current" # Actual current CUR_POWER = "cur_power" # Actual power CUR_VOLTAGE = "cur_voltage" # Actual voltage + DEHUMIDITY_SET_VALUE = "dehumidify_set_value" DOORCONTACT_STATE = "doorcontact_state" # Status of door window sensor ELECTRICITY_LEFT = "electricity_left" FAN_DIRECTION = "fan_direction" # Fan direction diff --git a/homeassistant/components/tuya/humidifier.py b/homeassistant/components/tuya/humidifier.py new file mode 100644 index 00000000000..4152399daec --- /dev/null +++ b/homeassistant/components/tuya/humidifier.py @@ -0,0 +1,174 @@ +"""Support for Tuya (de)humidifiers.""" +from __future__ import annotations + +from dataclasses import dataclass + +from tuya_iot import TuyaDevice, TuyaDeviceManager + +from homeassistant.components.humidifier import ( + DEVICE_CLASS_DEHUMIDIFIER, + DEVICE_CLASS_HUMIDIFIER, + SUPPORT_MODES, + HumidifierEntity, + HumidifierEntityDescription, +) +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 . import HomeAssistantTuyaData +from .base import EnumTypeData, IntegerTypeData, TuyaEntity +from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode + + +@dataclass +class TuyaHumidifierEntityDescription(HumidifierEntityDescription): + """Describe an Tuya (de)humidifier entity.""" + + # DPCode, to use. If None, the key will be used as DPCode + dpcode: DPCode | tuple[DPCode, ...] | None = None + + humidity: DPCode | None = None + + +HUMIDIFIERS: dict[str, TuyaHumidifierEntityDescription] = { + # Dehumidifier + # https://developer.tuya.com/en/docs/iot/categorycs?id=Kaiuz1vcz4dha + "cs": TuyaHumidifierEntityDescription( + key=DPCode.SWITCH, + dpcode=(DPCode.SWITCH, DPCode.SWITCH_SPRAY), + humidity=DPCode.DEHUMIDITY_SET_VALUE, + device_class=DEVICE_CLASS_DEHUMIDIFIER, + ), + # Humidifier + # https://developer.tuya.com/en/docs/iot/categoryjsq?id=Kaiuz1smr440b + "jsq": TuyaHumidifierEntityDescription( + key=DPCode.SWITCH, + humidity=DPCode.HUMIDITY_SET, + device_class=DEVICE_CLASS_HUMIDIFIER, + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Tuya (de)humidifier dynamically through Tuya discovery.""" + hass_data: HomeAssistantTuyaData = hass.data[DOMAIN][entry.entry_id] + + @callback + def async_discover_device(device_ids: list[str]) -> None: + """Discover and add a discovered Tuya (de)humidifier.""" + entities: list[TuyaHumidifierEntity] = [] + for device_id in device_ids: + device = hass_data.device_manager.device_map[device_id] + if description := HUMIDIFIERS.get(device.category): + entities.append( + TuyaHumidifierEntity(device, hass_data.device_manager, description) + ) + async_add_entities(entities) + + async_discover_device([*hass_data.device_manager.device_map]) + + entry.async_on_unload( + async_dispatcher_connect(hass, TUYA_DISCOVERY_NEW, async_discover_device) + ) + + +class TuyaHumidifierEntity(TuyaEntity, HumidifierEntity): + """Tuya (de)humidifier Device.""" + + _set_humidity_type: IntegerTypeData | None = None + _switch_dpcode: DPCode | None = None + entity_description: TuyaHumidifierEntityDescription + + def __init__( + self, + device: TuyaDevice, + device_manager: TuyaDeviceManager, + description: TuyaHumidifierEntityDescription, + ) -> None: + """Init Tuya (de)humidier.""" + super().__init__(device, device_manager) + self.entity_description = description + self._attr_unique_id = f"{super().unique_id}{description.key}" + self._attr_supported_features = 0 + + # Determine main switch DPCode + possible_dpcodes = description.dpcode or description.key + if isinstance(possible_dpcodes, DPCode) and possible_dpcodes in device.function: + self._switch_dpcode = possible_dpcodes + elif isinstance(possible_dpcodes, tuple): + self._switch_dpcode = next( + (dpcode for dpcode in possible_dpcodes if dpcode in device.function), + None, + ) + + # Determine humidity parameters + if description.humidity in device.status_range: + type_data = IntegerTypeData.from_json( + device.status_range[description.humidity].values + ) + self._set_humidity_type = type_data + self._attr_min_humidity = int(type_data.min_scaled) + self._attr_max_humidity = int(type_data.max_scaled) + + # Determine mode support and provided modes + if DPCode.MODE in device.function: + self._attr_supported_features |= SUPPORT_MODES + self._attr_available_modes = EnumTypeData.from_json( + device.function[DPCode.MODE].values + ).range + + @property + def is_on(self) -> bool: + """Return the device is on or off.""" + if self._switch_dpcode is None: + return False + return self.device.status.get(self._switch_dpcode, False) + + @property + def mode(self) -> str | None: + """Return the current mode.""" + return self.device.status.get(DPCode.MODE) + + @property + def target_humidity(self) -> int | None: + """Return the humidity we try to reach.""" + if self._set_humidity_type is None: + return None + + humidity = self.device.status.get(self.entity_description.humidity) + if humidity is None: + return None + + return round(self._set_humidity_type.scale_value(humidity)) + + def turn_on(self, **kwargs): + """Turn the device on.""" + self._send_command([{"code": self._switch_dpcode, "value": True}]) + + def turn_off(self, **kwargs): + """Turn the device off.""" + self._send_command([{"code": self._switch_dpcode, "value": False}]) + + def set_humidity(self, humidity): + """Set new target humidity.""" + if self._set_humidity_type is None: + raise RuntimeError( + "Cannot set humidity, device doesn't provide methods to set it" + ) + + self._send_command( + [ + { + "code": self.entity_description.humidity, + "value": self._set_humidity_type.scale_value(humidity), + } + ] + ) + + def set_mode(self, mode): + """Set new target preset mode.""" + self._send_command([{"code": DPCode.MODE, "value": mode}]) From 4fe4e65e3e003c23dffc66d6d4e9606b3e3d88fb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 19 Oct 2021 03:29:43 -0700 Subject: [PATCH 0538/1038] Add entity category to Hue (#58011) --- homeassistant/components/hue/sensor.py | 2 ++ tests/components/hue/test_sensor_base.py | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index 80658fff21e..9bd701fe526 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -11,6 +11,7 @@ from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, LIGHT_LUX, PERCENTAGE, TEMP_CELSIUS, @@ -95,6 +96,7 @@ class HueBattery(GenericHueSensor, SensorEntity): _attr_device_class = DEVICE_CLASS_BATTERY _attr_state_class = STATE_CLASS_MEASUREMENT _attr_native_unit_of_measurement = PERCENTAGE + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC @property def unique_id(self): diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py index a639ca31113..8b8eb45a222 100644 --- a/tests/components/hue/test_sensor_base.py +++ b/tests/components/hue/test_sensor_base.py @@ -8,6 +8,8 @@ import pytest from homeassistant.components import hue from homeassistant.components.hue import sensor_base from homeassistant.components.hue.hue_event import CONF_HUE_EVENT +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC +from homeassistant.helpers.entity_registry import async_get from homeassistant.util import dt as dt_util from .conftest import create_mock_bridge, setup_bridge_for_sensors as setup_bridge @@ -357,6 +359,12 @@ async def test_sensors(hass, mock_bridge): assert battery_remote_1.state == "100" assert battery_remote_1.name == "Hue dimmer switch 1 battery level" + ent_reg = async_get(hass) + assert ( + ent_reg.async_get("sensor.hue_dimmer_switch_1_battery_level").entity_category + == ENTITY_CATEGORY_DIAGNOSTIC + ) + async def test_unsupported_sensors(hass, mock_bridge): """Test that unsupported sensors don't get added and don't fail.""" From d1e30fdd54ea71b75daf327a1990e44bb5577929 Mon Sep 17 00:00:00 2001 From: Brig Lamoreaux Date: Tue, 19 Oct 2021 08:15:56 -0700 Subject: [PATCH 0539/1038] Rewrite test for feedreader (#57292) Co-authored-by: Martin Hjelmare --- tests/components/feedreader/test_init.py | 335 +++++++++++++---------- 1 file changed, 188 insertions(+), 147 deletions(-) diff --git a/tests/components/feedreader/test_init.py b/tests/components/feedreader/test_init.py index a433bbe9935..a10018f9b2e 100644 --- a/tests/components/feedreader/test_init.py +++ b/tests/components/feedreader/test_init.py @@ -2,187 +2,228 @@ from datetime import timedelta from os import remove from os.path import exists -import time -import unittest from unittest import mock from unittest.mock import patch +import pytest + from homeassistant.components import feedreader from homeassistant.components.feedreader import ( CONF_MAX_ENTRIES, CONF_URLS, - DEFAULT_MAX_ENTRIES, DEFAULT_SCAN_INTERVAL, EVENT_FEEDREADER, - FeedManager, - StoredData, ) from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START -from homeassistant.core import callback -from homeassistant.setup import setup_component +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util -from tests.common import get_test_home_assistant, load_fixture +from tests.common import async_capture_events, async_fire_time_changed, load_fixture URL = "http://some.rss.local/rss_feed.xml" VALID_CONFIG_1 = {feedreader.DOMAIN: {CONF_URLS: [URL]}} VALID_CONFIG_2 = {feedreader.DOMAIN: {CONF_URLS: [URL], CONF_SCAN_INTERVAL: 60}} VALID_CONFIG_3 = {feedreader.DOMAIN: {CONF_URLS: [URL], CONF_MAX_ENTRIES: 100}} +VALID_CONFIG_4 = {feedreader.DOMAIN: {CONF_URLS: [URL], CONF_MAX_ENTRIES: 5}} -class TestFeedreaderComponent(unittest.TestCase): - """Test the feedreader component.""" +def load_fixture_bytes(src): + """Return byte stream of fixture.""" + feed_data = load_fixture(src) + raw = bytes(feed_data, "utf-8") + return raw - def setUp(self): - """Initialize values for this testcase class.""" - self.hass = get_test_home_assistant() - self.addCleanup(self.tear_down_cleanup) - def tear_down_cleanup(self): - """Clean up files and stop Home Assistant.""" - data_file = self.hass.config.path(f"{feedreader.DOMAIN}.pickle") - if exists(data_file): - remove(data_file) - self.hass.stop() +@pytest.fixture(name="feed_one_event") +def fixture_feed_one_event(hass): + """Load test feed data for one event.""" + return load_fixture_bytes("feedreader.xml") - def test_setup_one_feed(self): - """Test the general setup of this component.""" - with patch( - "homeassistant.components.feedreader.track_time_interval" - ) as track_method: - assert setup_component(self.hass, feedreader.DOMAIN, VALID_CONFIG_1) - track_method.assert_called_once_with( - self.hass, mock.ANY, DEFAULT_SCAN_INTERVAL - ) - def test_setup_scan_interval(self): - """Test the setup of this component with scan interval.""" - with patch( - "homeassistant.components.feedreader.track_time_interval" - ) as track_method: - assert setup_component(self.hass, feedreader.DOMAIN, VALID_CONFIG_2) - track_method.assert_called_once_with( - self.hass, mock.ANY, timedelta(seconds=60) - ) +@pytest.fixture(name="feed_two_event") +def fixture_feed_two_events(hass): + """Load test feed data for two event.""" + return load_fixture_bytes("feedreader1.xml") - def test_setup_max_entries(self): - """Test the setup of this component with max entries.""" - assert setup_component(self.hass, feedreader.DOMAIN, VALID_CONFIG_3) - def setup_manager(self, feed_data, max_entries=DEFAULT_MAX_ENTRIES): - """Set up feed manager.""" - events = [] +@pytest.fixture(name="feed_21_events") +def fixture_feed_21_events(hass): + """Load test feed data for twenty one events.""" + return load_fixture_bytes("feedreader2.xml") - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) - self.hass.bus.listen(EVENT_FEEDREADER, record_event) +@pytest.fixture(name="feed_three_events") +def fixture_feed_three_events(hass): + """Load test feed data for three events.""" + return load_fixture_bytes("feedreader3.xml") - # Loading raw data from fixture and plug in to data object as URL - # works since the third-party feedparser library accepts a URL - # as well as the actual data. - data_file = self.hass.config.path(f"{feedreader.DOMAIN}.pickle") - storage = StoredData(data_file) - with patch( - "homeassistant.components.feedreader.track_time_interval" - ) as track_method: - manager = FeedManager( - feed_data, DEFAULT_SCAN_INTERVAL, max_entries, self.hass, storage - ) - # Can't use 'assert_called_once' here because it's not available - # in Python 3.5 yet. - track_method.assert_called_once_with( - self.hass, mock.ANY, DEFAULT_SCAN_INTERVAL - ) - # Artificially trigger update. - self.hass.bus.fire(EVENT_HOMEASSISTANT_START) - # Collect events. - self.hass.block_till_done() - return manager, events - def test_feed(self): - """Test simple feed with valid data.""" - feed_data = load_fixture("feedreader.xml") - manager, events = self.setup_manager(feed_data) - assert len(events) == 1 - assert events[0].data.title == "Title 1" - assert events[0].data.description == "Description 1" - assert events[0].data.link == "http://www.example.com/link/1" - assert events[0].data.id == "GUID 1" - assert events[0].data.published_parsed.tm_year == 2018 - assert events[0].data.published_parsed.tm_mon == 4 - assert events[0].data.published_parsed.tm_mday == 30 - assert events[0].data.published_parsed.tm_hour == 5 - assert events[0].data.published_parsed.tm_min == 10 - assert manager.last_update_successful is True +@pytest.fixture(name="events") +async def fixture_events(hass): + """Fixture that catches alexa events.""" + return async_capture_events(hass, EVENT_FEEDREADER) - def test_feed_updates(self): - """Test feed updates.""" - # 1. Run - feed_data = load_fixture("feedreader.xml") - manager, events = self.setup_manager(feed_data) - assert len(events) == 1 - # 2. Run - feed_data2 = load_fixture("feedreader1.xml") - # Must patch 'get_timestamp' method because the timestamp is stored - # with the URL which in these tests is the raw XML data. - with patch( - "homeassistant.components.feedreader.StoredData.get_timestamp", - return_value=time.struct_time((2018, 4, 30, 5, 10, 0, 0, 120, 0)), - ): - manager2, events2 = self.setup_manager(feed_data2) - assert len(events2) == 1 - # 3. Run - feed_data3 = load_fixture("feedreader1.xml") - with patch( - "homeassistant.components.feedreader.StoredData.get_timestamp", - return_value=time.struct_time((2018, 4, 30, 5, 11, 0, 0, 120, 0)), - ): - manager3, events3 = self.setup_manager(feed_data3) - assert len(events3) == 0 - def test_feed_default_max_length(self): - """Test long feed beyond the default 20 entry limit.""" - feed_data = load_fixture("feedreader2.xml") - manager, events = self.setup_manager(feed_data) - assert len(events) == 20 +@pytest.fixture(name="feed_storage", autouse=True) +def fixture_feed_storage(hass): + """Create storage account for feedreader.""" + data_file = hass.config.path(f"{feedreader.DOMAIN}.pickle") - def test_feed_max_length(self): - """Test long feed beyond a configured 5 entry limit.""" - feed_data = load_fixture("feedreader2.xml") - manager, events = self.setup_manager(feed_data, max_entries=5) - assert len(events) == 5 + yield - def test_feed_without_publication_date_and_title(self): - """Test simple feed with entry without publication date and title.""" - feed_data = load_fixture("feedreader3.xml") - manager, events = self.setup_manager(feed_data) - assert len(events) == 3 + if exists(data_file): + remove(data_file) + + +async def test_setup_one_feed(hass): + """Test the general setup of this component.""" + with patch( + "homeassistant.components.feedreader.track_time_interval" + ) as track_method: + assert await async_setup_component(hass, feedreader.DOMAIN, VALID_CONFIG_1) + await hass.async_block_till_done() + + track_method.assert_called_once_with(hass, mock.ANY, DEFAULT_SCAN_INTERVAL) + + +async def test_setup_scan_interval(hass): + """Test the setup of this component with scan interval.""" + with patch( + "homeassistant.components.feedreader.track_time_interval" + ) as track_method: + assert await async_setup_component(hass, feedreader.DOMAIN, VALID_CONFIG_2) + await hass.async_block_till_done() + + track_method.assert_called_once_with(hass, mock.ANY, timedelta(seconds=60)) + + +async def test_setup_max_entries(hass): + """Test the setup of this component with max entries.""" + assert await async_setup_component(hass, feedreader.DOMAIN, VALID_CONFIG_3) + await hass.async_block_till_done() + + +async def test_feed(hass, events, feed_one_event): + """Test simple feed with valid data.""" + with patch( + "feedparser.http.get", + return_value=feed_one_event, + ): + assert await async_setup_component(hass, feedreader.DOMAIN, VALID_CONFIG_2) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].data.title == "Title 1" + assert events[0].data.description == "Description 1" + assert events[0].data.link == "http://www.example.com/link/1" + assert events[0].data.id == "GUID 1" + assert events[0].data.published_parsed.tm_year == 2018 + assert events[0].data.published_parsed.tm_mon == 4 + assert events[0].data.published_parsed.tm_mday == 30 + assert events[0].data.published_parsed.tm_hour == 5 + assert events[0].data.published_parsed.tm_min == 10 + + +async def test_feed_updates(hass, events, feed_one_event, feed_two_event): + """Test feed updates.""" + side_effect = [ + feed_one_event, + feed_two_event, + feed_two_event, + ] + + with patch("feedparser.http.get", side_effect=side_effect): + assert await async_setup_component(hass, feedreader.DOMAIN, VALID_CONFIG_2) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() - def test_feed_with_unrecognized_publication_date(self): - """Test simple feed with entry with unrecognized publication date.""" - feed_data = load_fixture("feedreader4.xml") - manager, events = self.setup_manager(feed_data) assert len(events) == 1 - def test_feed_invalid_data(self): - """Test feed with invalid data.""" - feed_data = "INVALID DATA" - manager, events = self.setup_manager(feed_data) - assert len(events) == 0 - assert manager.last_update_successful is True + # Change time and fetch more entries + future = dt_util.utcnow() + timedelta(hours=1, seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - @mock.patch("feedparser.parse", return_value=None) - def test_feed_parsing_failed(self, mock_parse): - """Test feed where parsing fails.""" - data_file = self.hass.config.path(f"{feedreader.DOMAIN}.pickle") - storage = StoredData(data_file) - manager = FeedManager( - "FEED DATA", DEFAULT_SCAN_INTERVAL, DEFAULT_MAX_ENTRIES, self.hass, storage - ) - # Artificially trigger update. - self.hass.bus.fire(EVENT_HOMEASSISTANT_START) - # Collect events. - self.hass.block_till_done() - assert manager.last_update_successful is False + assert len(events) == 2 + + # Change time but no new entries + future = dt_util.utcnow() + timedelta(hours=2, seconds=2) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + assert len(events) == 2 + + +async def test_feed_default_max_length(hass, events, feed_21_events): + """Test long feed beyond the default 20 entry limit.""" + with patch("feedparser.http.get", return_value=feed_21_events): + assert await async_setup_component(hass, feedreader.DOMAIN, VALID_CONFIG_2) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + assert len(events) == 20 + + +async def test_feed_max_length(hass, events, feed_21_events): + """Test long feed beyond a configured 5 entry limit.""" + with patch("feedparser.http.get", return_value=feed_21_events): + assert await async_setup_component(hass, feedreader.DOMAIN, VALID_CONFIG_4) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + assert len(events) == 5 + + +async def test_feed_without_publication_date_and_title(hass, events, feed_three_events): + """Test simple feed with entry without publication date and title.""" + with patch("feedparser.http.get", return_value=feed_three_events): + assert await async_setup_component(hass, feedreader.DOMAIN, VALID_CONFIG_2) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + assert len(events) == 3 + + +async def test_feed_with_unrecognized_publication_date(hass, events): + """Test simple feed with entry with unrecognized publication date.""" + with patch( + "feedparser.http.get", return_value=load_fixture_bytes("feedreader4.xml") + ): + assert await async_setup_component(hass, feedreader.DOMAIN, VALID_CONFIG_2) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + assert len(events) == 1 + + +async def test_feed_invalid_data(hass, events): + """Test feed with invalid data.""" + invalid_data = bytes("INVALID DATA", "utf-8") + with patch("feedparser.http.get", return_value=invalid_data): + assert await async_setup_component(hass, feedreader.DOMAIN, VALID_CONFIG_2) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + assert len(events) == 0 + + +async def test_feed_parsing_failed(hass, events, caplog): + """Test feed where parsing fails.""" + assert "Error fetching feed data" not in caplog.text + + with patch("feedparser.parse", return_value=None): + assert await async_setup_component(hass, feedreader.DOMAIN, VALID_CONFIG_2) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + assert "Error fetching feed data" in caplog.text + assert not events From eb2f2d390537b552fb4710cee7e0fb8389401947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 19 Oct 2021 18:47:14 +0200 Subject: [PATCH 0540/1038] Add configuration url to Airthings (#58041) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/airthings/sensor.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/airthings/sensor.py b/homeassistant/components/airthings/sensor.py index cc54430c417..b2960ff6066 100644 --- a/homeassistant/components/airthings/sensor.py +++ b/homeassistant/components/airthings/sensor.py @@ -29,6 +29,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.update_coordinator import ( CoordinatorEntity, @@ -155,11 +156,12 @@ class AirthingsHeaterEnergySensor(CoordinatorEntity, SensorEntity): self._attr_name = f"{airthings_device.name} {entity_description.name}" self._attr_unique_id = f"{airthings_device.device_id}_{entity_description.key}" self._id = airthings_device.device_id - self._attr_device_info = { - "identifiers": {(DOMAIN, airthings_device.device_id)}, - "name": airthings_device.name, - "manufacturer": "Airthings", - } + self._attr_device_info = DeviceInfo( + configuration_url="https://dashboard.airthings.com/", + identifiers={(DOMAIN, airthings_device.device_id)}, + name=airthings_device.name, + manufacturer="Airthings", + ) @property def native_value(self) -> StateType: From fbe3ce1bf7829e6986480d2532c50ed0215f8296 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 19 Oct 2021 19:36:15 +0200 Subject: [PATCH 0541/1038] Add cover platform to Tuya (#58045) --- .coveragerc | 1 + homeassistant/components/tuya/const.py | 15 ++ homeassistant/components/tuya/cover.py | 340 +++++++++++++++++++++++++ 3 files changed, 356 insertions(+) create mode 100644 homeassistant/components/tuya/cover.py diff --git a/.coveragerc b/.coveragerc index aa6f63ca915..9f122c09007 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1115,6 +1115,7 @@ omit = homeassistant/components/tuya/camera.py homeassistant/components/tuya/climate.py homeassistant/components/tuya/const.py + homeassistant/components/tuya/cover.py homeassistant/components/tuya/fan.py homeassistant/components/tuya/humidifier.py homeassistant/components/tuya/light.py diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index b6564ab9024..233edeac445 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -91,6 +91,7 @@ PLATFORMS = [ "binary_sensor", "camera", "climate", + "cover", "fan", "humidifier", "light", @@ -122,6 +123,8 @@ class DPCode(str, Enum): ALARM_SWITCH = "alarm_switch" # Alarm switch ALARM_TIME = "alarm_time" # Alarm time ALARM_VOLUME = "alarm_volume" # Alarm volume + ANGLE_HORIZONTAL = "angle_horizontal" + ANGLE_VERTICAL = "angle_vertical" ANION = "anion" # Ionizer unit BATTERY_PERCENTAGE = "battery_percentage" # Battery percentage BATTERY_STATE = "battery_state" # Battery state @@ -138,12 +141,17 @@ class DPCode(str, Enum): COLOUR_DATA = "colour_data" # Colored light mode COLOUR_DATA_V2 = "colour_data_v2" # Colored light mode CONCENTRATION_SET = "concentration_set" # Concentration setting + CONTROL = "control" + CONTROL_2 = "control_2" + CONTROL_3 = "control_3" CUP_NUMBER = "cup_number" # NUmber of cups CUR_CURRENT = "cur_current" # Actual current CUR_POWER = "cur_power" # Actual power CUR_VOLTAGE = "cur_voltage" # Actual voltage DEHUMIDITY_SET_VALUE = "dehumidify_set_value" DOORCONTACT_STATE = "doorcontact_state" # Status of door window sensor + DOORCONTACT_STATE_2 = "doorcontact_state_3" + DOORCONTACT_STATE_3 = "doorcontact_state_3" ELECTRICITY_LEFT = "electricity_left" FAN_DIRECTION = "fan_direction" # Fan direction FAN_SPEED_ENUM = "fan_speed_enum" # Speed mode @@ -159,6 +167,12 @@ class DPCode(str, Enum): MOTION_SWITCH = "motion_switch" # Motion switch MUFFLING = "muffling" # Muffling PAUSE = "pause" + PERCENT_CONTROL = "percent_control" + PERCENT_CONTROL_2 = "percent_control_2" + PERCENT_CONTROL_3 = "percent_control_3" + PERCENT_STATE = "percent_state" + PERCENT_STATE_2 = "percent_state_2" + PERCENT_STATE_3 = "percent_state_3" PIR = "pir" # Motion sensor POWDER_SET = "powder_set" # Powder POWER_GO = "power_go" @@ -168,6 +182,7 @@ class DPCode(str, Enum): SENSITIVITY = "sensitivity" # Sensitivity SHAKE = "shake" # Oscillating SHOCK_STATE = "shock_state" # Vibration status + SITUATION_SET = "situation_set" SOS = "sos" # Emergency State SOS_STATE = "sos_state" # Emergency mode SPEED = "speed" # Speed level diff --git a/homeassistant/components/tuya/cover.py b/homeassistant/components/tuya/cover.py new file mode 100644 index 00000000000..bc5a5b85772 --- /dev/null +++ b/homeassistant/components/tuya/cover.py @@ -0,0 +1,340 @@ +"""Support for Tuya Cover.""" +from __future__ import annotations + +from dataclasses import dataclass +import logging +from typing import Any + +from tuya_iot import TuyaDevice, TuyaDeviceManager + +from homeassistant.components.cover import ( + ATTR_POSITION, + ATTR_TILT_POSITION, + DEVICE_CLASS_CURTAIN, + DEVICE_CLASS_GARAGE, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + SUPPORT_SET_TILT_POSITION, + SUPPORT_STOP, + CoverEntity, + CoverEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import HomeAssistantTuyaData +from .base import EnumTypeData, IntegerTypeData, TuyaEntity +from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class TuyaCoverEntityDescription(CoverEntityDescription): + """Describe an Tuya cover entity.""" + + current_state: DPCode | None = None + current_position: DPCode | None = None + set_position: DPCode | None = None + + +COVERS: dict[str, tuple[TuyaCoverEntityDescription, ...]] = { + # Curtain + # Note: Multiple curtains isn't documented + # https://developer.tuya.com/en/docs/iot/categorycl?id=Kaiuz1hnpo7df + "cl": ( + TuyaCoverEntityDescription( + key=DPCode.CONTROL, + name="Curtain", + current_state=DPCode.SITUATION_SET, + current_position=DPCode.PERCENT_STATE, + set_position=DPCode.PERCENT_CONTROL, + device_class=DEVICE_CLASS_CURTAIN, + ), + TuyaCoverEntityDescription( + key=DPCode.CONTROL_2, + name="Curtain 2", + current_position=DPCode.PERCENT_STATE_2, + set_position=DPCode.PERCENT_CONTROL_2, + device_class=DEVICE_CLASS_CURTAIN, + ), + TuyaCoverEntityDescription( + key=DPCode.CONTROL_3, + name="Curtain 3", + current_position=DPCode.PERCENT_STATE_3, + set_position=DPCode.PERCENT_CONTROL_3, + device_class=DEVICE_CLASS_CURTAIN, + ), + ), + # Garage Door Opener + # https://developer.tuya.com/en/docs/iot/categoryckmkzq?id=Kaiuz0ipcboee + "ckmkzq": ( + TuyaCoverEntityDescription( + key=DPCode.SWITCH_1, + name="Door", + current_state=DPCode.DOORCONTACT_STATE, + device_class=DEVICE_CLASS_GARAGE, + ), + TuyaCoverEntityDescription( + key=DPCode.SWITCH_2, + name="Door 2", + current_state=DPCode.DOORCONTACT_STATE_2, + device_class=DEVICE_CLASS_GARAGE, + ), + TuyaCoverEntityDescription( + key=DPCode.SWITCH_3, + name="Door 3", + current_state=DPCode.DOORCONTACT_STATE_3, + device_class=DEVICE_CLASS_GARAGE, + ), + ), + # Curtain Switch + # https://developer.tuya.com/en/docs/iot/category-clkg?id=Kaiuz0gitil39 + "clkg": ( + TuyaCoverEntityDescription( + key=DPCode.CONTROL, + name="Curtain", + current_position=DPCode.PERCENT_CONTROL, + set_position=DPCode.PERCENT_CONTROL, + device_class=DEVICE_CLASS_CURTAIN, + ), + TuyaCoverEntityDescription( + key=DPCode.CONTROL_2, + name="Curtain 2", + current_position=DPCode.PERCENT_CONTROL_2, + set_position=DPCode.PERCENT_CONTROL_2, + device_class=DEVICE_CLASS_CURTAIN, + ), + ), + # Curtain Robot + # Note: Not documented + "jdcljqr": ( + TuyaCoverEntityDescription( + key=DPCode.CONTROL, + current_position=DPCode.PERCENT_STATE, + set_position=DPCode.PERCENT_CONTROL, + device_class=DEVICE_CLASS_CURTAIN, + ), + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +) -> None: + """Set up Tuya cover dynamically through Tuya discovery.""" + hass_data: HomeAssistantTuyaData = hass.data[DOMAIN][entry.entry_id] + + @callback + def async_discover_device(device_ids: list[str]) -> None: + """Discover and add a discovered tuya cover.""" + entities: list[TuyaCoverEntity] = [] + for device_id in device_ids: + device = hass_data.device_manager.device_map[device_id] + if descriptions := COVERS.get(device.category): + for description in descriptions: + if ( + description.key in device.function + or description.key in device.status + ): + entities.append( + TuyaCoverEntity( + device, hass_data.device_manager, description + ) + ) + + async_add_entities(entities) + + async_discover_device([*hass_data.device_manager.device_map]) + + entry.async_on_unload( + async_dispatcher_connect(hass, TUYA_DISCOVERY_NEW, async_discover_device) + ) + + +class TuyaCoverEntity(TuyaEntity, CoverEntity): + """Tuya Cover Device.""" + + _current_position_type: IntegerTypeData | None = None + _set_position_type: IntegerTypeData | None = None + _tilt_dpcode: DPCode | None = None + _tilt_type: IntegerTypeData | None = None + entity_description: TuyaCoverEntityDescription + + def __init__( + self, + device: TuyaDevice, + device_manager: TuyaDeviceManager, + description: TuyaCoverEntityDescription, + ) -> None: + """Init Tuya Cover.""" + super().__init__(device, device_manager) + self.entity_description = description + self._attr_unique_id = f"{super().unique_id}{description.key}" + self._attr_supported_features = 0 + + # Check if this cover is based on a switch or has controls + if device.function[description.key].type == "Boolean": + self._attr_supported_features |= SUPPORT_OPEN | SUPPORT_CLOSE + elif device.function[description.key].type == "Enum": + data_type = EnumTypeData.from_json( + device.status_range[description.key].values + ) + if "open" in data_type.range: + self._attr_supported_features |= SUPPORT_OPEN + if "close" in data_type.range: + self._attr_supported_features |= SUPPORT_CLOSE + if "stop" in data_type.range: + self._attr_supported_features |= SUPPORT_STOP + + # Determine type to use for setting the position + if ( + description.set_position is not None + and description.set_position in device.status_range + ): + self._attr_supported_features |= SUPPORT_SET_POSITION + self._set_position_type = IntegerTypeData.from_json( + device.status_range[description.set_position].values + ) + # Set as default, unless overwritten below + self._current_position_type = self._set_position_type + + # Determine type for getting the position + if ( + description.current_position is not None + and description.current_position in device.status_range + ): + self._current_position_type = IntegerTypeData.from_json( + device.status_range[description.current_position].values + ) + + # Determine type to use for setting the tilt + if tilt_dpcode := next( + ( + dpcode + for dpcode in (DPCode.ANGLE_HORIZONTAL, DPCode.ANGLE_VERTICAL) + if dpcode in device.function + ), + None, + ): + self._attr_supported_features |= SUPPORT_SET_TILT_POSITION + self._tilt_dpcode = tilt_dpcode + self._tilt_type = IntegerTypeData.from_json( + device.status_range[tilt_dpcode].values + ) + + @property + def current_cover_position(self) -> int | None: + """Return cover current position.""" + if self._current_position_type is None: + return None + + if not ( + dpcode := ( + self.entity_description.current_position + or self.entity_description.set_position + ) + ): + return None + + position = self.device.status.get(dpcode) + if position is None: + return None + + return round( + self._current_position_type.remap_value_to(position, 0, 100, reverse=True) + ) + + @property + def current_cover_tilt_position(self) -> int | None: + """Return current position of cover tilt. + + None is unknown, 0 is closed, 100 is fully open. + """ + if self._tilt_dpcode is None or self._tilt_type is None: + return None + + angle = self.device.status.get(self._tilt_dpcode) + if angle is None: + return None + + return round(self._tilt_type.remap_value_to(angle, 0, 100)) + + @property + def is_closed(self) -> bool | None: + """Return is cover is closed.""" + if ( + self.entity_description.current_state is not None + and ( + current_state := self.device.status.get( + self.entity_description.current_state + ) + ) + is not None + ): + return current_state in (True, "fully_close") + + if (position := self.current_cover_position) is not None: + return position == 0 + + return None + + def open_cover(self, **kwargs: Any) -> None: + """Open the cover.""" + value: bool | str = True + if self.device.function[self.entity_description.key].type == "Enum": + value = "open" + self._send_command([{"code": self.entity_description.key, "value": value}]) + + def close_cover(self, **kwargs: Any) -> None: + """Close cover.""" + value: bool | str = True + if self.device.function[self.entity_description.key].type == "Enum": + value = "close" + self._send_command([{"code": self.entity_description.key, "value": value}]) + + def set_cover_position(self, **kwargs: Any) -> None: + """Move the cover to a specific position.""" + if self._set_position_type is None: + raise RuntimeError( + "Cannot set position, device doesn't provide methods to set it" + ) + + self._send_command( + [ + { + "code": self.entity_description.set_position, + "value": round( + self._set_position_type.remap_value_from( + kwargs[ATTR_POSITION], 0, 100, reverse=True + ) + ), + } + ] + ) + + def stop_cover(self, **kwargs: Any) -> None: + """Stop the cover.""" + self._send_command([{"code": self.entity_description.key, "value": "stop"}]) + + def set_cover_tilt_position(self, **kwargs): + """Move the cover tilt to a specific position.""" + if self._tilt_type is None: + raise RuntimeError( + "Cannot set tilt, device doesn't provide methods to set it" + ) + + self._send_command( + [ + { + "code": self._tilt_dpcode, + "value": round( + self._tilt_type.remap_value_from( + kwargs[ATTR_TILT_POSITION], 0, 100, reverse=True + ) + ), + } + ] + ) From 24d3bf09300e0695862cf04c714ba9114cfcced5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 19 Oct 2021 19:57:01 +0200 Subject: [PATCH 0542/1038] Add configuration url to Tractive (#58038) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/tractive/entity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/tractive/entity.py b/homeassistant/components/tractive/entity.py index abdcbb4586c..fd29c6c6c6f 100644 --- a/homeassistant/components/tractive/entity.py +++ b/homeassistant/components/tractive/entity.py @@ -16,6 +16,7 @@ class TractiveEntity(Entity): ) -> None: """Initialize tracker entity.""" self._attr_device_info = DeviceInfo( + configuration_url="https://my.tractive.com/", identifiers={(DOMAIN, tracker_details["_id"])}, name=f"Tractive ({tracker_details['_id']})", manufacturer="Tractive GmbH", From b7db8dd62a323b2f6707c2e660acd94471037f10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 19 Oct 2021 20:00:08 +0200 Subject: [PATCH 0543/1038] Add configuration url to Surepetcare (#58039) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/surepetcare/entity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/surepetcare/entity.py b/homeassistant/components/surepetcare/entity.py index 8cdf0a74189..e1faaf07e26 100644 --- a/homeassistant/components/surepetcare/entity.py +++ b/homeassistant/components/surepetcare/entity.py @@ -35,6 +35,7 @@ class SurePetcareEntity(CoordinatorEntity): self._device_id = f"{surepy_entity.household_id}-{surepetcare_id}" self._attr_device_info = DeviceInfo( + configuration_url="https://surepetcare.io/dashboard/", identifiers={(DOMAIN, self._device_id)}, name=self._device_name, manufacturer="Sure Petcare", From 8eef2113c7df5cc88c506a63ebb52f1fc79ed5bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 19 Oct 2021 20:08:03 +0200 Subject: [PATCH 0544/1038] Add more info to OpenGarage device info (#58037) --- homeassistant/components/opengarage/entity.py | 7 ++++++- homeassistant/components/opengarage/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/opengarage/entity.py b/homeassistant/components/opengarage/entity.py index 7c6d169935a..706ff0e81be 100644 --- a/homeassistant/components/opengarage/entity.py +++ b/homeassistant/components/opengarage/entity.py @@ -2,6 +2,7 @@ from homeassistant.components.opengarage import DOMAIN from homeassistant.core import callback +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -36,8 +37,12 @@ class OpenGarageEntity(CoordinatorEntity): def device_info(self): """Return the device_info of the device.""" device_info = DeviceInfo( + configuration_url=self.coordinator.open_garage_connection.device_url, + connections={(CONNECTION_NETWORK_MAC, self.coordinator.data["mac"])}, identifiers={(DOMAIN, self._device_id)}, - name=self.coordinator.data["name"], manufacturer="Open Garage", + name=self.coordinator.data["name"], + suggested_area="Garage", + sw_version=self.coordinator.data["fwv"], ) return device_info diff --git a/homeassistant/components/opengarage/manifest.json b/homeassistant/components/opengarage/manifest.json index bf32b060f11..929a0a0080d 100644 --- a/homeassistant/components/opengarage/manifest.json +++ b/homeassistant/components/opengarage/manifest.json @@ -6,7 +6,7 @@ "@danielhiversen" ], "requirements": [ - "open-garage==0.1.5" + "open-garage==0.1.6" ], "iot_class": "local_polling", "config_flow": true diff --git a/requirements_all.txt b/requirements_all.txt index 9467dfde3a0..bb61a6c9947 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1120,7 +1120,7 @@ onkyo-eiscp==1.2.7 onvif-zeep-async==1.2.0 # homeassistant.components.opengarage -open-garage==0.1.5 +open-garage==0.1.6 # homeassistant.components.opencv # opencv-python-headless==4.5.2.54 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a3311d132af..abada917b08 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -670,7 +670,7 @@ ondilo==0.2.0 onvif-zeep-async==1.2.0 # homeassistant.components.opengarage -open-garage==0.1.5 +open-garage==0.1.6 # homeassistant.components.openerz openerz-api==0.1.0 From bb9053e93de6d4d957832d7724a255abf8b99997 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 19 Oct 2021 20:08:44 +0200 Subject: [PATCH 0545/1038] Add MWh as an energy unit (#58034) --- homeassistant/components/sensor/recorder.py | 2 ++ homeassistant/const.py | 1 + 2 files changed, 3 insertions(+) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 3ac4cf4e53b..9cc4ea9c434 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -39,6 +39,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, + ENERGY_MEGA_WATT_HOUR, ENERGY_WATT_HOUR, POWER_KILO_WATT, POWER_WATT, @@ -95,6 +96,7 @@ UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = { # Convert energy to kWh DEVICE_CLASS_ENERGY: { ENERGY_KILO_WATT_HOUR: lambda x: x, + ENERGY_MEGA_WATT_HOUR: lambda x: x * 1000, ENERGY_WATT_HOUR: lambda x: x / 1000, }, # Convert power W diff --git a/homeassistant/const.py b/homeassistant/const.py index 1c3029a9d34..6e9f8ee97d7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -419,6 +419,7 @@ POWER_VOLT_AMPERE: Final = "VA" # Energy units ENERGY_WATT_HOUR: Final = "Wh" ENERGY_KILO_WATT_HOUR: Final = "kWh" +ENERGY_MEGA_WATT_HOUR: Final = "MWh" # Electric_current units ELECTRIC_CURRENT_MILLIAMPERE: Final = "mA" From 6d898a631ccafe700dd86145ff25e99dd5bbb66b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 19 Oct 2021 20:09:06 +0200 Subject: [PATCH 0546/1038] Add Water Detector (sj) device support to Tuya (#58049) --- homeassistant/components/tuya/binary_sensor.py | 16 ++++++++++++++++ homeassistant/components/tuya/const.py | 1 + homeassistant/components/tuya/sensor.py | 9 ++++++--- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index a083cfd73f5..22db498a1c4 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -7,6 +7,7 @@ from tuya_iot import TuyaDevice, TuyaDeviceManager from homeassistant.components.binary_sensor import ( DEVICE_CLASS_DOOR, + DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, DEVICE_CLASS_SAFETY, DEVICE_CLASS_TAMPER, @@ -80,6 +81,21 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ), + # Water Detector + # https://developer.tuya.com/en/docs/iot/categorysj?id=Kaiuz3iub2sli + "sj": ( + TuyaBinarySensorEntityDescription( + key=DPCode.WATERSENSOR_STATE, + device_class=DEVICE_CLASS_MOISTURE, + on_value="alarm", + ), + TuyaBinarySensorEntityDescription( + key=DPCode.TEMPER_ALARM, + name="Tamper", + device_class=DEVICE_CLASS_TAMPER, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + ), # Emergency Button # https://developer.tuya.com/en/docs/iot/categorysos?id=Kaiuz3oi6agjy "sos": ( diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 233edeac445..6c6f84214d7 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -227,6 +227,7 @@ class DPCode(str, Enum): WARM_TIME = "warm_time" # Heat preservation time WATER_RESET = "water_reset" # Resetting of water usage days WATER_SET = "water_set" # Water level + WATERSENSOR_STATE = "watersensor_state" WET = "wet" # Humidification WORK_MODE = "work_mode" # Working mode diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index fc12b9b9ace..739ababfadd 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -127,12 +127,15 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { # PIR Detector # https://developer.tuya.com/en/docs/iot/categorypir?id=Kaiuz3ss11b80 "pir": BATTERY_SENSORS, - # Vibration Sensor - # https://developer.tuya.com/en/docs/iot/categoryzd?id=Kaiuz3a5vrzno - "zd": BATTERY_SENSORS, + # Water Detector + # https://developer.tuya.com/en/docs/iot/categorysj?id=Kaiuz3iub2sli + "sj": BATTERY_SENSORS, # Emergency Button # https://developer.tuya.com/en/docs/iot/categorysos?id=Kaiuz3oi6agjy "sos": BATTERY_SENSORS, + # Vibration Sensor + # https://developer.tuya.com/en/docs/iot/categoryzd?id=Kaiuz3a5vrzno + "zd": BATTERY_SENSORS, } # Socket (duplicate of `kg`) From d2f7f418c3a3f9e2c035deeff5b2652acc876a1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Tue, 19 Oct 2021 20:15:40 +0200 Subject: [PATCH 0547/1038] Add more sensors to Opengarage (#58042) --- homeassistant/components/opengarage/sensor.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/homeassistant/components/opengarage/sensor.py b/homeassistant/components/opengarage/sensor.py index e6b6a73c0c6..ed42b5fef3d 100644 --- a/homeassistant/components/opengarage/sensor.py +++ b/homeassistant/components/opengarage/sensor.py @@ -9,10 +9,14 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.const import ( + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TEMPERATURE, ENTITY_CATEGORY_DIAGNOSTIC, LENGTH_CENTIMETERS, + PERCENTAGE, SIGNAL_STRENGTH_DECIBELS, + TEMP_CELSIUS, ) from homeassistant.core import callback @@ -35,6 +39,18 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, state_class=STATE_CLASS_MEASUREMENT, ), + SensorEntityDescription( + key="temp", + device_class=DEVICE_CLASS_TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="humid", + device_class=DEVICE_CLASS_HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), ) @@ -49,6 +65,7 @@ async def async_setup_entry(hass, entry, async_add_entities): description, ) for description in SENSOR_TYPES + if description.key in open_garage_data_coordinator.data ], ) From bc9b134c5d4c2e0690b798277fedeba8f230097a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 19 Oct 2021 21:04:46 +0200 Subject: [PATCH 0548/1038] Clean up self references from Tuya climate platform constructor (#58051) --- homeassistant/components/tuya/climate.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index 1ba061463f5..b26ff34bc6d 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -252,7 +252,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): ): self._current_humidity_dpcode = DPCode.HUMIDITY_CURRENT self._current_humidity_type = IntegerTypeData.from_json( - self.device.status_range[DPCode.HUMIDITY_CURRENT].values + device.status_range[DPCode.HUMIDITY_CURRENT].values ) # Determine dpcode to use for getting the current humidity @@ -262,7 +262,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): ): self._current_humidity_dpcode = DPCode.HUMIDITY_CURRENT self._current_humidity_type = IntegerTypeData.from_json( - self.device.status_range[DPCode.HUMIDITY_CURRENT].values + device.status_range[DPCode.HUMIDITY_CURRENT].values ) # Determine fan modes @@ -272,12 +272,12 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): ): self._attr_supported_features |= SUPPORT_FAN_MODE self._attr_fan_modes = EnumTypeData.from_json( - self.device.status_range[DPCode.FAN_SPEED_ENUM].values + device.status_range[DPCode.FAN_SPEED_ENUM].values ).range # Determine swing modes if any( - dpcode in self.device.function + dpcode in device.function for dpcode in ( DPCode.SHAKE, DPCode.SWING, @@ -288,15 +288,14 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): self._attr_supported_features |= SUPPORT_SWING_MODE self._attr_swing_modes = [SWING_OFF] if any( - dpcode in self.device.function - for dpcode in (DPCode.SHAKE, DPCode.SWING) + dpcode in device.function for dpcode in (DPCode.SHAKE, DPCode.SWING) ): self._attr_swing_modes.append(SWING_ON) - if DPCode.SWITCH_HORIZONTAL in self.device.function: + if DPCode.SWITCH_HORIZONTAL in device.function: self._attr_swing_modes.append(SWING_HORIZONTAL) - if DPCode.SWITCH_VERTICAL in self.device.function: + if DPCode.SWITCH_VERTICAL in device.function: self._attr_swing_modes.append(SWING_VERTICAL) def set_hvac_mode(self, hvac_mode: str) -> None: From ab0247d1129c62c208686230b3136e55ec8ca622 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 19 Oct 2021 12:29:22 -0700 Subject: [PATCH 0549/1038] Add entity category and state class to mobile app (#58012) --- homeassistant/components/mobile_app/const.py | 2 + homeassistant/components/mobile_app/entity.py | 6 +++ homeassistant/components/mobile_app/sensor.py | 8 +++ .../components/mobile_app/webhook.py | 52 +++++++++++++------ tests/components/mobile_app/test_sensor.py | 10 +++- tests/components/mobile_app/test_webhook.py | 41 +++++++++++++++ 6 files changed, 103 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index 48fed416c33..2f5db21b815 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -61,9 +61,11 @@ ERR_INVALID_FORMAT = "invalid_format" ATTR_SENSOR_ATTRIBUTES = "attributes" ATTR_SENSOR_DEVICE_CLASS = "device_class" +ATTR_SENSOR_ENTITY_CATEGORY = "entity_category" ATTR_SENSOR_ICON = "icon" ATTR_SENSOR_NAME = "name" ATTR_SENSOR_STATE = "state" +ATTR_SENSOR_STATE_CLASS = "state_class" ATTR_SENSOR_TYPE = "type" ATTR_SENSOR_TYPE_BINARY_SENSOR = "binary_sensor" ATTR_SENSOR_TYPE_SENSOR = "sensor" diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py index d0de89a94a1..0cdec984f55 100644 --- a/homeassistant/components/mobile_app/entity.py +++ b/homeassistant/components/mobile_app/entity.py @@ -9,6 +9,7 @@ from homeassistant.helpers.restore_state import RestoreEntity from .const import ( ATTR_SENSOR_ATTRIBUTES, ATTR_SENSOR_DEVICE_CLASS, + ATTR_SENSOR_ENTITY_CATEGORY, ATTR_SENSOR_ICON, ATTR_SENSOR_STATE, ATTR_SENSOR_TYPE, @@ -86,6 +87,11 @@ class MobileAppEntity(RestoreEntity): """Return the icon to use in the frontend, if any.""" return self._config[ATTR_SENSOR_ICON] + @property + def entity_category(self): + """Return the entity category, if any.""" + return self._config.get(ATTR_SENSOR_ENTITY_CATEGORY) + @property def unique_id(self): """Return the unique ID of this sensor.""" diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index f6652f7f889..9d56e55a106 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -1,4 +1,6 @@ """Sensor platform for mobile_app.""" +from __future__ import annotations + from homeassistant.components.sensor import SensorEntity from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, CONF_WEBHOOK_ID from homeassistant.core import callback @@ -12,6 +14,7 @@ from .const import ( ATTR_SENSOR_ICON, ATTR_SENSOR_NAME, ATTR_SENSOR_STATE, + ATTR_SENSOR_STATE_CLASS, ATTR_SENSOR_TYPE, ATTR_SENSOR_TYPE_SENSOR as ENTITY_TYPE, ATTR_SENSOR_UNIQUE_ID, @@ -82,3 +85,8 @@ class MobileAppSensor(MobileAppEntity, SensorEntity): def native_unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" return self._config.get(ATTR_SENSOR_UOM) + + @property + def state_class(self) -> str | None: + """Return state class.""" + return self._config.get(ATTR_SENSOR_STATE_CLASS) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index eb2d64114b3..8a06b693e9a 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -22,7 +22,10 @@ from homeassistant.components.device_tracker import ( ATTR_LOCATION_NAME, ) from homeassistant.components.frontend import MANIFEST_JSON -from homeassistant.components.sensor import DEVICE_CLASSES as SENSOR_CLASSES +from homeassistant.components.sensor import ( + DEVICE_CLASSES as SENSOR_CLASSES, + STATE_CLASSES as SENSOSR_STATE_CLASSES, +) from homeassistant.components.zone.const import DOMAIN as ZONE_DOMAIN from homeassistant.const import ( ATTR_DEVICE_ID, @@ -41,6 +44,7 @@ from homeassistant.helpers import ( template, ) from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA from homeassistant.util.decorator import Registry from .const import ( @@ -57,9 +61,11 @@ from .const import ( ATTR_OS_VERSION, ATTR_SENSOR_ATTRIBUTES, ATTR_SENSOR_DEVICE_CLASS, + ATTR_SENSOR_ENTITY_CATEGORY, ATTR_SENSOR_ICON, ATTR_SENSOR_NAME, ATTR_SENSOR_STATE, + ATTR_SENSOR_STATE_CLASS, ATTR_SENSOR_TYPE, ATTR_SENSOR_TYPE_BINARY_SENSOR, ATTR_SENSOR_TYPE_SENSOR, @@ -389,22 +395,38 @@ async def webhook_enable_encryption(hass, config_entry, data): return json_response({"secret": secret}) +def _validate_state_class_sensor(value: dict): + """Validate we only set state class for sensors.""" + if ( + ATTR_SENSOR_STATE_CLASS in value + and value[ATTR_SENSOR_TYPE] != ATTR_SENSOR_TYPE_SENSOR + ): + raise vol.Invalid("state_class only allowed for sensors") + + return value + + @WEBHOOK_COMMANDS.register("register_sensor") @validate_schema( - { - vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict, - vol.Optional(ATTR_SENSOR_DEVICE_CLASS): vol.All( - vol.Lower, vol.In(COMBINED_CLASSES) - ), - vol.Required(ATTR_SENSOR_NAME): cv.string, - vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES), - vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string, - vol.Optional(ATTR_SENSOR_UOM): cv.string, - vol.Optional(ATTR_SENSOR_STATE, default=None): vol.Any( - None, bool, str, int, float - ), - vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon, - } + vol.All( + { + vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict, + vol.Optional(ATTR_SENSOR_DEVICE_CLASS): vol.All( + vol.Lower, vol.In(COMBINED_CLASSES) + ), + vol.Required(ATTR_SENSOR_NAME): cv.string, + vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES), + vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string, + vol.Optional(ATTR_SENSOR_UOM): cv.string, + vol.Optional(ATTR_SENSOR_STATE, default=None): vol.Any( + None, bool, str, int, float + ), + vol.Optional(ATTR_SENSOR_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, + vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon, + vol.Optional(ATTR_SENSOR_STATE_CLASS): vol.In(SENSOSR_STATE_CLASSES), + }, + _validate_state_class_sensor, + ) ) async def webhook_register_sensor(hass, config_entry, data): """Handle a register sensor webhook.""" diff --git a/tests/components/mobile_app/test_sensor.py b/tests/components/mobile_app/test_sensor.py index ed638301bd6..fea43ffba9e 100644 --- a/tests/components/mobile_app/test_sensor.py +++ b/tests/components/mobile_app/test_sensor.py @@ -1,6 +1,6 @@ """Entity tests for mobile_app.""" from homeassistant.const import PERCENTAGE, STATE_UNKNOWN -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er async def test_sensor(hass, create_registrations, webhook_client): @@ -19,7 +19,9 @@ async def test_sensor(hass, create_registrations, webhook_client): "name": "Battery State", "state": 100, "type": "sensor", + "entity_category": "diagnostic", "unique_id": "battery_state", + "state_class": "total", "unit_of_measurement": PERCENTAGE, }, }, @@ -38,10 +40,16 @@ async def test_sensor(hass, create_registrations, webhook_client): assert entity.attributes["icon"] == "mdi:battery" assert entity.attributes["unit_of_measurement"] == PERCENTAGE 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 ( + er.async_get(hass).async_get("sensor.test_1_battery_state").entity_category + == "diagnostic" + ) + update_resp = await webhook_client.post( webhook_url, json={ diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index d5cb72fa850..8dc2086c495 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -473,3 +473,44 @@ async def test_webhook_handle_scan_tag(hass, create_registrations, webhook_clien assert len(events) == 1 assert events[0].data["tag_id"] == "mock-tag-id" assert events[0].data["device_id"] == "mock-device-id" + + +async def test_register_sensor_limits_state_class( + hass, create_registrations, webhook_client +): + """Test that we limit state classes to sensors only.""" + webhook_id = create_registrations[1]["webhook_id"] + webhook_url = f"/api/webhook/{webhook_id}" + + reg_resp = await webhook_client.post( + webhook_url, + json={ + "type": "register_sensor", + "data": { + "name": "Battery State", + "state": 100, + "type": "sensor", + "state_class": "total", + "unique_id": "abcd", + }, + }, + ) + + assert reg_resp.status == 201 + + reg_resp = await webhook_client.post( + webhook_url, + json={ + "type": "register_sensor", + "data": { + "name": "Battery State", + "state": 100, + "type": "binary_sensor", + "state_class": "total", + "unique_id": "efgh", + }, + }, + ) + + # This means it was ignored. + assert reg_resp.status == 200 From 1b0118a81bb7685b172dfb27902d483a255beec1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 19 Oct 2021 21:41:01 +0200 Subject: [PATCH 0550/1038] Use constants in Onewire tests (#58017) * Use ATTR_ENTITY_ID constant * Add ATTR_UNIQUE_ID constant * Add new attribute constants * Fix missing ATTR_DEFAULT_DISABLED Co-authored-by: epenet --- tests/components/onewire/__init__.py | 14 +- tests/components/onewire/conftest.py | 2 +- tests/components/onewire/const.py | 1007 +++++++++-------- .../components/onewire/test_binary_sensor.py | 19 +- tests/components/onewire/test_sensor.py | 68 +- tests/components/onewire/test_switch.py | 32 +- 6 files changed, 595 insertions(+), 547 deletions(-) diff --git a/tests/components/onewire/__init__.py b/tests/components/onewire/__init__.py index 27091b895bf..639379e7acc 100644 --- a/tests/components/onewire/__init__.py +++ b/tests/components/onewire/__init__.py @@ -8,7 +8,7 @@ from pyownet.protocol import ProtocolError from homeassistant.components.onewire.const import DEFAULT_SYSBUS_MOUNT_DIR -from .const import MOCK_OWPROXY_DEVICES, MOCK_SYSBUS_DEVICES +from .const import ATTR_INJECT_READS, MOCK_OWPROXY_DEVICES, MOCK_SYSBUS_DEVICES def setup_owproxy_mock_devices( @@ -27,13 +27,13 @@ def setup_owproxy_mock_devices( # Setup device reads main_read_side_effect += [device_id[0:2].encode()] - if "inject_reads" in mock_device: - main_read_side_effect += mock_device["inject_reads"] + if ATTR_INJECT_READS in mock_device: + main_read_side_effect += mock_device[ATTR_INJECT_READS] # Setup sub-device reads device_sensors = mock_device.get(platform, []) for expected_sensor in device_sensors: - sub_read_side_effect.append(expected_sensor["injected_value"]) + sub_read_side_effect.append(expected_sensor[ATTR_INJECT_READS]) # Ensure enough read side effect read_side_effect = ( @@ -61,10 +61,10 @@ def setup_sysbus_mock_devices( # Setup sub-device reads device_sensors = mock_device.get(platform, []) for expected_sensor in device_sensors: - if isinstance(expected_sensor["injected_value"], list): - read_side_effect += expected_sensor["injected_value"] + if isinstance(expected_sensor[ATTR_INJECT_READS], list): + read_side_effect += expected_sensor[ATTR_INJECT_READS] else: - read_side_effect.append(expected_sensor["injected_value"]) + read_side_effect.append(expected_sensor[ATTR_INJECT_READS]) # Ensure enough read side effect read_side_effect.extend([FileNotFoundError("Missing injected value")] * 20) diff --git a/tests/components/onewire/conftest.py b/tests/components/onewire/conftest.py index 455602c348f..d951ff17cdf 100644 --- a/tests/components/onewire/conftest.py +++ b/tests/components/onewire/conftest.py @@ -57,7 +57,7 @@ def get_sysbus_config_entry(hass: HomeAssistant) -> ConfigEntry: data={ CONF_TYPE: CONF_TYPE_SYSBUS, CONF_MOUNT_DIR: DEFAULT_SYSBUS_MOUNT_DIR, - "names": { + CONF_NAMES: { "10-111111111111": "My DS18B20", }, }, diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index 9652b6d89a7..c7384d0fc2d 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -13,10 +13,12 @@ from homeassistant.components.sensor import ( from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, ATTR_IDENTIFIERS, ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME, + ATTR_STATE, ATTR_UNIT_OF_MEASUREMENT, DEVICE_CLASS_CURRENT, DEVICE_CLASS_HUMIDITY, @@ -31,25 +33,30 @@ from homeassistant.const import ( PRESSURE_MBAR, STATE_OFF, STATE_ON, + STATE_UNKNOWN, TEMP_CELSIUS, ) ATTR_DEFAULT_DISABLED = "default_disabled" +ATTR_DEVICE_FILE = "device_file" +ATTR_DEVICE_INFO = "device_info" +ATTR_INJECT_READS = "inject_reads" +ATTR_UNIQUE_ID = "unique_id" MANUFACTURER = "Maxim Integrated" MOCK_OWPROXY_DEVICES = { "00.111111111111": { - "inject_reads": [ + ATTR_INJECT_READS: [ b"", # read device type ], SENSOR_DOMAIN: [], }, "05.111111111111": { - "inject_reads": [ + ATTR_INJECT_READS: [ b"DS2405", # read device type ], - "device_info": { + ATTR_DEVICE_INFO: { ATTR_IDENTIFIERS: {(DOMAIN, "05.111111111111")}, ATTR_MANUFACTURER: MANUFACTURER, ATTR_MODEL: "DS2405", @@ -57,21 +64,21 @@ MOCK_OWPROXY_DEVICES = { }, SWITCH_DOMAIN: [ { - "entity_id": "switch.05_111111111111_pio", - "unique_id": "/05.111111111111/PIO", - "injected_value": b" 1", - "result": STATE_ON, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "switch.05_111111111111_pio", + ATTR_INJECT_READS: b" 1", + ATTR_STATE: STATE_ON, + ATTR_UNIQUE_ID: "/05.111111111111/PIO", + ATTR_UNIT_OF_MEASUREMENT: None, }, ], }, "10.111111111111": { - "inject_reads": [ + ATTR_INJECT_READS: [ b"DS18S20", # read device type ], - "device_info": { + ATTR_DEVICE_INFO: { ATTR_IDENTIFIERS: {(DOMAIN, "10.111111111111")}, ATTR_MANUFACTURER: MANUFACTURER, ATTR_MODEL: "DS18S20", @@ -79,21 +86,21 @@ MOCK_OWPROXY_DEVICES = { }, SENSOR_DOMAIN: [ { - "entity_id": "sensor.my_ds18b20_temperature", - "unique_id": "/10.111111111111/temperature", - "injected_value": b" 25.123", - "result": "25.1", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ENTITY_ID: "sensor.my_ds18b20_temperature", + ATTR_INJECT_READS: b" 25.123", + ATTR_STATE: "25.1", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/10.111111111111/temperature", + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, ], }, "12.111111111111": { - "inject_reads": [ + ATTR_INJECT_READS: [ b"DS2406", # read device type ], - "device_info": { + ATTR_DEVICE_INFO: { ATTR_IDENTIFIERS: {(DOMAIN, "12.111111111111")}, ATTR_MANUFACTURER: MANUFACTURER, ATTR_MODEL: "DS2406", @@ -101,90 +108,90 @@ MOCK_OWPROXY_DEVICES = { }, BINARY_SENSOR_DOMAIN: [ { - "entity_id": "binary_sensor.12_111111111111_sensed_a", - "unique_id": "/12.111111111111/sensed.A", - "injected_value": b" 1", - "result": STATE_ON, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "binary_sensor.12_111111111111_sensed_a", + ATTR_INJECT_READS: b" 1", + ATTR_STATE: STATE_ON, + ATTR_UNIQUE_ID: "/12.111111111111/sensed.A", + ATTR_UNIT_OF_MEASUREMENT: None, }, { - "entity_id": "binary_sensor.12_111111111111_sensed_b", - "unique_id": "/12.111111111111/sensed.B", - "injected_value": b" 0", - "result": STATE_OFF, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "binary_sensor.12_111111111111_sensed_b", + ATTR_INJECT_READS: b" 0", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "/12.111111111111/sensed.B", + ATTR_UNIT_OF_MEASUREMENT: None, }, ], SENSOR_DOMAIN: [ { - "entity_id": "sensor.12_111111111111_temperature", - "unique_id": "/12.111111111111/TAI8570/temperature", - "injected_value": b" 25.123", - "result": "25.1", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ENTITY_ID: "sensor.12_111111111111_temperature", + ATTR_INJECT_READS: b" 25.123", + ATTR_STATE: "25.1", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/12.111111111111/TAI8570/temperature", + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, { - "entity_id": "sensor.12_111111111111_pressure", - "unique_id": "/12.111111111111/TAI8570/pressure", - "injected_value": b" 1025.123", - "result": "1025.1", - ATTR_UNIT_OF_MEASUREMENT: PRESSURE_MBAR, - ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + ATTR_ENTITY_ID: "sensor.12_111111111111_pressure", + ATTR_INJECT_READS: b" 1025.123", + ATTR_STATE: "1025.1", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/12.111111111111/TAI8570/pressure", + ATTR_UNIT_OF_MEASUREMENT: PRESSURE_MBAR, }, ], SWITCH_DOMAIN: [ { - "entity_id": "switch.12_111111111111_pio_a", - "unique_id": "/12.111111111111/PIO.A", - "injected_value": b" 1", - "result": STATE_ON, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "switch.12_111111111111_pio_a", + ATTR_INJECT_READS: b" 1", + ATTR_STATE: STATE_ON, + ATTR_UNIQUE_ID: "/12.111111111111/PIO.A", + ATTR_UNIT_OF_MEASUREMENT: None, }, { - "entity_id": "switch.12_111111111111_pio_b", - "unique_id": "/12.111111111111/PIO.B", - "injected_value": b" 0", - "result": STATE_OFF, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "switch.12_111111111111_pio_b", + ATTR_INJECT_READS: b" 0", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "/12.111111111111/PIO.B", + ATTR_UNIT_OF_MEASUREMENT: None, }, { - "entity_id": "switch.12_111111111111_latch_a", - "unique_id": "/12.111111111111/latch.A", - "injected_value": b" 1", - "result": STATE_ON, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "switch.12_111111111111_latch_a", + ATTR_INJECT_READS: b" 1", + ATTR_STATE: STATE_ON, + ATTR_UNIQUE_ID: "/12.111111111111/latch.A", + ATTR_UNIT_OF_MEASUREMENT: None, }, { - "entity_id": "switch.12_111111111111_latch_b", - "unique_id": "/12.111111111111/latch.B", - "injected_value": b" 0", - "result": STATE_OFF, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "switch.12_111111111111_latch_b", + ATTR_INJECT_READS: b" 0", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "/12.111111111111/latch.B", + ATTR_UNIT_OF_MEASUREMENT: None, }, ], }, "1D.111111111111": { - "inject_reads": [ + ATTR_INJECT_READS: [ b"DS2423", # read device type ], - "device_info": { + ATTR_DEVICE_INFO: { ATTR_IDENTIFIERS: {(DOMAIN, "1D.111111111111")}, ATTR_MANUFACTURER: MANUFACTURER, ATTR_MODEL: "DS2423", @@ -192,30 +199,30 @@ MOCK_OWPROXY_DEVICES = { }, SENSOR_DOMAIN: [ { - "entity_id": "sensor.1d_111111111111_counter_a", - "unique_id": "/1D.111111111111/counter.A", - "injected_value": b" 251123", - "result": "251123", - ATTR_UNIT_OF_MEASUREMENT: "count", ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "sensor.1d_111111111111_counter_a", + ATTR_INJECT_READS: b" 251123", + ATTR_STATE: "251123", ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_UNIQUE_ID: "/1D.111111111111/counter.A", + ATTR_UNIT_OF_MEASUREMENT: "count", }, { - "entity_id": "sensor.1d_111111111111_counter_b", - "unique_id": "/1D.111111111111/counter.B", - "injected_value": b" 248125", - "result": "248125", - ATTR_UNIT_OF_MEASUREMENT: "count", ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "sensor.1d_111111111111_counter_b", + ATTR_INJECT_READS: b" 248125", + ATTR_STATE: "248125", ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_UNIQUE_ID: "/1D.111111111111/counter.B", + ATTR_UNIT_OF_MEASUREMENT: "count", }, ], }, "1F.111111111111": { - "inject_reads": [ + ATTR_INJECT_READS: [ b"DS2409", # read device type ], - "device_info": { + ATTR_DEVICE_INFO: { ATTR_IDENTIFIERS: {(DOMAIN, "1F.111111111111")}, ATTR_MANUFACTURER: MANUFACTURER, ATTR_MODEL: "DS2409", @@ -225,10 +232,10 @@ MOCK_OWPROXY_DEVICES = { "aux": {}, "main": { "1D.111111111111": { - "inject_reads": [ + ATTR_INJECT_READS: [ b"DS2423", # read device type ], - "device_info": { + ATTR_DEVICE_INFO: { ATTR_IDENTIFIERS: {(DOMAIN, "1D.111111111111")}, ATTR_MANUFACTURER: MANUFACTURER, ATTR_MODEL: "DS2423", @@ -236,24 +243,24 @@ MOCK_OWPROXY_DEVICES = { }, SENSOR_DOMAIN: [ { - "entity_id": "sensor.1d_111111111111_counter_a", - "device_file": "/1F.111111111111/main/1D.111111111111/counter.A", - "unique_id": "/1D.111111111111/counter.A", - "injected_value": b" 251123", - "result": "251123", - ATTR_UNIT_OF_MEASUREMENT: "count", ATTR_DEVICE_CLASS: None, + ATTR_DEVICE_FILE: "/1F.111111111111/main/1D.111111111111/counter.A", + ATTR_ENTITY_ID: "sensor.1d_111111111111_counter_a", + ATTR_INJECT_READS: b" 251123", + ATTR_STATE: "251123", ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_UNIQUE_ID: "/1D.111111111111/counter.A", + ATTR_UNIT_OF_MEASUREMENT: "count", }, { - "entity_id": "sensor.1d_111111111111_counter_b", - "device_file": "/1F.111111111111/main/1D.111111111111/counter.B", - "unique_id": "/1D.111111111111/counter.B", - "injected_value": b" 248125", - "result": "248125", - ATTR_UNIT_OF_MEASUREMENT: "count", ATTR_DEVICE_CLASS: None, + ATTR_DEVICE_FILE: "/1F.111111111111/main/1D.111111111111/counter.B", + ATTR_ENTITY_ID: "sensor.1d_111111111111_counter_b", + ATTR_INJECT_READS: b" 248125", + ATTR_STATE: "248125", ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_UNIQUE_ID: "/1D.111111111111/counter.B", + ATTR_UNIT_OF_MEASUREMENT: "count", }, ], }, @@ -261,10 +268,10 @@ MOCK_OWPROXY_DEVICES = { }, }, "22.111111111111": { - "inject_reads": [ + ATTR_INJECT_READS: [ b"DS1822", # read device type ], - "device_info": { + ATTR_DEVICE_INFO: { ATTR_IDENTIFIERS: {(DOMAIN, "22.111111111111")}, ATTR_MANUFACTURER: MANUFACTURER, ATTR_MODEL: "DS1822", @@ -272,21 +279,21 @@ MOCK_OWPROXY_DEVICES = { }, SENSOR_DOMAIN: [ { - "entity_id": "sensor.22_111111111111_temperature", - "unique_id": "/22.111111111111/temperature", - "injected_value": ProtocolError, - "result": "unknown", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ENTITY_ID: "sensor.22_111111111111_temperature", + ATTR_INJECT_READS: ProtocolError, + ATTR_STATE: STATE_UNKNOWN, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/22.111111111111/temperature", + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, ], }, "26.111111111111": { - "inject_reads": [ + ATTR_INJECT_READS: [ b"DS2438", # read device type ], - "device_info": { + ATTR_DEVICE_INFO: { ATTR_IDENTIFIERS: {(DOMAIN, "26.111111111111")}, ATTR_MANUFACTURER: MANUFACTURER, ATTR_MODEL: "DS2438", @@ -294,121 +301,121 @@ MOCK_OWPROXY_DEVICES = { }, SENSOR_DOMAIN: [ { - "entity_id": "sensor.26_111111111111_temperature", - "unique_id": "/26.111111111111/temperature", - "injected_value": b" 25.123", - "result": "25.1", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ENTITY_ID: "sensor.26_111111111111_temperature", + ATTR_INJECT_READS: b" 25.123", + ATTR_STATE: "25.1", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/26.111111111111/temperature", + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, { - "entity_id": "sensor.26_111111111111_humidity", - "unique_id": "/26.111111111111/humidity", - "injected_value": b" 72.7563", - "result": "72.8", - ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, - ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, ATTR_DEFAULT_DISABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - { - "entity_id": "sensor.26_111111111111_humidity_hih3600", - "unique_id": "/26.111111111111/HIH3600/humidity", - "injected_value": b" 73.7563", - "result": "73.8", - ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, - ATTR_DEFAULT_DISABLED: True, + ATTR_ENTITY_ID: "sensor.26_111111111111_humidity", + ATTR_INJECT_READS: b" 72.7563", + ATTR_STATE: "72.8", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - { - "entity_id": "sensor.26_111111111111_humidity_hih4000", - "unique_id": "/26.111111111111/HIH4000/humidity", - "injected_value": b" 74.7563", - "result": "74.8", + ATTR_UNIQUE_ID: "/26.111111111111/humidity", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, - ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, - ATTR_DEFAULT_DISABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, { - "entity_id": "sensor.26_111111111111_humidity_hih5030", - "unique_id": "/26.111111111111/HIH5030/humidity", - "injected_value": b" 75.7563", - "result": "75.8", + ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_ENTITY_ID: "sensor.26_111111111111_humidity_hih3600", + ATTR_INJECT_READS: b" 73.7563", + ATTR_STATE: "73.8", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/26.111111111111/HIH3600/humidity", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, - ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, - ATTR_DEFAULT_DISABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, { - "entity_id": "sensor.26_111111111111_humidity_htm1735", - "unique_id": "/26.111111111111/HTM1735/humidity", - "injected_value": ProtocolError, - "result": "unknown", + ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_ENTITY_ID: "sensor.26_111111111111_humidity_hih4000", + ATTR_INJECT_READS: b" 74.7563", + ATTR_STATE: "74.8", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/26.111111111111/HIH4000/humidity", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, - ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, - ATTR_DEFAULT_DISABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, { - "entity_id": "sensor.26_111111111111_pressure", - "unique_id": "/26.111111111111/B1-R1-A/pressure", - "injected_value": b" 969.265", - "result": "969.3", - ATTR_UNIT_OF_MEASUREMENT: PRESSURE_MBAR, + ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_ENTITY_ID: "sensor.26_111111111111_humidity_hih5030", + ATTR_INJECT_READS: b" 75.7563", + ATTR_STATE: "75.8", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/26.111111111111/HIH5030/humidity", + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_ENTITY_ID: "sensor.26_111111111111_humidity_htm1735", + ATTR_INJECT_READS: ProtocolError, + ATTR_STATE: STATE_UNKNOWN, + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/26.111111111111/HTM1735/humidity", + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + }, + { + ATTR_DEFAULT_DISABLED: True, ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, - ATTR_DEFAULT_DISABLED: True, + ATTR_ENTITY_ID: "sensor.26_111111111111_pressure", + ATTR_INJECT_READS: b" 969.265", + ATTR_STATE: "969.3", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/26.111111111111/B1-R1-A/pressure", + ATTR_UNIT_OF_MEASUREMENT: PRESSURE_MBAR, }, { - "entity_id": "sensor.26_111111111111_illuminance", - "unique_id": "/26.111111111111/S3-R1-A/illuminance", - "injected_value": b" 65.8839", - "result": "65.9", - ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX, + ATTR_DEFAULT_DISABLED: True, ATTR_DEVICE_CLASS: DEVICE_CLASS_ILLUMINANCE, - ATTR_DEFAULT_DISABLED: True, + ATTR_ENTITY_ID: "sensor.26_111111111111_illuminance", + ATTR_INJECT_READS: b" 65.8839", + ATTR_STATE: "65.9", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/26.111111111111/S3-R1-A/illuminance", + ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX, }, { - "entity_id": "sensor.26_111111111111_voltage_vad", - "unique_id": "/26.111111111111/VAD", - "injected_value": b" 2.97", - "result": "3.0", - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, + ATTR_DEFAULT_DISABLED: True, ATTR_DEVICE_CLASS: DEVICE_CLASS_VOLTAGE, - ATTR_DEFAULT_DISABLED: True, + ATTR_ENTITY_ID: "sensor.26_111111111111_voltage_vad", + ATTR_INJECT_READS: b" 2.97", + ATTR_STATE: "3.0", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - { - "entity_id": "sensor.26_111111111111_voltage_vdd", - "unique_id": "/26.111111111111/VDD", - "injected_value": b" 4.74", - "result": "4.7", + ATTR_UNIQUE_ID: "/26.111111111111/VAD", ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, - ATTR_DEVICE_CLASS: DEVICE_CLASS_VOLTAGE, - ATTR_DEFAULT_DISABLED: True, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, }, { - "entity_id": "sensor.26_111111111111_current", - "unique_id": "/26.111111111111/IAD", - "injected_value": b" 1", - "result": "1.0", - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, + ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: DEVICE_CLASS_VOLTAGE, + ATTR_ENTITY_ID: "sensor.26_111111111111_voltage_vdd", + ATTR_INJECT_READS: b" 4.74", + ATTR_STATE: "4.7", + ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/26.111111111111/VDD", + ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, + }, + { + ATTR_DEFAULT_DISABLED: True, ATTR_DEVICE_CLASS: DEVICE_CLASS_CURRENT, - ATTR_DEFAULT_DISABLED: True, + ATTR_ENTITY_ID: "sensor.26_111111111111_current", + ATTR_INJECT_READS: b" 1", + ATTR_STATE: "1.0", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/26.111111111111/IAD", + ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, }, ], }, "28.111111111111": { - "inject_reads": [ + ATTR_INJECT_READS: [ b"DS18B20", # read device type ], - "device_info": { + ATTR_DEVICE_INFO: { ATTR_IDENTIFIERS: {(DOMAIN, "28.111111111111")}, ATTR_MANUFACTURER: MANUFACTURER, ATTR_MODEL: "DS18B20", @@ -416,21 +423,21 @@ MOCK_OWPROXY_DEVICES = { }, SENSOR_DOMAIN: [ { - "entity_id": "sensor.28_111111111111_temperature", - "unique_id": "/28.111111111111/temperature", - "injected_value": b" 26.984", - "result": "27.0", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ENTITY_ID: "sensor.28_111111111111_temperature", + ATTR_INJECT_READS: b" 26.984", + ATTR_STATE: "27.0", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/28.111111111111/temperature", + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, ], }, "29.111111111111": { - "inject_reads": [ + ATTR_INJECT_READS: [ b"DS2408", # read device type ], - "device_info": { + ATTR_DEVICE_INFO: { ATTR_IDENTIFIERS: {(DOMAIN, "29.111111111111")}, ATTR_MANUFACTURER: MANUFACTURER, ATTR_MODEL: "DS2408", @@ -438,230 +445,230 @@ MOCK_OWPROXY_DEVICES = { }, BINARY_SENSOR_DOMAIN: [ { - "entity_id": "binary_sensor.29_111111111111_sensed_0", - "unique_id": "/29.111111111111/sensed.0", - "injected_value": b" 1", - "result": STATE_ON, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "binary_sensor.29_111111111111_sensed_0", + ATTR_INJECT_READS: b" 1", + ATTR_STATE: STATE_ON, + ATTR_UNIQUE_ID: "/29.111111111111/sensed.0", + ATTR_UNIT_OF_MEASUREMENT: None, }, { - "entity_id": "binary_sensor.29_111111111111_sensed_1", - "unique_id": "/29.111111111111/sensed.1", - "injected_value": b" 0", - "result": STATE_OFF, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "binary_sensor.29_111111111111_sensed_1", + ATTR_INJECT_READS: b" 0", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "/29.111111111111/sensed.1", + ATTR_UNIT_OF_MEASUREMENT: None, }, { - "entity_id": "binary_sensor.29_111111111111_sensed_2", - "unique_id": "/29.111111111111/sensed.2", - "injected_value": b" 0", - "result": STATE_OFF, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "binary_sensor.29_111111111111_sensed_2", + ATTR_INJECT_READS: b" 0", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "/29.111111111111/sensed.2", + ATTR_UNIT_OF_MEASUREMENT: None, }, { - "entity_id": "binary_sensor.29_111111111111_sensed_3", - "unique_id": "/29.111111111111/sensed.3", - "injected_value": b" 0", - "result": STATE_OFF, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "binary_sensor.29_111111111111_sensed_3", + ATTR_INJECT_READS: b" 0", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "/29.111111111111/sensed.3", + ATTR_UNIT_OF_MEASUREMENT: None, }, { - "entity_id": "binary_sensor.29_111111111111_sensed_4", - "unique_id": "/29.111111111111/sensed.4", - "injected_value": b" 0", - "result": STATE_OFF, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "binary_sensor.29_111111111111_sensed_4", + ATTR_INJECT_READS: b" 0", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "/29.111111111111/sensed.4", + ATTR_UNIT_OF_MEASUREMENT: None, }, { - "entity_id": "binary_sensor.29_111111111111_sensed_5", - "unique_id": "/29.111111111111/sensed.5", - "injected_value": b" 0", - "result": STATE_OFF, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "binary_sensor.29_111111111111_sensed_5", + ATTR_INJECT_READS: b" 0", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "/29.111111111111/sensed.5", + ATTR_UNIT_OF_MEASUREMENT: None, }, { - "entity_id": "binary_sensor.29_111111111111_sensed_6", - "unique_id": "/29.111111111111/sensed.6", - "injected_value": b" 0", - "result": STATE_OFF, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "binary_sensor.29_111111111111_sensed_6", + ATTR_INJECT_READS: b" 0", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "/29.111111111111/sensed.6", + ATTR_UNIT_OF_MEASUREMENT: None, }, { - "entity_id": "binary_sensor.29_111111111111_sensed_7", - "unique_id": "/29.111111111111/sensed.7", - "injected_value": b" 0", - "result": STATE_OFF, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "binary_sensor.29_111111111111_sensed_7", + ATTR_INJECT_READS: b" 0", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "/29.111111111111/sensed.7", + ATTR_UNIT_OF_MEASUREMENT: None, }, ], SWITCH_DOMAIN: [ { - "entity_id": "switch.29_111111111111_pio_0", - "unique_id": "/29.111111111111/PIO.0", - "injected_value": b" 1", - "result": STATE_ON, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "switch.29_111111111111_pio_0", + ATTR_INJECT_READS: b" 1", + ATTR_STATE: STATE_ON, + ATTR_UNIQUE_ID: "/29.111111111111/PIO.0", + ATTR_UNIT_OF_MEASUREMENT: None, }, { - "entity_id": "switch.29_111111111111_pio_1", - "unique_id": "/29.111111111111/PIO.1", - "injected_value": b" 0", - "result": STATE_OFF, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "switch.29_111111111111_pio_1", + ATTR_INJECT_READS: b" 0", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "/29.111111111111/PIO.1", + ATTR_UNIT_OF_MEASUREMENT: None, }, { - "entity_id": "switch.29_111111111111_pio_2", - "unique_id": "/29.111111111111/PIO.2", - "injected_value": b" 1", - "result": STATE_ON, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "switch.29_111111111111_pio_2", + ATTR_INJECT_READS: b" 1", + ATTR_STATE: STATE_ON, + ATTR_UNIQUE_ID: "/29.111111111111/PIO.2", + ATTR_UNIT_OF_MEASUREMENT: None, }, { - "entity_id": "switch.29_111111111111_pio_3", - "unique_id": "/29.111111111111/PIO.3", - "injected_value": b" 0", - "result": STATE_OFF, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "switch.29_111111111111_pio_3", + ATTR_INJECT_READS: b" 0", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "/29.111111111111/PIO.3", + ATTR_UNIT_OF_MEASUREMENT: None, }, { - "entity_id": "switch.29_111111111111_pio_4", - "unique_id": "/29.111111111111/PIO.4", - "injected_value": b" 1", - "result": STATE_ON, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "switch.29_111111111111_pio_4", + ATTR_INJECT_READS: b" 1", + ATTR_STATE: STATE_ON, + ATTR_UNIT_OF_MEASUREMENT: None, + ATTR_UNIQUE_ID: "/29.111111111111/PIO.4", }, { - "entity_id": "switch.29_111111111111_pio_5", - "unique_id": "/29.111111111111/PIO.5", - "injected_value": b" 0", - "result": STATE_OFF, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "switch.29_111111111111_pio_5", + ATTR_INJECT_READS: b" 0", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "/29.111111111111/PIO.5", + ATTR_UNIT_OF_MEASUREMENT: None, }, { - "entity_id": "switch.29_111111111111_pio_6", - "unique_id": "/29.111111111111/PIO.6", - "injected_value": b" 1", - "result": STATE_ON, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "switch.29_111111111111_pio_6", + ATTR_INJECT_READS: b" 1", + ATTR_STATE: STATE_ON, + ATTR_UNIQUE_ID: "/29.111111111111/PIO.6", + ATTR_UNIT_OF_MEASUREMENT: None, }, { - "entity_id": "switch.29_111111111111_pio_7", - "unique_id": "/29.111111111111/PIO.7", - "injected_value": b" 0", - "result": STATE_OFF, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "switch.29_111111111111_pio_7", + ATTR_INJECT_READS: b" 0", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "/29.111111111111/PIO.7", + ATTR_UNIT_OF_MEASUREMENT: None, }, { - "entity_id": "switch.29_111111111111_latch_0", - "unique_id": "/29.111111111111/latch.0", - "injected_value": b" 1", - "result": STATE_ON, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "switch.29_111111111111_latch_0", + ATTR_INJECT_READS: b" 1", + ATTR_STATE: STATE_ON, + ATTR_UNIQUE_ID: "/29.111111111111/latch.0", + ATTR_UNIT_OF_MEASUREMENT: None, }, { - "entity_id": "switch.29_111111111111_latch_1", - "unique_id": "/29.111111111111/latch.1", - "injected_value": b" 0", - "result": STATE_OFF, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "switch.29_111111111111_latch_1", + ATTR_INJECT_READS: b" 0", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "/29.111111111111/latch.1", + ATTR_UNIT_OF_MEASUREMENT: None, }, { - "entity_id": "switch.29_111111111111_latch_2", - "unique_id": "/29.111111111111/latch.2", - "injected_value": b" 1", - "result": STATE_ON, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "switch.29_111111111111_latch_2", + ATTR_INJECT_READS: b" 1", + ATTR_STATE: STATE_ON, + ATTR_UNIQUE_ID: "/29.111111111111/latch.2", + ATTR_UNIT_OF_MEASUREMENT: None, }, { - "entity_id": "switch.29_111111111111_latch_3", - "unique_id": "/29.111111111111/latch.3", - "injected_value": b" 0", - "result": STATE_OFF, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "switch.29_111111111111_latch_3", + ATTR_INJECT_READS: b" 0", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "/29.111111111111/latch.3", + ATTR_UNIT_OF_MEASUREMENT: None, }, { - "entity_id": "switch.29_111111111111_latch_4", - "unique_id": "/29.111111111111/latch.4", - "injected_value": b" 1", - "result": STATE_ON, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "switch.29_111111111111_latch_4", + ATTR_INJECT_READS: b" 1", + ATTR_STATE: STATE_ON, + ATTR_UNIQUE_ID: "/29.111111111111/latch.4", + ATTR_UNIT_OF_MEASUREMENT: None, }, { - "entity_id": "switch.29_111111111111_latch_5", - "unique_id": "/29.111111111111/latch.5", - "injected_value": b" 0", - "result": STATE_OFF, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "switch.29_111111111111_latch_5", + ATTR_INJECT_READS: b" 0", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "/29.111111111111/latch.5", + ATTR_UNIT_OF_MEASUREMENT: None, }, { - "entity_id": "switch.29_111111111111_latch_6", - "unique_id": "/29.111111111111/latch.6", - "injected_value": b" 1", - "result": STATE_ON, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "switch.29_111111111111_latch_6", + ATTR_INJECT_READS: b" 1", + ATTR_STATE: STATE_ON, + ATTR_UNIQUE_ID: "/29.111111111111/latch.6", + ATTR_UNIT_OF_MEASUREMENT: None, }, { - "entity_id": "switch.29_111111111111_latch_7", - "unique_id": "/29.111111111111/latch.7", - "injected_value": b" 0", - "result": STATE_OFF, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "switch.29_111111111111_latch_7", + ATTR_INJECT_READS: b" 0", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "/29.111111111111/latch.7", + ATTR_UNIT_OF_MEASUREMENT: None, }, ], }, "3A.111111111111": { - "inject_reads": [ + ATTR_INJECT_READS: [ b"DS2413", # read device type ], - "device_info": { + ATTR_DEVICE_INFO: { ATTR_IDENTIFIERS: {(DOMAIN, "3A.111111111111")}, ATTR_MANUFACTURER: MANUFACTURER, ATTR_MODEL: "DS2413", @@ -669,50 +676,50 @@ MOCK_OWPROXY_DEVICES = { }, BINARY_SENSOR_DOMAIN: [ { - "entity_id": "binary_sensor.3a_111111111111_sensed_a", - "unique_id": "/3A.111111111111/sensed.A", - "injected_value": b" 1", - "result": STATE_ON, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "binary_sensor.3a_111111111111_sensed_a", + ATTR_INJECT_READS: b" 1", + ATTR_STATE: STATE_ON, + ATTR_UNIQUE_ID: "/3A.111111111111/sensed.A", + ATTR_UNIT_OF_MEASUREMENT: None, }, { - "entity_id": "binary_sensor.3a_111111111111_sensed_b", - "unique_id": "/3A.111111111111/sensed.B", - "injected_value": b" 0", - "result": STATE_OFF, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "binary_sensor.3a_111111111111_sensed_b", + ATTR_INJECT_READS: b" 0", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "/3A.111111111111/sensed.B", + ATTR_UNIT_OF_MEASUREMENT: None, }, ], SWITCH_DOMAIN: [ { - "entity_id": "switch.3a_111111111111_pio_a", - "unique_id": "/3A.111111111111/PIO.A", - "injected_value": b" 1", - "result": STATE_ON, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "switch.3a_111111111111_pio_a", + ATTR_INJECT_READS: b" 1", + ATTR_STATE: STATE_ON, + ATTR_UNIQUE_ID: "/3A.111111111111/PIO.A", + ATTR_UNIT_OF_MEASUREMENT: None, }, { - "entity_id": "switch.3a_111111111111_pio_b", - "unique_id": "/3A.111111111111/PIO.B", - "injected_value": b" 0", - "result": STATE_OFF, - ATTR_UNIT_OF_MEASUREMENT: None, - ATTR_DEVICE_CLASS: None, ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: None, + ATTR_ENTITY_ID: "switch.3a_111111111111_pio_b", + ATTR_INJECT_READS: b" 0", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "/3A.111111111111/PIO.B", + ATTR_UNIT_OF_MEASUREMENT: None, }, ], }, "3B.111111111111": { - "inject_reads": [ + ATTR_INJECT_READS: [ b"DS1825", # read device type ], - "device_info": { + ATTR_DEVICE_INFO: { ATTR_IDENTIFIERS: {(DOMAIN, "3B.111111111111")}, ATTR_MANUFACTURER: MANUFACTURER, ATTR_MODEL: "DS1825", @@ -720,21 +727,21 @@ MOCK_OWPROXY_DEVICES = { }, SENSOR_DOMAIN: [ { - "entity_id": "sensor.3b_111111111111_temperature", - "unique_id": "/3B.111111111111/temperature", - "injected_value": b" 28.243", - "result": "28.2", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ENTITY_ID: "sensor.3b_111111111111_temperature", + ATTR_INJECT_READS: b" 28.243", + ATTR_STATE: "28.2", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/3B.111111111111/temperature", + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, ], }, "42.111111111111": { - "inject_reads": [ + ATTR_INJECT_READS: [ b"DS28EA00", # read device type ], - "device_info": { + ATTR_DEVICE_INFO: { ATTR_IDENTIFIERS: {(DOMAIN, "42.111111111111")}, ATTR_MANUFACTURER: MANUFACTURER, ATTR_MODEL: "DS28EA00", @@ -742,21 +749,21 @@ MOCK_OWPROXY_DEVICES = { }, SENSOR_DOMAIN: [ { - "entity_id": "sensor.42_111111111111_temperature", - "unique_id": "/42.111111111111/temperature", - "injected_value": b" 29.123", - "result": "29.1", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ENTITY_ID: "sensor.42_111111111111_temperature", + ATTR_INJECT_READS: b" 29.123", + ATTR_STATE: "29.1", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/42.111111111111/temperature", + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, ], }, "EF.111111111111": { - "inject_reads": [ + ATTR_INJECT_READS: [ b"HobbyBoards_EF", # read type ], - "device_info": { + ATTR_DEVICE_INFO: { ATTR_IDENTIFIERS: {(DOMAIN, "EF.111111111111")}, ATTR_MANUFACTURER: MANUFACTURER, ATTR_MODEL: "HobbyBoards_EF", @@ -764,43 +771,43 @@ MOCK_OWPROXY_DEVICES = { }, SENSOR_DOMAIN: [ { - "entity_id": "sensor.ef_111111111111_humidity", - "unique_id": "/EF.111111111111/humidity/humidity_corrected", - "injected_value": b" 67.745", - "result": "67.7", - ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_ENTITY_ID: "sensor.ef_111111111111_humidity", + ATTR_INJECT_READS: b" 67.745", + ATTR_STATE: "67.7", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/EF.111111111111/humidity/humidity_corrected", + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, { - "entity_id": "sensor.ef_111111111111_humidity_raw", - "unique_id": "/EF.111111111111/humidity/humidity_raw", - "injected_value": b" 65.541", - "result": "65.5", - ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_ENTITY_ID: "sensor.ef_111111111111_humidity_raw", + ATTR_INJECT_READS: b" 65.541", + ATTR_STATE: "65.5", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/EF.111111111111/humidity/humidity_raw", + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, { - "entity_id": "sensor.ef_111111111111_temperature", - "unique_id": "/EF.111111111111/humidity/temperature", - "injected_value": b" 25.123", - "result": "25.1", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ENTITY_ID: "sensor.ef_111111111111_temperature", + ATTR_INJECT_READS: b" 25.123", + ATTR_STATE: "25.1", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/EF.111111111111/humidity/temperature", + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, ], }, "EF.111111111112": { - "inject_reads": [ + ATTR_INJECT_READS: [ b"HB_MOISTURE_METER", # read type b" 1", # read is_leaf_0 b" 1", # read is_leaf_1 b" 0", # read is_leaf_2 b" 0", # read is_leaf_3 ], - "device_info": { + ATTR_DEVICE_INFO: { ATTR_IDENTIFIERS: {(DOMAIN, "EF.111111111112")}, ATTR_MANUFACTURER: MANUFACTURER, ATTR_MODEL: "HB_MOISTURE_METER", @@ -808,49 +815,49 @@ MOCK_OWPROXY_DEVICES = { }, SENSOR_DOMAIN: [ { - "entity_id": "sensor.ef_111111111112_wetness_0", - "unique_id": "/EF.111111111112/moisture/sensor.0", - "injected_value": b" 41.745", - "result": "41.7", - ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_ENTITY_ID: "sensor.ef_111111111112_wetness_0", + ATTR_INJECT_READS: b" 41.745", + ATTR_STATE: "41.7", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - { - "entity_id": "sensor.ef_111111111112_wetness_1", - "unique_id": "/EF.111111111112/moisture/sensor.1", - "injected_value": b" 42.541", - "result": "42.5", + ATTR_UNIQUE_ID: "/EF.111111111112/moisture/sensor.0", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + }, + { ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_ENTITY_ID: "sensor.ef_111111111112_wetness_1", + ATTR_INJECT_READS: b" 42.541", + ATTR_STATE: "42.5", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/EF.111111111112/moisture/sensor.1", + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, { - "entity_id": "sensor.ef_111111111112_moisture_2", - "unique_id": "/EF.111111111112/moisture/sensor.2", - "injected_value": b" 43.123", - "result": "43.1", - ATTR_UNIT_OF_MEASUREMENT: PRESSURE_CBAR, ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + ATTR_ENTITY_ID: "sensor.ef_111111111112_moisture_2", + ATTR_INJECT_READS: b" 43.123", + ATTR_STATE: "43.1", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/EF.111111111112/moisture/sensor.2", + ATTR_UNIT_OF_MEASUREMENT: PRESSURE_CBAR, }, { - "entity_id": "sensor.ef_111111111112_moisture_3", - "unique_id": "/EF.111111111112/moisture/sensor.3", - "injected_value": b" 44.123", - "result": "44.1", - ATTR_UNIT_OF_MEASUREMENT: PRESSURE_CBAR, ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + ATTR_ENTITY_ID: "sensor.ef_111111111112_moisture_3", + ATTR_INJECT_READS: b" 44.123", + ATTR_STATE: "44.1", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/EF.111111111112/moisture/sensor.3", + ATTR_UNIT_OF_MEASUREMENT: PRESSURE_CBAR, }, ], }, "7E.111111111111": { - "inject_reads": [ + ATTR_INJECT_READS: [ b"EDS", # read type b"EDS0068", # read device_type - note EDS specific ], - "device_info": { + ATTR_DEVICE_INFO: { ATTR_IDENTIFIERS: {(DOMAIN, "7E.111111111111")}, ATTR_MANUFACTURER: MANUFACTURER, ATTR_MODEL: "EDS", @@ -858,49 +865,49 @@ MOCK_OWPROXY_DEVICES = { }, SENSOR_DOMAIN: [ { - "entity_id": "sensor.7e_111111111111_temperature", - "unique_id": "/7E.111111111111/EDS0068/temperature", - "injected_value": b" 13.9375", - "result": "13.9", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ENTITY_ID: "sensor.7e_111111111111_temperature", + ATTR_INJECT_READS: b" 13.9375", + ATTR_STATE: "13.9", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/7E.111111111111/EDS0068/temperature", + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, { - "entity_id": "sensor.7e_111111111111_pressure", - "unique_id": "/7E.111111111111/EDS0068/pressure", - "injected_value": b" 1012.21", - "result": "1012.2", - ATTR_UNIT_OF_MEASUREMENT: PRESSURE_MBAR, ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + ATTR_ENTITY_ID: "sensor.7e_111111111111_pressure", + ATTR_INJECT_READS: b" 1012.21", + ATTR_STATE: "1012.2", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/7E.111111111111/EDS0068/pressure", + ATTR_UNIT_OF_MEASUREMENT: PRESSURE_MBAR, }, { - "entity_id": "sensor.7e_111111111111_illuminance", - "unique_id": "/7E.111111111111/EDS0068/light", - "injected_value": b" 65.8839", - "result": "65.9", - ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX, ATTR_DEVICE_CLASS: DEVICE_CLASS_ILLUMINANCE, + ATTR_ENTITY_ID: "sensor.7e_111111111111_illuminance", + ATTR_INJECT_READS: b" 65.8839", + ATTR_STATE: "65.9", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/7E.111111111111/EDS0068/light", + ATTR_UNIT_OF_MEASUREMENT: LIGHT_LUX, }, { - "entity_id": "sensor.7e_111111111111_humidity", - "unique_id": "/7E.111111111111/EDS0068/humidity", - "injected_value": b" 41.375", - "result": "41.4", - ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_ENTITY_ID: "sensor.7e_111111111111_humidity", + ATTR_INJECT_READS: b" 41.375", + ATTR_STATE: "41.4", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/7E.111111111111/EDS0068/humidity", + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, ], }, "7E.222222222222": { - "inject_reads": [ + ATTR_INJECT_READS: [ b"EDS", # read type b"EDS0066", # read device_type - note EDS specific ], - "device_info": { + ATTR_DEVICE_INFO: { ATTR_IDENTIFIERS: {(DOMAIN, "7E.222222222222")}, ATTR_MANUFACTURER: MANUFACTURER, ATTR_MODEL: "EDS", @@ -908,31 +915,33 @@ MOCK_OWPROXY_DEVICES = { }, SENSOR_DOMAIN: [ { - "entity_id": "sensor.7e_222222222222_temperature", - "unique_id": "/7E.222222222222/EDS0066/temperature", - "injected_value": b" 13.9375", - "result": "13.9", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ENTITY_ID: "sensor.7e_222222222222_temperature", + ATTR_INJECT_READS: b" 13.9375", + ATTR_STATE: "13.9", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/7E.222222222222/EDS0066/temperature", + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, { - "entity_id": "sensor.7e_222222222222_pressure", - "unique_id": "/7E.222222222222/EDS0066/pressure", - "injected_value": b" 1012.21", - "result": "1012.2", - ATTR_UNIT_OF_MEASUREMENT: PRESSURE_MBAR, ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + ATTR_ENTITY_ID: "sensor.7e_222222222222_pressure", + ATTR_INJECT_READS: b" 1012.21", + ATTR_STATE: "1012.2", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/7E.222222222222/EDS0066/pressure", + ATTR_UNIT_OF_MEASUREMENT: PRESSURE_MBAR, }, ], }, } MOCK_SYSBUS_DEVICES = { - "00-111111111111": {SENSOR_DOMAIN: []}, + "00-111111111111": { + SENSOR_DOMAIN: [], + }, "10-111111111111": { - "device_info": { + ATTR_DEVICE_INFO: { ATTR_IDENTIFIERS: {(DOMAIN, "10-111111111111")}, ATTR_MANUFACTURER: MANUFACTURER, ATTR_MODEL: "10", @@ -940,20 +949,24 @@ MOCK_SYSBUS_DEVICES = { }, SENSOR_DOMAIN: [ { - "entity_id": "sensor.my_ds18b20_temperature", - "unique_id": "/sys/bus/w1/devices/10-111111111111/w1_slave", - "injected_value": 25.123, - "result": "25.1", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ENTITY_ID: "sensor.my_ds18b20_temperature", + ATTR_INJECT_READS: 25.123, + ATTR_STATE: "25.1", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/sys/bus/w1/devices/10-111111111111/w1_slave", + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, ], }, - "12-111111111111": {SENSOR_DOMAIN: []}, - "1D-111111111111": {SENSOR_DOMAIN: []}, + "12-111111111111": { + SENSOR_DOMAIN: [], + }, + "1D-111111111111": { + SENSOR_DOMAIN: [], + }, "22-111111111111": { - "device_info": { + ATTR_DEVICE_INFO: { ATTR_IDENTIFIERS: {(DOMAIN, "22-111111111111")}, ATTR_MANUFACTURER: MANUFACTURER, ATTR_MODEL: "22", @@ -961,19 +974,21 @@ MOCK_SYSBUS_DEVICES = { }, "sensor": [ { - "entity_id": "sensor.22_111111111111_temperature", - "unique_id": "/sys/bus/w1/devices/22-111111111111/w1_slave", - "injected_value": FileNotFoundError, - "result": "unknown", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ENTITY_ID: "sensor.22_111111111111_temperature", + ATTR_INJECT_READS: FileNotFoundError, + ATTR_STATE: STATE_UNKNOWN, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/sys/bus/w1/devices/22-111111111111/w1_slave", + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, ], }, - "26-111111111111": {SENSOR_DOMAIN: []}, + "26-111111111111": { + SENSOR_DOMAIN: [], + }, "28-111111111111": { - "device_info": { + ATTR_DEVICE_INFO: { ATTR_IDENTIFIERS: {(DOMAIN, "28-111111111111")}, ATTR_MANUFACTURER: MANUFACTURER, ATTR_MODEL: "28", @@ -981,20 +996,24 @@ MOCK_SYSBUS_DEVICES = { }, SENSOR_DOMAIN: [ { - "entity_id": "sensor.28_111111111111_temperature", - "unique_id": "/sys/bus/w1/devices/28-111111111111/w1_slave", - "injected_value": InvalidCRCException, - "result": "unknown", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ENTITY_ID: "sensor.28_111111111111_temperature", + ATTR_INJECT_READS: InvalidCRCException, + ATTR_STATE: STATE_UNKNOWN, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/sys/bus/w1/devices/28-111111111111/w1_slave", + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, ], }, - "29-111111111111": {SENSOR_DOMAIN: []}, - "3A-111111111111": {SENSOR_DOMAIN: []}, + "29-111111111111": { + SENSOR_DOMAIN: [], + }, + "3A-111111111111": { + SENSOR_DOMAIN: [], + }, "3B-111111111111": { - "device_info": { + ATTR_DEVICE_INFO: { ATTR_IDENTIFIERS: {(DOMAIN, "3B-111111111111")}, ATTR_MANUFACTURER: MANUFACTURER, ATTR_MODEL: "3B", @@ -1002,18 +1021,18 @@ MOCK_SYSBUS_DEVICES = { }, SENSOR_DOMAIN: [ { - "entity_id": "sensor.3b_111111111111_temperature", - "unique_id": "/sys/bus/w1/devices/3B-111111111111/w1_slave", - "injected_value": 29.993, - "result": "30.0", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ENTITY_ID: "sensor.3b_111111111111_temperature", + ATTR_INJECT_READS: 29.993, + ATTR_STATE: "30.0", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/sys/bus/w1/devices/3B-111111111111/w1_slave", + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, ], }, "42-111111111111": { - "device_info": { + ATTR_DEVICE_INFO: { ATTR_IDENTIFIERS: {(DOMAIN, "42-111111111111")}, ATTR_MANUFACTURER: MANUFACTURER, ATTR_MODEL: "42", @@ -1021,18 +1040,18 @@ MOCK_SYSBUS_DEVICES = { }, SENSOR_DOMAIN: [ { - "entity_id": "sensor.42_111111111111_temperature", - "unique_id": "/sys/bus/w1/devices/42-111111111111/w1_slave", - "injected_value": UnsupportResponseException, - "result": "unknown", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ENTITY_ID: "sensor.42_111111111111_temperature", + ATTR_INJECT_READS: UnsupportResponseException, + ATTR_STATE: STATE_UNKNOWN, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/sys/bus/w1/devices/42-111111111111/w1_slave", + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, ], }, "42-111111111112": { - "device_info": { + ATTR_DEVICE_INFO: { ATTR_IDENTIFIERS: {(DOMAIN, "42-111111111112")}, ATTR_MANUFACTURER: MANUFACTURER, ATTR_MODEL: "42", @@ -1040,18 +1059,18 @@ MOCK_SYSBUS_DEVICES = { }, SENSOR_DOMAIN: [ { - "entity_id": "sensor.42_111111111112_temperature", - "unique_id": "/sys/bus/w1/devices/42-111111111112/w1_slave", - "injected_value": [UnsupportResponseException] * 9 + [27.993], - "result": "28.0", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ENTITY_ID: "sensor.42_111111111112_temperature", + ATTR_INJECT_READS: [UnsupportResponseException] * 9 + [27.993], + ATTR_STATE: "28.0", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/sys/bus/w1/devices/42-111111111112/w1_slave", + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, ], }, "42-111111111113": { - "device_info": { + ATTR_DEVICE_INFO: { ATTR_IDENTIFIERS: {(DOMAIN, "42-111111111113")}, ATTR_MANUFACTURER: MANUFACTURER, ATTR_MODEL: "42", @@ -1059,13 +1078,13 @@ MOCK_SYSBUS_DEVICES = { }, SENSOR_DOMAIN: [ { - "entity_id": "sensor.42_111111111113_temperature", - "unique_id": "/sys/bus/w1/devices/42-111111111113/w1_slave", - "injected_value": [UnsupportResponseException] * 10 + [27.993], - "result": "unknown", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ENTITY_ID: "sensor.42_111111111113_temperature", + ATTR_INJECT_READS: [UnsupportResponseException] * 10 + [27.993], + ATTR_STATE: STATE_UNKNOWN, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "/sys/bus/w1/devices/42-111111111113/w1_slave", + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, ], }, diff --git a/tests/components/onewire/test_binary_sensor.py b/tests/components/onewire/test_binary_sensor.py index eabe96481ea..5ac45997b41 100644 --- a/tests/components/onewire/test_binary_sensor.py +++ b/tests/components/onewire/test_binary_sensor.py @@ -5,10 +5,16 @@ import pytest from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ENTITY_ID, ATTR_STATE from homeassistant.core import HomeAssistant from . import setup_owproxy_mock_devices -from .const import ATTR_DEFAULT_DISABLED, MOCK_OWPROXY_DEVICES +from .const import ( + ATTR_DEFAULT_DISABLED, + ATTR_DEVICE_FILE, + ATTR_UNIQUE_ID, + MOCK_OWPROXY_DEVICES, +) from tests.common import mock_registry @@ -41,7 +47,7 @@ async def test_owserver_binary_sensor( # Ensure all entities are enabled for expected_entity in expected_entities: if expected_entity.get(ATTR_DEFAULT_DISABLED): - entity_id = expected_entity["entity_id"] + entity_id = expected_entity[ATTR_ENTITY_ID] registry_entry = entity_registry.entities.get(entity_id) assert registry_entry.disabled assert registry_entry.disabled_by == "integration" @@ -52,11 +58,12 @@ async def test_owserver_binary_sensor( await hass.async_block_till_done() for expected_entity in expected_entities: - entity_id = expected_entity["entity_id"] + entity_id = expected_entity[ATTR_ENTITY_ID] registry_entry = entity_registry.entities.get(entity_id) assert registry_entry is not None + assert registry_entry.unique_id == expected_entity[ATTR_UNIQUE_ID] state = hass.states.get(entity_id) - assert state.state == expected_entity["result"] - assert state.attributes["device_file"] == expected_entity.get( - "device_file", registry_entry.unique_id + assert state.state == expected_entity[ATTR_STATE] + assert state.attributes[ATTR_DEVICE_FILE] == expected_entity.get( + ATTR_DEVICE_FILE, registry_entry.unique_id ) diff --git a/tests/components/onewire/test_sensor.py b/tests/components/onewire/test_sensor.py index 1b4c53b0a63..df8654f9e1e 100644 --- a/tests/components/onewire/test_sensor.py +++ b/tests/components/onewire/test_sensor.py @@ -9,16 +9,26 @@ from homeassistant.components.sensor import ATTR_STATE_CLASS, DOMAIN as SENSOR_D from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME, + ATTR_STATE, ATTR_UNIT_OF_MEASUREMENT, ) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from . import setup_owproxy_mock_devices, setup_sysbus_mock_devices -from .const import ATTR_DEFAULT_DISABLED, MOCK_OWPROXY_DEVICES, MOCK_SYSBUS_DEVICES +from .const import ( + ATTR_DEFAULT_DISABLED, + ATTR_DEVICE_FILE, + ATTR_DEVICE_INFO, + ATTR_INJECT_READS, + ATTR_UNIQUE_ID, + MOCK_OWPROXY_DEVICES, + MOCK_SYSBUS_DEVICES, +) from tests.common import assert_setup_component, mock_device_registry, mock_registry @@ -86,10 +96,10 @@ async def test_sensors_on_owserver_coupler( dir_side_effect.append([f"/{device_id}/"]) # dir on root read_side_effect.append(device_id[0:2].encode()) # read family on root - if "inject_reads" in mock_coupler: - read_side_effect += mock_coupler["inject_reads"] + if ATTR_INJECT_READS in mock_coupler: + read_side_effect += mock_coupler[ATTR_INJECT_READS] - expected_sensors = [] + expected_entities = [] for branch, branch_details in mock_coupler["branches"].items(): dir_side_effect.append( [ # dir on branch @@ -100,12 +110,12 @@ async def test_sensors_on_owserver_coupler( for sub_device_id, sub_device in branch_details.items(): read_side_effect.append(sub_device_id[0:2].encode()) - if "inject_reads" in sub_device: - read_side_effect.extend(sub_device["inject_reads"]) + if ATTR_INJECT_READS in sub_device: + read_side_effect.extend(sub_device[ATTR_INJECT_READS]) - expected_sensors += sub_device[SENSOR_DOMAIN] - for expected_sensor in sub_device[SENSOR_DOMAIN]: - read_side_effect.append(expected_sensor["injected_value"]) + expected_entities += sub_device[SENSOR_DOMAIN] + for expected_entity in sub_device[SENSOR_DOMAIN]: + read_side_effect.append(expected_entity[ATTR_INJECT_READS]) # Ensure enough read side effect read_side_effect.extend([ProtocolError("Missing injected value")] * 10) @@ -115,18 +125,18 @@ async def test_sensors_on_owserver_coupler( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert len(entity_registry.entities) == len(expected_sensors) + assert len(entity_registry.entities) == len(expected_entities) - for expected_sensor in expected_sensors: - entity_id = expected_sensor["entity_id"] + for expected_entity in expected_entities: + entity_id = expected_entity[ATTR_ENTITY_ID] registry_entry = entity_registry.entities.get(entity_id) assert registry_entry is not None - assert registry_entry.unique_id == expected_sensor["unique_id"] + assert registry_entry.unique_id == expected_entity[ATTR_UNIQUE_ID] state = hass.states.get(entity_id) - assert state.state == expected_sensor["result"] + assert state.state == expected_entity[ATTR_STATE] for attr in (ATTR_DEVICE_CLASS, ATTR_STATE_CLASS, ATTR_UNIT_OF_MEASUREMENT): - assert state.attributes.get(attr) == expected_sensor[attr] - assert state.attributes["device_file"] == expected_sensor["device_file"] + assert state.attributes.get(attr) == expected_entity[attr] + assert state.attributes[ATTR_DEVICE_FILE] == expected_entity[ATTR_DEVICE_FILE] async def test_owserver_setup_valid_device( @@ -151,7 +161,7 @@ async def test_owserver_setup_valid_device( # Ensure all entities are enabled for expected_entity in expected_entities: if expected_entity.get(ATTR_DEFAULT_DISABLED): - entity_id = expected_entity["entity_id"] + entity_id = expected_entity[ATTR_ENTITY_ID] registry_entry = entity_registry.entities.get(entity_id) assert registry_entry.disabled assert registry_entry.disabled_by == "integration" @@ -162,7 +172,7 @@ async def test_owserver_setup_valid_device( await hass.async_block_till_done() if len(expected_entities) > 0: - device_info = mock_device["device_info"] + device_info = mock_device[ATTR_DEVICE_INFO] assert len(device_registry.devices) == 1 registry_entry = device_registry.async_get_device({(DOMAIN, device_id)}) assert registry_entry is not None @@ -172,16 +182,16 @@ async def test_owserver_setup_valid_device( assert registry_entry.model == device_info[ATTR_MODEL] for expected_entity in expected_entities: - entity_id = expected_entity["entity_id"] + entity_id = expected_entity[ATTR_ENTITY_ID] registry_entry = entity_registry.entities.get(entity_id) assert registry_entry is not None - assert registry_entry.unique_id == expected_entity["unique_id"] + assert registry_entry.unique_id == expected_entity[ATTR_UNIQUE_ID] state = hass.states.get(entity_id) - assert state.state == expected_entity["result"] + assert state.state == expected_entity[ATTR_STATE] for attr in (ATTR_DEVICE_CLASS, ATTR_STATE_CLASS, ATTR_UNIT_OF_MEASUREMENT): assert state.attributes.get(attr) == expected_entity[attr] - assert state.attributes["device_file"] == expected_entity.get( - "device_file", registry_entry.unique_id + assert state.attributes[ATTR_DEVICE_FILE] == expected_entity.get( + ATTR_DEVICE_FILE, registry_entry.unique_id ) @@ -212,7 +222,7 @@ async def test_onewiredirect_setup_valid_device( assert len(entity_registry.entities) == len(expected_entities) if len(expected_entities) > 0: - device_info = mock_device["device_info"] + device_info = mock_device[ATTR_DEVICE_INFO] assert len(device_registry.devices) == 1 registry_entry = device_registry.async_get_device({(DOMAIN, device_id)}) assert registry_entry is not None @@ -221,12 +231,12 @@ async def test_onewiredirect_setup_valid_device( assert registry_entry.name == device_info[ATTR_NAME] assert registry_entry.model == device_info[ATTR_MODEL] - for expected_sensor in expected_entities: - entity_id = expected_sensor["entity_id"] + for expected_entity in expected_entities: + entity_id = expected_entity[ATTR_ENTITY_ID] registry_entry = entity_registry.entities.get(entity_id) assert registry_entry is not None - assert registry_entry.unique_id == expected_sensor["unique_id"] + assert registry_entry.unique_id == expected_entity[ATTR_UNIQUE_ID] state = hass.states.get(entity_id) - assert state.state == expected_sensor["result"] + assert state.state == expected_entity[ATTR_STATE] for attr in (ATTR_DEVICE_CLASS, ATTR_STATE_CLASS, ATTR_UNIT_OF_MEASUREMENT): - assert state.attributes.get(attr) == expected_sensor[attr] + assert state.attributes.get(attr) == expected_entity[attr] diff --git a/tests/components/onewire/test_switch.py b/tests/components/onewire/test_switch.py index aa5230fbb20..2f8f96ee638 100644 --- a/tests/components/onewire/test_switch.py +++ b/tests/components/onewire/test_switch.py @@ -5,11 +5,22 @@ import pytest from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TOGGLE, STATE_OFF, STATE_ON +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_STATE, + SERVICE_TOGGLE, + STATE_OFF, + STATE_ON, +) from homeassistant.core import HomeAssistant from . import setup_owproxy_mock_devices -from .const import ATTR_DEFAULT_DISABLED, MOCK_OWPROXY_DEVICES +from .const import ( + ATTR_DEFAULT_DISABLED, + ATTR_DEVICE_FILE, + ATTR_UNIQUE_ID, + MOCK_OWPROXY_DEVICES, +) from tests.common import mock_registry @@ -42,7 +53,7 @@ async def test_owserver_switch( # Ensure all entities are enabled for expected_entity in expected_entities: if expected_entity.get(ATTR_DEFAULT_DISABLED): - entity_id = expected_entity["entity_id"] + entity_id = expected_entity[ATTR_ENTITY_ID] registry_entry = entity_registry.entities.get(entity_id) assert registry_entry.disabled assert registry_entry.disabled_by == "integration" @@ -53,18 +64,19 @@ async def test_owserver_switch( await hass.async_block_till_done() for expected_entity in expected_entities: - entity_id = expected_entity["entity_id"] + entity_id = expected_entity[ATTR_ENTITY_ID] registry_entry = entity_registry.entities.get(entity_id) assert registry_entry is not None + assert registry_entry.unique_id == expected_entity[ATTR_UNIQUE_ID] state = hass.states.get(entity_id) - assert state.state == expected_entity["result"] + assert state.state == expected_entity[ATTR_STATE] if state.state == STATE_ON: owproxy.return_value.read.side_effect = [b" 0"] - expected_entity["result"] = STATE_OFF + expected_entity[ATTR_STATE] = STATE_OFF elif state.state == STATE_OFF: owproxy.return_value.read.side_effect = [b" 1"] - expected_entity["result"] = STATE_ON + expected_entity[ATTR_STATE] = STATE_ON await hass.services.async_call( SWITCH_DOMAIN, @@ -75,7 +87,7 @@ async def test_owserver_switch( await hass.async_block_till_done() state = hass.states.get(entity_id) - assert state.state == expected_entity["result"] - assert state.attributes["device_file"] == expected_entity.get( - "device_file", registry_entry.unique_id + assert state.state == expected_entity[ATTR_STATE] + assert state.attributes[ATTR_DEVICE_FILE] == expected_entity.get( + ATTR_DEVICE_FILE, registry_entry.unique_id ) From dde8ac4eb52b98d2caf76e1f13dc1dbe11cc5420 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 19 Oct 2021 21:43:08 +0200 Subject: [PATCH 0551/1038] Add support for kPa to sensor statistics (#58032) --- homeassistant/components/sensor/recorder.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 9cc4ea9c434..d98cfdb500e 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -46,6 +46,7 @@ from homeassistant.const import ( PRESSURE_BAR, PRESSURE_HPA, PRESSURE_INHG, + PRESSURE_KPA, PRESSURE_MBAR, PRESSURE_PA, PRESSURE_PSI, @@ -110,6 +111,7 @@ UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = { PRESSURE_BAR: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_BAR], PRESSURE_HPA: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_HPA], PRESSURE_INHG: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_INHG], + PRESSURE_KPA: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_KPA], PRESSURE_MBAR: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_MBAR], PRESSURE_PA: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_PA], PRESSURE_PSI: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_PSI], From 8e0cb5fcec3d00064adff44f14cd6a5e7f06b1f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 19 Oct 2021 23:04:13 +0300 Subject: [PATCH 0552/1038] Fix clickatell send_error error check (#57985) --- homeassistant/components/clickatell/notify.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/clickatell/notify.py b/homeassistant/components/clickatell/notify.py index 09096f44b74..fdefb25aef4 100644 --- a/homeassistant/components/clickatell/notify.py +++ b/homeassistant/components/clickatell/notify.py @@ -1,11 +1,12 @@ """Clickatell platform for notify component.""" +from http import HTTPStatus import logging import requests import voluptuous as vol from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService -from homeassistant.const import CONF_API_KEY, CONF_RECIPIENT, HTTP_ACCEPTED, HTTP_OK +from homeassistant.const import CONF_API_KEY, CONF_RECIPIENT import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -37,5 +38,5 @@ class ClickatellNotificationService(BaseNotificationService): data = {"apiKey": self.api_key, "to": self.recipient, "content": message} resp = requests.get(BASE_API_URL, params=data, timeout=5) - if (resp.status_code != HTTP_OK) or (resp.status_code != HTTP_ACCEPTED): + if resp.status_code not in (HTTPStatus.OK, HTTPStatus.ACCEPTED): _LOGGER.error("Error %s : %s", resp.status_code, resp.text) From bf7c99c1f8f33720149b58a0a3b1687189b29179 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 19 Oct 2021 14:09:48 -0600 Subject: [PATCH 0553/1038] Migrate SimpliSafe to new web-based authentication (#57212) --- .../components/simplisafe/__init__.py | 128 +++++---- .../simplisafe/alarm_control_panel.py | 24 +- .../components/simplisafe/binary_sensor.py | 42 +-- .../components/simplisafe/config_flow.py | 206 ++++++-------- homeassistant/components/simplisafe/const.py | 17 +- homeassistant/components/simplisafe/lock.py | 8 +- .../components/simplisafe/manifest.json | 2 +- homeassistant/components/simplisafe/sensor.py | 11 +- .../components/simplisafe/strings.json | 22 +- .../simplisafe/translations/en.json | 22 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/simplisafe/test_config_flow.py | 268 +++++++++--------- 13 files changed, 366 insertions(+), 388 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index fce989bc953..28ece7d45cd 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -3,25 +3,30 @@ from __future__ import annotations import asyncio from collections.abc import Awaitable, Callable -from typing import cast -from uuid import UUID +from datetime import timedelta +from typing import TYPE_CHECKING, cast -from simplipy import get_api -from simplipy.api import API +from simplipy import API +from simplipy.device.sensor.v2 import SensorV2 +from simplipy.device.sensor.v3 import SensorV3 from simplipy.errors import ( EndpointUnavailableError, InvalidCredentialsError, SimplipyError, ) -from simplipy.sensor.v2 import SensorV2 -from simplipy.sensor.v3 import SensorV3 from simplipy.system import SystemNotification from simplipy.system.v2 import SystemV2 -from simplipy.system.v3 import SystemV3 +from simplipy.system.v3 import ( + VOLUME_HIGH, + VOLUME_LOW, + VOLUME_MEDIUM, + VOLUME_OFF, + SystemV3, +) import voluptuous as vol from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_CODE, CONF_CODE, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ATTR_CODE, CONF_CODE, CONF_TOKEN from homeassistant.core import CoreState, HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import ( @@ -49,15 +54,15 @@ from .const import ( ATTR_EXIT_DELAY_HOME, ATTR_LIGHT, ATTR_VOICE_PROMPT_VOLUME, + CONF_USER_ID, DATA_CLIENT, - DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER, - VOLUMES, ) EVENT_SIMPLISAFE_NOTIFICATION = "SIMPLISAFE_NOTIFICATION" +DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) DEFAULT_SOCKET_MIN_RETRY = 15 PLATFORMS = ( @@ -75,6 +80,8 @@ ATTR_PIN_VALUE = "pin" ATTR_SYSTEM_ID = "system_id" ATTR_TIMESTAMP = "timestamp" +VOLUMES = [VOLUME_OFF, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_HIGH] + SERVICE_BASE_SCHEMA = vol.Schema({vol.Required(ATTR_SYSTEM_ID): cv.positive_int}) SERVICE_REMOVE_PIN_SCHEMA = SERVICE_BASE_SCHEMA.extend( @@ -120,13 +127,29 @@ SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = SERVICE_BASE_SCHEMA.extend( CONFIG_SCHEMA = cv.deprecated(DOMAIN) -async def async_get_client_id(hass: HomeAssistant) -> str: - """Get a client ID (based on the HASS unique ID) for the SimpliSafe API. +@callback +def _async_standardize_config_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Bring a config entry up to current standards.""" + if CONF_TOKEN not in entry.data: + raise ConfigEntryAuthFailed( + "New SimpliSafe OAuth standard requires re-authentication" + ) - Note that SimpliSafe requires full, "dashed" versions of UUIDs. - """ - hass_id = await hass.helpers.instance_id.async_get() - return str(UUID(hass_id)) + entry_updates = {} + if not entry.unique_id: + # If the config entry doesn't already have a unique ID, set one: + entry_updates["unique_id"] = entry.data[CONF_USER_ID] + if CONF_CODE in entry.data: + # If an alarm code was provided as part of configuration.yaml, pop it out of + # the config entry's data and move it to options: + data = {**entry.data} + entry_updates["data"] = data + entry_updates["options"] = { + **entry.options, + CONF_CODE: data.pop(CONF_CODE), + } + if entry_updates: + hass.config_entries.async_update_entry(entry, **entry_updates) async def async_register_base_station( @@ -143,47 +166,19 @@ async def async_register_base_station( ) -@callback -def _async_standardize_config_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Bring a config entry up to current standards.""" - if CONF_PASSWORD not in entry.data: - raise ConfigEntryAuthFailed("Config schema change requires re-authentication") - - entry_updates = {} - if not entry.unique_id: - # If the config entry doesn't already have a unique ID, set one: - entry_updates["unique_id"] = entry.data[CONF_USERNAME] - if CONF_CODE in entry.data: - # If an alarm code was provided as part of configuration.yaml, pop it out of - # the config entry's data and move it to options: - data = {**entry.data} - entry_updates["data"] = data - entry_updates["options"] = { - **entry.options, - CONF_CODE: data.pop(CONF_CODE), - } - if entry_updates: - hass.config_entries.async_update_entry(entry, **entry_updates) - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SimpliSafe as config entry.""" - hass.data.setdefault(DOMAIN, {DATA_CLIENT: {}}) - hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] = [] + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = {} _async_standardize_config_entry(hass, entry) _verify_domain_control = verify_domain_control(hass, DOMAIN) - - client_id = await async_get_client_id(hass) websession = aiohttp_client.async_get_clientsession(hass) try: - api = await get_api( - entry.data[CONF_USERNAME], - entry.data[CONF_PASSWORD], - client_id=client_id, - session=websession, + api = await API.async_from_refresh_token( + entry.data[CONF_TOKEN], session=websession ) except InvalidCredentialsError as err: raise ConfigEntryAuthFailed from err @@ -198,7 +193,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except SimplipyError as err: raise ConfigEntryNotReady from err - hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] = simplisafe + hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] = simplisafe hass.config_entries.async_setup_platforms(entry, PLATFORMS) @callback @@ -237,7 +232,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Clear all active notifications.""" system = simplisafe.systems[call.data[ATTR_SYSTEM_ID]] try: - await system.clear_notifications() + await system.async_clear_notifications() except SimplipyError as err: LOGGER.error("Error during service call: %s", err) @@ -247,7 +242,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Remove a PIN.""" system = simplisafe.systems[call.data[ATTR_SYSTEM_ID]] try: - await system.remove_pin(call.data[ATTR_PIN_LABEL_OR_VALUE]) + await system.async_remove_pin(call.data[ATTR_PIN_LABEL_OR_VALUE]) except SimplipyError as err: LOGGER.error("Error during service call: %s", err) @@ -257,7 +252,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set a PIN.""" system = simplisafe.systems[call.data[ATTR_SYSTEM_ID]] try: - await system.set_pin(call.data[ATTR_PIN_LABEL], call.data[ATTR_PIN_VALUE]) + await system.async_set_pin( + call.data[ATTR_PIN_LABEL], call.data[ATTR_PIN_VALUE] + ) except SimplipyError as err: LOGGER.error("Error during service call: %s", err) @@ -268,7 +265,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set one or more system parameters.""" system = cast(SystemV3, simplisafe.systems[call.data[ATTR_SYSTEM_ID]]) try: - await system.set_properties( + await system.async_set_properties( { prop: value for prop, value in call.data.items() @@ -299,7 +296,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a SimpliSafe config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - hass.data[DOMAIN][DATA_CLIENT].pop(entry.entry_id) + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok @@ -362,7 +359,7 @@ class SimpliSafe: async def async_init(self) -> None: """Initialize the data class.""" - self.systems = await self._api.get_systems() + self.systems = await self._api.async_get_systems() for system in self.systems.values(): self._system_notifications[system.system_id] = set() @@ -373,17 +370,34 @@ class SimpliSafe: self.coordinator = DataUpdateCoordinator( self._hass, LOGGER, - name=self.entry.data[CONF_USERNAME], + name=self.entry.data[CONF_USER_ID], update_interval=DEFAULT_SCAN_INTERVAL, update_method=self.async_update, ) + @callback + def async_save_refresh_token(token: str) -> None: + """Save a refresh token to the config entry.""" + LOGGER.info("Saving new refresh token to HASS storage") + self._hass.config_entries.async_update_entry( + self.entry, + data={**self.entry.data, CONF_TOKEN: token}, + ) + + self.entry.async_on_unload( + self._api.add_refresh_token_listener(async_save_refresh_token) + ) + + if TYPE_CHECKING: + assert self._api.refresh_token + async_save_refresh_token(self._api.refresh_token) + async def async_update(self) -> None: """Get updated data from SimpliSafe.""" async def async_update_system(system: SystemV2 | SystemV3) -> None: """Update a system.""" - await system.update(cached=system.version != 3) + await system.async_update(cached=system.version != 3) self._async_process_new_notifications(system) tasks = [async_update_system(system) for system in self.systems.values()] diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index fb74aa1d26d..278d7579edf 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -4,7 +4,13 @@ from __future__ import annotations from simplipy.errors import SimplipyError from simplipy.system import SystemStates from simplipy.system.v2 import SystemV2 -from simplipy.system.v3 import SystemV3 +from simplipy.system.v3 import ( + VOLUME_HIGH, + VOLUME_LOW, + VOLUME_MEDIUM, + VOLUME_OFF, + SystemV3, +) from homeassistant.components.alarm_control_panel import ( FORMAT_NUMBER, @@ -41,7 +47,6 @@ from .const import ( DATA_CLIENT, DOMAIN, LOGGER, - VOLUME_STRING_MAP, ) ATTR_BATTERY_BACKUP_POWER_LEVEL = "battery_backup_power_level" @@ -51,12 +56,19 @@ ATTR_RF_JAMMING = "rf_jamming" ATTR_WALL_POWER_LEVEL = "wall_power_level" ATTR_WIFI_STRENGTH = "wifi_strength" +VOLUME_STRING_MAP = { + VOLUME_HIGH: "high", + VOLUME_LOW: "low", + VOLUME_MEDIUM: "medium", + VOLUME_OFF: "off", +} + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up a SimpliSafe alarm control panel based on a config entry.""" - simplisafe = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] + simplisafe = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] async_add_entities( [SimpliSafeAlarm(simplisafe, system) for system in simplisafe.systems.values()], True, @@ -115,7 +127,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): return try: - await self._system.set_off() + await self._system.async_set_off() except SimplipyError as err: LOGGER.error('Error while disarming "%s": %s', self._system.system_id, err) return @@ -129,7 +141,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): return try: - await self._system.set_home() + await self._system.async_set_home() except SimplipyError as err: LOGGER.error( 'Error while arming "%s" (home): %s', self._system.system_id, err @@ -145,7 +157,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): return try: - await self._system.set_away() + await self._system.async_set_away() except SimplipyError as err: LOGGER.error( 'Error while arming "%s" (away): %s', self._system.system_id, err diff --git a/homeassistant/components/simplisafe/binary_sensor.py b/homeassistant/components/simplisafe/binary_sensor.py index 1c471d10ce8..4afc2dab247 100644 --- a/homeassistant/components/simplisafe/binary_sensor.py +++ b/homeassistant/components/simplisafe/binary_sensor.py @@ -1,7 +1,9 @@ """Support for SimpliSafe binary sensors.""" from __future__ import annotations -from simplipy.entity import Entity as SimplipyEntity, EntityTypes +from simplipy.device import DeviceTypes +from simplipy.device.sensor.v2 import SensorV2 +from simplipy.device.sensor.v3 import SensorV3 from simplipy.system.v2 import SystemV2 from simplipy.system.v3 import SystemV3 @@ -23,25 +25,25 @@ from . import SimpliSafe, SimpliSafeBaseSensor from .const import DATA_CLIENT, DOMAIN, LOGGER SUPPORTED_BATTERY_SENSOR_TYPES = [ - EntityTypes.carbon_monoxide, - EntityTypes.entry, - EntityTypes.glass_break, - EntityTypes.leak, - EntityTypes.lock_keypad, - EntityTypes.motion, - EntityTypes.siren, - EntityTypes.smoke, - EntityTypes.temperature, + DeviceTypes.carbon_monoxide, + DeviceTypes.entry, + DeviceTypes.glass_break, + DeviceTypes.leak, + DeviceTypes.lock_keypad, + DeviceTypes.motion, + DeviceTypes.siren, + DeviceTypes.smoke, + DeviceTypes.temperature, ] TRIGGERED_SENSOR_TYPES = { - EntityTypes.carbon_monoxide: DEVICE_CLASS_GAS, - EntityTypes.entry: DEVICE_CLASS_DOOR, - EntityTypes.glass_break: DEVICE_CLASS_SAFETY, - EntityTypes.leak: DEVICE_CLASS_MOISTURE, - EntityTypes.motion: DEVICE_CLASS_MOTION, - EntityTypes.siren: DEVICE_CLASS_SAFETY, - EntityTypes.smoke: DEVICE_CLASS_SMOKE, + DeviceTypes.carbon_monoxide: DEVICE_CLASS_GAS, + DeviceTypes.entry: DEVICE_CLASS_DOOR, + DeviceTypes.glass_break: DEVICE_CLASS_SAFETY, + DeviceTypes.leak: DEVICE_CLASS_MOISTURE, + DeviceTypes.motion: DEVICE_CLASS_MOTION, + DeviceTypes.siren: DEVICE_CLASS_SAFETY, + DeviceTypes.smoke: DEVICE_CLASS_SMOKE, } @@ -49,7 +51,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up SimpliSafe binary sensors based on a config entry.""" - simplisafe = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] + simplisafe = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] sensors: list[BatteryBinarySensor | TriggeredBinarySensor] = [] @@ -81,7 +83,7 @@ class TriggeredBinarySensor(SimpliSafeBaseSensor, BinarySensorEntity): self, simplisafe: SimpliSafe, system: SystemV2 | SystemV3, - sensor: SimplipyEntity, + sensor: SensorV2 | SensorV3, device_class: str, ) -> None: """Initialize.""" @@ -104,7 +106,7 @@ class BatteryBinarySensor(SimpliSafeBaseSensor, BinarySensorEntity): self, simplisafe: SimpliSafe, system: SystemV2 | SystemV3, - sensor: SimplipyEntity, + sensor: SensorV2 | SensorV3, ) -> None: """Initialize.""" super().__init__(simplisafe, system, sensor) diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 31ae125046c..8f8ec6cdc16 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -1,36 +1,50 @@ """Config flow to configure the SimpliSafe component.""" from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any, NamedTuple -from simplipy import get_api -from simplipy.api import API -from simplipy.errors import ( - InvalidCredentialsError, - PendingAuthorizationError, - SimplipyError, +from simplipy import API +from simplipy.errors import InvalidCredentialsError, SimplipyError +from simplipy.util.auth import ( + get_auth0_code_challenge, + get_auth0_code_verifier, + get_auth_url, ) import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_CODE, CONF_TOKEN, CONF_URL, CONF_USERNAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers import aiohttp_client +from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.typing import ConfigType -from . import async_get_client_id -from .const import DOMAIN, LOGGER +from .const import CONF_USER_ID, DOMAIN, LOGGER -FULL_DATA_SCHEMA = vol.Schema( +CONF_AUTH_CODE = "auth_code" + +STEP_INPUT_AUTH_CODE_SCHEMA = vol.Schema( { - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, - vol.Optional(CONF_CODE): str, + vol.Required(CONF_AUTH_CODE): cv.string, } ) -PASSWORD_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) + + +class SimpliSafeOAuthValues(NamedTuple): + """Define a named tuple to handle SimpliSafe OAuth strings.""" + + code_verifier: str + auth_url: str + + +@callback +def async_get_simplisafe_oauth_values() -> SimpliSafeOAuthValues: + """Get a SimpliSafe OAuth code verifier and auth URL.""" + code_verifier = get_auth0_code_verifier() + code_challenge = get_auth0_code_challenge(code_verifier) + auth_url = get_auth_url(code_challenge) + return SimpliSafeOAuthValues(code_verifier, auth_url) class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -40,8 +54,9 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._code: str | None = None - self._password: str | None = None + self._errors: dict[str, Any] = {} + self._oauth_values: SimpliSafeOAuthValues = async_get_simplisafe_oauth_values() + self._reauth: bool = False self._username: str | None = None @staticmethod @@ -52,128 +67,79 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Define the config flow to handle options.""" return SimpliSafeOptionsFlowHandler(config_entry) - async def _async_get_simplisafe_api(self) -> API: - """Get an authenticated SimpliSafe API client.""" - assert self._username - assert self._password - - client_id = await async_get_client_id(self.hass) - websession = aiohttp_client.async_get_clientsession(self.hass) - - return await get_api( - self._username, - self._password, - client_id=client_id, - session=websession, - ) - - async def _async_login_during_step( - self, *, step_id: str, form_schema: vol.Schema + async def async_step_input_auth_code( + self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Attempt to log into the API from within a config flow step.""" - errors = {} - - try: - await self._async_get_simplisafe_api() - except PendingAuthorizationError: - LOGGER.info("Awaiting confirmation of MFA email click") - return await self.async_step_mfa() - except InvalidCredentialsError: - errors = {"base": "invalid_auth"} - except SimplipyError as err: - LOGGER.error("Unknown error while logging into SimpliSafe: %s", err) - errors = {"base": "unknown"} - - if errors: + """Handle the input of a SimpliSafe OAuth authorization code.""" + if user_input is None: return self.async_show_form( - step_id=step_id, - data_schema=form_schema, - errors=errors, + step_id="input_auth_code", data_schema=STEP_INPUT_AUTH_CODE_SCHEMA ) - return await self.async_step_finish( - { - CONF_USERNAME: self._username, - CONF_PASSWORD: self._password, - CONF_CODE: self._code, - } - ) + if TYPE_CHECKING: + assert self._oauth_values - async def async_step_finish(self, user_input: dict[str, Any]) -> FlowResult: - """Handle finish config entry setup.""" - assert self._username + self._errors = {} + session = aiohttp_client.async_get_clientsession(self.hass) - existing_entry = await self.async_set_unique_id(self._username) - if existing_entry: - self.hass.config_entries.async_update_entry(existing_entry, data=user_input) + try: + simplisafe = await API.async_from_auth( + user_input[CONF_AUTH_CODE], + self._oauth_values.code_verifier, + session=session, + ) + except InvalidCredentialsError: + self._errors = {"base": "invalid_auth"} + except SimplipyError as err: + LOGGER.error("Unknown error while logging into SimpliSafe: %s", err) + self._errors = {"base": "unknown"} + + if self._errors: + return await self.async_step_user() + + data = {CONF_USER_ID: simplisafe.user_id, CONF_TOKEN: simplisafe.refresh_token} + unique_id = str(simplisafe.user_id) + + if self._reauth: + # "Old" config entries utilized the user's email address (username) as the + # unique ID, whereas "new" config entries utilize the SimpliSafe user ID – + # either one is a candidate for re-auth: + existing_entry = await self.async_set_unique_id(self._username or unique_id) + if not existing_entry: + # If we don't have an entry that matches this user ID, the user logged + # in with different credentials: + return self.async_abort(reason="wrong_account") + + self.hass.config_entries.async_update_entry( + existing_entry, unique_id=unique_id, data=data + ) self.hass.async_create_task( self.hass.config_entries.async_reload(existing_entry.entry_id) ) return self.async_abort(reason="reauth_successful") - return self.async_create_entry(title=self._username, data=user_input) - async def async_step_mfa( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle multi-factor auth confirmation.""" - if user_input is None: - return self.async_show_form(step_id="mfa") - - try: - await self._async_get_simplisafe_api() - except PendingAuthorizationError: - LOGGER.error("Still awaiting confirmation of MFA email click") - return self.async_show_form( - step_id="mfa", errors={"base": "still_awaiting_mfa"} - ) - - return await self.async_step_finish( - { - CONF_USERNAME: self._username, - CONF_PASSWORD: self._password, - CONF_CODE: self._code, - } - ) + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=unique_id, data=data) async def async_step_reauth(self, config: ConfigType) -> FlowResult: """Handle configuration by re-auth.""" - self._code = config.get(CONF_CODE) - self._username = config[CONF_USERNAME] - - return await self.async_step_reauth_confirm() - - async def async_step_reauth_confirm( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle re-auth completion.""" - if not user_input: - return self.async_show_form( - step_id="reauth_confirm", data_schema=PASSWORD_DATA_SCHEMA - ) - - self._password = user_input[CONF_PASSWORD] - - return await self._async_login_during_step( - step_id="reauth_confirm", form_schema=PASSWORD_DATA_SCHEMA - ) + self._username = config.get(CONF_USERNAME) + self._reauth = True + return await self.async_step_user() async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle the start of the config flow.""" - if not user_input: - return self.async_show_form(step_id="user", data_schema=FULL_DATA_SCHEMA) + if user_input is None: + return self.async_show_form( + step_id="user", + errors=self._errors, + description_placeholders={CONF_URL: self._oauth_values.auth_url}, + ) - await self.async_set_unique_id(user_input[CONF_USERNAME]) - self._abort_if_unique_id_configured() - - self._code = user_input.get(CONF_CODE) - self._password = user_input[CONF_PASSWORD] - self._username = user_input[CONF_USERNAME] - - return await self._async_login_during_step( - step_id="user", form_schema=FULL_DATA_SCHEMA - ) + return await self.async_step_input_auth_code() class SimpliSafeOptionsFlowHandler(config_entries.OptionsFlow): diff --git a/homeassistant/components/simplisafe/const.py b/homeassistant/components/simplisafe/const.py index 36d191d0ab8..a0073fa8122 100644 --- a/homeassistant/components/simplisafe/const.py +++ b/homeassistant/components/simplisafe/const.py @@ -1,17 +1,10 @@ """Define constants for the SimpliSafe component.""" -from datetime import timedelta import logging -from simplipy.system.v3 import VOLUME_HIGH, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_OFF - LOGGER = logging.getLogger(__package__) DOMAIN = "simplisafe" -DATA_CLIENT = "client" - -DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) - ATTR_ALARM_DURATION = "alarm_duration" ATTR_ALARM_VOLUME = "alarm_volume" ATTR_CHIME_VOLUME = "chime_volume" @@ -22,10 +15,6 @@ ATTR_EXIT_DELAY_HOME = "exit_delay_home" ATTR_LIGHT = "light" ATTR_VOICE_PROMPT_VOLUME = "voice_prompt_volume" -VOLUMES = [VOLUME_OFF, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_HIGH] -VOLUME_STRING_MAP = { - VOLUME_HIGH: "high", - VOLUME_LOW: "low", - VOLUME_MEDIUM: "medium", - VOLUME_OFF: "off", -} +CONF_USER_ID = "user_id" + +DATA_CLIENT = "client" diff --git a/homeassistant/components/simplisafe/lock.py b/homeassistant/components/simplisafe/lock.py index e5f6faba10f..34fa141745b 100644 --- a/homeassistant/components/simplisafe/lock.py +++ b/homeassistant/components/simplisafe/lock.py @@ -3,8 +3,8 @@ from __future__ import annotations from typing import Any +from simplipy.device.lock import Lock, LockStates from simplipy.errors import SimplipyError -from simplipy.lock import Lock, LockStates from simplipy.system.v3 import SystemV3 from homeassistant.components.lock import LockEntity @@ -23,7 +23,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up SimpliSafe locks based on a config entry.""" - simplisafe = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] + simplisafe = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] locks = [] for system in simplisafe.systems.values(): @@ -49,7 +49,7 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity): async def async_lock(self, **kwargs: Any) -> None: """Lock the lock.""" try: - await self._lock.lock() + await self._lock.async_lock() except SimplipyError as err: LOGGER.error('Error while locking "%s": %s', self._lock.name, err) return @@ -60,7 +60,7 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity): async def async_unlock(self, **kwargs: Any) -> None: """Unlock the lock.""" try: - await self._lock.unlock() + await self._lock.async_unlock() except SimplipyError as err: LOGGER.error('Error while unlocking "%s": %s', self._lock.name, err) return diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 8b610c6c28c..6f6025308eb 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==11.0.7"], + "requirements": ["simplisafe-python==12.0.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/simplisafe/sensor.py b/homeassistant/components/simplisafe/sensor.py index fceb90fc9eb..96aed33979d 100644 --- a/homeassistant/components/simplisafe/sensor.py +++ b/homeassistant/components/simplisafe/sensor.py @@ -1,5 +1,8 @@ """Support for SimpliSafe freeze sensor.""" -from simplipy.entity import EntityTypes +from typing import TYPE_CHECKING + +from simplipy.device import DeviceTypes +from simplipy.device.sensor.v3 import SensorV3 from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.config_entries import ConfigEntry @@ -15,7 +18,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up SimpliSafe freeze sensors based on a config entry.""" - simplisafe = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] + simplisafe = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] sensors = [] for system in simplisafe.systems.values(): @@ -24,7 +27,7 @@ async def async_setup_entry( continue for sensor in system.sensors.values(): - if sensor.type == EntityTypes.temperature: + if sensor.type == DeviceTypes.temperature: sensors.append(SimplisafeFreezeSensor(simplisafe, system, sensor)) async_add_entities(sensors) @@ -40,4 +43,6 @@ class SimplisafeFreezeSensor(SimpliSafeBaseSensor, SensorEntity): @callback def async_update_from_rest_api(self) -> None: """Update the entity with the provided REST API data.""" + if TYPE_CHECKING: + assert isinstance(self._sensor, SensorV3) self._attr_native_value = self._sensor.temperature diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index 23f85495025..55a916bfe6b 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -1,24 +1,15 @@ { "config": { "step": { - "mfa": { - "title": "SimpliSafe Multi-Factor Authentication", - "description": "Check your email for a link from SimpliSafe. After verifying the link, return here to complete the installation of the integration." - }, - "reauth_confirm": { - "title": "[%key:common::config_flow::title::reauth%]", - "description": "Your access has expired or been revoked. Enter your password to re-link your account.", + "input_auth_code": { + "title": "Finish Authorization", + "description": "Input the authorization code from the SimpliSafe web app URL:", "data": { - "password": "[%key:common::config_flow::data::password%]" + "auth_code": "Authorization Code" } }, "user": { - "title": "Fill in your information.", - "data": { - "username": "[%key:common::config_flow::data::email%]", - "password": "[%key:common::config_flow::data::password%]", - "code": "Code (used in Home Assistant UI)" - } + "description": "Starting in 2021, SimpliSafe has moved to a new authentication mechanism 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 click Submit." } }, "error": { @@ -29,7 +20,8 @@ }, "abort": { "already_configured": "This SimpliSafe account is already in use.", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "wrong_account": "The user credentials provided do not match this SimpliSafe account." } }, "options": { diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index 331eb65ca83..69a25b1bdc2 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "This SimpliSafe account is already in use.", - "reauth_successful": "Re-authentication was successful" + "reauth_successful": "Re-authentication was successful", + "wrong_account": "The user credentials provided do not match this SimpliSafe account." }, "error": { "identifier_exists": "Account already registered", @@ -11,24 +12,15 @@ "unknown": "Unexpected error" }, "step": { - "mfa": { - "description": "Check your email for a link from SimpliSafe. After verifying the link, return here to complete the installation of the integration.", - "title": "SimpliSafe Multi-Factor Authentication" - }, - "reauth_confirm": { + "input_auth_code": { "data": { - "password": "Password" + "auth_code": "Authorization Code" }, - "description": "Your access has expired or been revoked. Enter your password to re-link your account.", - "title": "Reauthenticate Integration" + "description": "Input the authorization code from the SimpliSafe web app URL:", + "title": "Finish Authorization" }, "user": { - "data": { - "code": "Code (used in Home Assistant UI)", - "password": "Password", - "username": "Email" - }, - "title": "Fill in your information." + "description": "Starting in 2021, SimpliSafe has moved to a new authentication mechanism 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 click Submit." } } }, diff --git a/requirements_all.txt b/requirements_all.txt index bb61a6c9947..fb58fe31336 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2137,7 +2137,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==11.0.7 +simplisafe-python==12.0.0 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index abada917b08..2b87d50d8c8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1233,7 +1233,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==11.0.7 +simplisafe-python==12.0.0 # homeassistant.components.slack slackclient==2.5.0 diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index 395b2c98962..4546b7d3383 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -1,68 +1,94 @@ """Define tests for the SimpliSafe config flow.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, Mock, patch -from simplipy.errors import ( - InvalidCredentialsError, - PendingAuthorizationError, - SimplipyError, -) +import pytest +from simplipy.errors import InvalidCredentialsError, SimplipyError from homeassistant import data_entry_flow from homeassistant.components.simplisafe import DOMAIN +from homeassistant.components.simplisafe.config_flow import CONF_AUTH_CODE +from homeassistant.components.simplisafe.const import CONF_USER_ID from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER -from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME from tests.common import MockConfigEntry -async def test_duplicate_error(hass): - """Test that errors are shown when duplicates are added.""" - conf = { - CONF_USERNAME: "user@email.com", - CONF_PASSWORD: "password", - CONF_CODE: "1234", - } +@pytest.fixture(name="api") +def api_fixture(): + """Define a fixture for simplisafe-python API object.""" + api = Mock() + api.refresh_token = "token123" + api.user_id = "12345" + return api + +@pytest.fixture(name="mock_async_from_auth") +def mock_async_from_auth_fixture(api): + """Define a fixture for simplipy.API.async_from_auth.""" + with patch( + "homeassistant.components.simplisafe.config_flow.API.async_from_auth", + ) as mock_async_from_auth: + mock_async_from_auth.side_effect = AsyncMock(return_value=api) + yield mock_async_from_auth + + +async def test_duplicate_error(hass, mock_async_from_auth): + """Test that errors are shown when duplicates are added.""" MockConfigEntry( domain=DOMAIN, - unique_id="user@email.com", + unique_id="12345", data={ - CONF_USERNAME: "user@email.com", - CONF_PASSWORD: "password", - CONF_CODE: "1234", + CONF_USER_ID: "12345", + CONF_TOKEN: "token123", }, ).add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=conf - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - -async def test_invalid_credentials(hass): - """Test that invalid credentials throws an error.""" - conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} - with patch( - "homeassistant.components.simplisafe.config_flow.get_api", - new=AsyncMock(side_effect=InvalidCredentialsError), + "homeassistant.components.simplisafe.async_setup_entry", return_value=True ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=conf + DOMAIN, context={"source": SOURCE_USER} ) - assert result["errors"] == {"base": "invalid_auth"} + 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={} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_invalid_credentials(hass, mock_async_from_auth): + """Test that invalid credentials show the correct error.""" + mock_async_from_auth.side_effect = AsyncMock(side_effect=InvalidCredentialsError) + + 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={} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_auth"} async def test_options_flow(hass): """Test config flow options.""" - conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} - entry = MockConfigEntry( domain=DOMAIN, unique_id="abcde12345", - data=conf, + data={CONF_USER_ID: "12345", CONF_TOKEN: "token456"}, options={CONF_CODE: "1234"}, ) entry.add_to_hass(hass) @@ -84,134 +110,114 @@ async def test_options_flow(hass): assert entry.options == {CONF_CODE: "4321"} -async def test_show_form(hass): - """Test that the form is served with no input.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - - -async def test_step_reauth(hass): - """Test that the reauth step works.""" +async def test_step_reauth_old_format(hass, mock_async_from_auth): + """Test the re-auth step with "old" config entries (those with user IDs).""" MockConfigEntry( domain=DOMAIN, unique_id="user@email.com", data={ CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password", - CONF_CODE: "1234", }, ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH}, - data={CONF_CODE: "1234", CONF_USERNAME: "user@email.com"}, + data={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}, ) - assert result["step_id"] == "reauth_confirm" - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "reauth_confirm" + assert result["step_id"] == "user" with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True - ), patch("homeassistant.components.simplisafe.config_flow.get_api"), patch( - "homeassistant.config_entries.ConfigEntries.async_reload" - ): + ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_PASSWORD: "password"} + result["flow_id"], user_input={} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 + [config_entry] = hass.config_entries.async_entries(DOMAIN) + assert config_entry.data == {CONF_USER_ID: "12345", CONF_TOKEN: "token123"} -async def test_step_user(hass): - """Test that the user step works (without MFA).""" - conf = { - CONF_USERNAME: "user@email.com", - CONF_PASSWORD: "password", - CONF_CODE: "1234", - } +async def test_step_reauth_new_format(hass, mock_async_from_auth): + """Test the re-auth step with "new" config entries (those with user IDs).""" + MockConfigEntry( + domain=DOMAIN, + unique_id="12345", + data={ + CONF_USER_ID: "12345", + CONF_TOKEN: "token123", + }, + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH}, + data={CONF_USER_ID: "12345", CONF_TOKEN: "token123"}, + ) + assert result["step_id"] == "user" with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True - ), patch( - "homeassistant.components.simplisafe.config_flow.get_api", new=AsyncMock() - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=conf - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "user@email.com" - assert result["data"] == { - CONF_USERNAME: "user@email.com", - CONF_PASSWORD: "password", - CONF_CODE: "1234", - } - - -async def test_step_user_mfa(hass): - """Test that the user step works when MFA is in the middle.""" - conf = { - CONF_USERNAME: "user@email.com", - CONF_PASSWORD: "password", - CONF_CODE: "1234", - } - - with patch( - "homeassistant.components.simplisafe.config_flow.get_api", - new=AsyncMock(side_effect=PendingAuthorizationError), - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=conf - ) - assert result["step_id"] == "mfa" - - with patch( - "homeassistant.components.simplisafe.config_flow.get_api", - new=AsyncMock(side_effect=PendingAuthorizationError), - ): - # Simulate the user pressing the MFA submit button without having clicked - # the link in the MFA email: + ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["step_id"] == "mfa" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + + assert len(hass.config_entries.async_entries()) == 1 + [config_entry] = hass.config_entries.async_entries(DOMAIN) + assert config_entry.data == {CONF_USER_ID: "12345", CONF_TOKEN: "token123"} + + +async def test_step_user(hass, mock_async_from_auth): + """Test the user step.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["step_id"] == "user" with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True - ), patch( - "homeassistant.components.simplisafe.config_flow.get_api", new=AsyncMock() - ): + ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "user@email.com" - assert result["data"] == { - CONF_USERNAME: "user@email.com", - CONF_PASSWORD: "password", - CONF_CODE: "1234", - } - - -async def test_unknown_error(hass): - """Test that an unknown error raises the correct error.""" - conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} - - with patch( - "homeassistant.components.simplisafe.config_flow.get_api", - new=AsyncMock(side_effect=SimplipyError), - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=conf + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} ) - assert result["errors"] == {"base": "unknown"} + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + assert len(hass.config_entries.async_entries()) == 1 + [config_entry] = hass.config_entries.async_entries(DOMAIN) + assert config_entry.data == {CONF_USER_ID: "12345", CONF_TOKEN: "token123"} + + +async def test_unknown_error(hass, mock_async_from_auth): + """Test that an unknown error shows ohe correct error.""" + mock_async_from_auth.side_effect = AsyncMock(side_effect=SimplipyError) + + 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={} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "unknown"} From 0fe5baa4255fe5a4f879b2ad031335d6ab199c69 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 19 Oct 2021 14:10:51 -0600 Subject: [PATCH 0554/1038] Rework RainMachine entity ID generation (#58055) --- homeassistant/components/rainmachine/__init__.py | 1 + homeassistant/components/rainmachine/sensor.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 0fee1259349..b8ea030cb71 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -344,6 +344,7 @@ class RainMachineEntity(CoordinatorEntity): "sw_version": controller.software_version, } self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._attr_name = f"{controller.name} {description.name}" # The colons are removed from the device MAC simply because that value # (unnecessarily) makes up the existing unique ID formula and we want to avoid # a breaking change: diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index beb893ddb74..6a1da223758 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -46,7 +46,7 @@ class RainMachineSensorEntityDescription( SENSOR_DESCRIPTIONS = ( RainMachineSensorEntityDescription( key=TYPE_FLOW_SENSOR_CLICK_M3, - name="Flow Sensor Clicks", + name="Flow Sensor Clicks per Cubic Meter", icon="mdi:water-pump", native_unit_of_measurement=f"clicks/{VOLUME_CUBIC_METERS}", entity_registry_enabled_default=False, From e65345900ff22959492c9c32e46ec17308389a68 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 19 Oct 2021 23:17:00 +0200 Subject: [PATCH 0555/1038] Add Human Presence Sensor (hps) device support to Tuya (#58054) --- .../components/tuya/binary_sensor.py | 9 ++++++++ homeassistant/components/tuya/const.py | 3 +++ homeassistant/components/tuya/number.py | 21 +++++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index 22db498a1c4..0a54aa74dfe 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -42,6 +42,15 @@ class TuyaBinarySensorEntityDescription(BinarySensorEntityDescription): # end up being a binary sensor. # https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { + # Human Presence Sensor + # https://developer.tuya.com/en/docs/iot/categoryhps?id=Kaiuz42yhn1hs + "hps": ( + TuyaBinarySensorEntityDescription( + key=DPCode.PRESENCE_STATE, + device_class=DEVICE_CLASS_MOTION, + on_value="presence", + ), + ), # Door Window Sensor # https://developer.tuya.com/en/docs/iot/s?id=K9gf48hm02l8m "mcs": ( diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 6c6f84214d7..bb98227f3b9 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -156,6 +156,7 @@ class DPCode(str, Enum): FAN_DIRECTION = "fan_direction" # Fan direction FAN_SPEED_ENUM = "fan_speed_enum" # Speed mode FAN_SPEED_PERCENT = "fan_speed_percent" # Stepless speed + FAR_DETECTION = "far_detection" FILTER_RESET = "filter_reset" # Filter (cartridge) reset HUMIDITY_CURRENT = "humidity_current" # Current humidity HUMIDITY_SET = "humidity_set" # Humidity setting @@ -166,6 +167,7 @@ class DPCode(str, Enum): MODE = "mode" # Working mode / Mode MOTION_SWITCH = "motion_switch" # Motion switch MUFFLING = "muffling" # Muffling + NEAR_DETECTION = "near_detection" PAUSE = "pause" PERCENT_CONTROL = "percent_control" PERCENT_CONTROL_2 = "percent_control_2" @@ -176,6 +178,7 @@ class DPCode(str, Enum): PIR = "pir" # Motion sensor POWDER_SET = "powder_set" # Powder POWER_GO = "power_go" + PRESENCE_STATE = "presence_state" PUMP_RESET = "pump_reset" # Water pump reset RECORD_SWITCH = "record_switch" # Recording switch SEEK = "seek" diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index 34325e63d98..0c5aa288f29 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -21,6 +21,27 @@ from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode # default instructions set of each category end up being a number. # https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { + # Human Presence Sensor + # https://developer.tuya.com/en/docs/iot/categoryhps?id=Kaiuz42yhn1hs + "hps": ( + NumberEntityDescription( + key=DPCode.SENSITIVITY, + name="Sensitivity", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + NumberEntityDescription( + key=DPCode.NEAR_DETECTION, + name="Near Detection", + icon="mdi:signal-distance-variant", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + NumberEntityDescription( + key=DPCode.FAR_DETECTION, + name="Far Detection", + icon="mdi:signal-distance-variant", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), # Coffee maker # https://developer.tuya.com/en/docs/iot/categorykfj?id=Kaiuz2p12pc7f "kfj": ( From 4f0886fd7d20fc8f4c26358a0d5381461b5171ad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 19 Oct 2021 11:40:35 -1000 Subject: [PATCH 0556/1038] Bump flux_led to 0.24.11 (#58020) --- .../components/flux_led/manifest.json | 33 ++++++++++++----- homeassistant/generated/dhcp.py | 36 ++++++++++++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 54 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 3019f07a1be..13ee80cb5e8 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.24.9"], + "requirements": ["flux_led==0.24.11"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", @@ -29,28 +29,43 @@ "hostname": "lwip*" }, { - "hostname": "zengge_06_*" + "hostname": "zengge_0[6789b]_*" }, { - "hostname": "zengge_07_*" + "hostname": "zengge_1[06789abc]_*" + }, + { + "hostname": "zengge_2[15]_*" }, { - "hostname": "zengge_21_*" + "hostname": "zengge_3[35]_*" }, { - "hostname": "zengge_33_*" + "hostname": "zengge_4[14]_*" }, { - "hostname": "zengge_35_*" + "hostname": "zengge_5[24]_*" + }, + { + "hostname": "zengge_62_*" + }, + { + "hostname": "zengge_81_*" }, { - "hostname": "zengge_41_*" + "hostname": "zengge_0[0e]_*" }, { - "hostname": "zengge_0e_*" + "hostname": "zengge_9[34567]_*" }, { - "hostname": "zengge_00_*" + "hostname": "zengge_a[123]_*" + }, + { + "hostname": "zengge_d1_*" + }, + { + "hostname": "zengge_e[12]_*" }, { "macaddress": "C82E47*", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 5f861f1a56e..1bb37e6c736 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -98,35 +98,55 @@ DHCP = [ }, { "domain": "flux_led", - "hostname": "zengge_06_*" + "hostname": "zengge_0[6789b]_*" }, { "domain": "flux_led", - "hostname": "zengge_07_*" + "hostname": "zengge_1[06789abc]_*" }, { "domain": "flux_led", - "hostname": "zengge_21_*" + "hostname": "zengge_2[15]_*" }, { "domain": "flux_led", - "hostname": "zengge_33_*" + "hostname": "zengge_3[35]_*" }, { "domain": "flux_led", - "hostname": "zengge_35_*" + "hostname": "zengge_4[14]_*" }, { "domain": "flux_led", - "hostname": "zengge_41_*" + "hostname": "zengge_5[24]_*" }, { "domain": "flux_led", - "hostname": "zengge_0e_*" + "hostname": "zengge_62_*" }, { "domain": "flux_led", - "hostname": "zengge_00_*" + "hostname": "zengge_81_*" + }, + { + "domain": "flux_led", + "hostname": "zengge_0[0e]_*" + }, + { + "domain": "flux_led", + "hostname": "zengge_9[34567]_*" + }, + { + "domain": "flux_led", + "hostname": "zengge_a[123]_*" + }, + { + "domain": "flux_led", + "hostname": "zengge_d1_*" + }, + { + "domain": "flux_led", + "hostname": "zengge_e[12]_*" }, { "domain": "flux_led", diff --git a/requirements_all.txt b/requirements_all.txt index fb58fe31336..23dd5a07c08 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -652,7 +652,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.9 +flux_led==0.24.11 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2b87d50d8c8..9a17ad55703 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -384,7 +384,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.9 +flux_led==0.24.11 # homeassistant.components.homekit fnvhash==0.1.0 From c6aa767be6be8f1f3e7ed2e21ae53dc8b83eac1d Mon Sep 17 00:00:00 2001 From: Tom Schneider Date: Wed, 20 Oct 2021 00:14:55 +0200 Subject: [PATCH 0557/1038] Add volume_up and volume_down to musiccast (#57919) --- .../components/yamaha_musiccast/manifest.json | 2 +- .../components/yamaha_musiccast/media_player.py | 11 ++++++++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/yamaha_musiccast/manifest.json b/homeassistant/components/yamaha_musiccast/manifest.json index f7751dfe859..0ace71dc7dd 100644 --- a/homeassistant/components/yamaha_musiccast/manifest.json +++ b/homeassistant/components/yamaha_musiccast/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yamaha_musiccast", "requirements": [ - "aiomusiccast==0.10.0" + "aiomusiccast==0.11.0" ], "ssdp": [ { diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index 758d39a8dfb..e76a707c7b9 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -33,6 +33,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( @@ -288,6 +289,14 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): await self.coordinator.musiccast.set_volume_level(self._zone_id, volume) self.async_write_ha_state() + async def async_volume_up(self): + """Turn volume up for media player.""" + await self.coordinator.musiccast.volume_up(self._zone_id) + + async def async_volume_down(self): + """Turn volume down for media player.""" + await self.coordinator.musiccast.volume_down(self._zone_id) + async def async_media_play(self): """Send play command.""" if self._is_netusb: @@ -460,7 +469,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): if ZoneFeature.POWER in zone.features: supported_features |= SUPPORT_TURN_ON | SUPPORT_TURN_OFF if ZoneFeature.VOLUME in zone.features: - supported_features |= SUPPORT_VOLUME_SET + supported_features |= SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP if ZoneFeature.MUTE in zone.features: supported_features |= SUPPORT_VOLUME_MUTE diff --git a/requirements_all.txt b/requirements_all.txt index 23dd5a07c08..9f4ac6b0c06 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -213,7 +213,7 @@ aiolyric==1.0.7 aiomodernforms==0.1.8 # homeassistant.components.yamaha_musiccast -aiomusiccast==0.10.0 +aiomusiccast==0.11.0 # homeassistant.components.nanoleaf aionanoleaf==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9a17ad55703..51315d6d3a0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -143,7 +143,7 @@ aiolyric==1.0.7 aiomodernforms==0.1.8 # homeassistant.components.yamaha_musiccast -aiomusiccast==0.10.0 +aiomusiccast==0.11.0 # homeassistant.components.nanoleaf aionanoleaf==0.0.3 From 518151fbe6bd7e15528a59fe0ec811d11061aa9b Mon Sep 17 00:00:00 2001 From: micha91 Date: Wed, 20 Oct 2021 00:18:08 +0200 Subject: [PATCH 0558/1038] Fix Yamaha MusicCast media_stop (#58024) --- homeassistant/components/yamaha_musiccast/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index e76a707c7b9..502a0b0c3f1 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -318,7 +318,7 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): async def async_media_stop(self): """Send stop command.""" if self._is_netusb: - await self.coordinator.musiccast.netusb_pause() + await self.coordinator.musiccast.netusb_stop() else: raise HomeAssistantError( "Service stop is not supported for non NetUSB sources." From 7a7f5ccc31aeccf776782bc8087d3708f11e775c Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 20 Oct 2021 00:12:17 +0000 Subject: [PATCH 0559/1038] [ci skip] Translation update --- .../components/abode/translations/bg.json | 7 +++++ .../advantage_air/translations/bg.json | 3 ++ .../components/airnow/translations/bg.json | 15 ++++++++++ .../components/apple_tv/translations/bg.json | 28 ++++++++++++++++++- .../components/aurora/translations/bg.json | 25 +++++++++++++++++ .../bmw_connected_drive/translations/bg.json | 7 +++++ .../components/bsblan/translations/bg.json | 4 ++- .../components/cloud/translations/bg.json | 3 +- .../components/energy/translations/da.json | 3 ++ .../fireservicerota/translations/bg.json | 28 +++++++++++++++++++ .../components/gree/translations/bg.json | 13 +++++++++ .../components/hassio/translations/bg.json | 8 ++++++ .../homeassistant/translations/bg.json | 7 +++++ .../components/hyperion/translations/bg.json | 9 ++++++ .../components/kulersky/translations/bg.json | 13 +++++++++ .../components/local_ip/translations/bg.json | 1 + .../logi_circle/translations/bg.json | 4 ++- .../components/lovelace/translations/bg.json | 9 ++++++ .../mobile_app/translations/bg.json | 5 ++++ .../motion_blinds/translations/bg.json | 15 ++++++++++ .../motion_blinds/translations/de.json | 17 +++++++++-- .../motion_blinds/translations/et.json | 17 +++++++++-- .../motion_blinds/translations/hu.json | 17 +++++++++-- .../motion_blinds/translations/ru.json | 17 +++++++++-- .../motion_blinds/translations/zh-Hant.json | 17 +++++++++-- .../components/neato/translations/bg.json | 12 +++++++- .../components/nest/translations/bg.json | 3 ++ .../ovo_energy/translations/bg.json | 10 +++++++ .../components/ozw/translations/bg.json | 1 + .../components/ps4/translations/bg.json | 1 + .../components/roku/translations/bg.json | 9 ++++++ .../simplisafe/translations/de.json | 11 +++++++- .../simplisafe/translations/en.json | 19 ++++++++++++- .../simplisafe/translations/et.json | 11 +++++++- .../simplisafe/translations/hu.json | 11 +++++++- .../simplisafe/translations/it.json | 11 +++++++- .../smartthings/translations/he.json | 14 +++++++--- .../components/solaredge/translations/bg.json | 7 +++++ .../srp_energy/translations/bg.json | 20 +++++++++++++ .../components/tibber/translations/bg.json | 3 ++ .../components/tuya/translations/bg.json | 3 ++ .../components/twinkly/translations/bg.json | 10 +++++++ .../components/vizio/translations/bg.json | 3 ++ .../components/withings/translations/bg.json | 3 ++ .../components/xbox/translations/bg.json | 16 +++++++++++ 45 files changed, 446 insertions(+), 24 deletions(-) create mode 100644 homeassistant/components/aurora/translations/bg.json create mode 100644 homeassistant/components/energy/translations/da.json create mode 100644 homeassistant/components/fireservicerota/translations/bg.json create mode 100644 homeassistant/components/gree/translations/bg.json create mode 100644 homeassistant/components/hassio/translations/bg.json create mode 100644 homeassistant/components/kulersky/translations/bg.json create mode 100644 homeassistant/components/lovelace/translations/bg.json create mode 100644 homeassistant/components/roku/translations/bg.json create mode 100644 homeassistant/components/srp_energy/translations/bg.json create mode 100644 homeassistant/components/twinkly/translations/bg.json create mode 100644 homeassistant/components/xbox/translations/bg.json diff --git a/homeassistant/components/abode/translations/bg.json b/homeassistant/components/abode/translations/bg.json index 7ed0fae081a..955ed18c82c 100644 --- a/homeassistant/components/abode/translations/bg.json +++ b/homeassistant/components/abode/translations/bg.json @@ -1,12 +1,19 @@ { "config": { "abort": { + "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", "single_instance_allowed": "\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 Abode." }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "Email" + } + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", diff --git a/homeassistant/components/advantage_air/translations/bg.json b/homeassistant/components/advantage_air/translations/bg.json index 9afe310830d..2293e4d4c53 100644 --- a/homeassistant/components/advantage_air/translations/bg.json +++ b/homeassistant/components/advantage_air/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, diff --git a/homeassistant/components/airnow/translations/bg.json b/homeassistant/components/airnow/translations/bg.json index 5d274ec2b73..11928153a09 100644 --- a/homeassistant/components/airnow/translations/bg.json +++ b/homeassistant/components/airnow/translations/bg.json @@ -1,7 +1,22 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "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_location": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0440\u0435\u0437\u0443\u043b\u0442\u0430\u0442\u0438 \u0437\u0430 \u0442\u043e\u0432\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447", + "latitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0448\u0438\u0440\u0438\u043d\u0430", + "longitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0434\u044a\u043b\u0436\u0438\u043d\u0430" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/bg.json b/homeassistant/components/apple_tv/translations/bg.json index b5195ba46a5..5a7ae474102 100644 --- a/homeassistant/components/apple_tv/translations/bg.json +++ b/homeassistant/components/apple_tv/translations/bg.json @@ -1,9 +1,35 @@ { "config": { + "abort": { + "already_configured_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "no_devices_found": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "error": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "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", + "no_devices_found": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "flow_title": "{name}", "step": { + "pair_with_pin": { + "data": { + "pin": "\u041f\u0418\u041d \u043a\u043e\u0434" + } + }, + "reconfigure": { + "title": "\u041f\u0440\u0435\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e" + }, "service_problem": { "title": "\u0414\u043e\u0431\u0430\u0432\u044f\u043d\u0435\u0442\u043e \u043d\u0430 \u0443\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u043d\u0435 \u0431\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, + "user": { + "data": { + "device_input": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + } } } - } + }, + "title": "Apple TV" } \ No newline at end of file diff --git a/homeassistant/components/aurora/translations/bg.json b/homeassistant/components/aurora/translations/bg.json new file mode 100644 index 00000000000..fea56662ef3 --- /dev/null +++ b/homeassistant/components/aurora/translations/bg.json @@ -0,0 +1,25 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "latitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0448\u0438\u0440\u0438\u043d\u0430", + "longitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0434\u044a\u043b\u0436\u0438\u043d\u0430", + "name": "\u0418\u043c\u0435" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "\u041f\u0440\u0430\u0433 (%)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/bg.json b/homeassistant/components/bmw_connected_drive/translations/bg.json index 67a484573aa..90901301faf 100644 --- a/homeassistant/components/bmw_connected_drive/translations/bg.json +++ b/homeassistant/components/bmw_connected_drive/translations/bg.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + }, + "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" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/bsblan/translations/bg.json b/homeassistant/components/bsblan/translations/bg.json index 6b82cd374e7..09a1668e142 100644 --- a/homeassistant/components/bsblan/translations/bg.json +++ b/homeassistant/components/bsblan/translations/bg.json @@ -11,7 +11,9 @@ "user": { "data": { "host": "\u0425\u043e\u0441\u0442", - "port": "\u041f\u043e\u0440\u0442" + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } } } diff --git a/homeassistant/components/cloud/translations/bg.json b/homeassistant/components/cloud/translations/bg.json index 957c2a49d5c..d6ab160fd29 100644 --- a/homeassistant/components/cloud/translations/bg.json +++ b/homeassistant/components/cloud/translations/bg.json @@ -1,7 +1,8 @@ { "system_health": { "info": { - "remote_server": "\u041e\u0442\u0434\u0430\u043b\u0435\u0447\u0435\u043d \u0441\u044a\u0440\u0432\u044a\u0440" + "remote_server": "\u041e\u0442\u0434\u0430\u043b\u0435\u0447\u0435\u043d \u0441\u044a\u0440\u0432\u044a\u0440", + "subscription_expiration": "\u0418\u0437\u0442\u0438\u0447\u0430\u043d\u0435 \u043d\u0430 \u0430\u0431\u043e\u043d\u0430\u043c\u0435\u043d\u0442\u0430" } } } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/da.json b/homeassistant/components/energy/translations/da.json new file mode 100644 index 00000000000..168ae4ae877 --- /dev/null +++ b/homeassistant/components/energy/translations/da.json @@ -0,0 +1,3 @@ +{ + "title": "Energi" +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/bg.json b/homeassistant/components/fireservicerota/translations/bg.json new file mode 100644 index 00000000000..22cc783d4e9 --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/bg.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u0430 \u0432\u0435\u0447\u0435 \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" + }, + "create_entry": { + "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u0435\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" + }, + "step": { + "reauth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "url": "\u0423\u0435\u0431\u0441\u0430\u0439\u0442", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gree/translations/bg.json b/homeassistant/components/gree/translations/bg.json new file mode 100644 index 00000000000..e7ed81d36f5 --- /dev/null +++ b/homeassistant/components/gree/translations/bg.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, + "step": { + "confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0442\u0430?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/bg.json b/homeassistant/components/hassio/translations/bg.json new file mode 100644 index 00000000000..960dc53b5ea --- /dev/null +++ b/homeassistant/components/hassio/translations/bg.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "disk_total": "\u0414\u0438\u0441\u043a \u043e\u0431\u0449\u043e", + "disk_used": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d \u0434\u0438\u0441\u043a" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/bg.json b/homeassistant/components/homeassistant/translations/bg.json index 7467c64f64a..dab7fd6426a 100644 --- a/homeassistant/components/homeassistant/translations/bg.json +++ b/homeassistant/components/homeassistant/translations/bg.json @@ -1,6 +1,13 @@ { "system_health": { "info": { + "arch": "\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u043d\u0430 CPU", + "docker": "Docker", + "hassio": "Supervisor", + "installation_type": "\u0422\u0438\u043f \u0438\u043d\u0441\u0442\u0430\u043b\u0430\u0446\u0438\u044f", + "os_version": "\u0412\u0435\u0440\u0441\u0438\u044f \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0430\u0442\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430", + "python_version": "\u0412\u0435\u0440\u0441\u0438\u044f \u043d\u0430 Python", + "timezone": "\u0427\u0430\u0441\u043e\u0432\u0430 \u0437\u043e\u043d\u0430", "user": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b", "version": "\u0412\u0435\u0440\u0441\u0438\u044f" } diff --git a/homeassistant/components/hyperion/translations/bg.json b/homeassistant/components/hyperion/translations/bg.json index 4983c9a14b2..38499f1fefa 100644 --- a/homeassistant/components/hyperion/translations/bg.json +++ b/homeassistant/components/hyperion/translations/bg.json @@ -1,8 +1,17 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "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\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { + "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" } } diff --git a/homeassistant/components/kulersky/translations/bg.json b/homeassistant/components/kulersky/translations/bg.json new file mode 100644 index 00000000000..e7ed81d36f5 --- /dev/null +++ b/homeassistant/components/kulersky/translations/bg.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, + "step": { + "confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0442\u0430?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/translations/bg.json b/homeassistant/components/local_ip/translations/bg.json index bbc8018aa4d..a1b6dd48088 100644 --- a/homeassistant/components/local_ip/translations/bg.json +++ b/homeassistant/components/local_ip/translations/bg.json @@ -5,6 +5,7 @@ }, "step": { "user": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0442\u0430?", "title": "\u041b\u043e\u043a\u0430\u043b\u0435\u043d IP \u0430\u0434\u0440\u0435\u0441" } } diff --git a/homeassistant/components/logi_circle/translations/bg.json b/homeassistant/components/logi_circle/translations/bg.json index 469532fdc73..c352cf88f3b 100644 --- a/homeassistant/components/logi_circle/translations/bg.json +++ b/homeassistant/components/logi_circle/translations/bg.json @@ -1,11 +1,13 @@ { "config": { "abort": { + "already_configured": "\u041f\u0440\u043e\u0444\u0438\u043b\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "external_error": "\u0413\u0440\u0435\u0448\u043a\u0430 \u0432\u044a\u0437\u043d\u0438\u043a\u043d\u0430 \u0432 \u0434\u0440\u0443\u0433 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u0435\u043d \u043f\u0440\u043e\u0446\u0435\u0441.", "external_setup": "Logi Circle \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d \u043e\u0442 \u0434\u0440\u0443\u0433 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u0435\u043d \u043f\u0440\u043e\u0446\u0435\u0441." }, "error": { - "follow_link": "\u041c\u043e\u043b\u044f, \u043f\u043e\u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0432\u0440\u044a\u0437\u043a\u0430\u0442\u0430 \u0438 \u0441\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u0439\u0442\u0435, \u043f\u0440\u0435\u0434\u0438 \u0434\u0430 \u043d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0418\u0437\u043f\u0440\u0430\u0449\u0430\u043d\u0435." + "follow_link": "\u041c\u043e\u043b\u044f, \u043f\u043e\u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0432\u0440\u044a\u0437\u043a\u0430\u0442\u0430 \u0438 \u0441\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u0439\u0442\u0435, \u043f\u0440\u0435\u0434\u0438 \u0434\u0430 \u043d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0418\u0437\u043f\u0440\u0430\u0449\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" }, "step": { "auth": { diff --git a/homeassistant/components/lovelace/translations/bg.json b/homeassistant/components/lovelace/translations/bg.json new file mode 100644 index 00000000000..3a9370b5486 --- /dev/null +++ b/homeassistant/components/lovelace/translations/bg.json @@ -0,0 +1,9 @@ +{ + "system_health": { + "info": { + "mode": "\u0420\u0435\u0436\u0438\u043c", + "resources": "\u0420\u0435\u0441\u0443\u0440\u0441\u0438", + "views": "\u0418\u0437\u0433\u043b\u0435\u0434\u0438" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/bg.json b/homeassistant/components/mobile_app/translations/bg.json index 6cbe826000c..60f7f5db406 100644 --- a/homeassistant/components/mobile_app/translations/bg.json +++ b/homeassistant/components/mobile_app/translations/bg.json @@ -8,5 +8,10 @@ "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \"\u041c\u043e\u0431\u0438\u043b\u043d\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\"?" } } + }, + "device_automation": { + "action_type": { + "notify": "\u0418\u0437\u043f\u0440\u0430\u0442\u0435\u0442\u0435 \u0438\u0437\u0432\u0435\u0441\u0442\u0438\u0435" + } } } \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/bg.json b/homeassistant/components/motion_blinds/translations/bg.json index 39f706036fd..53a8814ba2d 100644 --- a/homeassistant/components/motion_blinds/translations/bg.json +++ b/homeassistant/components/motion_blinds/translations/bg.json @@ -1,10 +1,25 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "connection_error": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { + "connect": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + } + }, "select": { "data": { "select_ip": "IP \u0430\u0434\u0440\u0435\u0441" } + }, + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447", + "host": "IP \u0430\u0434\u0440\u0435\u0441" + } } } } diff --git a/homeassistant/components/motion_blinds/translations/de.json b/homeassistant/components/motion_blinds/translations/de.json index 6c145898598..a4758c85d4b 100644 --- a/homeassistant/components/motion_blinds/translations/de.json +++ b/homeassistant/components/motion_blinds/translations/de.json @@ -6,13 +6,15 @@ "connection_error": "Verbindung fehlgeschlagen" }, "error": { - "discovery_error": "Motion-Gateway konnte nicht gefunden werden" + "discovery_error": "Motion-Gateway konnte nicht gefunden werden", + "invalid_interface": "Ung\u00fcltige Netzwerkschnittstelle" }, "flow_title": "Jalousien", "step": { "connect": { "data": { - "api_key": "API-Schl\u00fcssel" + "api_key": "API-Schl\u00fcssel", + "interface": "Die zu verwendende Netzwerkschnittstelle" }, "description": "Ein 16-Zeichen-API-Schl\u00fcssel wird ben\u00f6tigt, siehe https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key", "title": "Motion Jalousien" @@ -33,5 +35,16 @@ "title": "Jalousien" } } + }, + "options": { + "step": { + "init": { + "data": { + "wait_for_push": "Warten auf Multicast-Push bei Aktualisierung" + }, + "description": "Optionale Einstellungen angeben", + "title": "Motion Jalousien" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/et.json b/homeassistant/components/motion_blinds/translations/et.json index 5e585dec1a3..7091c402a2e 100644 --- a/homeassistant/components/motion_blinds/translations/et.json +++ b/homeassistant/components/motion_blinds/translations/et.json @@ -6,13 +6,15 @@ "connection_error": "\u00dchendamine nurjus" }, "error": { - "discovery_error": "Motion Gateway avastamine nurjus" + "discovery_error": "Motion Gateway avastamine nurjus", + "invalid_interface": "Sobimatu v\u00f5rguliides" }, "flow_title": "", "step": { "connect": { "data": { - "api_key": "API v\u00f5ti" + "api_key": "API v\u00f5ti", + "interface": "Kasutatav v\u00f5rguliides" }, "description": "On vaja 16-kohalist API-v\u00f5tit, juhiste saamiseks vaata https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key", "title": "" @@ -33,5 +35,16 @@ "title": "" } } + }, + "options": { + "step": { + "init": { + "data": { + "wait_for_push": "Oota multicast'i t\u00f5ukev\u00e4rskendust" + }, + "description": "Valikuliste s\u00e4tete m\u00e4\u00e4ramine", + "title": "Motion Blinds" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/hu.json b/homeassistant/components/motion_blinds/translations/hu.json index 32ff2dcc58e..e65c43fcf8a 100644 --- a/homeassistant/components/motion_blinds/translations/hu.json +++ b/homeassistant/components/motion_blinds/translations/hu.json @@ -6,13 +6,15 @@ "connection_error": "Sikertelen csatlakoz\u00e1s" }, "error": { - "discovery_error": "Nem siker\u00fclt felfedezni a Motion Gateway-t" + "discovery_error": "Nem siker\u00fclt felfedezni a Motion Gateway-t", + "invalid_interface": "\u00c9rv\u00e9nytelen h\u00e1l\u00f3zati interf\u00e9sz" }, "flow_title": "Mozg\u00f3 red\u0151ny", "step": { "connect": { "data": { - "api_key": "API kulcs" + "api_key": "API kulcs", + "interface": "A haszn\u00e1lni k\u00edv\u00e1nt h\u00e1l\u00f3zati interf\u00e9sz" }, "description": "Sz\u00fcks\u00e9ge lesz a 16 karakteres API kulcsra, \u00fatmutat\u00e1s\u00e9rt l\u00e1sd: https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key", "title": "Mozg\u00f3 red\u0151ny" @@ -33,5 +35,16 @@ "title": "Mozg\u00f3 red\u0151ny" } } + }, + "options": { + "step": { + "init": { + "data": { + "wait_for_push": "Multicast adatokra v\u00e1rakoz\u00e1s friss\u00edt\u00e9skor" + }, + "description": "Opcion\u00e1lis be\u00e1ll\u00edt\u00e1sok megad\u00e1sa", + "title": "Motion Blinds" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/ru.json b/homeassistant/components/motion_blinds/translations/ru.json index ae2d3229c20..83fe77ff9eb 100644 --- a/homeassistant/components/motion_blinds/translations/ru.json +++ b/homeassistant/components/motion_blinds/translations/ru.json @@ -6,13 +6,15 @@ "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, "error": { - "discovery_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c \u0448\u043b\u044e\u0437 Motion." + "discovery_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c \u0448\u043b\u044e\u0437 Motion.", + "invalid_interface": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0441\u0435\u0442\u0435\u0432\u043e\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441." }, "flow_title": "Motion Blinds", "step": { "connect": { "data": { - "api_key": "\u041a\u043b\u044e\u0447 API" + "api_key": "\u041a\u043b\u044e\u0447 API", + "interface": "\u0421\u0435\u0442\u0435\u0432\u043e\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441" }, "description": "\u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c 16-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key.", "title": "Motion Blinds" @@ -33,5 +35,16 @@ "title": "Motion Blinds" } } + }, + "options": { + "step": { + "init": { + "data": { + "wait_for_push": "\u041e\u0436\u0438\u0434\u0430\u0442\u044c \u043c\u043d\u043e\u0433\u043e\u0430\u0434\u0440\u0435\u0441\u043d\u043e\u0439 \u0440\u0430\u0441\u0441\u044b\u043b\u043a\u0438 \u043e\u0431 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0438" + }, + "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438", + "title": "Motion Blinds" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/zh-Hant.json b/homeassistant/components/motion_blinds/translations/zh-Hant.json index 1c538d7de14..f014a43f4ba 100644 --- a/homeassistant/components/motion_blinds/translations/zh-Hant.json +++ b/homeassistant/components/motion_blinds/translations/zh-Hant.json @@ -6,13 +6,15 @@ "connection_error": "\u9023\u7dda\u5931\u6557" }, "error": { - "discovery_error": "\u63a2\u7d22 Motion \u9598\u9053\u5668\u5931\u6557" + "discovery_error": "\u63a2\u7d22 Motion \u9598\u9053\u5668\u5931\u6557", + "invalid_interface": "\u7db2\u8def\u4ecb\u9762\u7121\u6548" }, "flow_title": "Motion Blinds", "step": { "connect": { "data": { - "api_key": "API \u5bc6\u9470" + "api_key": "API \u5bc6\u9470", + "interface": "\u4f7f\u7528\u7684\u7db2\u8def\u4ecb\u9762" }, "description": "\u5c07\u9700\u8981\u8f38\u5165 16 \u4f4d\u5b57\u5143 API \u5bc6\u9470\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key \u4ee5\u7372\u5f97\u7372\u53d6\u5bc6\u9470\u7684\u6559\u5b78\u3002", "title": "Motion Blinds" @@ -33,5 +35,16 @@ "title": "Motion Blinds" } } + }, + "options": { + "step": { + "init": { + "data": { + "wait_for_push": "\u7b49\u5019 Multicast \u63a8\u9001\u901a\u77e5" + }, + "description": "\u6307\u5b9a\u9078\u9805\u8a2d\u5b9a", + "title": "Motion Blinds" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/bg.json b/homeassistant/components/neato/translations/bg.json index d8f67f2185d..e0e4b9a410d 100644 --- a/homeassistant/components/neato/translations/bg.json +++ b/homeassistant/components/neato/translations/bg.json @@ -1,10 +1,20 @@ { "config": { "abort": { - "already_configured": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + "already_configured": "\u0412\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.", + "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" }, "create_entry": { "default": "\u0412\u0438\u0436\u0442\u0435 [Neato \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f]({docs_url})." + }, + "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" + }, + "reauth_confirm": { + "title": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0442\u0430?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/bg.json b/homeassistant/components/nest/translations/bg.json index 2509668a859..efa1378b81e 100644 --- a/homeassistant/components/nest/translations/bg.json +++ b/homeassistant/components/nest/translations/bg.json @@ -25,6 +25,9 @@ }, "description": "\u0417\u0430 \u0434\u0430 \u0441\u0432\u044a\u0440\u0436\u0435\u0442\u0435 \u043f\u0440\u043e\u0444\u0438\u043b\u0430 \u0441\u0438 \u0432 Nest, [\u043e\u0442\u043e\u0440\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u043f\u0440\u043e\u0444\u0438\u043b\u0430 \u0441\u0438]({url}). \n\n \u0421\u043b\u0435\u0434 \u043a\u0430\u0442\u043e \u0441\u0442\u0435 \u043e\u0442\u043e\u0440\u0438\u0437\u0438\u0440\u0430\u043d\u0435, \u043a\u043e\u043f\u0438\u0440\u0430\u0439\u0442\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u0438\u044f \u043f\u043e-\u0434\u043e\u043b\u0443 PIN \u043a\u043e\u0434.", "title": "\u0421\u0432\u044a\u0440\u0436\u0435\u0442\u0435 \u0412\u0430\u0448\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b \u0432 Nest" + }, + "reauth_confirm": { + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" } } } diff --git a/homeassistant/components/ovo_energy/translations/bg.json b/homeassistant/components/ovo_energy/translations/bg.json index 946b62a8690..16c1a3f9b0e 100644 --- a/homeassistant/components/ovo_energy/translations/bg.json +++ b/homeassistant/components/ovo_energy/translations/bg.json @@ -1,7 +1,17 @@ { "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" + }, + "flow_title": "{username}", + "step": { + "reauth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + } } } } \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/bg.json b/homeassistant/components/ozw/translations/bg.json index 1c6120581b0..6f032fa973a 100644 --- a/homeassistant/components/ozw/translations/bg.json +++ b/homeassistant/components/ozw/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." } } diff --git a/homeassistant/components/ps4/translations/bg.json b/homeassistant/components/ps4/translations/bg.json index 3bc3542d02e..8ed9242fc3e 100644 --- a/homeassistant/components/ps4/translations/bg.json +++ b/homeassistant/components/ps4/translations/bg.json @@ -7,6 +7,7 @@ "port_997_bind_error": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0440\u0435\u0437\u0435\u0440\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043f\u043e\u0440\u0442 997. \u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0447\u0435\u0442\u0435\u0442\u0435 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430](https://www.home-assistant.io/components/ps4/) \u0437\u0430 \u0434\u043e\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b\u043d\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f." }, "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "credential_timeout": "\u0412\u0440\u0435\u043c\u0435\u0442\u043e \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0443\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0437\u0430 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0438\u0437\u0442\u0435\u0447\u0435. \u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \"\u0418\u0437\u043f\u0440\u0430\u0449\u0430\u043d\u0435\" \u0437\u0430 \u0434\u0430 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0442\u0435.", "login_failed": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 PlayStation 4. \u041f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0434\u0430\u043b\u0438 \u0432\u044a\u0432\u0435\u0434\u0435\u043d\u0438\u044f PIN \u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u0435\u043d.", "no_ipaddress": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 IP \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0430 PlayStation 4, \u043a\u043e\u0439\u0442\u043e \u0438\u0441\u043a\u0430\u0442\u0435 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435." diff --git a/homeassistant/components/roku/translations/bg.json b/homeassistant/components/roku/translations/bg.json new file mode 100644 index 00000000000..ef4bfded40c --- /dev/null +++ b/homeassistant/components/roku/translations/bg.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "discovery_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name}?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/de.json b/homeassistant/components/simplisafe/translations/de.json index ae354b2138a..5ccd44c8379 100644 --- a/homeassistant/components/simplisafe/translations/de.json +++ b/homeassistant/components/simplisafe/translations/de.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Dieses SimpliSafe-Konto wird bereits verwendet.", - "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "wrong_account": "Die angegebenen Benutzeranmeldeinformationen stimmen nicht mit diesem SimpliSafe-Konto \u00fcberein." }, "error": { "identifier_exists": "Konto bereits registriert", @@ -11,6 +12,13 @@ "unknown": "Unerwarteter Fehler" }, "step": { + "input_auth_code": { + "data": { + "auth_code": "Autorisierungscode" + }, + "description": "Gib den Autorisierungscode von der URL der SimpliSafe-Webanwendung ein:", + "title": "Autorisierung abschlie\u00dfen" + }, "mfa": { "description": "Pr\u00fcfe deine E-Mail auf einen Link von SimpliSafe. Kehre nach der Verifizierung des Links hierher zur\u00fcck, um die Installation der Integration abzuschlie\u00dfen.", "title": "SimpliSafe Multi-Faktor-Authentifizierung" @@ -28,6 +36,7 @@ "password": "Passwort", "username": "E-Mail" }, + "description": "Ab 2021 hat SimpliSafe auf einen neuen Authentifizierungsmechanismus \u00fcber seine Web-App umgestellt. 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) liest, bevor du beginnst.\n\nWenn du bereit bist, klicke [hier]({url}), um die SimpliSafe-Webanwendung zu \u00f6ffnen und deine Anmeldedaten einzugeben. Wenn der Vorgang abgeschlossen ist, kehre hierher zur\u00fcck und klicke auf Senden.", "title": "Gib deine Informationen ein" } } diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index 69a25b1bdc2..66843f86d27 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -19,8 +19,25 @@ "description": "Input the authorization code from the SimpliSafe web app URL:", "title": "Finish Authorization" }, + "mfa": { + "description": "Check your email for a link from SimpliSafe. After verifying the link, return here to complete the installation of the integration.", + "title": "SimpliSafe Multi-Factor Authentication" + }, + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "Your access has expired or been revoked. Enter your password to re-link your account.", + "title": "Reauthenticate Integration" + }, "user": { - "description": "Starting in 2021, SimpliSafe has moved to a new authentication mechanism 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 click Submit." + "data": { + "code": "Code (used in Home Assistant UI)", + "password": "Password", + "username": "Email" + }, + "description": "Starting in 2021, SimpliSafe has moved to a new authentication mechanism 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 click Submit.", + "title": "Fill in your information." } } }, diff --git a/homeassistant/components/simplisafe/translations/et.json b/homeassistant/components/simplisafe/translations/et.json index 7eea7dc100d..7bbba70281a 100644 --- a/homeassistant/components/simplisafe/translations/et.json +++ b/homeassistant/components/simplisafe/translations/et.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "See SimpliSafe'i konto on juba kasutusel.", - "reauth_successful": "Taastuvastamine \u00f5nnestus" + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "wrong_account": "Esitatud kasutaja mandaadid ei \u00fchti selle SimpliSafe kontoga." }, "error": { "identifier_exists": "Konto on juba registreeritud", @@ -11,6 +12,13 @@ "unknown": "Tundmatu viga" }, "step": { + "input_auth_code": { + "data": { + "auth_code": "Tuvastuskood" + }, + "description": "Sisesta tuvastuskood SimpliSafe veebirakenduse URL-ist:", + "title": "L\u00f5peta tuvastamine" + }, "mfa": { "description": "Kontrolli oma e-posti: link SimpliSafe-lt. P\u00e4rast lingi kontrollimist naase siia, et viia l\u00f5pule sidumise installimine.", "title": "SimpliSafe mitmeastmeline autentimine" @@ -28,6 +36,7 @@ "password": "Salas\u00f5na", "username": "E-post" }, + "description": "Alates 2021. aastast on SimpliSafe oma veebirakenduse kaudu \u00fcle l\u00e4inud uuele tuvastusmehhanismile. Tehniliste piirangute t\u00f5ttu on selle protsessi l\u00f5pus k\u00e4sitsi tehtud samm; palun loe enne alustamist l\u00e4bi [dokumentatsioon] (http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code). \n\n Kui oled valmis, kl\u00f5psa SimpliSafe'i veebirakenduse avamiseks ja oma mandaadi sisestamiseks {url} Kui protsess on l\u00f5pule j\u00f5udnud, naase siia ja kl\u00f5psa nuppu Esita.", "title": "Sisesta oma teave." } } diff --git a/homeassistant/components/simplisafe/translations/hu.json b/homeassistant/components/simplisafe/translations/hu.json index ed0eb0b2212..5bb3d73ae42 100644 --- a/homeassistant/components/simplisafe/translations/hu.json +++ b/homeassistant/components/simplisafe/translations/hu.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Ez a SimpliSafe-fi\u00f3k m\u00e1r haszn\u00e1latban van.", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt", + "wrong_account": "A megadott felhaszn\u00e1l\u00f3i hiteles\u00edt\u0151 adatok nem j\u00f3k ehhez a SimpliSafe fi\u00f3khoz." }, "error": { "identifier_exists": "Fi\u00f3k m\u00e1r regisztr\u00e1lva van", @@ -11,6 +12,13 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "input_auth_code": { + "data": { + "auth_code": "Enged\u00e9lyez\u00e9si k\u00f3d" + }, + "description": "Adja meg a SimpliSafe webes alkalmaz\u00e1s URL-c\u00edm\u00e9n tal\u00e1lhat\u00f3 enged\u00e9lyez\u00e9si k\u00f3dot:", + "title": "Enged\u00e9lyez\u00e9s befejez\u00e9se" + }, "mfa": { "description": "Ellen\u0151rizze e-mailj\u00e9ben a SimpliSafe linkj\u00e9t. A link ellen\u0151rz\u00e9se ut\u00e1n t\u00e9rjen vissza ide, \u00e9s fejezze be az integr\u00e1ci\u00f3 telep\u00edt\u00e9s\u00e9t.", "title": "SimpliSafe t\u00f6bbt\u00e9nyez\u0151s hiteles\u00edt\u00e9s" @@ -28,6 +36,7 @@ "password": "Jelsz\u00f3", "username": "E-mail" }, + "description": "2021-t\u0151l kezd\u0151d\u0151en a SimpliSafe egy \u00faj hiteles\u00edt\u00e9si mechanizmusra v\u00e1ltott a webalkalmaz\u00e1son kereszt\u00fcl. A technikai korl\u00e1toz\u00e1sok miatt a folyamat v\u00e9g\u00e9n van egy k\u00e9zi l\u00e9p\u00e9s; k\u00e9rj\u00fck, indul\u00e1s 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}) SimpliSafe webalkalmaz\u00e1s megnyit\u00e1s\u00e1hoz \u00e9s a hiteles\u00edt\u0151 adatok bevitel\u00e9hez. Amikor a folyamat befejez\u0151d\u00f6tt, t\u00e9rjen vissza ide, \u00e9s kattintson a K\u00fcld\u00e9s gombra.", "title": "T\u00f6ltse ki az adatait" } } diff --git a/homeassistant/components/simplisafe/translations/it.json b/homeassistant/components/simplisafe/translations/it.json index 37671d75917..445e61d835b 100644 --- a/homeassistant/components/simplisafe/translations/it.json +++ b/homeassistant/components/simplisafe/translations/it.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Questo account SimpliSafe \u00e8 gi\u00e0 in uso.", - "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "wrong_account": "Le credenziali utente fornite non corrispondono a questo account SimpliSafe." }, "error": { "identifier_exists": "Account gi\u00e0 registrato", @@ -11,6 +12,13 @@ "unknown": "Errore imprevisto" }, "step": { + "input_auth_code": { + "data": { + "auth_code": "Codice di autorizzazione" + }, + "description": "Immettere il codice di autorizzazione dall'URL dell'app web SimpliSafe:", + "title": "Completa l'autorizzazione" + }, "mfa": { "description": "Controlla la tua e-mail per trovare un link da SimpliSafe. Dopo aver verificato il link, torna qui per completare l'installazione dell'integrazione.", "title": "Autenticazione a pi\u00f9 fattori (MFA) SimpliSafe " @@ -28,6 +36,7 @@ "password": "Password", "username": "E-mail" }, + "description": "A partire dal 2021, SimpliSafe \u00e8 passato a un nuovo meccanismo di autenticazione tramite la sua app web. A causa di limitazioni tecniche, alla fine di questo processo \u00e8 previsto un passaggio manuale; assicurati di leggere la [documentazione](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) prima di iniziare. \n\nQuando sei pronto, fai clic su [qui]({url}) per aprire l'app Web SimpliSafe e inserire le tue credenziali. Al termine del processo, torna qui e fai clic su Invia.", "title": "Inserisci le tue informazioni." } } diff --git a/homeassistant/components/smartthings/translations/he.json b/homeassistant/components/smartthings/translations/he.json index db5bc91bf7b..8b7c47304ff 100644 --- a/homeassistant/components/smartthings/translations/he.json +++ b/homeassistant/components/smartthings/translations/he.json @@ -1,25 +1,31 @@ { "config": { + "abort": { + "invalid_webhook_url": "\u05ea\u05e6\u05d5\u05e8\u05ea Home Assistant \u05d0\u05d9\u05e0\u05d4 \u05de\u05d5\u05d2\u05d3\u05e8\u05ea \u05db\u05e8\u05d0\u05d5\u05d9 \u05dc\u05e7\u05d1\u05dc\u05ea \u05e2\u05d3\u05db\u05d5\u05e0\u05d9\u05dd \u05de-SmartThings. \u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d0\u05ea\u05e8 \u05e9\u05dc webhook \u05d0\u05d9\u05e0\u05d4 \u05d7\u05d5\u05e7\u05d9\u05ea:\n> {webhook_url}\n\n\u05e0\u05d0 \u05dc\u05e2\u05d3\u05db\u05df \u05d0\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05dc\u05e4\u05d9 [\u05d4\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea]({component_url}), \u05dc\u05d0\u05d7\u05e8 \u05de\u05db\u05df \u05dc\u05d4\u05e4\u05e2\u05d9\u05dc \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea Home Assistant \u05d5\u05dc\u05e0\u05e1\u05d5\u05ea \u05e9\u05d5\u05d1.", + "no_available_locations": "\u05d0\u05d9\u05df \u05de\u05d9\u05e7\u05d5\u05de\u05d9 SmartThings \u05d6\u05de\u05d9\u05e0\u05d9\u05dd \u05dc\u05d4\u05ea\u05e7\u05e0\u05d4 \u05d1-Home Assistant." + }, "error": { "app_setup_error": "\u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea SmartApp. \u05e0\u05d0 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1.", "token_forbidden": "\u05dc\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d0\u05d9\u05df \u05d0\u05ea \u05d8\u05d5\u05d5\u05d7\u05d9 OAuth \u05d4\u05d3\u05e8\u05d5\u05e9\u05d9\u05dd.", "token_invalid_format": "\u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d7\u05d9\u05d9\u05d1 \u05dc\u05d4\u05d9\u05d5\u05ea \u05d1\u05e4\u05d5\u05e8\u05de\u05d8 UID / GUID", "token_unauthorized": "\u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d0\u05d9\u05e0\u05d5 \u05d7\u05d5\u05e7\u05d9 \u05d0\u05d5 \u05d0\u05d9\u05e0\u05d5 \u05de\u05d5\u05e8\u05e9\u05d4 \u05e2\u05d5\u05d3.", - "webhook_error": "SmartThings \u05dc\u05d0 \u05d4\u05e6\u05dc\u05d9\u05d7 \u05dc\u05d0\u05de\u05ea \u05d0\u05ea \u05e0\u05e7\u05d5\u05d3\u05ea \u05d4\u05e7\u05e6\u05d4 \u05e9\u05d4\u05d5\u05d2\u05d3\u05e8\u05d4 \u05d1- `base_url`. \u05e2\u05d9\u05d9\u05df \u05d1\u05d3\u05e8\u05d9\u05e9\u05d5\u05ea \u05d4\u05e8\u05db\u05d9\u05d1." + "webhook_error": "SmartThings \u05dc\u05d0 \u05d4\u05e6\u05dc\u05d9\u05d7 \u05dc\u05d0\u05de\u05ea \u05d0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d0\u05ea\u05e8 \u05e9\u05dc webhook. \u05e0\u05d0 \u05dc\u05d5\u05d5\u05d3\u05d0 \u05e9\u05db\u05ea\u05d5\u05d1\u05ea \u05d4-webhook \u05e0\u05d2\u05d9\u05e9\u05d4 \u05de\u05d4\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05d5\u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1." }, "step": { "pat": { "data": { "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4" - } + }, + "description": "\u05e0\u05d0 \u05dc\u05d4\u05d6\u05d9\u05df SmartThings [\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05d0\u05d9\u05e9\u05d9\u05ea]({token_url}) \u05e9\u05e0\u05d5\u05e6\u05e8 \u05dc\u05e4\u05d9 [\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea]({component_url}). \u05e4\u05e2\u05d5\u05dc\u05d4 \u05d6\u05d5 \u05ea\u05e9\u05de\u05e9 \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05d4\u05e9\u05d9\u05dc\u05d5\u05d1 \u05e9\u05dc Home Assistant \u05d1\u05d7\u05e9\u05d1\u05d5\u05df SmartThings \u05e9\u05dc\u05da." }, "select_location": { "data": { "location_id": "\u05de\u05d9\u05e7\u05d5\u05dd" - } + }, + "description": "\u05e0\u05d0 \u05dc\u05d1\u05d7\u05d5\u05e8 \u05d0\u05ea \u05de\u05d9\u05e7\u05d5\u05dd \u05d4-SmartThings \u05e9\u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d5\u05e1\u05d9\u05e3 \u05dc-Home Assistant. \u05dc\u05d0\u05d7\u05e8 \u05de\u05db\u05df \u05d9\u05e4\u05ea\u05d7 \u05d7\u05dc\u05d5\u05df \u05d7\u05d3\u05e9 \u05d5\u05e0\u05d1\u05e7\u05e9 \u05de\u05de\u05da \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05d5\u05dc\u05d0\u05e9\u05e8 \u05d4\u05ea\u05e7\u05e0\u05d4 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1 Home Assistant \u05d1\u05de\u05d9\u05e7\u05d5\u05dd \u05e9\u05e0\u05d1\u05d7\u05e8." }, "user": { - "description": "SmartThings \u05d9\u05d5\u05d2\u05d3\u05e8 \u05dc\u05e9\u05dc\u05d5\u05d7 \u05e2\u05d3\u05db\u05d5\u05e0\u05d9 \u05d3\u05d7\u05d9\u05e4\u05d4 \u05dc-Home Assistant \u05d1:\n> {webhook_url}\n\n\u05d0\u05dd \u05d4\u05d3\u05d1\u05e8 \u05d0\u05d9\u05e0\u05d5 \u05e0\u05db\u05d5\u05df, \u05e0\u05d0 \u05dc\u05e2\u05d3\u05db\u05df \u05d0\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4, \u05d5\u05dc\u05d4\u05e4\u05e2\u05d9\u05dc \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea Home Assistant \u05d5\u05dc\u05e0\u05e1\u05d5\u05ea \u05e9\u05d5\u05d1.", + "description": "SmartThings \u05d9\u05d5\u05d2\u05d3\u05e8 \u05dc\u05e9\u05dc\u05d5\u05d7 \u05e2\u05d3\u05db\u05d5\u05e0\u05d9 \u05d3\u05d7\u05d9\u05e4\u05d4 \u05dc-Home Assistant \u05d1\u05db\u05ea\u05d5\u05d1\u05ea:\n> {webhook_url}\n\n\u05d0\u05dd \u05d4\u05d3\u05d1\u05e8 \u05d0\u05d9\u05e0\u05d5 \u05e0\u05db\u05d5\u05df, \u05e0\u05d0 \u05dc\u05e2\u05d3\u05db\u05df \u05d0\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4, \u05d5\u05dc\u05d4\u05e4\u05e2\u05d9\u05dc \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea Home Assistant \u05d5\u05dc\u05e0\u05e1\u05d5\u05ea \u05e9\u05d5\u05d1.", "title": "\u05d0\u05d9\u05e9\u05d5\u05e8 \u05e7\u05d9\u05e9\u05d5\u05e8 \u05dc\u05d4\u05ea\u05e7\u05e9\u05e8\u05d5\u05ea \u05d7\u05d6\u05e8\u05d4" } } diff --git a/homeassistant/components/solaredge/translations/bg.json b/homeassistant/components/solaredge/translations/bg.json index 6871eb6210e..661d5ce05a3 100644 --- a/homeassistant/components/solaredge/translations/bg.json +++ b/homeassistant/components/solaredge/translations/bg.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "invalid_api_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/srp_energy/translations/bg.json b/homeassistant/components/srp_energy/translations/bg.json new file mode 100644 index 00000000000..10a7388a24c --- /dev/null +++ b/homeassistant/components/srp_energy/translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tibber/translations/bg.json b/homeassistant/components/tibber/translations/bg.json index 80a7cc489a9..24a19d53d9d 100644 --- a/homeassistant/components/tibber/translations/bg.json +++ b/homeassistant/components/tibber/translations/bg.json @@ -2,6 +2,9 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" } } } \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/bg.json b/homeassistant/components/tuya/translations/bg.json index 4be356255bd..7a393100351 100644 --- a/homeassistant/components/tuya/translations/bg.json +++ b/homeassistant/components/tuya/translations/bg.json @@ -35,6 +35,9 @@ } }, "options": { + "abort": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "error": { "dev_not_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u043e" }, diff --git a/homeassistant/components/twinkly/translations/bg.json b/homeassistant/components/twinkly/translations/bg.json new file mode 100644 index 00000000000..cd6031ffaf0 --- /dev/null +++ b/homeassistant/components/twinkly/translations/bg.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "device_exists": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/translations/bg.json b/homeassistant/components/vizio/translations/bg.json index a051d6ca487..d10f511e3b3 100644 --- a/homeassistant/components/vizio/translations/bg.json +++ b/homeassistant/components/vizio/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/withings/translations/bg.json b/homeassistant/components/withings/translations/bg.json index de34abbeed0..959b2b99066 100644 --- a/homeassistant/components/withings/translations/bg.json +++ b/homeassistant/components/withings/translations/bg.json @@ -3,6 +3,9 @@ "create_entry": { "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u043d\u0435 \u0441 Withings \u0437\u0430 \u0438\u0437\u0431\u0440\u0430\u043d\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b." }, + "error": { + "already_configured": "\u041f\u0440\u043e\u0444\u0438\u043b\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + }, "step": { "profile": { "data": { diff --git a/homeassistant/components/xbox/translations/bg.json b/homeassistant/components/xbox/translations/bg.json new file mode 100644 index 00000000000..9b3005a8289 --- /dev/null +++ b/homeassistant/components/xbox/translations/bg.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "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.", + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, + "create_entry": { + "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u0435\u043d" + }, + "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" + } + } + } +} \ No newline at end of file From cafb3067cec331108825b6c20995be2fd4319951 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 19 Oct 2021 21:02:02 -1000 Subject: [PATCH 0560/1038] Bump flux_led to 0.24.12 (#58071) --- 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 13ee80cb5e8..dfe166e65cf 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.24.11"], + "requirements": ["flux_led==0.24.12"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 9f4ac6b0c06..fb1681629c3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -652,7 +652,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.11 +flux_led==0.24.12 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 51315d6d3a0..a306146b608 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -384,7 +384,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.11 +flux_led==0.24.12 # homeassistant.components.homekit fnvhash==0.1.0 From 704929ddd0d7d6a463ff0761eb2a569baa4b9711 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 20 Oct 2021 09:10:32 +0200 Subject: [PATCH 0561/1038] Remove legacy tests in onewire (#58075) Co-authored-by: epenet --- tests/components/onewire/test_sensor.py | 42 ++----------------------- 1 file changed, 2 insertions(+), 40 deletions(-) diff --git a/tests/components/onewire/test_sensor.py b/tests/components/onewire/test_sensor.py index df8654f9e1e..8b229d1a743 100644 --- a/tests/components/onewire/test_sensor.py +++ b/tests/components/onewire/test_sensor.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch from pyownet.protocol import Error as ProtocolError import pytest -from homeassistant.components.onewire.const import DEFAULT_SYSBUS_MOUNT_DIR, DOMAIN +from homeassistant.components.onewire.const import DOMAIN from homeassistant.components.sensor import ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -17,7 +17,6 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, ) from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from . import setup_owproxy_mock_devices, setup_sysbus_mock_devices from .const import ( @@ -30,7 +29,7 @@ from .const import ( MOCK_SYSBUS_DEVICES, ) -from tests.common import assert_setup_component, mock_device_registry, mock_registry +from tests.common import mock_device_registry, mock_registry MOCK_COUPLERS = { key: value for (key, value) in MOCK_OWPROXY_DEVICES.items() if "branches" in value @@ -44,43 +43,6 @@ def override_platforms(): yield -async def test_setup_minimum(hass: HomeAssistant): - """Test old platform setup with minimum configuration.""" - config = {"sensor": {"platform": "onewire"}} - with assert_setup_component(1, "sensor"): - assert await async_setup_component(hass, SENSOR_DOMAIN, config) - await hass.async_block_till_done() - - -async def test_setup_sysbus(hass: HomeAssistant): - """Test old platform setup with SysBus configuration.""" - config = { - "sensor": { - "platform": "onewire", - "mount_dir": DEFAULT_SYSBUS_MOUNT_DIR, - } - } - with assert_setup_component(1, "sensor"): - assert await async_setup_component(hass, SENSOR_DOMAIN, config) - await hass.async_block_till_done() - - -async def test_setup_owserver(hass: HomeAssistant): - """Test old platform setup with OWServer configuration.""" - config = {"sensor": {"platform": "onewire", "host": "localhost"}} - with assert_setup_component(1, "sensor"): - assert await async_setup_component(hass, SENSOR_DOMAIN, config) - await hass.async_block_till_done() - - -async def test_setup_owserver_with_port(hass: HomeAssistant): - """Test old platform setup with OWServer configuration.""" - config = {"sensor": {"platform": "onewire", "host": "localhost", "port": "1234"}} - with assert_setup_component(1, "sensor"): - assert await async_setup_component(hass, SENSOR_DOMAIN, config) - await hass.async_block_till_done() - - @pytest.mark.parametrize("device_id", ["1F.111111111111"], indirect=True) async def test_sensors_on_owserver_coupler( hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock, device_id: str From edefa9f4f4647ee1ab63094b0fb065266f2d849c Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 20 Oct 2021 09:11:17 +0200 Subject: [PATCH 0562/1038] Assign entity category diagnostics to deCONZ battery sensors (#58077) --- homeassistant/components/deconz/sensor.py | 2 ++ tests/components/deconz/test_sensor.py | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index aed4d2df7ee..f9d177523f9 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -33,6 +33,7 @@ from homeassistant.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, + ENTITY_CATEGORY_DIAGNOSTIC, LIGHT_LUX, PERCENTAGE, POWER_WATT, @@ -73,6 +74,7 @@ ENTITY_DESCRIPTIONS = { device_class=DEVICE_CLASS_BATTERY, state_class=STATE_CLASS_MEASUREMENT, native_unit_of_measurement=PERCENTAGE, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), Consumption: SensorEntityDescription( key="consumption", diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 624a1bec7ff..5f9762c339b 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -14,6 +14,7 @@ from homeassistant.const import ( DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, STATE_UNAVAILABLE, ) from homeassistant.helpers import entity_registry as er @@ -92,6 +93,8 @@ async def test_sensors(hass, aioclient_mock, mock_deconz_websocket): assert len(hass.states.async_all()) == 6 + ent_reg = er.async_get(hass) + light_level_sensor = hass.states.get("sensor.light_level_sensor") assert light_level_sensor.state == "999.8" assert light_level_sensor.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_ILLUMINANCE @@ -109,6 +112,10 @@ async def test_sensors(hass, aioclient_mock, mock_deconz_websocket): switch_2_battery_level = hass.states.get("sensor.switch_2_battery_level") assert switch_2_battery_level.state == "100" assert switch_2_battery_level.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_BATTERY + assert ( + ent_reg.async_get("sensor.switch_2_battery_level").entity_category + == ENTITY_CATEGORY_DIAGNOSTIC + ) assert not hass.states.get("sensor.daylight_sensor") From 5f37fffcd1c09c4717756a070f987d0cbd7e4a24 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 20 Oct 2021 09:44:20 +0200 Subject: [PATCH 0563/1038] Bump pychromecast to 9.3.1 (#58035) --- homeassistant/components/cast/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 89c13cf04bd..e74f0840a6c 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==9.3.0"], + "requirements": ["pychromecast==9.3.1"], "after_dependencies": [ "cloud", "http", diff --git a/requirements_all.txt b/requirements_all.txt index fb1681629c3..12034253ce8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1384,7 +1384,7 @@ pycfdns==1.2.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==9.3.0 +pychromecast==9.3.1 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a306146b608..b95685930c2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -823,7 +823,7 @@ pybotvac==0.0.22 pycfdns==1.2.1 # homeassistant.components.cast -pychromecast==9.3.0 +pychromecast==9.3.1 # homeassistant.components.climacell pyclimacell==0.18.2 From b8cf6513d904279a8c0f9bfa1b7b327a9f92bf9b Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 20 Oct 2021 10:59:07 +0200 Subject: [PATCH 0564/1038] Address late review of unifi (#58080) * Fix late comment from 57570 * Remove other references to legacy ways --- homeassistant/components/unifi/__init__.py | 3 ++- homeassistant/components/unifi/controller.py | 4 ++-- homeassistant/components/unifi/device_tracker.py | 4 ++-- homeassistant/components/unifi/services.py | 3 ++- homeassistant/components/unifi/unifi_entity_base.py | 5 +++-- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 03816c03df7..e7364e6665f 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -1,6 +1,7 @@ """Integration to UniFi controllers and its various features.""" from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from .const import ( @@ -57,7 +58,7 @@ async def async_setup_entry(hass, config_entry): if controller.mac is None: return True - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, connections={(CONNECTION_NETWORK_MAC, controller.mac)}, diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index cea17e4e54c..b1ebbbe3475 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -39,7 +39,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import aiohttp_client +from homeassistant.helpers import aiohttp_client, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_registry import async_entries_for_config_entry from homeassistant.helpers.event import async_track_time_interval @@ -333,7 +333,7 @@ class UniFiController: self._site_role = description[0]["site_role"] # Restore clients that are not a part of active clients list. - entity_registry = await self.hass.helpers.entity_registry.async_get_registry() + entity_registry = er.async_get(self.hass) for entry in async_entries_for_config_entry( entity_registry, self.config_entry.entry_id ): diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index faf51e6c853..b6e14717484 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -19,6 +19,7 @@ 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.core import callback +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.dt as dt_util @@ -417,8 +418,7 @@ class UniFiDeviceTracker(UniFiBase, ScannerEntity): async def async_update_device_registry(self) -> None: """Update device registry.""" - device_registry = await self.hass.helpers.device_registry.async_get_registry() - + device_registry = dr.async_get(self.hass) device_registry.async_get_or_create( config_entry_id=self.controller.config_entry.entry_id, **self.device_info ) diff --git a/homeassistant/components/unifi/services.py b/homeassistant/components/unifi/services.py index 10d297df883..17e894df913 100644 --- a/homeassistant/components/unifi/services.py +++ b/homeassistant/components/unifi/services.py @@ -4,6 +4,7 @@ import voluptuous as vol from homeassistant.const import ATTR_DEVICE_ID from homeassistant.core import callback +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from .const import DOMAIN as UNIFI_DOMAIN @@ -53,7 +54,7 @@ def async_unload_services(hass) -> None: async def async_reconnect_client(hass, data) -> None: """Try to get wireless client to reconnect to Wi-Fi.""" - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) device_entry = device_registry.async_get(data[ATTR_DEVICE_ID]) mac = "" diff --git a/homeassistant/components/unifi/unifi_entity_base.py b/homeassistant/components/unifi/unifi_entity_base.py index 9d2d8071fca..e3b6e4f9970 100644 --- a/homeassistant/components/unifi/unifi_entity_base.py +++ b/homeassistant/components/unifi/unifi_entity_base.py @@ -3,6 +3,7 @@ import logging from typing import Any from homeassistant.core import callback +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_registry import async_entries_for_device @@ -89,13 +90,13 @@ class UniFiBase(Entity): if self.key not in keys: return - entity_registry = await self.hass.helpers.entity_registry.async_get_registry() + entity_registry = er.async_get(self.hass) entity_entry = entity_registry.async_get(self.entity_id) if not entity_entry: await self.async_remove(force_remove=True) return - device_registry = await self.hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(self.hass) device_entry = device_registry.async_get(entity_entry.device_id) if not device_entry: entity_registry.async_remove(self.entity_id) From 25f4f2d86eb0eee8e456dd6311a90bcb6d76634d Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 20 Oct 2021 11:16:28 +0200 Subject: [PATCH 0565/1038] Don't use deprecated methods of retrieving registries in deCONZ (#58081) --- homeassistant/components/deconz/deconz_event.py | 5 ++--- homeassistant/components/deconz/device_trigger.py | 7 ++++--- homeassistant/components/deconz/gateway.py | 10 +++++++--- homeassistant/components/deconz/services.py | 14 +++++++------- homeassistant/components/deconz/switch.py | 3 ++- 5 files changed, 22 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index b04e393103f..a0470c8ac32 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -17,6 +17,7 @@ from homeassistant.const import ( CONF_XY, ) from homeassistant.core import callback +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import slugify @@ -142,9 +143,7 @@ class DeconzEvent(DeconzBase): if not self.device_info: return - device_registry = ( - await self.gateway.hass.helpers.device_registry.async_get_registry() - ) + device_registry = dr.async_get(self.gateway.hass) entry = device_registry.async_get_or_create( config_entry_id=self.gateway.config_entry.entry_id, **self.device_info diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 8234ed81aed..d1abbed0928 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -14,6 +14,7 @@ from homeassistant.const import ( CONF_TYPE, CONF_UNIQUE_ID, ) +from homeassistant.helpers import device_registry as dr from . import DOMAIN from .deconz_event import CONF_DECONZ_EVENT, CONF_GESTURE @@ -628,7 +629,7 @@ async def async_validate_trigger_config(hass, config): """Validate config.""" config = TRIGGER_SCHEMA(config) - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) device = device_registry.async_get(config[CONF_DEVICE_ID]) trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) @@ -650,7 +651,7 @@ async def async_validate_trigger_config(hass, config): async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) device = device_registry.async_get(config[CONF_DEVICE_ID]) trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) @@ -684,7 +685,7 @@ async def async_get_triggers(hass, device_id): Retrieve the deconz event object matching device entry. Generate device trigger list. """ - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) device = device_registry.async_get(device_id) if device.model not in REMOTES: diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 17c927b300e..6096edabb37 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -7,7 +7,11 @@ from pydeconz import DeconzSession, errors, group, light, sensor from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import aiohttp_client +from homeassistant.helpers import ( + aiohttp_client, + device_registry as dr, + entity_registry as er, +) from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -136,7 +140,7 @@ class DeconzGateway: async def async_update_device_registry(self) -> None: """Update device registry.""" - device_registry = await self.hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(self.hass) # Host device device_registry.async_get_or_create( @@ -218,7 +222,7 @@ class DeconzGateway: else: deconz_ids += [group.deconz_id for group in self.api.groups.values()] - entity_registry = await self.hass.helpers.entity_registry.async_get_registry() + entity_registry = er.async_get(self.hass) for entity_id, deconz_id in self.deconz_ids.items(): if deconz_id in deconz_ids and entity_registry.async_is_registered( diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index 88006a851f2..535dd9807fb 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -1,12 +1,14 @@ """deCONZ services.""" -import asyncio - from pydeconz.utils import normalize_bridge_id import voluptuous as vol from homeassistant.core import callback -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + entity_registry as er, +) from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity_registry import ( async_entries_for_config_entry, @@ -143,10 +145,8 @@ async def async_refresh_devices_service(gateway): async def async_remove_orphaned_entries_service(gateway): """Remove orphaned deCONZ entries from device and entity registries.""" - device_registry, entity_registry = await asyncio.gather( - gateway.hass.helpers.device_registry.async_get_registry(), - gateway.hass.helpers.entity_registry.async_get_registry(), - ) + device_registry = dr.async_get(gateway.hass) + entity_registry = er.async_get(gateway.hass) entity_entries = async_entries_for_config_entry( entity_registry, gateway.config_entry.entry_id diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index a00def33b72..39489fe1fc3 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -4,6 +4,7 @@ from pydeconz.light import Siren from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.core import callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DOMAIN as DECONZ_DOMAIN, POWER_PLUGS @@ -19,7 +20,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway = get_gateway_from_config_entry(hass, config_entry) gateway.entities[DOMAIN] = set() - entity_registry = await hass.helpers.entity_registry.async_get_registry() + entity_registry = er.async_get(hass) # Siren platform replacing sirens in switch platform added in 2021.10 for light in gateway.api.lights.values(): From 008b784fc567b1cfde47063eb155ea602238e690 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 20 Oct 2021 11:23:24 +0200 Subject: [PATCH 0566/1038] Add deCONZ number config entity for Hue motion sensor delay (#58076) * First working draft of number platform * Replace duration with delay for Hue motion sensors Improve tests * Bump dependency to v85 * Use constant for entity category * Use type rather than using __class__ * Fix unique ID --- homeassistant/components/deconz/const.py | 2 + homeassistant/components/deconz/manifest.json | 2 +- homeassistant/components/deconz/number.py | 126 ++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/deconz/test_gateway.py | 10 +- tests/components/deconz/test_number.py | 104 +++++++++++++++ 7 files changed, 241 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/deconz/number.py create mode 100644 tests/components/deconz/test_number.py diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index f0372273253..09f0cd15141 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -10,6 +10,7 @@ from homeassistant.components.cover import DOMAIN as COVER_DOMAIN from homeassistant.components.fan import DOMAIN as FAN_DOMAIN 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.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.siren import DOMAIN as SIREN_DOMAIN @@ -40,6 +41,7 @@ PLATFORMS = [ FAN_DOMAIN, LIGHT_DOMAIN, LOCK_DOMAIN, + NUMBER_DOMAIN, SCENE_DOMAIN, SENSOR_DOMAIN, SIREN_DOMAIN, diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index a3dae8f5470..68b89b70b9c 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", "requirements": [ - "pydeconz==84" + "pydeconz==85" ], "ssdp": [ { diff --git a/homeassistant/components/deconz/number.py b/homeassistant/components/deconz/number.py new file mode 100644 index 00000000000..e7b9bf274e7 --- /dev/null +++ b/homeassistant/components/deconz/number.py @@ -0,0 +1,126 @@ +"""Support for configuring different deCONZ sensors.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from pydeconz.sensor import PRESENCE_DELAY, Presence + +from homeassistant.components.number import ( + DOMAIN, + NumberEntity, + NumberEntityDescription, +) +from homeassistant.const import ENTITY_CATEGORY_CONFIG +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .deconz_device import DeconzDevice +from .gateway import get_gateway_from_config_entry + + +@dataclass +class DeconzNumberEntityDescription(NumberEntityDescription): + """Class describing deCONZ number entities.""" + + entity_category = ENTITY_CATEGORY_CONFIG + device_property: str | None = None + suffix: str | None = None + update_key: str | None = None + max_value: int | None = None + min_value: int | None = None + step: int | None = None + + +ENTITY_DESCRIPTIONS = { + Presence: [ + DeconzNumberEntityDescription( + key="delay", + device_property="delay", + suffix="Delay", + update_key=PRESENCE_DELAY, + max_value=65535, + min_value=0, + step=1, + ) + ] +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the deCONZ number entity.""" + gateway = get_gateway_from_config_entry(hass, config_entry) + gateway.entities[DOMAIN] = set() + + @callback + def async_add_sensor(sensors=gateway.api.sensors.values()): + """Add number config sensor from deCONZ.""" + entities = [] + + for sensor in sensors: + + if sensor.type.startswith("CLIP"): + continue + + known_number_entities = set(gateway.entities[DOMAIN]) + for description in ENTITY_DESCRIPTIONS.get(type(sensor), []): + + if getattr(sensor, description.device_property) is None: + continue + + new_number_entity = DeconzNumber(sensor, gateway, description) + if new_number_entity.unique_id not in known_number_entities: + entities.append(new_number_entity) + + if entities: + async_add_entities(entities) + + config_entry.async_on_unload( + async_dispatcher_connect( + hass, + gateway.signal_new_sensor, + async_add_sensor, + ) + ) + + async_add_sensor( + [gateway.api.sensors[key] for key in sorted(gateway.api.sensors, key=int)] + ) + + +class DeconzNumber(DeconzDevice, NumberEntity): + """Representation of a deCONZ number entity.""" + + TYPE = DOMAIN + + def __init__(self, device, gateway, description): + """Initialize deCONZ number entity.""" + self.entity_description = description + super().__init__(device, gateway) + + self._attr_name = f"{self._device.name} {description.suffix}" + self._attr_max_value = description.max_value + self._attr_min_value = description.min_value + self._attr_step = description.step + + @callback + def async_update_callback(self, force_update: bool = False) -> None: + """Update the number value.""" + keys = {self.entity_description.update_key, "reachable"} + if force_update or self._device.changed_keys.intersection(keys): + super().async_update_callback(force_update=force_update) + + @property + def value(self) -> float: + """Return the value of the sensor property.""" + return getattr(self._device, self.entity_description.device_property) + + async def async_set_value(self, value: float) -> None: + """Set sensor config.""" + data = {self.entity_description.device_property: int(value)} + await self._device.set_config(**data) + + @property + def unique_id(self) -> str: + """Return a unique identifier for this entity.""" + return f"{self.serial}-{self.entity_description.suffix.lower()}" diff --git a/requirements_all.txt b/requirements_all.txt index 12034253ce8..52c34405a78 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1417,7 +1417,7 @@ pydaikin==2.6.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==84 +pydeconz==85 # homeassistant.components.delijn pydelijn==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b95685930c2..c926bc73d8c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -838,7 +838,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.6.0 # homeassistant.components.deconz -pydeconz==84 +pydeconz==85 # homeassistant.components.dexcom pydexcom==0.2.0 diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index 4ee071f10d3..dc57c679fb7 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -23,6 +23,7 @@ from homeassistant.components.deconz.gateway import ( from homeassistant.components.fan import DOMAIN as FAN_DOMAIN 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.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.siren import DOMAIN as SIREN_DOMAIN @@ -162,10 +163,11 @@ async def test_gateway_setup(hass, aioclient_mock): assert forward_entry_setup.mock_calls[4][1] == (config_entry, FAN_DOMAIN) assert forward_entry_setup.mock_calls[5][1] == (config_entry, LIGHT_DOMAIN) assert forward_entry_setup.mock_calls[6][1] == (config_entry, LOCK_DOMAIN) - assert forward_entry_setup.mock_calls[7][1] == (config_entry, SCENE_DOMAIN) - assert forward_entry_setup.mock_calls[8][1] == (config_entry, SENSOR_DOMAIN) - assert forward_entry_setup.mock_calls[9][1] == (config_entry, SIREN_DOMAIN) - assert forward_entry_setup.mock_calls[10][1] == (config_entry, SWITCH_DOMAIN) + assert forward_entry_setup.mock_calls[7][1] == (config_entry, NUMBER_DOMAIN) + assert forward_entry_setup.mock_calls[8][1] == (config_entry, SCENE_DOMAIN) + assert forward_entry_setup.mock_calls[9][1] == (config_entry, SENSOR_DOMAIN) + assert forward_entry_setup.mock_calls[10][1] == (config_entry, SIREN_DOMAIN) + assert forward_entry_setup.mock_calls[11][1] == (config_entry, SWITCH_DOMAIN) async def test_gateway_retry(hass): diff --git a/tests/components/deconz/test_number.py b/tests/components/deconz/test_number.py new file mode 100644 index 00000000000..0cf0650e3d1 --- /dev/null +++ b/tests/components/deconz/test_number.py @@ -0,0 +1,104 @@ +"""deCONZ number platform tests.""" + +from unittest.mock import patch + +import pytest + +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE + +from .test_gateway import ( + DECONZ_WEB_REQUEST, + mock_deconz_put_request, + setup_deconz_integration, +) + + +async def test_no_number_entities(hass, aioclient_mock): + """Test that no sensors in deconz results in no number entities.""" + await setup_deconz_integration(hass, aioclient_mock) + assert len(hass.states.async_all()) == 0 + + +async def test_binary_sensors(hass, aioclient_mock, mock_deconz_websocket): + """Test successful creation of binary sensor entities.""" + data = { + "sensors": { + "0": { + "name": "Presence sensor", + "type": "ZHAPresence", + "state": {"dark": False, "presence": False}, + "config": { + "delay": 0, + "on": True, + "reachable": True, + "temperature": 10, + }, + "uniqueid": "00:00:00:00:00:00:00:00-00", + }, + } + } + with patch.dict(DECONZ_WEB_REQUEST, data): + config_entry = await setup_deconz_integration(hass, aioclient_mock) + + assert len(hass.states.async_all()) == 3 + assert hass.states.get("number.presence_sensor_delay").state == "0" + + event_changed_sensor = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "0", + "config": {"delay": 10}, + } + await mock_deconz_websocket(data=event_changed_sensor) + await hass.async_block_till_done() + + assert hass.states.get("number.presence_sensor_delay").state == "10" + + # Verify service calls + + mock_deconz_put_request(aioclient_mock, config_entry.data, "/sensors/0/config") + + # Service set supported value + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: "number.presence_sensor_delay", ATTR_VALUE: 111}, + blocking=True, + ) + assert aioclient_mock.mock_calls[1][2] == {"delay": 111} + + # Service set float value + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: "number.presence_sensor_delay", ATTR_VALUE: 0.1}, + blocking=True, + ) + assert aioclient_mock.mock_calls[2][2] == {"delay": 0} + + # Service set value beyond the supported range + + with pytest.raises(ValueError): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: "number.presence_sensor_delay", ATTR_VALUE: 66666}, + blocking=True, + ) + + await hass.config_entries.async_unload(config_entry.entry_id) + + assert hass.states.get("number.presence_sensor_delay").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 From dfd2501c2c135e4aa5558518bf51ad633253dfb5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 20 Oct 2021 11:43:59 +0200 Subject: [PATCH 0567/1038] Cleanup duplicate code in Onewire tests (#58082) Co-authored-by: epenet --- tests/components/onewire/__init__.py | 47 ++++++++++- tests/components/onewire/const.py | 80 ++----------------- .../components/onewire/test_binary_sensor.py | 32 ++------ tests/components/onewire/test_sensor.py | 42 +++------- tests/components/onewire/test_switch.py | 35 +++----- 5 files changed, 80 insertions(+), 156 deletions(-) diff --git a/tests/components/onewire/__init__.py b/tests/components/onewire/__init__.py index 639379e7acc..8223b1bc841 100644 --- a/tests/components/onewire/__init__.py +++ b/tests/components/onewire/__init__.py @@ -1,14 +1,59 @@ """Tests for 1-Wire integration.""" from __future__ import annotations +from types import MappingProxyType from typing import Any from unittest.mock import MagicMock from pyownet.protocol import ProtocolError from homeassistant.components.onewire.const import DEFAULT_SYSBUS_MOUNT_DIR +from homeassistant.const import ATTR_ENTITY_ID, ATTR_STATE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_registry import EntityRegistry -from .const import ATTR_INJECT_READS, MOCK_OWPROXY_DEVICES, MOCK_SYSBUS_DEVICES +from .const import ( + ATTR_DEFAULT_DISABLED, + ATTR_DEVICE_FILE, + ATTR_INJECT_READS, + ATTR_UNIQUE_ID, + FIXED_ATTRIBUTES, + MOCK_OWPROXY_DEVICES, + MOCK_SYSBUS_DEVICES, +) + + +def check_and_enable_disabled_entities( + entity_registry: EntityRegistry, expected_entities: MappingProxyType +) -> None: + """Ensure that the expected_entities are correctly disabled.""" + for expected_entity in expected_entities: + if expected_entity.get(ATTR_DEFAULT_DISABLED): + entity_id = expected_entity[ATTR_ENTITY_ID] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry.disabled + assert registry_entry.disabled_by == "integration" + entity_registry.async_update_entity(entity_id, **{"disabled_by": None}) + + +def check_entities( + hass: HomeAssistant, + entity_registry: EntityRegistry, + expected_entities: MappingProxyType, +) -> None: + """Ensure that the expected_entities are correct.""" + for expected_entity in expected_entities: + entity_id = expected_entity[ATTR_ENTITY_ID] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry is not None + assert registry_entry.unique_id == expected_entity[ATTR_UNIQUE_ID] + state = hass.states.get(entity_id) + assert state.state == expected_entity[ATTR_STATE] + assert state.attributes[ATTR_DEVICE_FILE] == expected_entity.get( + ATTR_DEVICE_FILE, registry_entry.unique_id + ) + for attr in FIXED_ATTRIBUTES: + assert state.attributes.get(attr) == expected_entity.get(attr) def setup_owproxy_mock_devices( diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index c7384d0fc2d..93006ee4f81 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -43,6 +43,12 @@ ATTR_DEVICE_INFO = "device_info" ATTR_INJECT_READS = "inject_reads" ATTR_UNIQUE_ID = "unique_id" +FIXED_ATTRIBUTES = ( + ATTR_DEVICE_CLASS, + ATTR_STATE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, +) + MANUFACTURER = "Maxim Integrated" MOCK_OWPROXY_DEVICES = { @@ -65,12 +71,10 @@ MOCK_OWPROXY_DEVICES = { SWITCH_DOMAIN: [ { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "switch.05_111111111111_pio", ATTR_INJECT_READS: b" 1", ATTR_STATE: STATE_ON, ATTR_UNIQUE_ID: "/05.111111111111/PIO", - ATTR_UNIT_OF_MEASUREMENT: None, }, ], }, @@ -109,21 +113,17 @@ MOCK_OWPROXY_DEVICES = { BINARY_SENSOR_DOMAIN: [ { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "binary_sensor.12_111111111111_sensed_a", ATTR_INJECT_READS: b" 1", ATTR_STATE: STATE_ON, ATTR_UNIQUE_ID: "/12.111111111111/sensed.A", - ATTR_UNIT_OF_MEASUREMENT: None, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "binary_sensor.12_111111111111_sensed_b", ATTR_INJECT_READS: b" 0", ATTR_STATE: STATE_OFF, ATTR_UNIQUE_ID: "/12.111111111111/sensed.B", - ATTR_UNIT_OF_MEASUREMENT: None, }, ], SENSOR_DOMAIN: [ @@ -151,39 +151,31 @@ MOCK_OWPROXY_DEVICES = { SWITCH_DOMAIN: [ { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "switch.12_111111111111_pio_a", ATTR_INJECT_READS: b" 1", ATTR_STATE: STATE_ON, ATTR_UNIQUE_ID: "/12.111111111111/PIO.A", - ATTR_UNIT_OF_MEASUREMENT: None, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "switch.12_111111111111_pio_b", ATTR_INJECT_READS: b" 0", ATTR_STATE: STATE_OFF, ATTR_UNIQUE_ID: "/12.111111111111/PIO.B", - ATTR_UNIT_OF_MEASUREMENT: None, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "switch.12_111111111111_latch_a", ATTR_INJECT_READS: b" 1", ATTR_STATE: STATE_ON, ATTR_UNIQUE_ID: "/12.111111111111/latch.A", - ATTR_UNIT_OF_MEASUREMENT: None, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "switch.12_111111111111_latch_b", ATTR_INJECT_READS: b" 0", ATTR_STATE: STATE_OFF, ATTR_UNIQUE_ID: "/12.111111111111/latch.B", - ATTR_UNIT_OF_MEASUREMENT: None, }, ], }, @@ -199,7 +191,6 @@ MOCK_OWPROXY_DEVICES = { }, SENSOR_DOMAIN: [ { - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "sensor.1d_111111111111_counter_a", ATTR_INJECT_READS: b" 251123", ATTR_STATE: "251123", @@ -208,7 +199,6 @@ MOCK_OWPROXY_DEVICES = { ATTR_UNIT_OF_MEASUREMENT: "count", }, { - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "sensor.1d_111111111111_counter_b", ATTR_INJECT_READS: b" 248125", ATTR_STATE: "248125", @@ -243,7 +233,6 @@ MOCK_OWPROXY_DEVICES = { }, SENSOR_DOMAIN: [ { - ATTR_DEVICE_CLASS: None, ATTR_DEVICE_FILE: "/1F.111111111111/main/1D.111111111111/counter.A", ATTR_ENTITY_ID: "sensor.1d_111111111111_counter_a", ATTR_INJECT_READS: b" 251123", @@ -253,7 +242,6 @@ MOCK_OWPROXY_DEVICES = { ATTR_UNIT_OF_MEASUREMENT: "count", }, { - ATTR_DEVICE_CLASS: None, ATTR_DEVICE_FILE: "/1F.111111111111/main/1D.111111111111/counter.B", ATTR_ENTITY_ID: "sensor.1d_111111111111_counter_b", ATTR_INJECT_READS: b" 248125", @@ -446,221 +434,173 @@ MOCK_OWPROXY_DEVICES = { BINARY_SENSOR_DOMAIN: [ { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "binary_sensor.29_111111111111_sensed_0", ATTR_INJECT_READS: b" 1", ATTR_STATE: STATE_ON, ATTR_UNIQUE_ID: "/29.111111111111/sensed.0", - ATTR_UNIT_OF_MEASUREMENT: None, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "binary_sensor.29_111111111111_sensed_1", ATTR_INJECT_READS: b" 0", ATTR_STATE: STATE_OFF, ATTR_UNIQUE_ID: "/29.111111111111/sensed.1", - ATTR_UNIT_OF_MEASUREMENT: None, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "binary_sensor.29_111111111111_sensed_2", ATTR_INJECT_READS: b" 0", ATTR_STATE: STATE_OFF, ATTR_UNIQUE_ID: "/29.111111111111/sensed.2", - ATTR_UNIT_OF_MEASUREMENT: None, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "binary_sensor.29_111111111111_sensed_3", ATTR_INJECT_READS: b" 0", ATTR_STATE: STATE_OFF, ATTR_UNIQUE_ID: "/29.111111111111/sensed.3", - ATTR_UNIT_OF_MEASUREMENT: None, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "binary_sensor.29_111111111111_sensed_4", ATTR_INJECT_READS: b" 0", ATTR_STATE: STATE_OFF, ATTR_UNIQUE_ID: "/29.111111111111/sensed.4", - ATTR_UNIT_OF_MEASUREMENT: None, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "binary_sensor.29_111111111111_sensed_5", ATTR_INJECT_READS: b" 0", ATTR_STATE: STATE_OFF, ATTR_UNIQUE_ID: "/29.111111111111/sensed.5", - ATTR_UNIT_OF_MEASUREMENT: None, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "binary_sensor.29_111111111111_sensed_6", ATTR_INJECT_READS: b" 0", ATTR_STATE: STATE_OFF, ATTR_UNIQUE_ID: "/29.111111111111/sensed.6", - ATTR_UNIT_OF_MEASUREMENT: None, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "binary_sensor.29_111111111111_sensed_7", ATTR_INJECT_READS: b" 0", ATTR_STATE: STATE_OFF, ATTR_UNIQUE_ID: "/29.111111111111/sensed.7", - ATTR_UNIT_OF_MEASUREMENT: None, }, ], SWITCH_DOMAIN: [ { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "switch.29_111111111111_pio_0", ATTR_INJECT_READS: b" 1", ATTR_STATE: STATE_ON, ATTR_UNIQUE_ID: "/29.111111111111/PIO.0", - ATTR_UNIT_OF_MEASUREMENT: None, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "switch.29_111111111111_pio_1", ATTR_INJECT_READS: b" 0", ATTR_STATE: STATE_OFF, ATTR_UNIQUE_ID: "/29.111111111111/PIO.1", - ATTR_UNIT_OF_MEASUREMENT: None, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "switch.29_111111111111_pio_2", ATTR_INJECT_READS: b" 1", ATTR_STATE: STATE_ON, ATTR_UNIQUE_ID: "/29.111111111111/PIO.2", - ATTR_UNIT_OF_MEASUREMENT: None, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "switch.29_111111111111_pio_3", ATTR_INJECT_READS: b" 0", ATTR_STATE: STATE_OFF, ATTR_UNIQUE_ID: "/29.111111111111/PIO.3", - ATTR_UNIT_OF_MEASUREMENT: None, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "switch.29_111111111111_pio_4", ATTR_INJECT_READS: b" 1", ATTR_STATE: STATE_ON, - ATTR_UNIT_OF_MEASUREMENT: None, ATTR_UNIQUE_ID: "/29.111111111111/PIO.4", }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "switch.29_111111111111_pio_5", ATTR_INJECT_READS: b" 0", ATTR_STATE: STATE_OFF, ATTR_UNIQUE_ID: "/29.111111111111/PIO.5", - ATTR_UNIT_OF_MEASUREMENT: None, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "switch.29_111111111111_pio_6", ATTR_INJECT_READS: b" 1", ATTR_STATE: STATE_ON, ATTR_UNIQUE_ID: "/29.111111111111/PIO.6", - ATTR_UNIT_OF_MEASUREMENT: None, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "switch.29_111111111111_pio_7", ATTR_INJECT_READS: b" 0", ATTR_STATE: STATE_OFF, ATTR_UNIQUE_ID: "/29.111111111111/PIO.7", - ATTR_UNIT_OF_MEASUREMENT: None, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "switch.29_111111111111_latch_0", ATTR_INJECT_READS: b" 1", ATTR_STATE: STATE_ON, ATTR_UNIQUE_ID: "/29.111111111111/latch.0", - ATTR_UNIT_OF_MEASUREMENT: None, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "switch.29_111111111111_latch_1", ATTR_INJECT_READS: b" 0", ATTR_STATE: STATE_OFF, ATTR_UNIQUE_ID: "/29.111111111111/latch.1", - ATTR_UNIT_OF_MEASUREMENT: None, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "switch.29_111111111111_latch_2", ATTR_INJECT_READS: b" 1", ATTR_STATE: STATE_ON, ATTR_UNIQUE_ID: "/29.111111111111/latch.2", - ATTR_UNIT_OF_MEASUREMENT: None, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "switch.29_111111111111_latch_3", ATTR_INJECT_READS: b" 0", ATTR_STATE: STATE_OFF, ATTR_UNIQUE_ID: "/29.111111111111/latch.3", - ATTR_UNIT_OF_MEASUREMENT: None, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "switch.29_111111111111_latch_4", ATTR_INJECT_READS: b" 1", ATTR_STATE: STATE_ON, ATTR_UNIQUE_ID: "/29.111111111111/latch.4", - ATTR_UNIT_OF_MEASUREMENT: None, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "switch.29_111111111111_latch_5", ATTR_INJECT_READS: b" 0", ATTR_STATE: STATE_OFF, ATTR_UNIQUE_ID: "/29.111111111111/latch.5", - ATTR_UNIT_OF_MEASUREMENT: None, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "switch.29_111111111111_latch_6", ATTR_INJECT_READS: b" 1", ATTR_STATE: STATE_ON, ATTR_UNIQUE_ID: "/29.111111111111/latch.6", - ATTR_UNIT_OF_MEASUREMENT: None, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "switch.29_111111111111_latch_7", ATTR_INJECT_READS: b" 0", ATTR_STATE: STATE_OFF, ATTR_UNIQUE_ID: "/29.111111111111/latch.7", - ATTR_UNIT_OF_MEASUREMENT: None, }, ], }, @@ -677,41 +617,33 @@ MOCK_OWPROXY_DEVICES = { BINARY_SENSOR_DOMAIN: [ { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "binary_sensor.3a_111111111111_sensed_a", ATTR_INJECT_READS: b" 1", ATTR_STATE: STATE_ON, ATTR_UNIQUE_ID: "/3A.111111111111/sensed.A", - ATTR_UNIT_OF_MEASUREMENT: None, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "binary_sensor.3a_111111111111_sensed_b", ATTR_INJECT_READS: b" 0", ATTR_STATE: STATE_OFF, ATTR_UNIQUE_ID: "/3A.111111111111/sensed.B", - ATTR_UNIT_OF_MEASUREMENT: None, }, ], SWITCH_DOMAIN: [ { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "switch.3a_111111111111_pio_a", ATTR_INJECT_READS: b" 1", ATTR_STATE: STATE_ON, ATTR_UNIQUE_ID: "/3A.111111111111/PIO.A", - ATTR_UNIT_OF_MEASUREMENT: None, }, { ATTR_DEFAULT_DISABLED: True, - ATTR_DEVICE_CLASS: None, ATTR_ENTITY_ID: "switch.3a_111111111111_pio_b", ATTR_INJECT_READS: b" 0", ATTR_STATE: STATE_OFF, ATTR_UNIQUE_ID: "/3A.111111111111/PIO.B", - ATTR_UNIT_OF_MEASUREMENT: None, }, ], }, diff --git a/tests/components/onewire/test_binary_sensor.py b/tests/components/onewire/test_binary_sensor.py index 5ac45997b41..ff9dd29c2c2 100644 --- a/tests/components/onewire/test_binary_sensor.py +++ b/tests/components/onewire/test_binary_sensor.py @@ -5,16 +5,14 @@ import pytest from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID, ATTR_STATE from homeassistant.core import HomeAssistant -from . import setup_owproxy_mock_devices -from .const import ( - ATTR_DEFAULT_DISABLED, - ATTR_DEVICE_FILE, - ATTR_UNIQUE_ID, - MOCK_OWPROXY_DEVICES, +from . import ( + check_and_enable_disabled_entities, + check_entities, + setup_owproxy_mock_devices, ) +from .const import MOCK_OWPROXY_DEVICES from tests.common import mock_registry @@ -44,26 +42,10 @@ async def test_owserver_binary_sensor( assert len(entity_registry.entities) == len(expected_entities) - # Ensure all entities are enabled - for expected_entity in expected_entities: - if expected_entity.get(ATTR_DEFAULT_DISABLED): - entity_id = expected_entity[ATTR_ENTITY_ID] - registry_entry = entity_registry.entities.get(entity_id) - assert registry_entry.disabled - assert registry_entry.disabled_by == "integration" - entity_registry.async_update_entity(entity_id, **{"disabled_by": None}) + check_and_enable_disabled_entities(entity_registry, expected_entities) setup_owproxy_mock_devices(owproxy, BINARY_SENSOR_DOMAIN, [device_id]) await hass.config_entries.async_reload(config_entry.entry_id) await hass.async_block_till_done() - for expected_entity in expected_entities: - entity_id = expected_entity[ATTR_ENTITY_ID] - registry_entry = entity_registry.entities.get(entity_id) - assert registry_entry is not None - assert registry_entry.unique_id == expected_entity[ATTR_UNIQUE_ID] - state = hass.states.get(entity_id) - assert state.state == expected_entity[ATTR_STATE] - assert state.attributes[ATTR_DEVICE_FILE] == expected_entity.get( - ATTR_DEVICE_FILE, registry_entry.unique_id - ) + check_entities(hass, entity_registry, expected_entities) diff --git a/tests/components/onewire/test_sensor.py b/tests/components/onewire/test_sensor.py index 8b229d1a743..23af72bac41 100644 --- a/tests/components/onewire/test_sensor.py +++ b/tests/components/onewire/test_sensor.py @@ -18,9 +18,13 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant -from . import setup_owproxy_mock_devices, setup_sysbus_mock_devices +from . import ( + check_and_enable_disabled_entities, + check_entities, + setup_owproxy_mock_devices, + setup_sysbus_mock_devices, +) from .const import ( - ATTR_DEFAULT_DISABLED, ATTR_DEVICE_FILE, ATTR_DEVICE_INFO, ATTR_INJECT_READS, @@ -97,7 +101,7 @@ async def test_sensors_on_owserver_coupler( state = hass.states.get(entity_id) assert state.state == expected_entity[ATTR_STATE] for attr in (ATTR_DEVICE_CLASS, ATTR_STATE_CLASS, ATTR_UNIT_OF_MEASUREMENT): - assert state.attributes.get(attr) == expected_entity[attr] + assert state.attributes.get(attr) == expected_entity.get(attr) assert state.attributes[ATTR_DEVICE_FILE] == expected_entity[ATTR_DEVICE_FILE] @@ -120,14 +124,7 @@ async def test_owserver_setup_valid_device( assert len(entity_registry.entities) == len(expected_entities) - # Ensure all entities are enabled - for expected_entity in expected_entities: - if expected_entity.get(ATTR_DEFAULT_DISABLED): - entity_id = expected_entity[ATTR_ENTITY_ID] - registry_entry = entity_registry.entities.get(entity_id) - assert registry_entry.disabled - assert registry_entry.disabled_by == "integration" - entity_registry.async_update_entity(entity_id, **{"disabled_by": None}) + check_and_enable_disabled_entities(entity_registry, expected_entities) setup_owproxy_mock_devices(owproxy, SENSOR_DOMAIN, [device_id]) await hass.config_entries.async_reload(config_entry.entry_id) @@ -143,18 +140,7 @@ async def test_owserver_setup_valid_device( assert registry_entry.name == device_info[ATTR_NAME] assert registry_entry.model == device_info[ATTR_MODEL] - for expected_entity in expected_entities: - entity_id = expected_entity[ATTR_ENTITY_ID] - registry_entry = entity_registry.entities.get(entity_id) - assert registry_entry is not None - assert registry_entry.unique_id == expected_entity[ATTR_UNIQUE_ID] - state = hass.states.get(entity_id) - assert state.state == expected_entity[ATTR_STATE] - for attr in (ATTR_DEVICE_CLASS, ATTR_STATE_CLASS, ATTR_UNIT_OF_MEASUREMENT): - assert state.attributes.get(attr) == expected_entity[attr] - assert state.attributes[ATTR_DEVICE_FILE] == expected_entity.get( - ATTR_DEVICE_FILE, registry_entry.unique_id - ) + check_entities(hass, entity_registry, expected_entities) @pytest.mark.usefixtures("sysbus") @@ -193,12 +179,4 @@ async def test_onewiredirect_setup_valid_device( assert registry_entry.name == device_info[ATTR_NAME] assert registry_entry.model == device_info[ATTR_MODEL] - for expected_entity in expected_entities: - entity_id = expected_entity[ATTR_ENTITY_ID] - registry_entry = entity_registry.entities.get(entity_id) - assert registry_entry is not None - assert registry_entry.unique_id == expected_entity[ATTR_UNIQUE_ID] - state = hass.states.get(entity_id) - assert state.state == expected_entity[ATTR_STATE] - for attr in (ATTR_DEVICE_CLASS, ATTR_STATE_CLASS, ATTR_UNIT_OF_MEASUREMENT): - assert state.attributes.get(attr) == expected_entity[attr] + check_entities(hass, entity_registry, expected_entities) diff --git a/tests/components/onewire/test_switch.py b/tests/components/onewire/test_switch.py index 2f8f96ee638..766a41a5862 100644 --- a/tests/components/onewire/test_switch.py +++ b/tests/components/onewire/test_switch.py @@ -14,13 +14,12 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant -from . import setup_owproxy_mock_devices -from .const import ( - ATTR_DEFAULT_DISABLED, - ATTR_DEVICE_FILE, - ATTR_UNIQUE_ID, - MOCK_OWPROXY_DEVICES, +from . import ( + check_and_enable_disabled_entities, + check_entities, + setup_owproxy_mock_devices, ) +from .const import MOCK_OWPROXY_DEVICES from tests.common import mock_registry @@ -50,31 +49,22 @@ async def test_owserver_switch( assert len(entity_registry.entities) == len(expected_entities) - # Ensure all entities are enabled - for expected_entity in expected_entities: - if expected_entity.get(ATTR_DEFAULT_DISABLED): - entity_id = expected_entity[ATTR_ENTITY_ID] - registry_entry = entity_registry.entities.get(entity_id) - assert registry_entry.disabled - assert registry_entry.disabled_by == "integration" - entity_registry.async_update_entity(entity_id, **{"disabled_by": None}) + check_and_enable_disabled_entities(entity_registry, expected_entities) setup_owproxy_mock_devices(owproxy, SWITCH_DOMAIN, [device_id]) await hass.config_entries.async_reload(config_entry.entry_id) await hass.async_block_till_done() + check_entities(hass, entity_registry, expected_entities) + + # Test TOGGLE service for expected_entity in expected_entities: entity_id = expected_entity[ATTR_ENTITY_ID] - registry_entry = entity_registry.entities.get(entity_id) - assert registry_entry is not None - assert registry_entry.unique_id == expected_entity[ATTR_UNIQUE_ID] - state = hass.states.get(entity_id) - assert state.state == expected_entity[ATTR_STATE] - if state.state == STATE_ON: + if expected_entity[ATTR_STATE] == STATE_ON: owproxy.return_value.read.side_effect = [b" 0"] expected_entity[ATTR_STATE] = STATE_OFF - elif state.state == STATE_OFF: + elif expected_entity[ATTR_STATE] == STATE_OFF: owproxy.return_value.read.side_effect = [b" 1"] expected_entity[ATTR_STATE] = STATE_ON @@ -88,6 +78,3 @@ async def test_owserver_switch( state = hass.states.get(entity_id) assert state.state == expected_entity[ATTR_STATE] - assert state.attributes[ATTR_DEVICE_FILE] == expected_entity.get( - ATTR_DEVICE_FILE, registry_entry.unique_id - ) From 4736bf3c323725293365e7451418c9f02ac5fba3 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 20 Oct 2021 13:03:52 +0200 Subject: [PATCH 0568/1038] Don't use deprecated ways of retrieving registries in Axis (#58083) --- homeassistant/components/axis/device.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index 90eacf47965..93d90e93fe2 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -23,6 +23,7 @@ from homeassistant.const import ( ) 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 from homeassistant.helpers.httpx_client import get_async_client @@ -168,7 +169,7 @@ class AxisNetworkDevice: async def async_update_device_registry(self): """Update device registry.""" - device_registry = await self.hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(self.hass) device_registry.async_get_or_create( config_entry_id=self.config_entry.entry_id, connections={(CONNECTION_NETWORK_MAC, self.unique_id)}, From a679ebcee7295adf98d5e6edb56171504c61db12 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 20 Oct 2021 13:05:11 +0200 Subject: [PATCH 0569/1038] Patch for Alexa percentage_step to precision (#58062) * speed_step must be an integer and a divider of 100 * use percentage_step in tests to test patch * test with not supported percentage_step * undo change in test_capabilities * Use a default precision of one not percentage_step * typo 2 * Update tests/components/alexa/test_smart_home.py Co-authored-by: Erik Montnemery * Update homeassistant/components/alexa/capabilities.py Co-authored-by: Erik Montnemery Co-authored-by: Erik Montnemery --- .../components/alexa/capabilities.py | 4 ++- tests/components/alexa/test_smart_home.py | 35 ++++++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 660ef46e478..ea8a1ed8681 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1535,7 +1535,9 @@ class AlexaRangeController(AlexaCapability): labels=["Percentage", AlexaGlobalCatalog.SETTING_FAN_SPEED], min_value=0, max_value=100, - precision=percentage_step if percentage_step else 100, + # precision must be a divider of 100 and must be an integer; set step + # size to 1 for a consistent behavior except for on/off fans + precision=1 if percentage_step else 100, unit=AlexaGlobalCatalog.UNIT_PERCENT, ) return self._resource.serialize_capability_resources() diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 71123ca27ba..99d43816050 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -365,7 +365,7 @@ async def test_fan(hass): assert appliance["endpointId"] == "fan#test_1" assert appliance["displayCategories"][0] == "FAN" assert appliance["friendlyName"] == "Test fan 1" - # Alexa.RangeController is added to make a van controllable when no other controllers are available + # Alexa.RangeController is added to make a fan controllable when no other controllers are available capabilities = assert_endpoint_capabilities( appliance, "Alexa.RangeController", @@ -402,6 +402,39 @@ async def test_fan(hass): ) +async def test_fan2(hass): + """Test fan discovery with percentage_step.""" + + # Test fan discovery with percentage_step + device = ( + "fan.test_2", + "on", + { + "friendly_name": "Test fan 2", + "percentage": 66, + "supported_features": 1, + "percentage_step": 33.3333, + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "fan#test_2" + assert appliance["displayCategories"][0] == "FAN" + assert appliance["friendlyName"] == "Test fan 2" + # Alexa.RangeController is added to make a fan controllable when no other controllers are available + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa.RangeController", + "Alexa.PowerController", + "Alexa.EndpointHealth", + "Alexa", + ) + + power_capability = get_capability(capabilities, "Alexa.PowerController") + assert "capabilityResources" not in power_capability + assert "configuration" not in power_capability + + async def test_variable_fan(hass): """Test fan discovery. From 62b7453719801dcb15f77c3edd8e64c3677d52f0 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 20 Oct 2021 13:06:06 +0200 Subject: [PATCH 0570/1038] Fix supported_features behaviour for fan platform (#58065) --- homeassistant/components/template/fan.py | 9 ++++----- tests/components/template/test_fan.py | 10 +++++++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 7289eeb72e6..f39d49fa9fd 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -18,6 +18,7 @@ from homeassistant.components.fan import ( SPEED_OFF, SUPPORT_DIRECTION, SUPPORT_OSCILLATE, + SUPPORT_PRESET_MODE, SUPPORT_SET_SPEED, FanEntity, preset_modes_from_speed_list, @@ -250,12 +251,10 @@ class TemplateFan(TemplateEntity, FanEntity): self._oscillating = None self._direction = None - if ( - self._speed_template - or self._percentage_template - or self._preset_mode_template - ): + if self._speed_template or self._percentage_template: self._supported_features |= SUPPORT_SET_SPEED + if self._preset_mode_template and preset_modes: + self._supported_features |= SUPPORT_PRESET_MODE if self._oscillating_template: self._supported_features |= SUPPORT_OSCILLATE if self._direction_template: diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index 91910a11e1f..2beb2e6fd2d 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -16,6 +16,8 @@ from homeassistant.components.fan import ( SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, + SUPPORT_PRESET_MODE, + SUPPORT_SET_SPEED, ) from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE @@ -234,8 +236,8 @@ async def test_templates_with_entities(hass, start_ha): [ ("0", None, None, None), ("invalid", None, None, None), - ("auto", None, "auto", "auto"), - ("smart", None, "smart", "smart"), + ("auto", None, None, "auto"), + ("smart", None, None, "smart"), ("invalid", None, None, None), ], ), @@ -869,6 +871,7 @@ async def test_implemented_percentage(hass, speed_count, percentage_step): state = hass.states.get("fan.mechanical_ventilation") attributes = state.attributes assert attributes["percentage_step"] == percentage_step + assert attributes.get("supported_features") or SUPPORT_SET_SPEED @pytest.mark.parametrize("count,domain", [(1, DOMAIN)]) @@ -935,7 +938,8 @@ async def test_implemented_preset_mode(hass, start_ha): state = hass.states.get("fan.mechanical_ventilation") attributes = state.attributes - assert attributes["percentage"] is None + assert attributes.get("percentage") is None + assert attributes.get("supported_features") or SUPPORT_PRESET_MODE @pytest.mark.parametrize("count,domain", [(1, DOMAIN)]) From 45983b5edff2f7aeed432a69076abc2123371218 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 20 Oct 2021 13:36:02 +0200 Subject: [PATCH 0571/1038] Activate tradfri in coverage and clean conftest for tradfri (#58058) --- .coveragerc | 8 +++++- tests/components/tradfri/__init__.py | 3 +- tests/components/tradfri/conftest.py | 30 +++++++------------- tests/components/tradfri/test_config_flow.py | 20 ++++++------- tests/components/tradfri/test_init.py | 8 ++++-- tests/components/tradfri/test_light.py | 20 ++++++------- 6 files changed, 44 insertions(+), 45 deletions(-) diff --git a/.coveragerc b/.coveragerc index 9f122c09007..d86464fa99e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1101,7 +1101,13 @@ omit = homeassistant/components/tractive/entity.py homeassistant/components/tractive/sensor.py homeassistant/components/tractive/switch.py - homeassistant/components/tradfri/* + homeassistant/components/tradfri/__init__.py + homeassistant/components/tradfri/base_class.py + homeassistant/components/tradfri/config_flow.py + homeassistant/components/tradfri/cover.py + homeassistant/components/tradfri/light.py + homeassistant/components/tradfri/sensor.py + homeassistant/components/tradfri/switch.py homeassistant/components/trafikverket_train/sensor.py homeassistant/components/trafikverket_weatherstation/sensor.py homeassistant/components/transmission/sensor.py diff --git a/tests/components/tradfri/__init__.py b/tests/components/tradfri/__init__.py index e7a6fcb9138..01b5edf5c44 100644 --- a/tests/components/tradfri/__init__.py +++ b/tests/components/tradfri/__init__.py @@ -1,2 +1,3 @@ """Tests for the tradfri component.""" -MOCK_GATEWAY_ID = "mock-gateway-id" +GATEWAY_ID = "mock-gateway-id" +TRADFRI_PATH = "homeassistant.components.tradfri" diff --git a/tests/components/tradfri/conftest.py b/tests/components/tradfri/conftest.py index 54a8625f23c..c5310d52b9c 100644 --- a/tests/components/tradfri/conftest.py +++ b/tests/components/tradfri/conftest.py @@ -3,9 +3,7 @@ from unittest.mock import Mock, patch import pytest -from . import MOCK_GATEWAY_ID - -from tests.components.light.conftest import mock_light_profiles # noqa: F401 +from . import GATEWAY_ID, TRADFRI_PATH # pylint: disable=protected-access @@ -13,28 +11,20 @@ from tests.components.light.conftest import mock_light_profiles # noqa: F401 @pytest.fixture def mock_gateway_info(): """Mock get_gateway_info.""" - with patch( - "homeassistant.components.tradfri.config_flow.get_gateway_info" - ) as gateway_info: + with patch(f"{TRADFRI_PATH}.config_flow.get_gateway_info") as gateway_info: yield gateway_info @pytest.fixture def mock_entry_setup(): """Mock entry setup.""" - with patch("homeassistant.components.tradfri.async_setup_entry") as mock_setup: + with patch(f"{TRADFRI_PATH}.async_setup_entry") as mock_setup: mock_setup.return_value = True yield mock_setup -@pytest.fixture(name="gateway_id") -def mock_gateway_id_fixture(): - """Return mock gateway_id.""" - return MOCK_GATEWAY_ID - - @pytest.fixture(name="mock_gateway") -def mock_gateway_fixture(gateway_id): +def mock_gateway_fixture(): """Mock a Tradfri gateway.""" def get_devices(): @@ -45,7 +35,7 @@ def mock_gateway_fixture(gateway_id): """Return mock groups.""" return gateway.mock_groups - gateway_info = Mock(id=gateway_id, firmware_version="1.2.1234") + gateway_info = Mock(id=GATEWAY_ID, firmware_version="1.2.1234") def get_gateway_info(): """Return mock gateway info.""" @@ -59,8 +49,8 @@ def mock_gateway_fixture(gateway_id): mock_groups=[], mock_responses=[], ) - with patch("homeassistant.components.tradfri.Gateway", return_value=gateway), patch( - "homeassistant.components.tradfri.config_flow.Gateway", return_value=gateway + with patch(f"{TRADFRI_PATH}.Gateway", return_value=gateway), patch( + f"{TRADFRI_PATH}.config_flow.Gateway", return_value=gateway ): yield gateway @@ -79,10 +69,10 @@ def mock_api_fixture(mock_gateway): return api -@pytest.fixture(name="api_factory") -def mock_api_factory_fixture(mock_api): +@pytest.fixture +def mock_api_factory(mock_api): """Mock pytradfri api factory.""" - with patch("homeassistant.components.tradfri.APIFactory", autospec=True) as factory: + with patch(f"{TRADFRI_PATH}.APIFactory", autospec=True) as factory: factory.init.return_value = factory.return_value factory.return_value.request = mock_api yield factory.return_value diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index c8c5323d6e4..60f3043f4f5 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -6,27 +6,27 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components.tradfri import config_flow +from . import TRADFRI_PATH + from tests.common import MockConfigEntry -@pytest.fixture -def mock_auth(): +@pytest.fixture(name="mock_auth") +def mock_auth_fixture(): """Mock authenticate.""" - with patch( - "homeassistant.components.tradfri.config_flow.authenticate" - ) as mock_auth: - yield mock_auth + with patch(f"{TRADFRI_PATH}.config_flow.authenticate") as auth: + yield auth async def test_already_paired(hass, mock_entry_setup): """Test Gateway already paired.""" with patch( - "homeassistant.components.tradfri.config_flow.APIFactory", + f"{TRADFRI_PATH}.config_flow.APIFactory", autospec=True, ) as mock_lib: - mx = AsyncMock() - mx.generate_psk.return_value = None - mock_lib.init.return_value = mx + mock_it = AsyncMock() + mock_it.generate_psk.return_value = None + mock_lib.init.return_value = mock_it result = await hass.config_entries.flow.async_init( "tradfri", context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/tradfri/test_init.py b/tests/components/tradfri/test_init.py index e8cc83a456c..f96d1c09050 100644 --- a/tests/components/tradfri/test_init.py +++ b/tests/components/tradfri/test_init.py @@ -4,10 +4,12 @@ from unittest.mock import patch from homeassistant.components import tradfri from homeassistant.helpers import device_registry as dr +from . import GATEWAY_ID + from tests.common import MockConfigEntry -async def test_entry_setup_unload(hass, api_factory, gateway_id): +async def test_entry_setup_unload(hass, mock_api_factory): """Test config entry setup and unload.""" entry = MockConfigEntry( domain=tradfri.DOMAIN, @@ -16,7 +18,7 @@ async def test_entry_setup_unload(hass, api_factory, gateway_id): tradfri.CONF_IDENTITY: "mock-identity", tradfri.CONF_KEY: "mock-key", tradfri.CONF_IMPORT_GROUPS: True, - tradfri.CONF_GATEWAY_ID: gateway_id, + tradfri.CONF_GATEWAY_ID: GATEWAY_ID, }, ) @@ -46,4 +48,4 @@ async def test_entry_setup_unload(hass, api_factory, gateway_id): assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() assert unload.call_count == len(tradfri.PLATFORMS) - assert api_factory.shutdown.call_count == 1 + assert mock_api_factory.shutdown.call_count == 1 diff --git a/tests/components/tradfri/test_light.py b/tests/components/tradfri/test_light.py index a4a1006d7fe..370fba42fba 100644 --- a/tests/components/tradfri/test_light.py +++ b/tests/components/tradfri/test_light.py @@ -10,7 +10,7 @@ from pytradfri.device.light_control import LightControl from homeassistant.components import tradfri -from . import MOCK_GATEWAY_ID +from . import GATEWAY_ID from tests.common import MockConfigEntry @@ -109,7 +109,7 @@ async def setup_integration(hass): "identity": "mock-identity", "key": "mock-key", "import_groups": True, - "gateway_id": MOCK_GATEWAY_ID, + "gateway_id": GATEWAY_ID, }, ) @@ -153,7 +153,7 @@ def mock_light(test_features=None, test_state=None, light_number=0): return _mock_light -async def test_light(hass, mock_gateway, api_factory): +async def test_light(hass, mock_gateway, mock_api_factory): """Test that lights are correctly added.""" features = {"can_set_dimmer": True, "can_set_color": True, "can_set_temp": True} @@ -176,7 +176,7 @@ async def test_light(hass, mock_gateway, api_factory): assert lamp_1.attributes["hs_color"] == (0.549, 0.153) -async def test_light_observed(hass, mock_gateway, api_factory): +async def test_light_observed(hass, mock_gateway, mock_api_factory): """Test that lights are correctly observed.""" light = mock_light() mock_gateway.mock_devices.append(light) @@ -184,7 +184,7 @@ async def test_light_observed(hass, mock_gateway, api_factory): assert len(light.observe.mock_calls) > 0 -async def test_light_available(hass, mock_gateway, api_factory): +async def test_light_available(hass, mock_gateway, mock_api_factory): """Test light available property.""" light = mock_light({"state": True}, light_number=1) light.reachable = True @@ -225,7 +225,7 @@ def create_all_turn_on_cases(): async def test_turn_on( hass, mock_gateway, - api_factory, + mock_api_factory, test_features, test_data, expected_result, @@ -288,7 +288,7 @@ async def test_turn_on( assert states.attributes[result] == pytest.approx(value, abs=0.01) -async def test_turn_off(hass, mock_gateway, api_factory): +async def test_turn_off(hass, mock_gateway, mock_api_factory): """Test turning off a light.""" state = {"state": True, "dimmer": 100} @@ -341,7 +341,7 @@ def mock_group(test_state=None, group_number=0): return _mock_group -async def test_group(hass, mock_gateway, api_factory): +async def test_group(hass, mock_gateway, mock_api_factory): """Test that groups are correctly added.""" mock_gateway.mock_groups.append(mock_group()) state = {"state": True, "dimmer": 100} @@ -358,7 +358,7 @@ async def test_group(hass, mock_gateway, api_factory): assert group.attributes["brightness"] == 100 -async def test_group_turn_on(hass, mock_gateway, api_factory): +async def test_group_turn_on(hass, mock_gateway, mock_api_factory): """Test turning on a group.""" group = mock_group() group2 = mock_group(group_number=1) @@ -391,7 +391,7 @@ async def test_group_turn_on(hass, mock_gateway, api_factory): group3.set_dimmer.assert_called_with(100, transition_time=10) -async def test_group_turn_off(hass, mock_gateway, api_factory): +async def test_group_turn_off(hass, mock_gateway, mock_api_factory): """Test turning off a group.""" group = mock_group({"state": True}) mock_gateway.mock_groups.append(group) From b3117ced755d8a5ed8d9aab3c8012b40ffe54b8a Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 20 Oct 2021 13:38:55 +0200 Subject: [PATCH 0572/1038] Please pylint for modbus test (#58089) --- tests/components/modbus/conftest.py | 48 +++++++++++++------------- tests/components/modbus/test_cover.py | 2 +- tests/components/modbus/test_fan.py | 2 +- tests/components/modbus/test_init.py | 28 +++++++-------- tests/components/modbus/test_light.py | 2 +- tests/components/modbus/test_switch.py | 2 +- 6 files changed, 42 insertions(+), 42 deletions(-) diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index 4c8df13fbe6..91327b4e2a2 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -32,8 +32,8 @@ class ReadResult: self.bits = register_words -@pytest.fixture -def mock_pymodbus(): +@pytest.fixture(name="mock_pymodbus") +def mock_pymodbus_fixture(): """Mock pymodbus.""" mock_pb = mock.MagicMock() with mock.patch( @@ -52,32 +52,32 @@ def mock_pymodbus(): yield mock_pb -@pytest.fixture -def check_config_loaded(): +@pytest.fixture(name="check_config_loaded") +def check_config_loaded_fixture(): """Set default for check_config_loaded.""" return True -@pytest.fixture -def register_words(): +@pytest.fixture(name="register_words") +def register_words_fixture(): """Set default for register_words.""" return [0x00, 0x00] -@pytest.fixture -def config_addon(): +@pytest.fixture(name="config_addon") +def config_addon_fixture(): """Add entra configuration items.""" return None -@pytest.fixture -def do_exception(): +@pytest.fixture(name="do_exception") +def do_exception_fixture(): """Remove side_effect to pymodbus calls.""" return False -@pytest.fixture -async def mock_modbus( +@pytest.fixture(name="mock_modbus") +async def mock_modbus_fixture( hass, caplog, register_words, check_config_loaded, config_addon, do_config ): """Load integration modbus using mocked pymodbus.""" @@ -115,8 +115,8 @@ async def mock_modbus( yield mock_pb -@pytest.fixture -async def mock_pymodbus_exception(hass, do_exception, mock_modbus): +@pytest.fixture(name="mock_pymodbus_exception") +async def mock_pymodbus_exception_fixture(hass, do_exception, mock_modbus): """Trigger update call with time_changed event.""" if do_exception: exc = ModbusException("fail read_coils") @@ -126,8 +126,8 @@ async def mock_pymodbus_exception(hass, do_exception, mock_modbus): mock_modbus.read_holding_registers.side_effect = exc -@pytest.fixture -async def mock_pymodbus_return(hass, register_words, mock_modbus): +@pytest.fixture(name="mock_pymodbus_return") +async def mock_pymodbus_return_fixture(hass, register_words, mock_modbus): """Trigger update call with time_changed event.""" read_result = ReadResult(register_words) mock_modbus.read_coils.return_value = read_result @@ -136,8 +136,8 @@ async def mock_pymodbus_return(hass, register_words, mock_modbus): mock_modbus.read_holding_registers.return_value = read_result -@pytest.fixture -async def mock_do_cycle(hass, mock_pymodbus_exception, mock_pymodbus_return): +@pytest.fixture(name="mock_do_cycle") +async def mock_do_cycle_fixture(hass, mock_pymodbus_exception, mock_pymodbus_return): """Trigger update call with time_changed event.""" now = dt_util.utcnow() + timedelta(seconds=90) with mock.patch( @@ -159,21 +159,21 @@ async def do_next_cycle(hass, now, cycle): return now -@pytest.fixture -async def mock_test_state(hass, request): +@pytest.fixture(name="mock_test_state") +async def mock_test_state_fixture(hass, request): """Mock restore cache.""" mock_restore_cache(hass, request.param) return request.param -@pytest.fixture -async def mock_ha(hass, mock_pymodbus_return): +@pytest.fixture(name="mock_ha") +async def mock_ha_fixture(hass, mock_pymodbus_return): """Load homeassistant to allow service calls.""" assert await async_setup_component(hass, "homeassistant", {}) await hass.async_block_till_done() -@pytest.fixture -async def caplog_setup_text(caplog): +@pytest.fixture(name="caplog_setup_text") +async def caplog_setup_text_fixture(caplog): """Return setup log of integration.""" yield caplog.text diff --git a/tests/components/modbus/test_cover.py b/tests/components/modbus/test_cover.py index f772955e55c..cc879b2c168 100644 --- a/tests/components/modbus/test_cover.py +++ b/tests/components/modbus/test_cover.py @@ -33,6 +33,7 @@ from homeassistant.core import State from .conftest import TEST_ENTITY_NAME, ReadResult, do_next_cycle ENTITY_ID = f"{COVER_DOMAIN}.{TEST_ENTITY_NAME}" +ENTITY_ID2 = f"{ENTITY_ID}2" @pytest.mark.parametrize( @@ -281,7 +282,6 @@ async def test_restore_state_cover(hass, mock_test_state, mock_modbus): async def test_service_cover_move(hass, mock_modbus, mock_ha): """Run test for service homeassistant.update_entity.""" - ENTITY_ID2 = f"{ENTITY_ID}2" mock_modbus.read_holding_registers.return_value = ReadResult([0x01]) await hass.services.async_call( "cover", "open_cover", {"entity_id": ENTITY_ID}, blocking=True diff --git a/tests/components/modbus/test_fan.py b/tests/components/modbus/test_fan.py index b2793d15bff..9ffa48a032a 100644 --- a/tests/components/modbus/test_fan.py +++ b/tests/components/modbus/test_fan.py @@ -38,6 +38,7 @@ from homeassistant.setup import async_setup_component from .conftest import TEST_ENTITY_NAME, TEST_MODBUS_HOST, TEST_PORT_TCP, ReadResult ENTITY_ID = f"{FAN_DOMAIN}.{TEST_ENTITY_NAME}" +ENTITY_ID2 = f"{ENTITY_ID}2" @pytest.mark.parametrize( @@ -223,7 +224,6 @@ async def test_restore_state_fan(hass, mock_test_state, mock_modbus): async def test_fan_service_turn(hass, caplog, mock_pymodbus): """Run test for service turn_on/turn_off.""" - ENTITY_ID2 = f"{FAN_DOMAIN}.{TEST_ENTITY_NAME}2" config = { MODBUS_DOMAIN: { CONF_TYPE: TCP, diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index a303e116307..dae19d4db50 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -99,8 +99,8 @@ from .conftest import ( from tests.common import async_fire_time_changed -@pytest.fixture -async def mock_modbus_with_pymodbus(hass, caplog, do_config, mock_pymodbus): +@pytest.fixture(name="mock_modbus_with_pymodbus") +async def mock_modbus_with_pymodbus_fixture(hass, caplog, do_config, mock_pymodbus): """Load integration modbus using mocked pymodbus.""" caplog.clear() caplog.set_level(logging.ERROR) @@ -115,7 +115,7 @@ async def mock_modbus_with_pymodbus(hass, caplog, do_config, mock_pymodbus): async def test_number_validator(): """Test number validator.""" - for value, value_type in [ + for value, value_type in ( (15, int), (15.1, float), ("15", int), @@ -124,7 +124,7 @@ async def test_number_validator(): (-15.1, float), ("-15", int), ("-15.1", float), - ]: + ): assert isinstance(number_validator(value), value_type) try: @@ -510,8 +510,8 @@ async def test_pb_service_write( assert caplog.messages[-1].startswith("Pymodbus:") -@pytest.fixture -async def mock_modbus_read_pymodbus( +@pytest.fixture(name="mock_modbus_read_pymodbus") +async def mock_modbus_read_pymodbus_fixture( hass, do_group, do_type, @@ -664,8 +664,8 @@ async def test_pymodbus_connect_fail(hass, caplog): "homeassistant.components.modbus.modbus.ModbusTcpClient", autospec=True ) as mock_pb: caplog.set_level(logging.ERROR) - ExceptionMessage = "test connect exception" - mock_pb.connect.side_effect = ModbusException(ExceptionMessage) + exception_message = "test connect exception" + mock_pb.connect.side_effect = ModbusException(exception_message) assert await async_setup_component(hass, DOMAIN, config) is True @@ -675,8 +675,8 @@ async def test_delay(hass, mock_pymodbus): # the purpose of this test is to test startup delay # We "hijiack" a binary_sensor to make a proper blackbox test. - test_delay = 15 - test_scan_interval = 5 + set_delay = 15 + set_scan_interval = 5 entity_id = f"{BINARY_SENSOR_DOMAIN}.{TEST_ENTITY_NAME}" config = { DOMAIN: [ @@ -685,13 +685,13 @@ async def test_delay(hass, mock_pymodbus): CONF_HOST: TEST_MODBUS_HOST, CONF_PORT: TEST_PORT_TCP, CONF_NAME: TEST_MODBUS_NAME, - CONF_DELAY: test_delay, + CONF_DELAY: set_delay, CONF_BINARY_SENSORS: [ { CONF_INPUT_TYPE: CALL_TYPE_COIL, CONF_NAME: TEST_ENTITY_NAME, CONF_ADDRESS: 52, - CONF_SCAN_INTERVAL: test_scan_interval, + CONF_SCAN_INTERVAL: set_scan_interval, }, ], } @@ -705,7 +705,7 @@ async def test_delay(hass, mock_pymodbus): # pass first scan_interval start_time = now - now = now + timedelta(seconds=(test_scan_interval + 1)) + now = now + timedelta(seconds=(set_scan_interval + 1)) with mock.patch( "homeassistant.helpers.event.dt_util.utcnow", return_value=now, autospec=True ): @@ -713,7 +713,7 @@ async def test_delay(hass, mock_pymodbus): await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - stop_time = start_time + timedelta(seconds=(test_delay + 1)) + stop_time = start_time + timedelta(seconds=(set_delay + 1)) step_timedelta = timedelta(seconds=1) while now < stop_time: now = now + step_timedelta diff --git a/tests/components/modbus/test_light.py b/tests/components/modbus/test_light.py index 65d42dff987..451d0beca13 100644 --- a/tests/components/modbus/test_light.py +++ b/tests/components/modbus/test_light.py @@ -38,6 +38,7 @@ from homeassistant.setup import async_setup_component from .conftest import TEST_ENTITY_NAME, TEST_MODBUS_HOST, TEST_PORT_TCP, ReadResult ENTITY_ID = f"{LIGHT_DOMAIN}.{TEST_ENTITY_NAME}" +ENTITY_ID2 = f"{ENTITY_ID}2" @pytest.mark.parametrize( @@ -223,7 +224,6 @@ async def test_restore_state_light(hass, mock_test_state, mock_modbus): async def test_light_service_turn(hass, caplog, mock_pymodbus): """Run test for service turn_on/turn_off.""" - ENTITY_ID2 = f"{ENTITY_ID}2" config = { MODBUS_DOMAIN: { CONF_TYPE: TCP, diff --git a/tests/components/modbus/test_switch.py b/tests/components/modbus/test_switch.py index 7e3c2d5f6c2..15a41956d3f 100644 --- a/tests/components/modbus/test_switch.py +++ b/tests/components/modbus/test_switch.py @@ -52,6 +52,7 @@ from .conftest import ( from tests.common import async_fire_time_changed ENTITY_ID = f"{SWITCH_DOMAIN}.{TEST_ENTITY_NAME}" +ENTITY_ID2 = f"{ENTITY_ID}2" @pytest.mark.parametrize( @@ -282,7 +283,6 @@ async def test_restore_state_switch(hass, mock_test_state, mock_modbus): async def test_switch_service_turn(hass, caplog, mock_pymodbus): """Run test for service turn_on/turn_off.""" - ENTITY_ID2 = f"{SWITCH_DOMAIN}.{TEST_ENTITY_NAME}2" config = { MODBUS_DOMAIN: { CONF_TYPE: TCP, From c75346addc9b4dd3ca9f262e7199aa86e8bae1a6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Oct 2021 14:20:24 +0200 Subject: [PATCH 0573/1038] Add CO2 Detector (co2bj) device support to Tuya (#58093) --- .../components/tuya/binary_sensor.py | 47 +++++++++---------- homeassistant/components/tuya/const.py | 1 + homeassistant/components/tuya/sensor.py | 29 ++++++++++-- 3 files changed, 50 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index 0a54aa74dfe..934f1fda7ef 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -37,11 +37,30 @@ class TuyaBinarySensorEntityDescription(BinarySensorEntityDescription): on_value: bool | float | int | str = True +# Commonly used sensors +TAMPER_BINARY_SENSOR = TuyaBinarySensorEntityDescription( + key=DPCode.TEMPER_ALARM, + name="Tamper", + device_class=DEVICE_CLASS_TAMPER, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, +) + + # All descriptions can be found here. Mostly the Boolean data types in the # default status set of each category (that don't have a set instruction) # end up being a binary sensor. # https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { + # CO2 Detector + # https://developer.tuya.com/en/docs/iot/categoryco2bj?id=Kaiuz3wes7yuy + "co2bj": ( + TuyaBinarySensorEntityDescription( + key=DPCode.CO2_STATE, + device_class=DEVICE_CLASS_SAFETY, + on_value="alarm", + ), + TAMPER_BINARY_SENSOR, + ), # Human Presence Sensor # https://developer.tuya.com/en/docs/iot/categoryhps?id=Kaiuz42yhn1hs "hps": ( @@ -58,12 +77,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { key=DPCode.DOORCONTACT_STATE, device_class=DEVICE_CLASS_DOOR, ), - TuyaBinarySensorEntityDescription( - key=DPCode.TEMPER_ALARM, - name="Tamper", - device_class=DEVICE_CLASS_TAMPER, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), + TAMPER_BINARY_SENSOR, ), # Luminance Sensor # https://developer.tuya.com/en/docs/iot/categoryldcg?id=Kaiuz3n7u69l8 @@ -83,12 +97,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { device_class=DEVICE_CLASS_MOTION, on_value="pir", ), - TuyaBinarySensorEntityDescription( - key=DPCode.TEMPER_ALARM, - name="Tamper", - device_class=DEVICE_CLASS_TAMPER, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), + TAMPER_BINARY_SENSOR, ), # Water Detector # https://developer.tuya.com/en/docs/iot/categorysj?id=Kaiuz3iub2sli @@ -98,12 +107,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { device_class=DEVICE_CLASS_MOISTURE, on_value="alarm", ), - TuyaBinarySensorEntityDescription( - key=DPCode.TEMPER_ALARM, - name="Tamper", - device_class=DEVICE_CLASS_TAMPER, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), + TAMPER_BINARY_SENSOR, ), # Emergency Button # https://developer.tuya.com/en/docs/iot/categorysos?id=Kaiuz3oi6agjy @@ -112,12 +116,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { key=DPCode.SOS_STATE, device_class=DEVICE_CLASS_SAFETY, ), - TuyaBinarySensorEntityDescription( - key=DPCode.TEMPER_ALARM, - name="Tamper", - device_class=DEVICE_CLASS_TAMPER, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ), + TAMPER_BINARY_SENSOR, ), # Vibration Sensor # https://developer.tuya.com/en/docs/iot/categoryzd?id=Kaiuz3a5vrzno diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index bb98227f3b9..f0a1d92c58c 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -136,6 +136,7 @@ class DPCode(str, Enum): BRIGHT_VALUE_V2 = "bright_value_v2" C_F = "c_f" # Temperature unit switching CHILD_LOCK = "child_lock" # Child lock + CO2_STATE = "co2_state" CO2_VALUE = "co2_value" # CO2 concentration COLOR_DATA_V2 = "color_data_v2" COLOUR_DATA = "colour_data" # Colored light mode diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 739ababfadd..469d2a2e83c 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -62,9 +62,29 @@ BATTERY_SENSORS: tuple[SensorEntityDescription, ...] = ( # end up being a sensor. # https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { - # Door Window Sensor - # https://developer.tuya.com/en/docs/iot/s?id=K9gf48hm02l8m - "mcs": BATTERY_SENSORS, + # CO2 Detector + # https://developer.tuya.com/en/docs/iot/categoryco2bj?id=Kaiuz3wes7yuy + "co2bj": ( + SensorEntityDescription( + key=DPCode.HUMIDITY_VALUE, + name="Humidity", + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.TEMP_CURRENT, + name="Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.CO2_VALUE, + name="Carbon Dioxide (CO2)", + device_class=DEVICE_CLASS_CO2, + state_class=STATE_CLASS_MEASUREMENT, + ), + *BATTERY_SENSORS, + ), # Switch # https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s "kg": ( @@ -124,6 +144,9 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { ), *BATTERY_SENSORS, ), + # Door Window Sensor + # https://developer.tuya.com/en/docs/iot/s?id=K9gf48hm02l8m + "mcs": BATTERY_SENSORS, # PIR Detector # https://developer.tuya.com/en/docs/iot/categorypir?id=Kaiuz3ss11b80 "pir": BATTERY_SENSORS, From 2bd64cec111e472863fa283193081718b1b0750f Mon Sep 17 00:00:00 2001 From: David Le Brun Date: Wed, 20 Oct 2021 14:53:14 +0200 Subject: [PATCH 0574/1038] Add state_class to current bandwith sensors for bbox integration (#58086) * Add state_class to current bandwith sensors * Fix isort test --- homeassistant/components/bbox/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index d86dd0c243b..53a7e8720b1 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, SensorEntity, SensorEntityDescription, ) @@ -48,12 +49,14 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="current_down_bandwidth", name="Currently Used Download Bandwidth", native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, + state_class=STATE_CLASS_MEASUREMENT, icon="mdi:download", ), SensorEntityDescription( key="current_up_bandwidth", name="Currently Used Upload Bandwidth", native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, + state_class=STATE_CLASS_MEASUREMENT, icon="mdi:upload", ), SensorEntityDescription( From 3ad3f4e2ba511699a6b5ffdb5dff0a7181b805d1 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 20 Oct 2021 14:59:36 +0200 Subject: [PATCH 0575/1038] Simplify signalling for updating available property of deCONZ entities (#58078) --- .../components/deconz/alarm_control_panel.py | 6 +++--- .../components/deconz/binary_sensor.py | 12 +++++------ .../components/deconz/deconz_device.py | 13 +++++++++--- .../components/deconz/deconz_event.py | 4 ++-- homeassistant/components/deconz/fan.py | 4 ++-- homeassistant/components/deconz/gateway.py | 2 +- homeassistant/components/deconz/sensor.py | 20 +++++++++---------- 7 files changed, 34 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/deconz/alarm_control_panel.py b/homeassistant/components/deconz/alarm_control_panel.py index c16a074bc06..823c9c67654 100644 --- a/homeassistant/components/deconz/alarm_control_panel.py +++ b/homeassistant/components/deconz/alarm_control_panel.py @@ -112,14 +112,14 @@ class DeconzAlarmControlPanel(DeconzDevice, AlarmControlPanelEntity): self.alarm_system = get_alarm_system_for_unique_id(gateway, device.unique_id) @callback - def async_update_callback(self, force_update: bool = False) -> None: + def async_update_callback(self) -> None: """Update the control panels state.""" keys = {"panel", "reachable"} - if force_update or ( + if ( self._device.changed_keys.intersection(keys) and self._device.state in DECONZ_TO_ALARM_STATE ): - super().async_update_callback(force_update=force_update) + super().async_update_callback() @property def state(self) -> str | None: diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 05c8a134074..b8af5a2971e 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -129,11 +129,11 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorEntity): self.entity_description = entity_description @callback - def async_update_callback(self, force_update=False): + def async_update_callback(self): """Update the sensor's state.""" keys = {"on", "reachable", "state"} - if force_update or self._device.changed_keys.intersection(keys): - super().async_update_callback(force_update=force_update) + if self._device.changed_keys.intersection(keys): + super().async_update_callback() @property def is_on(self): @@ -183,11 +183,11 @@ class DeconzTampering(DeconzDevice, BinarySensorEntity): return f"{self.serial}-tampered" @callback - def async_update_callback(self, force_update: bool = False) -> None: + def async_update_callback(self) -> None: """Update the sensor's state.""" keys = {"tampered", "reachable"} - if force_update or self._device.changed_keys.intersection(keys): - super().async_update_callback(force_update=force_update) + if self._device.changed_keys.intersection(keys): + super().async_update_callback() @property def is_on(self) -> bool: diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index fe9eaa8ff60..e8f27e35f98 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -66,7 +66,9 @@ class DeconzDevice(DeconzBase, Entity): self.gateway.deconz_ids[self.entity_id] = self._device.deconz_id self.async_on_remove( async_dispatcher_connect( - self.hass, self.gateway.signal_reachable, self.async_update_callback + self.hass, + self.gateway.signal_reachable, + self.async_update_connection_state, ) ) @@ -77,9 +79,14 @@ class DeconzDevice(DeconzBase, Entity): self.gateway.entities[self.TYPE].remove(self.unique_id) @callback - def async_update_callback(self, force_update=False): + def async_update_connection_state(self): + """Update the device's available state.""" + self.async_write_ha_state() + + @callback + def async_update_callback(self): """Update the device's state.""" - if not force_update and self.gateway.ignore_state_updates: + if self.gateway.ignore_state_updates: return self.async_write_ha_state() diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index a0470c8ac32..300aef3f82a 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -110,7 +110,7 @@ class DeconzEvent(DeconzBase): self._device.remove_callback(self.async_update_callback) @callback - def async_update_callback(self, force_update=False): + def async_update_callback(self): """Fire the event if reason is that state is updated.""" if ( self.gateway.ignore_state_updates @@ -155,7 +155,7 @@ class DeconzAlarmEvent(DeconzEvent): """Alarm control panel companion event when user interacts with a keypad.""" @callback - def async_update_callback(self, force_update=False): + def async_update_callback(self): """Fire the event if reason is new action is updated.""" if ( self.gateway.ignore_state_updates diff --git a/homeassistant/components/deconz/fan.py b/homeassistant/components/deconz/fan.py index af73135cd2a..40862bfcde1 100644 --- a/homeassistant/components/deconz/fan.py +++ b/homeassistant/components/deconz/fan.py @@ -159,11 +159,11 @@ class DeconzFan(DeconzDevice, FanEntity): return self._attr_supported_features @callback - def async_update_callback(self, force_update=False) -> None: + def async_update_callback(self) -> None: """Store latest configured speed from the device.""" if self._device.speed in ORDERED_NAMED_FAN_SPEEDS: self._default_on_speed = self._device.speed - super().async_update_callback(force_update) + super().async_update_callback() async def async_set_percentage(self, percentage: int) -> None: """Set the speed percentage of the fan.""" diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 6096edabb37..1199884fb5a 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -113,7 +113,7 @@ class DeconzGateway: """Handle signals of gateway connection status.""" self.available = available self.ignore_state_updates = False - async_dispatcher_send(self.hass, self.signal_reachable, True) + async_dispatcher_send(self.hass, self.signal_reachable) @callback def async_add_device_callback( diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index f9d177523f9..3f8c22d43d6 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -193,11 +193,11 @@ class DeconzSensor(DeconzDevice, SensorEntity): self.entity_description = entity_description @callback - def async_update_callback(self, force_update=False): + def async_update_callback(self): """Update the sensor's state.""" keys = {"on", "reachable", "state"} - if force_update or self._device.changed_keys.intersection(keys): - super().async_update_callback(force_update=force_update) + if self._device.changed_keys.intersection(keys): + super().async_update_callback() @property def native_value(self): @@ -257,11 +257,11 @@ class DeconzTemperature(DeconzDevice, SensorEntity): return f"{self.serial}-temperature" @callback - def async_update_callback(self, force_update=False): + def async_update_callback(self): """Update the sensor's state.""" keys = {"temperature", "reachable"} - if force_update or self._device.changed_keys.intersection(keys): - super().async_update_callback(force_update=force_update) + if self._device.changed_keys.intersection(keys): + super().async_update_callback() @property def native_value(self): @@ -282,11 +282,11 @@ class DeconzBattery(DeconzDevice, SensorEntity): self._attr_name = f"{self._device.name} Battery Level" @callback - def async_update_callback(self, force_update=False): + def async_update_callback(self): """Update the battery's state, if needed.""" keys = {"battery", "reachable"} - if force_update or self._device.changed_keys.intersection(keys): - super().async_update_callback(force_update=force_update) + if self._device.changed_keys.intersection(keys): + super().async_update_callback() @property def unique_id(self): @@ -339,7 +339,7 @@ class DeconzSensorStateTracker: self.sensor = None @callback - def async_update_callback(self, ignore_update=False): + def async_update_callback(self): """Sensor state updated.""" if "battery" in self.sensor.changed_keys: async_dispatcher_send( From b507b9ebfb9536095378f8c1341aae832cbd385c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 20 Oct 2021 03:02:08 -1000 Subject: [PATCH 0576/1038] Bump ismartgate to 4.0.3 (#58073) * Bump ismartgate to 4.0.3 Fixes #56245 Changelog: https://github.com/bdraco/ismartgate/compare/v4.0.0...v4.0.3 * restart ci --- homeassistant/components/gogogate2/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gogogate2/manifest.json b/homeassistant/components/gogogate2/manifest.json index 94a57c47be7..7d88340c597 100644 --- a/homeassistant/components/gogogate2/manifest.json +++ b/homeassistant/components/gogogate2/manifest.json @@ -3,7 +3,7 @@ "name": "Gogogate2 and ismartgate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gogogate2", - "requirements": ["ismartgate==4.0.0"], + "requirements": ["ismartgate==4.0.3"], "codeowners": ["@vangorra", "@bdraco"], "homekit": { "models": ["iSmartGate"] diff --git a/requirements_all.txt b/requirements_all.txt index 52c34405a78..8687ed3d09b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -888,7 +888,7 @@ iotawattpy==0.1.0 iperf3==0.1.11 # homeassistant.components.gogogate2 -ismartgate==4.0.0 +ismartgate==4.0.3 # homeassistant.components.rest jsonpath==0.82 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c926bc73d8c..c22adf8b197 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -540,7 +540,7 @@ influxdb==5.2.3 iotawattpy==0.1.0 # homeassistant.components.gogogate2 -ismartgate==4.0.0 +ismartgate==4.0.3 # homeassistant.components.rest jsonpath==0.82 From e2303dc7135fad056d8c61668d682b048c21ab74 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 20 Oct 2021 15:31:23 +0200 Subject: [PATCH 0577/1038] bitwise and for test supported_features (#58097) --- tests/components/template/test_fan.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index 2beb2e6fd2d..8ec771a05f3 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -871,7 +871,7 @@ async def test_implemented_percentage(hass, speed_count, percentage_step): state = hass.states.get("fan.mechanical_ventilation") attributes = state.attributes assert attributes["percentage_step"] == percentage_step - assert attributes.get("supported_features") or SUPPORT_SET_SPEED + assert attributes.get("supported_features") & SUPPORT_SET_SPEED @pytest.mark.parametrize("count,domain", [(1, DOMAIN)]) @@ -939,7 +939,7 @@ async def test_implemented_preset_mode(hass, start_ha): state = hass.states.get("fan.mechanical_ventilation") attributes = state.attributes assert attributes.get("percentage") is None - assert attributes.get("supported_features") or SUPPORT_PRESET_MODE + assert attributes.get("supported_features") & SUPPORT_PRESET_MODE @pytest.mark.parametrize("count,domain", [(1, DOMAIN)]) From e3534eec8756c73d6fb60c437fddc45eb2fabff7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 20 Oct 2021 15:57:22 +0200 Subject: [PATCH 0578/1038] Report orphaned statistics in statistic validation (#57324) --- .../components/recorder/migration.py | 2 +- .../components/recorder/statistics.py | 41 +++++++-- homeassistant/components/sensor/recorder.py | 18 +++- tests/components/sensor/test_recorder.py | 83 ++++++++++++++++++- 4 files changed, 128 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index a3d2955e55b..fec2e1e962c 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -560,7 +560,7 @@ def _apply_update(instance, session, new_version, old_version): # noqa: C901 # Copy last hourly statistic to the newly created 5-minute statistics table sum_statistics = get_metadata_with_session( - instance.hass, session, None, statistic_type="sum" + instance.hass, session, statistic_type="sum" ) for metadata_id, _ in sum_statistics.values(): last_statistic = ( diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 175a7e33fb0..374345c8303 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -204,7 +204,9 @@ def _update_or_add_metadata( Updating metadata source is not possible. """ statistic_id = new_metadata["statistic_id"] - old_metadata_dict = get_metadata_with_session(hass, session, [statistic_id], None) + old_metadata_dict = get_metadata_with_session( + hass, session, statistic_ids=[statistic_id] + ) if not old_metadata_dict: unit = new_metadata["unit_of_measurement"] has_mean = new_metadata["has_mean"] @@ -417,8 +419,10 @@ def compile_statistics(instance: Recorder, start: datetime) -> bool: def get_metadata_with_session( hass: HomeAssistant, session: scoped_session, - statistic_ids: Iterable[str] | None, - statistic_type: Literal["mean"] | Literal["sum"] | None, + *, + statistic_ids: Iterable[str] | None = None, + statistic_type: Literal["mean"] | Literal["sum"] | None = None, + statistic_source: str | None = None, ) -> dict[str, tuple[int, StatisticMetaData]]: """Fetch meta data. @@ -448,11 +452,19 @@ def get_metadata_with_session( baked_query += lambda q: q.filter( StatisticsMeta.statistic_id.in_(bindparam("statistic_ids")) ) + if statistic_source is not None: + baked_query += lambda q: q.filter( + StatisticsMeta.source == bindparam("statistic_source") + ) if statistic_type == "mean": baked_query += lambda q: q.filter(StatisticsMeta.has_mean == true()) elif statistic_type == "sum": baked_query += lambda q: q.filter(StatisticsMeta.has_sum == true()) - result = execute(baked_query(session).params(statistic_ids=statistic_ids)) + result = execute( + baked_query(session).params( + statistic_ids=statistic_ids, statistic_source=statistic_source + ) + ) if not result: return {} @@ -468,11 +480,20 @@ def get_metadata_with_session( def get_metadata( hass: HomeAssistant, - statistic_ids: Iterable[str], + *, + statistic_ids: Iterable[str] | None = None, + statistic_type: Literal["mean"] | Literal["sum"] | None = None, + statistic_source: str | None = None, ) -> dict[str, tuple[int, StatisticMetaData]]: """Return metadata for statistic_ids.""" with session_scope(hass=hass) as session: - return get_metadata_with_session(hass, session, statistic_ids, None) + return get_metadata_with_session( + hass, + session, + statistic_ids=statistic_ids, + statistic_type=statistic_type, + statistic_source=statistic_source, + ) def _configured_unit(unit: str, units: UnitSystem) -> str: @@ -521,7 +542,9 @@ def list_statistic_ids( # Query the database with session_scope(hass=hass) as session: - metadata = get_metadata_with_session(hass, session, None, statistic_type) + metadata = get_metadata_with_session( + hass, session, statistic_type=statistic_type + ) for _, meta in metadata.values(): if (unit := meta["unit_of_measurement"]) is not None: @@ -693,7 +716,7 @@ def statistics_during_period( metadata = None with session_scope(hass=hass) as session: # Fetch metadata for the given (or all) statistic_ids - metadata = get_metadata_with_session(hass, session, statistic_ids, None) + metadata = get_metadata_with_session(hass, session, statistic_ids=statistic_ids) if not metadata: return {} @@ -744,7 +767,7 @@ def get_last_statistics( statistic_ids = [statistic_id] with session_scope(hass=hass) as session: # Fetch metadata for the given statistic_id - metadata = get_metadata_with_session(hass, session, statistic_ids, None) + metadata = get_metadata_with_session(hass, session, statistic_ids=statistic_ids) if not metadata: return {} diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index d98cfdb500e..d17f01ff47f 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -17,6 +17,7 @@ from homeassistant.components.recorder import ( statistics, util as recorder_util, ) +from homeassistant.components.recorder.const import DOMAIN as RECORDER_DOMAIN from homeassistant.components.recorder.models import ( StatisticData, StatisticMetaData, @@ -416,7 +417,7 @@ def _compile_statistics( # noqa: C901 sensor_states = _get_sensor_states(hass) wanted_statistics = _wanted_statistics(sensor_states) old_metadatas = statistics.get_metadata_with_session( - hass, session, [i.entity_id for i in sensor_states], None + hass, session, statistic_ids=[i.entity_id for i in sensor_states] ) # Get history between start and end @@ -656,7 +657,9 @@ def validate_statistics( validation_result = defaultdict(list) sensor_states = hass.states.all(DOMAIN) - metadatas = statistics.get_metadata(hass, [i.entity_id for i in sensor_states]) + metadatas = statistics.get_metadata(hass, statistic_source=RECORDER_DOMAIN) + sensor_entity_ids = {i.entity_id for i in sensor_states} + sensor_statistic_ids = set(metadatas) for state in sensor_states: entity_id = state.entity_id @@ -727,4 +730,15 @@ def validate_statistics( ) ) + for statistic_id in sensor_statistic_ids - sensor_entity_ids: + # There is no sensor matching the statistics_id + validation_result[statistic_id].append( + statistics.ValidationIssue( + "no_state", + { + "statistic_id": statistic_id, + }, + ) + ) + return validation_result diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 19dea9a8466..897652cef15 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -1945,7 +1945,7 @@ def test_compile_hourly_statistics_changing_statistics( assert statistic_ids == [ {"statistic_id": "sensor.test1", "unit_of_measurement": None} ] - metadata = get_metadata(hass, ("sensor.test1",)) + metadata = get_metadata(hass, statistic_ids=("sensor.test1",)) assert metadata == { "sensor.test1": ( 1, @@ -1970,7 +1970,7 @@ def test_compile_hourly_statistics_changing_statistics( assert statistic_ids == [ {"statistic_id": "sensor.test1", "unit_of_measurement": None} ] - metadata = get_metadata(hass, ("sensor.test1",)) + metadata = get_metadata(hass, statistic_ids=("sensor.test1",)) assert metadata == { "sensor.test1": ( 1, @@ -2521,7 +2521,15 @@ async def test_validate_statistics_supported_device_class( # Remove the state - empty response hass.states.async_remove("sensor.test") - await assert_validation_result(client, {}) + expected = { + "sensor.test": [ + { + "data": {"statistic_id": "sensor.test"}, + "type": "no_state", + } + ], + } + await assert_validation_result(client, expected) @pytest.mark.parametrize( @@ -2742,6 +2750,65 @@ async def test_validate_statistics_sensor_not_recorded( await assert_validation_result(client, expected) +@pytest.mark.parametrize( + "units, attributes, unit", + [ + (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"), + ], +) +async def test_validate_statistics_sensor_removed( + hass, hass_ws_client, units, attributes, unit +): + """Test validate_statistics.""" + id = 1 + + def next_id(): + nonlocal id + id += 1 + return id + + async def assert_validation_result(client, expected_result): + await client.send_json( + {"id": next_id(), "type": "recorder/validate_statistics"} + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == expected_result + + now = dt_util.utcnow() + + hass.config.units = units + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "sensor", {}) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + client = await hass_ws_client() + + # No statistics, no state - empty response + await assert_validation_result(client, {}) + + # No statistics, valid state - empty response + hass.states.async_set("sensor.test", 10, attributes=attributes) + await hass.async_block_till_done() + await assert_validation_result(client, {}) + + # Statistics has run, empty response + hass.data[DATA_INSTANCE].do_adhoc_statistics(start=now) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + await assert_validation_result(client, {}) + + # Sensor removed, expect error + hass.states.async_remove("sensor.test") + expected = { + "sensor.test": [ + { + "data": {"statistic_id": "sensor.test"}, + "type": "no_state", + } + ], + } + await assert_validation_result(client, expected) + + @pytest.mark.parametrize( "attributes", [BATTERY_SENSOR_ATTRIBUTES, NONE_SENSOR_ATTRIBUTES], @@ -2850,7 +2917,15 @@ async def test_validate_statistics_unsupported_device_class( # Remove the state - empty response hass.states.async_remove("sensor.test") - await assert_validation_result(client, {}) + expected = { + "sensor.test": [ + { + "data": {"statistic_id": "sensor.test"}, + "type": "no_state", + } + ], + } + await assert_validation_result(client, expected) def record_meter_states(hass, zero, entity_id, _attributes, seq): From 558c2556f1d9219da01f98715e51c32cdfa7ffb6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 20 Oct 2021 15:58:28 +0200 Subject: [PATCH 0579/1038] Report all unrecorded sensors in statistics tool (#58092) --- homeassistant/components/sensor/recorder.py | 14 ++++- tests/components/sensor/test_recorder.py | 61 ++++++++++++++++++++- 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index d17f01ff47f..9422e51f5a6 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -672,7 +672,7 @@ def validate_statistics( # Sensor was previously recorded, but no longer is validation_result[entity_id].append( statistics.ValidationIssue( - "entity_not_recorded", + "entity_no_longer_recorded", {"statistic_id": entity_id}, ) ) @@ -713,9 +713,19 @@ def validate_statistics( }, ) ) + elif state_class in STATE_CLASSES: + if not is_entity_recorded(hass, state.entity_id): + # Sensor is not recorded + validation_result[entity_id].append( + statistics.ValidationIssue( + "entity_not_recorded", + {"statistic_id": entity_id}, + ) + ) if ( - device_class in UNIT_CONVERSIONS + state_class in STATE_CLASSES + and device_class in UNIT_CONVERSIONS and state_unit not in UNIT_CONVERSIONS[device_class] ): # The unit in the state is not supported for this device class diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 897652cef15..aea53597c36 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -2694,7 +2694,7 @@ async def test_validate_statistics_unsupported_state_class( (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"), ], ) -async def test_validate_statistics_sensor_not_recorded( +async def test_validate_statistics_sensor_no_longer_recorded( hass, hass_ws_client, units, attributes, unit ): """Test validate_statistics.""" @@ -2735,6 +2735,58 @@ async def test_validate_statistics_sensor_not_recorded( await assert_validation_result(client, {}) # Sensor no longer recorded, expect error + expected = { + "sensor.test": [ + { + "data": {"statistic_id": "sensor.test"}, + "type": "entity_no_longer_recorded", + } + ], + } + with patch( + "homeassistant.components.sensor.recorder.is_entity_recorded", + return_value=False, + ): + await assert_validation_result(client, expected) + + +@pytest.mark.parametrize( + "units, attributes, unit", + [ + (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"), + ], +) +async def test_validate_statistics_sensor_not_recorded( + hass, hass_ws_client, units, attributes, unit +): + """Test validate_statistics.""" + id = 1 + + def next_id(): + nonlocal id + id += 1 + return id + + async def assert_validation_result(client, expected_result): + await client.send_json( + {"id": next_id(), "type": "recorder/validate_statistics"} + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == expected_result + + now = dt_util.utcnow() + + hass.config.units = units + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "sensor", {}) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + client = await hass_ws_client() + + # No statistics, no state - empty response + await assert_validation_result(client, {}) + + # Sensor not recorded, expect error expected = { "sensor.test": [ { @@ -2747,6 +2799,13 @@ async def test_validate_statistics_sensor_not_recorded( "homeassistant.components.sensor.recorder.is_entity_recorded", return_value=False, ): + hass.states.async_set("sensor.test", 10, attributes=attributes) + await hass.async_block_till_done() + await assert_validation_result(client, expected) + + # Statistics has run, expect same error + hass.data[DATA_INSTANCE].do_adhoc_statistics(start=now) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) await assert_validation_result(client, expected) From b301ab25a374ba1c8d24fc297b83223b7b5e96d4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 20 Oct 2021 16:00:59 +0200 Subject: [PATCH 0580/1038] Purge short term statistics (#58028) * Purge short term statistics * Less meep * Add tests --- homeassistant/components/recorder/purge.py | 78 ++++++++++++++- tests/components/recorder/test_purge.py | 109 ++++++++++++++++++++- 2 files changed, 179 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 2b84a439871..e44ae9aafff 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -6,11 +6,12 @@ from datetime import datetime import logging from typing import TYPE_CHECKING +from sqlalchemy import func from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import distinct from .const import MAX_ROWS_TO_PURGE -from .models import Events, RecorderRuns, States +from .models import Events, RecorderRuns, States, StatisticsRuns, StatisticsShortTerm from .repack import repack_database from .util import retryable_database_job, session_scope @@ -37,18 +38,32 @@ def purge_old_data( # Purge a max of MAX_ROWS_TO_PURGE, based on the oldest states or events record event_ids = _select_event_ids_to_purge(session, purge_before) state_ids = _select_state_ids_to_purge(session, purge_before, event_ids) + statistics_runs = _select_statistics_runs_to_purge(session, purge_before) + short_term_statistics = _select_short_term_statistics_to_purge( + session, purge_before + ) + if state_ids: _purge_state_ids(instance, session, state_ids) if event_ids: _purge_event_ids(session, event_ids) - # If states or events purging isn't processing the purge_before yet, - # return false, as we are not done yet. + + if statistics_runs: + _purge_statistics_runs(session, statistics_runs) + + if short_term_statistics: + _purge_short_term_statistics(session, short_term_statistics) + + if event_ids or statistics_runs or short_term_statistics: + # Return false, as we might not be done yet. _LOGGER.debug("Purging hasn't fully completed yet") return False + if apply_filter and _purge_filtered_data(instance, session) is False: _LOGGER.debug("Cleanup filtered data hasn't fully completed yet") return False + _purge_old_recorder_runs(instance, session, purge_before) if repack: repack_database(instance) @@ -83,6 +98,41 @@ def _select_state_ids_to_purge( return {state.state_id for state in states} +def _select_statistics_runs_to_purge( + session: Session, purge_before: datetime +) -> list[int]: + """Return a list of statistic runs to purge, but take care to keep the newest run.""" + statistic_runs = ( + session.query(StatisticsRuns.run_id) + .filter(StatisticsRuns.start < purge_before) + .limit(MAX_ROWS_TO_PURGE) + .all() + ) + statistic_runs_list = [run.run_id for run in statistic_runs] + # Exclude the newest statistics run + if ( + last_run := session.query(func.max(StatisticsRuns.run_id)).scalar() + ) and last_run in statistic_runs_list: + statistic_runs_list.remove(last_run) + + _LOGGER.debug("Selected %s statistic runs to remove", len(statistic_runs)) + return statistic_runs_list + + +def _select_short_term_statistics_to_purge( + session: Session, purge_before: datetime +) -> list[int]: + """Return a list of short term statistics to purge.""" + statistics = ( + session.query(StatisticsShortTerm.id) + .filter(StatisticsShortTerm.start < purge_before) + .limit(MAX_ROWS_TO_PURGE) + .all() + ) + _LOGGER.debug("Selected %s short term statistics to remove", len(statistics)) + return [statistic.id for statistic in statistics] + + def _purge_state_ids(instance: Recorder, session: Session, state_ids: set[int]) -> None: """Disconnect states and delete by state id.""" @@ -125,6 +175,28 @@ def _evict_purged_states_from_old_states_cache( old_states.pop(old_state_reversed[purged_state_id], None) +def _purge_statistics_runs(session: Session, statistics_runs: list[int]) -> None: + """Delete by run_id.""" + deleted_rows = ( + session.query(StatisticsRuns) + .filter(StatisticsRuns.run_id.in_(statistics_runs)) + .delete(synchronize_session=False) + ) + _LOGGER.debug("Deleted %s statistic runs", deleted_rows) + + +def _purge_short_term_statistics( + session: Session, short_term_statistics: list[int] +) -> None: + """Delete by id.""" + deleted_rows = ( + session.query(StatisticsShortTerm) + .filter(StatisticsShortTerm.id.in_(short_term_statistics)) + .delete(synchronize_session=False) + ) + _LOGGER.debug("Deleted %s short term statistics", deleted_rows) + + def _purge_event_ids(session: Session, event_ids: list[int]) -> None: """Delete by event id.""" deleted_rows = ( diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 0e66beecd87..8920843e8fe 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -10,7 +10,13 @@ from sqlalchemy.orm.session import Session from homeassistant.components import recorder from homeassistant.components.recorder import PurgeTask from homeassistant.components.recorder.const import MAX_ROWS_TO_PURGE -from homeassistant.components.recorder.models import Events, RecorderRuns, States +from homeassistant.components.recorder.models import ( + Events, + RecorderRuns, + States, + StatisticsRuns, + StatisticsShortTerm, +) from homeassistant.components.recorder.purge import purge_old_data from homeassistant.components.recorder.util import session_scope from homeassistant.const import EVENT_STATE_CHANGED @@ -227,6 +233,30 @@ async def test_purge_old_recorder_runs( assert recorder_runs.count() == 1 +async def test_purge_old_statistics_runs( + hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT +): + """Test deleting old statistics runs keeps the latest run.""" + instance = await async_setup_recorder_instance(hass) + + await _add_test_statistics_runs(hass, instance) + + # make sure we start with 7 statistics runs + with session_scope(hass=hass) as session: + statistics_runs = session.query(StatisticsRuns) + assert statistics_runs.count() == 7 + + purge_before = dt_util.utcnow() + + # run purge_old_data() + finished = purge_old_data(instance, purge_before, repack=False) + assert not finished + + finished = purge_old_data(instance, purge_before, repack=False) + assert finished + assert statistics_runs.count() == 1 + + async def test_purge_method( hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT, @@ -238,7 +268,9 @@ async def test_purge_method( service_data = {"keep_days": 4} await _add_test_events(hass, instance) await _add_test_states(hass, instance) + await _add_test_statistics(hass, instance) await _add_test_recorder_runs(hass, instance) + await _add_test_statistics_runs(hass, instance) await hass.async_block_till_done() await async_wait_recording_done(hass, instance) @@ -250,10 +282,17 @@ async def test_purge_method( events = session.query(Events).filter(Events.event_type.like("EVENT_TEST%")) assert events.count() == 6 + statistics = session.query(StatisticsShortTerm) + assert statistics.count() == 6 + recorder_runs = session.query(RecorderRuns) assert recorder_runs.count() == 7 runs_before_purge = recorder_runs.all() + statistics_runs = session.query(StatisticsRuns) + assert statistics_runs.count() == 7 + statistic_runs_before_purge = statistics_runs.all() + await hass.async_block_till_done() await async_wait_purge_done(hass, instance) @@ -264,9 +303,10 @@ async def test_purge_method( # Small wait for recorder thread await async_wait_purge_done(hass, instance) - # only purged old events + # only purged old states, events and statistics assert states.count() == 4 assert events.count() == 4 + assert statistics.count() == 4 # run purge method - correct service data await hass.services.async_call("recorder", "purge", service_data=service_data) @@ -275,11 +315,10 @@ async def test_purge_method( # Small wait for recorder thread await async_wait_purge_done(hass, instance) - # we should only have 2 states left after purging + # we should only have 2 states, events and statistics left after purging assert states.count() == 2 - - # now we should only have 2 events left assert events.count() == 2 + assert statistics.count() == 2 # now we should only have 3 recorder runs left runs = recorder_runs.all() @@ -287,6 +326,12 @@ async def test_purge_method( assert runs[1] == runs_before_purge[5] assert runs[2] == runs_before_purge[6] + # now we should only have 3 statistics runs left + runs = statistics_runs.all() + assert runs[0] == statistic_runs_before_purge[0] + assert runs[1] == statistic_runs_before_purge[5] + assert runs[2] == statistic_runs_before_purge[6] + assert "EVENT_TEST_PURGE" not in (event.event_type for event in events.all()) # run purge method - correct service data, with repack @@ -952,6 +997,35 @@ async def _add_test_events(hass: HomeAssistant, instance: recorder.Recorder): ) +async def _add_test_statistics(hass: HomeAssistant, instance: recorder.Recorder): + """Add multiple statistics to the db for testing.""" + utcnow = dt_util.utcnow() + five_days_ago = utcnow - timedelta(days=5) + eleven_days_ago = utcnow - timedelta(days=11) + + await hass.async_block_till_done() + await async_wait_recording_done(hass, instance) + + with recorder.session_scope(hass=hass) as session: + for event_id in range(6): + if event_id < 2: + timestamp = eleven_days_ago + state = "-11" + elif event_id < 4: + timestamp = five_days_ago + state = "-5" + else: + timestamp = utcnow + state = "0" + + session.add( + StatisticsShortTerm( + start=timestamp, + state=state, + ) + ) + + async def _add_test_recorder_runs(hass: HomeAssistant, instance: recorder.Recorder): """Add a few recorder_runs for testing.""" utcnow = dt_util.utcnow() @@ -979,6 +1053,31 @@ async def _add_test_recorder_runs(hass: HomeAssistant, instance: recorder.Record ) +async def _add_test_statistics_runs(hass: HomeAssistant, instance: recorder.Recorder): + """Add a few recorder_runs for testing.""" + utcnow = dt_util.utcnow() + five_days_ago = utcnow - timedelta(days=5) + eleven_days_ago = utcnow - timedelta(days=11) + + await hass.async_block_till_done() + await async_wait_recording_done(hass, instance) + + with recorder.session_scope(hass=hass) as session: + for rec_id in range(6): + if rec_id < 2: + timestamp = eleven_days_ago + elif rec_id < 4: + timestamp = five_days_ago + else: + timestamp = utcnow + + session.add( + StatisticsRuns( + start=timestamp, + ) + ) + + def _add_state_and_state_changed_event( session: Session, entity_id: str, From 326a302c22ca07482888691c593e72df3715546d Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 20 Oct 2021 17:06:27 +0200 Subject: [PATCH 0581/1038] Fix issue where Number still would send force_update to super method (#58110) --- homeassistant/components/deconz/number.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/number.py b/homeassistant/components/deconz/number.py index e7b9bf274e7..0ac355f7dd1 100644 --- a/homeassistant/components/deconz/number.py +++ b/homeassistant/components/deconz/number.py @@ -104,11 +104,11 @@ class DeconzNumber(DeconzDevice, NumberEntity): self._attr_step = description.step @callback - def async_update_callback(self, force_update: bool = False) -> None: + def async_update_callback(self) -> None: """Update the number value.""" keys = {self.entity_description.update_key, "reachable"} - if force_update or self._device.changed_keys.intersection(keys): - super().async_update_callback(force_update=force_update) + if self._device.changed_keys.intersection(keys): + super().async_update_callback() @property def value(self) -> float: From 9a58bfdf41699a61a4cdb97ef579e78c0610e547 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 20 Oct 2021 17:42:26 +0200 Subject: [PATCH 0582/1038] Use assignment expressions 17 (#57963) Co-authored-by: Tobias Sauerwein --- homeassistant/components/compensation/sensor.py | 3 +-- homeassistant/components/ios/notify.py | 4 +--- homeassistant/components/kodi/browse_media.py | 3 +-- homeassistant/components/kodi/config_flow.py | 6 ++---- homeassistant/components/kodi/media_player.py | 17 +++++------------ .../components/linksys_smart/device_tracker.py | 6 ++---- homeassistant/components/lovelace/dashboard.py | 4 +--- homeassistant/components/minio/__init__.py | 3 +-- .../components/rfxtrx/binary_sensor.py | 5 ++--- homeassistant/components/rfxtrx/config_flow.py | 6 ++---- homeassistant/components/rfxtrx/sensor.py | 12 ++++++------ homeassistant/components/statistics/sensor.py | 3 +-- .../components/synology_srm/device_tracker.py | 3 +-- .../components/tensorflow/image_processing.py | 6 ++---- homeassistant/components/xbee/__init__.py | 5 ++--- 15 files changed, 30 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py index 257c6b4a354..110326bc1b2 100644 --- a/homeassistant/components/compensation/sensor.py +++ b/homeassistant/components/compensation/sensor.py @@ -130,8 +130,7 @@ class CompensationSensor(SensorEntity): @callback def _async_compensation_sensor_state_listener(self, event): """Handle sensor state changes.""" - new_state = event.data.get("new_state") - if new_state is 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: diff --git a/homeassistant/components/ios/notify.py b/homeassistant/components/ios/notify.py index a096a43ac85..0c7ba59b533 100644 --- a/homeassistant/components/ios/notify.py +++ b/homeassistant/components/ios/notify.py @@ -76,9 +76,7 @@ class iOSNotificationService(BaseNotificationService): ): data[ATTR_TITLE] = kwargs.get(ATTR_TITLE) - targets = kwargs.get(ATTR_TARGET) - - if not targets: + if not (targets := kwargs.get(ATTR_TARGET)): targets = ios.enabled_push_ids(self.hass) if kwargs.get(ATTR_DATA) is not None: diff --git a/homeassistant/components/kodi/browse_media.py b/homeassistant/components/kodi/browse_media.py index c36f05fc0db..1b0c5d521c9 100644 --- a/homeassistant/components/kodi/browse_media.py +++ b/homeassistant/components/kodi/browse_media.py @@ -146,8 +146,7 @@ async def item_payload(item, get_thumbnail_url=None): elif "channelid" in item: media_content_type = MEDIA_TYPE_CHANNEL media_content_id = f"{item['channelid']}" - broadcasting = item.get("broadcastnow") - if broadcasting: + if broadcasting := item.get("broadcastnow"): show = broadcasting.get("title") title = f"{title} - {show}" can_play = True diff --git a/homeassistant/components/kodi/config_flow.py b/homeassistant/components/kodi/config_flow.py index ff3753af7ec..404540d47aa 100644 --- a/homeassistant/components/kodi/config_flow.py +++ b/homeassistant/components/kodi/config_flow.py @@ -57,8 +57,7 @@ async def validate_http(hass: core.HomeAssistant, data): async def validate_ws(hass: core.HomeAssistant, data): """Validate the user input allows us to connect over WS.""" - ws_port = data.get(CONF_WS_PORT) - if not ws_port: + if not (ws_port := data.get(CONF_WS_PORT)): return host = data[CONF_HOST] @@ -105,8 +104,7 @@ class KodiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._host = discovery_info["host"] self._port = int(discovery_info["port"]) self._name = discovery_info["hostname"][: -len(".local.")] - uuid = discovery_info["properties"].get("uuid") - if not uuid: + if not (uuid := discovery_info["properties"].get("uuid")): return self.async_abort(reason="no_uuid") self._discovery_name = discovery_info["name"] diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 9d23a5fa212..9fd46de026c 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -533,9 +533,7 @@ class KodiEntity(MediaPlayerEntity): if self._properties.get("live"): return None - total_time = self._properties.get("totaltime") - - if total_time is None: + if (total_time := self._properties.get("totaltime")) is None: return None return ( @@ -547,9 +545,7 @@ class KodiEntity(MediaPlayerEntity): @property def media_position(self): """Position of current playing media in seconds.""" - time = self._properties.get("time") - - if time is None: + if (time := self._properties.get("time")) is None: return None return time["hours"] * 3600 + time["minutes"] * 60 + time["seconds"] @@ -562,8 +558,7 @@ class KodiEntity(MediaPlayerEntity): @property def media_image_url(self): """Image url of current playing media.""" - thumbnail = self._item.get("thumbnail") - if thumbnail is None: + if (thumbnail := self._item.get("thumbnail")) is None: return None return self._kodi.thumbnail_url(thumbnail) @@ -598,8 +593,7 @@ class KodiEntity(MediaPlayerEntity): @property def media_artist(self): """Artist of current playing media, music track only.""" - artists = self._item.get("artist", []) - if artists: + if artists := self._item.get("artist"): return artists[0] return None @@ -607,8 +601,7 @@ class KodiEntity(MediaPlayerEntity): @property def media_album_artist(self): """Album artist of current playing media, music track only.""" - artists = self._item.get("albumartist", []) - if artists: + if artists := self._item.get("albumartist"): return artists[0] return None diff --git a/homeassistant/components/linksys_smart/device_tracker.py b/homeassistant/components/linksys_smart/device_tracker.py index 3761c047997..0ccfe36da24 100644 --- a/homeassistant/components/linksys_smart/device_tracker.py +++ b/homeassistant/components/linksys_smart/device_tracker.py @@ -66,13 +66,11 @@ class LinksysSmartWifiDeviceScanner(DeviceScanner): result = data["responses"][0] devices = result["output"]["devices"] for device in devices: - macs = device["knownMACAddresses"] - if not macs: + if not (macs := device["knownMACAddresses"]): _LOGGER.warning("Skipping device without known MAC address") continue mac = macs[-1] - connections = device["connections"] - if not connections: + if not device["connections"]: _LOGGER.debug("Device %s is not connected", mac) continue diff --git a/homeassistant/components/lovelace/dashboard.py b/homeassistant/components/lovelace/dashboard.py index bb043028ae6..3c9fb03d863 100644 --- a/homeassistant/components/lovelace/dashboard.py +++ b/homeassistant/components/lovelace/dashboard.py @@ -123,9 +123,7 @@ class LovelaceStorage(LovelaceConfig): if self._data is None: await self._load() - config = self._data["config"] - - if config is None: + if (config := self._data["config"]) is None: raise ConfigNotFound return config diff --git a/homeassistant/components/minio/__init__.py b/homeassistant/components/minio/__init__.py index f33dd6c389c..bd83b2b1d04 100644 --- a/homeassistant/components/minio/__init__.py +++ b/homeassistant/components/minio/__init__.py @@ -183,8 +183,7 @@ class QueueListener(threading.Thread): """Listen to queue events, and forward them to Home Assistant event bus.""" _LOGGER.info("Running QueueListener") while True: - event = self._queue.get() - if event is None: + if (event := self._queue.get()) is None: break _, file_name = os.path.split(event[ATTR_KEY]) diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index 788c5dec436..914303f1468 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -109,9 +109,8 @@ async def async_setup_entry( discovery_info = config_entry.data def get_sensor_description(type_string: str): - description = SENSOR_TYPES_DICT.get(type_string) - if description is None: - description = BinarySensorEntityDescription(key=type_string) + if (description := SENSOR_TYPES_DICT.get(type_string)) is None: + return BinarySensorEntityDescription(key=type_string) return description for packet_id, entity_info in discovery_info[CONF_DEVICES].items(): diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index 01bcc6ea035..be4ec111390 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -386,9 +386,8 @@ class OptionsFlow(config_entries.OptionsFlow): def _can_replace_device(self, entry_id): """Check if device can be replaced with selected device.""" device_data = self._get_device_data(entry_id) - event_code = device_data[CONF_EVENT_CODE] - if event_code is not None: + if (event_code := device_data[CONF_EVENT_CODE]) is not None: rfx_obj = get_rfx_object(event_code) if ( rfx_obj.device.packettype @@ -452,8 +451,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: - user_selection = user_input[CONF_TYPE] - if user_selection == "Serial": + if user_input[CONF_TYPE] == "Serial": return await self.async_step_setup_serial() return await self.async_step_setup_network() diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 68c29e20328..afd1d6a12ce 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -305,12 +305,12 @@ class RfxtrxSensor(RfxtrxEntity, SensorEntity): """Restore device state.""" await super().async_added_to_hass() - if self._event is None: - old_state = await self.async_get_last_state() - if old_state is not None: - event = old_state.attributes.get(ATTR_EVENT) - if event: - self._apply_event(get_rfx_object(event)) + if ( + self._event is None + and (old_state := await self.async_get_last_state()) is not None + and (event := old_state.attributes.get(ATTR_EVENT)) + ): + self._apply_event(get_rfx_object(event)) @property def native_value(self): diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index ea90346fe7c..51f6478b4c0 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -147,8 +147,7 @@ class StatisticsSensor(SensorEntity): @callback def async_stats_sensor_state_listener(event): """Handle the sensor state changes.""" - new_state = event.data.get("new_state") - if new_state is None: + if (new_state := event.data.get("new_state")) is None: return self._unit_of_measurement = new_state.attributes.get( diff --git a/homeassistant/components/synology_srm/device_tracker.py b/homeassistant/components/synology_srm/device_tracker.py index f97b1735e21..5981686e9b4 100644 --- a/homeassistant/components/synology_srm/device_tracker.py +++ b/homeassistant/components/synology_srm/device_tracker.py @@ -112,8 +112,7 @@ class SynologySrmDeviceScanner(DeviceScanner): if not device: return filtered_attributes for attribute, alias in ATTRIBUTE_ALIAS.items(): - value = device.get(attribute) - if value is None: + if (value := device.get(attribute)) is None: continue attr = alias or attribute filtered_attributes[attr] = value diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index f4e90a83154..323dbcb7882 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -247,8 +247,7 @@ class TensorFlowImageProcessor(ImageProcessingEntity): # Handle global detection area self._area = [0, 0, 1, 1] - area_config = model_config.get(CONF_AREA) - if area_config: + if area_config := model_config.get(CONF_AREA): self._area = [ area_config.get(CONF_TOP), area_config.get(CONF_LEFT), @@ -334,8 +333,7 @@ class TensorFlowImageProcessor(ImageProcessingEntity): def process_image(self, image): """Process the image.""" - model = self.hass.data[DOMAIN][CONF_MODEL] - if not model: + if not (model := self.hass.data[DOMAIN][CONF_MODEL]): _LOGGER.debug("Model not yet ready") return diff --git a/homeassistant/components/xbee/__init__.py b/homeassistant/components/xbee/__init__.py index 5ca9e4ef6f7..299d7052934 100644 --- a/homeassistant/components/xbee/__init__.py +++ b/homeassistant/components/xbee/__init__.py @@ -115,9 +115,8 @@ class XBeeConfig: If an address has been provided, unhexlify it, otherwise return None as we're talking to our local XBee device. """ - address = self._config.get("address") - if address is not None: - address = unhexlify(address) + if (address := self._config.get("address")) is not None: + return unhexlify(address) return address @property From d20936d1752ec3441c9b08f2a8180744c3a36c9f Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 20 Oct 2021 11:44:08 -0400 Subject: [PATCH 0583/1038] Fix referenced before assignment error in sonos speaker (#57924) --- homeassistant/components/sonos/speaker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 851711c2e12..549e4bacc9d 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -528,7 +528,7 @@ class SonosSpeaker: ) -> None: """Make this player unavailable when it was not seen recently.""" data = self.hass.data[DATA_SONOS] - if callback_timestamp and (zcname := data.mdns_names.get(self.soco.uid)): + if (zcname := data.mdns_names.get(self.soco.uid)) and callback_timestamp: # Called by a _seen_timer timeout, check mDNS one more time # This should not be checked in an "active" unseen scenario aiozeroconf = await zeroconf.async_get_async_instance(self.hass) From ea7252e3770ee9b40ef786ae7157503cf15713e0 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 20 Oct 2021 17:47:46 +0200 Subject: [PATCH 0584/1038] Use assignment expressions 21 (#57970) --- homeassistant/components/darksky/weather.py | 3 +-- homeassistant/components/edl21/sensor.py | 6 ++---- homeassistant/components/homekit/__init__.py | 6 ++---- homeassistant/components/homekit/accessories.py | 9 +++------ homeassistant/components/homekit/type_cameras.py | 6 ++---- .../components/homekit/type_media_players.py | 3 +-- homeassistant/components/homekit/type_remotes.py | 3 +-- homeassistant/components/homekit/type_thermostats.py | 12 ++++-------- .../components/imap_email_content/sensor.py | 3 +-- .../components/manual_mqtt/alarm_control_panel.py | 3 +-- homeassistant/components/philips_js/media_player.py | 9 +++------ homeassistant/components/somfy/climate.py | 3 +-- homeassistant/components/tts/__init__.py | 3 +-- homeassistant/components/vera/__init__.py | 4 +--- homeassistant/components/wemo/light.py | 3 +-- homeassistant/components/youless/sensor.py | 2 +- homeassistant/components/zhong_hong/climate.py | 6 ++---- 17 files changed, 28 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/darksky/weather.py b/homeassistant/components/darksky/weather.py index 0ad448ddfbd..b5aafd24c3d 100644 --- a/homeassistant/components/darksky/weather.py +++ b/homeassistant/components/darksky/weather.py @@ -91,8 +91,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): name = config.get(CONF_NAME) mode = config.get(CONF_MODE) - units = config.get(CONF_UNITS) - if not units: + if not (units := config.get(CONF_UNITS)): units = "ca" if hass.config.units.is_metric else "us" dark_sky = DarkSkyData(config.get(CONF_API_KEY), latitude, longitude, units) diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py index 407f5902198..d9997a9c1e3 100644 --- a/homeassistant/components/edl21/sensor.py +++ b/homeassistant/components/edl21/sensor.py @@ -178,8 +178,7 @@ class EDL21: new_entities = [] for telegram in message_body.get("valList", []): - obis = telegram.get("objName") - if not obis: + if not (obis := telegram.get("objName")): continue if (electricity_id, obis) in self._registered_obis: @@ -187,8 +186,7 @@ class EDL21: self._hass, SIGNAL_EDL21_TELEGRAM, electricity_id, telegram ) else: - name = self._OBIS_NAMES.get(obis) - if name: + if name := self._OBIS_NAMES.get(obis): if self._name: name = f"{self._name}: {name}" new_entities.append( diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 9ebc29a683f..2c93f9db61e 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -399,8 +399,7 @@ def _async_register_events_and_services(hass: HomeAssistant): referenced = async_extract_referenced_entity_ids(hass, service) dev_reg = device_registry.async_get(hass) for device_id in referenced.referenced_devices: - dev_reg_ent = dev_reg.async_get(device_id) - if not dev_reg_ent: + if not (dev_reg_ent := dev_reg.async_get(device_id)): raise HomeAssistantError(f"No device found for device id: {device_id}") macs = [ cval @@ -697,8 +696,7 @@ class HomeKit: if not self._filter(entity_id): continue - ent_reg_ent = ent_reg.async_get(entity_id) - if ent_reg_ent: + if ent_reg_ent := ent_reg.async_get(entity_id): await self._async_set_device_info_attributes( ent_reg_ent, dev_reg, entity_id ) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 3b7f2c0f9eb..30ee2e72589 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -413,8 +413,7 @@ class HomeAccessory(Accessory): @ha_callback def async_update_linked_battery_callback(self, event): """Handle linked battery sensor state change listener callback.""" - new_state = event.data.get("new_state") - if new_state is None: + if (new_state := event.data.get("new_state")) is None: return if self.linked_battery_charging_sensor: battery_charging_state = None @@ -425,8 +424,7 @@ class HomeAccessory(Accessory): @ha_callback def async_update_linked_battery_charging_callback(self, event): """Handle linked battery charging sensor state change listener callback.""" - new_state = event.data.get("new_state") - if new_state is None: + if (new_state := event.data.get("new_state")) is None: return self.async_update_battery(None, new_state.state == STATE_ON) @@ -524,8 +522,7 @@ class HomeBridge(Bridge): async def async_get_snapshot(self, info): """Get snapshot from accessory if supported.""" - acc = self.accessories.get(info["aid"]) - if acc is None: + if (acc := self.accessories.get(info["aid"])) is None: raise ValueError("Requested snapshot for missing accessory") if not hasattr(acc, "async_get_snapshot"): raise ValueError( diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index 2cdcd600932..6cf8735b075 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -314,8 +314,7 @@ class Camera(HomeAccessory, PyhapCamera): async def _async_get_stream_source(self): """Find the camera stream source url.""" - stream_source = self.config.get(CONF_STREAM_SOURCE) - if stream_source: + if stream_source := self.config.get(CONF_STREAM_SOURCE): return stream_source try: stream_source = await self.hass.components.camera.async_get_stream_source( @@ -447,8 +446,7 @@ class Camera(HomeAccessory, PyhapCamera): async def stop_stream(self, session_info): """Stop the stream for the given ``session_id``.""" session_id = session_info["id"] - stream = session_info.get("stream") - if not stream: + if not (stream := session_info.get("stream")): _LOGGER.debug("No stream for session ID %s", session_id) return diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index 7be1b98dcdb..61c043ebcaa 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -303,8 +303,7 @@ class TelevisionMediaPlayer(RemoteInputSelectAccessory): def set_remote_key(self, value): """Send remote key value if call came from HomeKit.""" _LOGGER.debug("%s: Set remote key to %s", self.entity_id, value) - key_name = REMOTE_KEYS.get(value) - if key_name is None: + if (key_name := REMOTE_KEYS.get(value)) is None: _LOGGER.warning("%s: Unhandled key press for %s", self.entity_id, value) return diff --git a/homeassistant/components/homekit/type_remotes.py b/homeassistant/components/homekit/type_remotes.py index 53659adef77..41a76ca7fed 100644 --- a/homeassistant/components/homekit/type_remotes.py +++ b/homeassistant/components/homekit/type_remotes.py @@ -207,8 +207,7 @@ class ActivityRemote(RemoteInputSelectAccessory): def set_remote_key(self, value): """Send remote key value if call came from HomeKit.""" _LOGGER.debug("%s: Set remote key to %s", self.entity_id, value) - key_name = REMOTE_KEYS.get(value) - if key_name is None: + if (key_name := REMOTE_KEYS.get(value)) is None: _LOGGER.warning("%s: Unhandled key press for %s", self.entity_id, value) return self.hass.bus.async_fire( diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index c75cc95b169..804f0b86167 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -462,8 +462,7 @@ class Thermostat(HomeAccessory): ) # Set current operation mode for supported thermostats - hvac_action = new_state.attributes.get(ATTR_HVAC_ACTION) - if hvac_action: + if hvac_action := new_state.attributes.get(ATTR_HVAC_ACTION): homekit_hvac_action = HC_HASS_TO_HOMEKIT_ACTION[hvac_action] self.char_current_heat_cool.set_value(homekit_hvac_action) @@ -575,8 +574,7 @@ class WaterHeater(HomeAccessory): def set_heat_cool(self, value): """Change operation mode to value if call came from HomeKit.""" _LOGGER.debug("%s: Set heat-cool to %d", self.entity_id, value) - hass_value = HC_HOMEKIT_TO_HASS[value] - if hass_value != HVAC_MODE_HEAT: + if HC_HOMEKIT_TO_HASS[value] != HVAC_MODE_HEAT: self.char_target_heat_cool.set_value(1) # Heat def set_target_temperature(self, value): @@ -615,14 +613,12 @@ class WaterHeater(HomeAccessory): def _get_temperature_range_from_state(state, unit, default_min, default_max): """Calculate the temperature range from a state.""" - min_temp = state.attributes.get(ATTR_MIN_TEMP) - if min_temp: + if min_temp := state.attributes.get(ATTR_MIN_TEMP): min_temp = round(temperature_to_homekit(min_temp, unit) * 2) / 2 else: min_temp = default_min - max_temp = state.attributes.get(ATTR_MAX_TEMP) - if max_temp: + if max_temp := state.attributes.get(ATTR_MAX_TEMP): max_temp = round(temperature_to_homekit(max_temp, unit) * 2) / 2 else: max_temp = default_max diff --git a/homeassistant/components/imap_email_content/sensor.py b/homeassistant/components/imap_email_content/sensor.py index 87c18a56bbe..e2dab5b9ef2 100644 --- a/homeassistant/components/imap_email_content/sensor.py +++ b/homeassistant/components/imap_email_content/sensor.py @@ -55,8 +55,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): config.get(CONF_FOLDER), ) - value_template = config.get(CONF_VALUE_TEMPLATE) - if value_template is not None: + if (value_template := config.get(CONF_VALUE_TEMPLATE)) is not None: value_template.hass = hass sensor = EmailContentSensor( hass, diff --git a/homeassistant/components/manual_mqtt/alarm_control_panel.py b/homeassistant/components/manual_mqtt/alarm_control_panel.py index 2fa0e631c1d..a94b1013782 100644 --- a/homeassistant/components/manual_mqtt/alarm_control_panel.py +++ b/homeassistant/components/manual_mqtt/alarm_control_panel.py @@ -450,8 +450,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanelEntity): async def _async_state_changed_listener(self, event): """Publish state change to MQTT.""" - new_state = event.data.get("new_state") - if new_state is None: + if (new_state := event.data.get("new_state")) is None: return mqtt.async_publish( self.hass, self._state_topic, new_state.state, self._qos, True diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 4499fb61e2a..1b2d5c25fd4 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -187,8 +187,7 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): async def async_select_source(self, source): """Set the input source.""" - source_id = _inverted(self._sources).get(source) - if source_id: + if source_id := _inverted(self._sources).get(source): await self._tv.setSource(source_id) await self._async_update_soon() @@ -316,8 +315,7 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): @property def app_name(self): """Name of the current running app.""" - app = self._tv.applications.get(self._tv.application_id) - if app: + if app := self._tv.applications.get(self._tv.application_id): return app.get("label") @property @@ -350,8 +348,7 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): else: _LOGGER.error("Unable to find channel <%s>", media_id) elif media_type == MEDIA_TYPE_APP: - app = self._tv.applications.get(media_id) - if app: + if app := self._tv.applications.get(media_id): await self._tv.setApplication(app["intent"]) await self._async_update_soon() else: diff --git a/homeassistant/components/somfy/climate.py b/homeassistant/components/somfy/climate.py index d4461e91e1b..88451f5b7e4 100644 --- a/homeassistant/components/somfy/climate.py +++ b/homeassistant/components/somfy/climate.py @@ -94,8 +94,7 @@ class SomfyClimate(SomfyEntity, ClimateEntity): def set_temperature(self, **kwargs) -> None: """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return self._climate.set_target(TargetMode.MANUAL, temperature, DurationType.NEXT_MODE) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 52b7c4aa034..00e1dae6a8c 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -427,8 +427,7 @@ class SpeechManager: This method is a coroutine. """ - filename = self.file_cache.get(key) - if not filename: + if not (filename := self.file_cache.get(key)): raise HomeAssistantError(f"Key {key} not in file cache!") voice_file = os.path.join(self.cache_dir, filename) diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index b6e13eb3832..3b7c11b943e 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -68,9 +68,7 @@ async def async_setup(hass: HomeAssistant, base_config: ConfigType) -> bool: """Set up for Vera controllers.""" hass.data[DOMAIN] = {} - config = base_config.get(DOMAIN) - - if not config: + if not (config := base_config.get(DOMAIN)): return True hass.async_create_task( diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index 4339e964b62..980675bd4ff 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -117,8 +117,7 @@ class WemoLight(WemoEntity, LightEntity): @property def hs_color(self): """Return the hs color values of this light.""" - xy_color = self.light.state.get("color_xy") - if xy_color: + if xy_color := self.light.state.get("color_xy"): return color_util.color_xy_to_hs(*xy_color) return None diff --git a/homeassistant/components/youless/sensor.py b/homeassistant/components/youless/sensor.py index d4465bd0c09..16ed918914d 100644 --- a/homeassistant/components/youless/sensor.py +++ b/homeassistant/components/youless/sensor.py @@ -35,7 +35,7 @@ async def async_setup_entry( """Initialize the integration.""" coordinator = hass.data[DOMAIN][entry.entry_id] device = entry.data[CONF_DEVICE] - if device is None: + if (device := entry.data[CONF_DEVICE]) is None: device = entry.entry_id async_add_entities( diff --git a/homeassistant/components/zhong_hong/climate.py b/homeassistant/components/zhong_hong/climate.py index f20c9f7e328..ad7b81dfe7b 100644 --- a/homeassistant/components/zhong_hong/climate.py +++ b/homeassistant/components/zhong_hong/climate.py @@ -239,12 +239,10 @@ class ZhongHongClimate(ClimateEntity): def set_temperature(self, **kwargs): """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is not None: + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is not None: self._device.set_temperature(temperature) - operation_mode = kwargs.get(ATTR_HVAC_MODE) - if operation_mode is not None: + if (operation_mode := kwargs.get(ATTR_HVAC_MODE)) is not None: self.set_hvac_mode(operation_mode) def set_hvac_mode(self, hvac_mode): From 5fc2897c084ad6106dcf1d1594d1d3b3c3291dca Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Oct 2021 18:24:34 +0200 Subject: [PATCH 0585/1038] Complete Ceiling Light (xdd) device support for Tuya (#58095) --- homeassistant/components/tuya/const.py | 2 ++ homeassistant/components/tuya/light.py | 6 +++++- homeassistant/components/tuya/switch.py | 10 ++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index f0a1d92c58c..9fc6a30e80c 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -150,6 +150,7 @@ class DPCode(str, Enum): CUR_POWER = "cur_power" # Actual power CUR_VOLTAGE = "cur_voltage" # Actual voltage DEHUMIDITY_SET_VALUE = "dehumidify_set_value" + DO_NOT_DISTURB = "do_not_disturb" DOORCONTACT_STATE = "doorcontact_state" # Status of door window sensor DOORCONTACT_STATE_2 = "doorcontact_state_3" DOORCONTACT_STATE_3 = "doorcontact_state_3" @@ -208,6 +209,7 @@ class DPCode(str, Enum): SWITCH_LED = "switch_led" # Switch SWITCH_LED_1 = "switch_led_1" SWITCH_LED_2 = "switch_led_2" + SWITCH_NIGHT_LIGHT = "switch_night_light" SWITCH_SPRAY = "switch_spray" # Spraying switch SWITCH_USB1 = "switch_usb1" # USB 1 SWITCH_USB2 = "switch_usb2" # USB 2 diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index ba0710c390c..c22050eb85e 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -130,7 +130,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { color_data=DPCode.COLOUR_DATA, ), ), - # Ceiling Light + # Ceiling Light # https://developer.tuya.com/en/docs/iot/ceiling-light?id=Kaiuz03xxfc4r "xdd": ( TuyaLightEntityDescription( @@ -140,6 +140,10 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { color_temp=DPCode.TEMP_VALUE, color_data=DPCode.COLOUR_DATA, ), + TuyaLightEntityDescription( + key=DPCode.SWITCH_NIGHT_LIGHT, + name="Night Light", + ), ), # Remote Control # https://developer.tuya.com/en/docs/iot/ykq?id=Kaof8ljn81aov diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index d4dddb5910a..2201ce30539 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -256,6 +256,16 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { entity_category=ENTITY_CATEGORY_CONFIG, ), ), + # Ceiling Light + # https://developer.tuya.com/en/docs/iot/ceiling-light?id=Kaiuz03xxfc4r + "xdd": ( + SwitchEntityDescription( + key=DPCode.DO_NOT_DISTURB, + name="Do not disturb", + icon="mdi:minus-circle-outline", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), # Diffuser # https://developer.tuya.com/en/docs/iot/categoryxxj?id=Kaiuz1f9mo6bl "xxj": ( From 3e57d0d3d30add2a7d126a499b4800c78e1043e4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Oct 2021 19:08:43 +0200 Subject: [PATCH 0586/1038] Complete Switch/Socket/Power Strip device support for Tuya (#58106) --- homeassistant/components/tuya/const.py | 5 +++ homeassistant/components/tuya/light.py | 16 +++++++++ homeassistant/components/tuya/select.py | 33 ++++++++++++++++++- .../components/tuya/strings.select.json | 14 ++++++++ .../tuya/translations/select.en.json | 14 ++++++++ 5 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/tuya/strings.select.json create mode 100644 homeassistant/components/tuya/translations/select.en.json diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 9fc6a30e80c..7825c919a31 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -75,6 +75,9 @@ CONF_PASSWORD = "password" CONF_COUNTRY_CODE = "country_code" CONF_APP_TYPE = "tuya_app_type" +DEVICE_CLASS_TUYA_RELAY_STATUS = "tuya__relay_status" +DEVICE_CLASS_TUYA_LIGHT_MODE = "tuya__light_mode" + TUYA_DISCOVERY_NEW = "tuya_discovery_new" TUYA_HA_SIGNAL_UPDATE_ENTITY = "tuya_entry_update" @@ -164,6 +167,7 @@ class DPCode(str, Enum): HUMIDITY_SET = "humidity_set" # Humidity setting HUMIDITY_VALUE = "humidity_value" # Humidity LIGHT = "light" # Light + LIGHT_MODE = "light_mode" LOCK = "lock" # Lock / Child lock MATERIAL = "material" # Material MODE = "mode" # Working mode / Mode @@ -183,6 +187,7 @@ class DPCode(str, Enum): PRESENCE_STATE = "presence_state" PUMP_RESET = "pump_reset" # Water pump reset RECORD_SWITCH = "record_switch" # Recording switch + RELAY_STATUS = "relay_status" SEEK = "seek" SENSITIVITY = "sensitivity" # Sensitivity SHAKE = "shake" # Oscillating diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index c22050eb85e..18aedb9366b 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -105,6 +105,14 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { color_data=DPCode.COLOUR_DATA, ), ), + # Switch + # https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s + "kg": ( + TuyaLightEntityDescription( + key=DPCode.SWITCH_BACKLIGHT, + name="Backlight", + ), + ), # Dimmer # https://developer.tuya.com/en/docs/iot/tgq?id=Kaof8ke9il4k4 "tgq": ( @@ -157,6 +165,14 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { ), } +# Socket (duplicate of `kg`) +# https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s +LIGHTS["cz"] = LIGHTS["kg"] + +# Power Socket (duplicate of `kg`) +# https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s +LIGHTS["pc"] = LIGHTS["kg"] + @dataclass class ColorTypeData: diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index 762c105632f..17d6a9c91cb 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -15,7 +15,13 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantTuyaData from .base import EnumTypeData, TuyaEntity -from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode +from .const import ( + DEVICE_CLASS_TUYA_LIGHT_MODE, + DEVICE_CLASS_TUYA_RELAY_STATUS, + DOMAIN, + TUYA_DISCOVERY_NEW, + DPCode, +) # All descriptions can be found here. Mostly the Enum data types in the # default instructions set of each category end up being a select. @@ -46,6 +52,22 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { icon="mdi:coffee", ), ), + # Switch + # https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s + "kg": ( + SelectEntityDescription( + key=DPCode.RELAY_STATUS, + name="Power on Behavior", + device_class=DEVICE_CLASS_TUYA_RELAY_STATUS, + entity_category=ENTITY_CATEGORY_CONFIG, + ), + SelectEntityDescription( + key=DPCode.LIGHT_MODE, + name="Indicator Light Mode", + device_class=DEVICE_CLASS_TUYA_LIGHT_MODE, + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), # Siren Alarm # https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu "sgbj": ( @@ -63,6 +85,15 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { } +# Socket (duplicate of `kg`) +# https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s +SELECTS["cz"] = SELECTS["kg"] + +# Power Socket (duplicate of `kg`) +# https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s +SELECTS["pc"] = SELECTS["kg"] + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: diff --git a/homeassistant/components/tuya/strings.select.json b/homeassistant/components/tuya/strings.select.json new file mode 100644 index 00000000000..18450f8d6ff --- /dev/null +++ b/homeassistant/components/tuya/strings.select.json @@ -0,0 +1,14 @@ +{ + "state": { + "tuya__light_mode": { + "none": "[%key:common::state::off%]", + "pos": "Indicate switch location", + "relay": "Indicate switch on/off state" + }, + "tuya__relay_status": { + "last": "Remember last state", + "power_off": "[%key:common::state::off%]", + "power_on": "[%key:common::state::on%]" + } + } +} diff --git a/homeassistant/components/tuya/translations/select.en.json b/homeassistant/components/tuya/translations/select.en.json new file mode 100644 index 00000000000..b1e8670f5d4 --- /dev/null +++ b/homeassistant/components/tuya/translations/select.en.json @@ -0,0 +1,14 @@ +{ + "state": { + "tuya__light_mode": { + "none": "Off", + "pos": "Indicate switch location", + "relay": "Indicate switch on/off state" + }, + "tuya__relay_status": { + "last": "Remember last state", + "power_off": "Off", + "power_on": "On" + } + } +} \ No newline at end of file From ad463b799419949a3fd17695c6f86487b3e6b6d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 20 Oct 2021 07:34:38 -1000 Subject: [PATCH 0587/1038] Ensure lutron_caseta triggers can still be attached in setup retry state (#57873) --- .../components/lutron_caseta/device_trigger.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index 3d1d179eac1..857ef9b56c5 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -22,6 +22,7 @@ from homeassistant.const import ( CONF_TYPE, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.typing import ConfigType from .const import ( @@ -255,6 +256,12 @@ async def async_get_triggers( return triggers +def _device_model_to_type(model: str) -> str: + """Convert a lutron_caseta device registry entry model to type.""" + _, device_type = model.split(" ") + return device_type.replace("(", "").replace(")", "") + + async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, @@ -262,15 +269,18 @@ async def async_attach_trigger( automation_info: AutomationTriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" - device = get_button_device_by_dr_id(hass, config[CONF_DEVICE_ID]) - schema = DEVICE_TYPE_SCHEMA_MAP.get(device["type"]) - valid_buttons = DEVICE_TYPE_SUBTYPE_MAP.get(device["type"]) + device_registry = dr.async_get(hass) + device = device_registry.async_get(config[CONF_DEVICE_ID]) + device_type = _device_model_to_type(device.model) + _, serial = list(device.identifiers)[0] + schema = DEVICE_TYPE_SCHEMA_MAP.get(device_type) + valid_buttons = DEVICE_TYPE_SUBTYPE_MAP.get(device_type) config = schema(config) event_config = { event_trigger.CONF_PLATFORM: CONF_EVENT, event_trigger.CONF_EVENT_TYPE: LUTRON_CASETA_BUTTON_EVENT, event_trigger.CONF_EVENT_DATA: { - ATTR_SERIAL: device["serial"], + ATTR_SERIAL: serial, ATTR_BUTTON_NUMBER: valid_buttons[config[CONF_SUBTYPE]], ATTR_ACTION: config[CONF_TYPE], }, From 2fcce7fd12a1cb003dce934b3f5a8c824a6accfc Mon Sep 17 00:00:00 2001 From: Yuval Aboulafia Date: Wed, 20 Oct 2021 21:17:46 +0300 Subject: [PATCH 0588/1038] Bump hdate to 0.10.4 (#58121) --- homeassistant/components/jewish_calendar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/jewish_calendar/manifest.json b/homeassistant/components/jewish_calendar/manifest.json index ec29a3e5d99..ef77dc04580 100644 --- a/homeassistant/components/jewish_calendar/manifest.json +++ b/homeassistant/components/jewish_calendar/manifest.json @@ -2,7 +2,7 @@ "domain": "jewish_calendar", "name": "Jewish Calendar", "documentation": "https://www.home-assistant.io/integrations/jewish_calendar", - "requirements": ["hdate==0.10.2"], + "requirements": ["hdate==0.10.4"], "codeowners": ["@tsvi"], "iot_class": "calculated" } diff --git a/requirements_all.txt b/requirements_all.txt index 8687ed3d09b..ed770e42b00 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -786,7 +786,7 @@ hass_splunk==0.1.1 hatasmota==0.2.21 # homeassistant.components.jewish_calendar -hdate==0.10.2 +hdate==0.10.4 # homeassistant.components.heatmiser heatmiserV3==1.1.18 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c22adf8b197..219cd3107b2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -479,7 +479,7 @@ hass-nabucasa==0.50.0 hatasmota==0.2.21 # homeassistant.components.jewish_calendar -hdate==0.10.2 +hdate==0.10.4 # homeassistant.components.here_travel_time herepy==2.0.0 From 333c80a6942eb624be52d40c8b2bd37785d2516f Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 20 Oct 2021 20:24:11 +0200 Subject: [PATCH 0589/1038] Assign entity category diagnostics to deCONZ tampering sensors (#58112) --- homeassistant/components/deconz/binary_sensor.py | 3 ++- tests/components/deconz/test_binary_sensor.py | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index b8af5a2971e..7f77bbb8809 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -22,7 +22,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.const import ATTR_TEMPERATURE +from homeassistant.const import ATTR_TEMPERATURE, ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -169,6 +169,7 @@ class DeconzTampering(DeconzDevice, BinarySensorEntity): TYPE = DOMAIN + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC _attr_device_class = DEVICE_CLASS_TAMPER def __init__(self, device, gateway): diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 96437800773..036e66f6e46 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -17,6 +17,7 @@ from homeassistant.components.deconz.services import SERVICE_DEVICE_REFRESH from homeassistant.const import ( ATTR_DEVICE_CLASS, DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, @@ -133,6 +134,8 @@ async def test_tampering_sensor(hass, aioclient_mock, mock_deconz_websocket): with patch.dict(DECONZ_WEB_REQUEST, data): config_entry = await setup_deconz_integration(hass, aioclient_mock) + ent_reg = er.async_get(hass) + assert len(hass.states.async_all()) == 3 presence_tamper = hass.states.get("binary_sensor.presence_sensor_tampered") assert presence_tamper.state == STATE_OFF @@ -149,6 +152,10 @@ async def test_tampering_sensor(hass, aioclient_mock, mock_deconz_websocket): await hass.async_block_till_done() assert hass.states.get("binary_sensor.presence_sensor_tampered").state == STATE_ON + assert ( + ent_reg.async_get("binary_sensor.presence_sensor_tampered").entity_category + == ENTITY_CATEGORY_DIAGNOSTIC + ) await hass.config_entries.async_unload(config_entry.entry_id) From 70fc3f84fc4f620bfad898c4a005171faec3c27c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 20 Oct 2021 12:24:38 -0600 Subject: [PATCH 0590/1038] Add entity categories for appropriate SimpliSafe entities (#58108) --- homeassistant/components/simplisafe/binary_sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/simplisafe/binary_sensor.py b/homeassistant/components/simplisafe/binary_sensor.py index 4afc2dab247..e7b156a5b1c 100644 --- a/homeassistant/components/simplisafe/binary_sensor.py +++ b/homeassistant/components/simplisafe/binary_sensor.py @@ -18,6 +18,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -101,6 +102,7 @@ class BatteryBinarySensor(SimpliSafeBaseSensor, BinarySensorEntity): """Define a SimpliSafe battery binary sensor entity.""" _attr_device_class = DEVICE_CLASS_BATTERY + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__( self, From 35e9cf68a2f9ba69319e9db1cee86d2271e0bcd3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 21 Oct 2021 07:25:29 +1300 Subject: [PATCH 0591/1038] Add configuration url to Sonarr (#58085) --- homeassistant/components/sonarr/entity.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/sonarr/entity.py b/homeassistant/components/sonarr/entity.py index d3f1b089d14..4d6eae8a669 100644 --- a/homeassistant/components/sonarr/entity.py +++ b/homeassistant/components/sonarr/entity.py @@ -35,10 +35,15 @@ class SonarrEntity(Entity): if self._device_id is None: return None + configuration_url = "https://" if self.sonarr.tls else "http://" + configuration_url += f"{self.sonarr.host}:{self.sonarr.port}" + configuration_url += self.sonarr.base_path.replace("/api", "") + return { ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)}, ATTR_NAME: "Activity Sensor", ATTR_MANUFACTURER: "Sonarr", ATTR_SW_VERSION: self.sonarr.app.info.version, "entry_type": "service", + "configuration_url": configuration_url, } From 398061706ca54c3f6c7567dc3b8459aa5e2534e3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 20 Oct 2021 20:28:48 +0200 Subject: [PATCH 0592/1038] Correct unit_of_measurement for statistics sensor (#58023) --- homeassistant/components/statistics/sensor.py | 11 +++++------ tests/components/statistics/test_sensor.py | 13 +++++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 51f6478b4c0..6bf882c861d 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -150,10 +150,6 @@ class StatisticsSensor(SensorEntity): if (new_state := event.data.get("new_state")) is None: return - self._unit_of_measurement = new_state.attributes.get( - ATTR_UNIT_OF_MEASUREMENT - ) - self._add_state_to_queue(new_state) self.async_schedule_update_ha_state(True) @@ -171,7 +167,7 @@ class StatisticsSensor(SensorEntity): if "recorder" in self.hass.config.components: # Only use the database if it's configured - self.hass.async_create_task(self._async_initialize_from_database()) + self.hass.async_create_task(self._initialize_from_database()) self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_START, async_stats_sensor_startup @@ -195,6 +191,9 @@ class StatisticsSensor(SensorEntity): self.entity_id, new_state.state, ) + return + + self._unit_of_measurement = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @property def name(self): @@ -355,7 +354,7 @@ class StatisticsSensor(SensorEntity): self.hass, _scheduled_update, next_to_purge_timestamp ) - async def _async_initialize_from_database(self): + async def _initialize_from_database(self): """Initialize the list of states from the database. The query will get the list of states in DESCENDING order so that we diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index bcbf13b8298..6cb53c9b93f 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -123,6 +123,18 @@ class TestStatisticsSensor(unittest.TestCase): assert self.change == state.attributes.get("change") assert self.average_change == state.attributes.get("average_change") + # Source sensor is unavailable, unit and state should not change + self.hass.states.set("sensor.test_monitored", "unavailable", {}) + self.hass.block_till_done() + new_state = self.hass.states.get("sensor.test") + assert state == new_state + + # Source sensor has a non float state, unit and state should not change + self.hass.states.set("sensor.test_monitored", "beer", {}) + self.hass.block_till_done() + new_state = self.hass.states.get("sensor.test") + assert state == new_state + def test_sampling_size(self): """Test rotation.""" assert setup_component( @@ -380,6 +392,7 @@ class TestStatisticsSensor(unittest.TestCase): # check if the result is as in test_sensor_source() state = self.hass.states.get("sensor.test") assert str(self.mean) == state.state + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS def test_initialize_from_database_with_maxage(self): """Test initializing the statistics from the database.""" From 487fa0a90518aac1fdb4d94f6fac251330d65f5c Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 20 Oct 2021 20:31:00 +0200 Subject: [PATCH 0593/1038] Use assignment expressions 20 (#57969) --- homeassistant/components/buienradar/camera.py | 3 +-- homeassistant/components/buienradar/sensor.py | 3 +-- homeassistant/components/buienradar/weather.py | 13 +++++++------ homeassistant/components/debugpy/__init__.py | 3 +-- homeassistant/components/mqtt/__init__.py | 13 ++++--------- .../components/mqtt/alarm_control_panel.py | 3 +-- homeassistant/components/mqtt/discovery.py | 3 +-- homeassistant/components/mqtt/light/schema_basic.py | 3 +-- homeassistant/components/upb/__init__.py | 3 +-- homeassistant/components/upb/config_flow.py | 3 +-- homeassistant/components/upb/light.py | 3 +-- .../components/zha/core/channels/homeautomation.py | 3 +-- .../components/zha/core/channels/smartenergy.py | 3 +-- homeassistant/components/zha/core/discovery.py | 6 ++---- homeassistant/components/zha/core/gateway.py | 6 ++---- homeassistant/components/zha/core/helpers.py | 3 +-- homeassistant/components/zha/core/store.py | 4 +--- homeassistant/components/zha/light.py | 3 +-- 18 files changed, 29 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/buienradar/camera.py b/homeassistant/components/buienradar/camera.py index 91e4bcffb17..e68b096ca05 100644 --- a/homeassistant/components/buienradar/camera.py +++ b/homeassistant/components/buienradar/camera.py @@ -131,8 +131,7 @@ class BuienradarCam(Camera): _LOGGER.debug("HTTP 304 - success") return True - last_modified = res.headers.get("Last-Modified") - if last_modified: + if last_modified := res.headers.get("Last-Modified"): self._last_modified = last_modified self._last_image = await res.read() diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index 2c6390f959b..0affe1e2c62 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -786,8 +786,7 @@ class BrSensor(SensorEntity): if sensor_type == SYMBOL or sensor_type.startswith(CONDITION): # update weather symbol & status text - condition = data.get(CONDITION) - if condition: + if condition := data.get(CONDITION): if sensor_type == SYMBOL: new_state = condition.get(EXACTNL) if sensor_type == CONDITION: diff --git a/homeassistant/components/buienradar/weather.py b/homeassistant/components/buienradar/weather.py index a1546120064..aa336d3929c 100644 --- a/homeassistant/components/buienradar/weather.py +++ b/homeassistant/components/buienradar/weather.py @@ -133,12 +133,13 @@ class BrWeather(WeatherEntity): @property def condition(self): """Return the current condition.""" - if self._data and self._data.condition: - ccode = self._data.condition.get(CONDCODE) - if ccode: - conditions = self.hass.data[DOMAIN].get(DATA_CONDITION) - if conditions: - return conditions.get(ccode) + if ( + self._data + and self._data.condition + and (ccode := self._data.condition.get(CONDCODE)) + and (conditions := self.hass.data[DOMAIN].get(DATA_CONDITION)) + ): + return conditions.get(ccode) @property def temperature(self): diff --git a/homeassistant/components/debugpy/__init__.py b/homeassistant/components/debugpy/__init__.py index 613ecfd8ffa..21cfeb15a80 100644 --- a/homeassistant/components/debugpy/__init__.py +++ b/homeassistant/components/debugpy/__init__.py @@ -48,8 +48,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: debugpy.listen((conf[CONF_HOST], conf[CONF_PORT])) - wait = conf[CONF_WAIT] - if wait: + if conf[CONF_WAIT]: _LOGGER.warning( "Waiting for remote debug connection on %s:%s", conf[CONF_HOST], diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 9ddf50aad43..994ae2d108a 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -598,8 +598,7 @@ class MQTT: """ self = hass.data[DATA_MQTT] - conf = hass.data.get(DATA_MQTT_CONFIG) - if conf is None: + if (conf := hass.data.get(DATA_MQTT_CONFIG)) is None: conf = CONFIG_SCHEMA({DOMAIN: dict(entry.data)})[DOMAIN] self.conf = _merge_config(entry, conf) @@ -622,8 +621,7 @@ class MQTT: else: proto = mqtt.MQTTv311 - client_id = self.conf.get(CONF_CLIENT_ID) - if client_id is None: + if (client_id := self.conf.get(CONF_CLIENT_ID)) is None: # PAHO MQTT relies on the MQTT server to generate random client IDs. # However, that feature is not mandatory so we generate our own. client_id = mqtt.base62(uuid.uuid4().int, padding=22) @@ -637,9 +635,7 @@ class MQTT: if username is not None: self._mqttc.username_pw_set(username, password) - certificate = self.conf.get(CONF_CERTIFICATE) - - if certificate == "auto": + if (certificate := self.conf.get(CONF_CERTIFICATE)) == "auto": certificate = certifi.where() client_key = self.conf.get(CONF_CLIENT_KEY) @@ -1002,8 +998,7 @@ async def websocket_remove_device(hass, connection, msg): device_id = msg["device_id"] dev_registry = await hass.helpers.device_registry.async_get_registry() - device = dev_registry.async_get(device_id) - if not device: + if not (device := dev_registry.async_get(device_id)): connection.send_error( msg["id"], websocket_api.const.ERR_NOT_FOUND, "Device not found" ) diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 3d632baf0f7..825ad345272 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -205,8 +205,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): @property def code_format(self): """Return one or more digits/characters.""" - code = self._config.get(CONF_CODE) - if code is None: + if (code := self._config.get(CONF_CODE)) is None: return None if code == REMOTE_CODE or (isinstance(code, str) and re.search("^\\d+$", code)): return alarm.FORMAT_NUMBER diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 5678f31f5b2..a237ea7aea1 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -142,8 +142,7 @@ async def async_start( # noqa: C901 payload[key] = f"{value[:-1]}{base}" if payload.get(CONF_AVAILABILITY): for availability_conf in payload[CONF_AVAILABILITY]: - topic = availability_conf.get(CONF_TOPIC) - if topic: + if topic := availability_conf.get(CONF_TOPIC): if topic[0] == TOPIC_BASE: availability_conf[CONF_TOPIC] = f"{base}{topic[1:]}" if topic[-1] == TOPIC_BASE: diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 409eb9d648c..cb350ae4c9c 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -826,8 +826,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): def render_rgbx(color, template, color_mode): """Render RGBx payload.""" - tpl = self._command_templates[template] - if tpl: + if tpl := self._command_templates[template]: keys = ["red", "green", "blue"] if color_mode == COLOR_MODE_RGBW: keys.append("white") diff --git a/homeassistant/components/upb/__init__.py b/homeassistant/components/upb/__init__.py index 7b3b30fdb29..a3c3016dc05 100644 --- a/homeassistant/components/upb/__init__.py +++ b/homeassistant/components/upb/__init__.py @@ -31,8 +31,7 @@ async def async_setup_entry(hass, config_entry): hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) def _element_changed(element, changeset): - change = changeset.get("last_change") - if change is None: + if (change := changeset.get("last_change")) is None: return if change.get("command") is None: return diff --git a/homeassistant/components/upb/config_flow.py b/homeassistant/components/upb/config_flow.py index 3268b5af9ab..c920fa405ef 100644 --- a/homeassistant/components/upb/config_flow.py +++ b/homeassistant/components/upb/config_flow.py @@ -62,8 +62,7 @@ async def _validate_input(data): def _make_url_from_data(data): - host = data.get(CONF_HOST) - if host: + if host := data.get(CONF_HOST): return host protocol = PROTOCOL_MAP[data[CONF_PROTOCOL]] diff --git a/homeassistant/components/upb/light.py b/homeassistant/components/upb/light.py index 404e45e0c62..22ae5925a72 100644 --- a/homeassistant/components/upb/light.py +++ b/homeassistant/components/upb/light.py @@ -67,8 +67,7 @@ class UpbLight(UpbAttachedEntity, LightEntity): async def async_turn_on(self, **kwargs): """Turn on the light.""" - flash = kwargs.get(ATTR_FLASH) - if flash: + if flash := kwargs.get(ATTR_FLASH): await self.async_light_blink(0.5 if flash == "short" else 1.5) else: rate = kwargs.get(ATTR_TRANSITION, -1) diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py index e25cc3eb0da..89a9d51395e 100644 --- a/homeassistant/components/zha/core/channels/homeautomation.py +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -139,8 +139,7 @@ class ElectricalMeasurementChannel(ZigbeeChannel): @property def measurement_type(self) -> str | None: """Return Measurement type.""" - meas_type = self.cluster.get("measurement_type") - if meas_type is None: + if (meas_type := self.cluster.get("measurement_type")) is None: return None meas_type = self.MeasurementType(meas_type) diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index f3f0e76a5fb..5877dad14fa 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -138,8 +138,7 @@ class Metering(ZigbeeChannel): @property def status(self) -> int | None: """Return metering device status.""" - status = self.cluster.get("status") - if status is None: + if (status := self.cluster.get("status")) is None: return None if self.cluster.get("metering_device_type") == 0: # Electric metering device type diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 43a0186d88b..df257dbbecc 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -206,8 +206,7 @@ class ProbeEndpoint: def initialize(self, hass: HomeAssistant) -> None: """Update device overrides config.""" zha_config = hass.data[zha_const.DATA_ZHA].get(zha_const.DATA_ZHA_CONFIG, {}) - overrides = zha_config.get(zha_const.CONF_DEVICE_CONFIG) - if overrides: + if overrides := zha_config.get(zha_const.CONF_DEVICE_CONFIG): self._device_configs.update(overrides) @@ -237,8 +236,7 @@ class GroupProbe: def _reprobe_group(self, group_id: int) -> None: """Reprobe a group for entities after its members change.""" zha_gateway = self._hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY] - zha_group = zha_gateway.groups.get(group_id) - if zha_group is None: + if (zha_group := zha_gateway.groups.get(group_id)) is None: return self.discover_group_entities(zha_group) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 4e793b39a8a..252893683ef 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -494,8 +494,7 @@ class ZHAGateway: self, zigpy_device: zha_typing.ZigpyDeviceType, restored: bool = False ): """Get or create a ZHA device.""" - zha_device = self._devices.get(zigpy_device.ieee) - if zha_device is None: + if (zha_device := self._devices.get(zigpy_device.ieee)) is None: zha_device = ZHADevice.new(self._hass, zigpy_device, self, restored) self._devices[zigpy_device.ieee] = zha_device device_registry_device = self.ha_device_registry.async_get_or_create( @@ -649,8 +648,7 @@ class ZHAGateway: async def async_remove_zigpy_group(self, group_id: int) -> None: """Remove a Zigbee group from Zigpy.""" - group = self.groups.get(group_id) - if not group: + if not (group := self.groups.get(group_id)): _LOGGER.debug("Group: %s:0x%04x could not be found", group.name, group_id) return if group.members: diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 47ee682b46e..d150ab9df45 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -167,8 +167,7 @@ async def async_get_zha_device(hass, device_id): def find_state_attributes(states: list[State], key: str) -> Iterator[Any]: """Find attributes with matching key from states.""" for state in states: - value = state.attributes.get(key) - if value is not None: + if (value := state.attributes.get(key)) is not None: yield value diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py index f1b2ee57aee..5d33375ace2 100644 --- a/homeassistant/components/zha/core/store.py +++ b/homeassistant/components/zha/core/store.py @@ -131,9 +131,7 @@ class ZhaStorage: @bind_hass async def async_get_registry(hass: HomeAssistant) -> ZhaStorage: """Return zha device storage instance.""" - task = hass.data.get(DATA_REGISTRY) - - if task is None: + if (task := hass.data.get(DATA_REGISTRY)) is None: async def _load_reg() -> ZhaStorage: registry = ZhaStorage(hass) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index a340ffae736..9398d0fde17 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -481,8 +481,7 @@ class Light(BaseLight, ZhaEntity): attributes, from_cache=False ) - color_mode = results.get("color_mode") - if color_mode is not None: + if (color_mode := results.get("color_mode")) is not None: if color_mode == LightColorMode.COLOR_TEMP: color_temp = results.get("color_temperature") if color_temp is not None and color_mode: From c204196a7ad1860897e2ecfb189f7bd1d6a9e219 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Oct 2021 20:49:10 +0200 Subject: [PATCH 0594/1038] Add Formaldehyde Detector (jqbj) device support to Tuya (#58118) --- .../components/tuya/binary_sensor.py | 10 ++++ homeassistant/components/tuya/const.py | 6 +++ homeassistant/components/tuya/sensor.py | 46 ++++++++++++++++++- 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index 934f1fda7ef..8adf930641c 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -70,6 +70,16 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { on_value="presence", ), ), + # Formaldehyde Detector + # Note: Not documented + "jqbj": ( + TuyaBinarySensorEntityDescription( + key=DPCode.CH2O_STATE, + device_class=DEVICE_CLASS_SAFETY, + on_value="alarm", + ), + TAMPER_BINARY_SENSOR, + ), # Door Window Sensor # https://developer.tuya.com/en/docs/iot/s?id=K9gf48hm02l8m "mcs": ( diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 7825c919a31..e445116d39c 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -138,6 +138,8 @@ class DPCode(str, Enum): BRIGHT_VALUE_2 = "bright_value_2" BRIGHT_VALUE_V2 = "bright_value_v2" C_F = "c_f" # Temperature unit switching + CH2O_STATE = "ch2o_state" + CH2O_VALUE = "ch2o_value" CHILD_LOCK = "child_lock" # Child lock CO2_STATE = "co2_state" CO2_VALUE = "co2_value" # CO2 concentration @@ -182,6 +184,7 @@ class DPCode(str, Enum): PERCENT_STATE_2 = "percent_state_2" PERCENT_STATE_3 = "percent_state_3" PIR = "pir" # Motion sensor + PM25_VALUE = "pm25_value" POWDER_SET = "powder_set" # Powder POWER_GO = "power_go" PRESENCE_STATE = "presence_state" @@ -234,6 +237,9 @@ class DPCode(str, Enum): TEMP_VALUE_V2 = "temp_value_v2" TEMPER_ALARM = "temper_alarm" # Tamper alarm UV = "uv" # UV sterilization + VA_HUMIDITY = "va_humidity" + VA_TEMPERATURE = "va_temperature" + VOC_VALUE = "voc_value" WARM = "warm" # Heat preservation WARM_TIME = "warm_time" # Heat preservation time WATER_RESET = "water_reset" # Resetting of water usage days diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 469d2a2e83c..c1407652d92 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -18,8 +18,10 @@ from homeassistant.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_PM25, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, DEVICE_CLASS_VOLTAGE, ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, @@ -79,7 +81,7 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { ), SensorEntityDescription( key=DPCode.CO2_VALUE, - name="Carbon Dioxide (CO2)", + name="Carbon Dioxide", device_class=DEVICE_CLASS_CO2, state_class=STATE_CLASS_MEASUREMENT, ), @@ -110,6 +112,46 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { entity_registry_enabled_default=False, ), ), + # Formaldehyde Detector + # Note: Not documented + "jqbj": ( + SensorEntityDescription( + key=DPCode.CO2_VALUE, + name="Carbon Dioxide", + device_class=DEVICE_CLASS_CO2, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.VOC_VALUE, + name="Volatile Organic Compound", + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.PM25_VALUE, + name="Particulate Matter 2.5 µm", + device_class=DEVICE_CLASS_PM25, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.VA_HUMIDITY, + name="Humidity", + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.VA_TEMPERATURE, + name="Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.CH2O_VALUE, + name="Formaldehyde", + state_class=STATE_CLASS_MEASUREMENT, + ), + *BATTERY_SENSORS, + ), # Luminance Sensor # https://developer.tuya.com/en/docs/iot/categoryldcg?id=Kaiuz3n7u69l8 "ldcg": ( @@ -138,7 +180,7 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { ), SensorEntityDescription( key=DPCode.CO2_VALUE, - name="Carbon Dioxide (CO2)", + name="Carbon Dioxide", device_class=DEVICE_CLASS_CO2, state_class=STATE_CLASS_MEASUREMENT, ), From 7353ea5416edf093bcd0f48af1b902bea4eb6dc8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Oct 2021 23:19:42 +0200 Subject: [PATCH 0595/1038] Add Dimmer Switch (tgkg) device support to Tuya (#58134) --- homeassistant/components/tuya/const.py | 8 ++++- homeassistant/components/tuya/light.py | 19 ++++++++++ homeassistant/components/tuya/select.py | 35 +++++++++++++++++++ .../components/tuya/strings.select.json | 8 +++++ .../tuya/translations/select.en.json | 8 +++++ 5 files changed, 77 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index e445116d39c..a57ee5e397e 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -75,8 +75,9 @@ CONF_PASSWORD = "password" CONF_COUNTRY_CODE = "country_code" CONF_APP_TYPE = "tuya_app_type" -DEVICE_CLASS_TUYA_RELAY_STATUS = "tuya__relay_status" +DEVICE_CLASS_TUYA_LED_TYPE = "tuya__led_type" DEVICE_CLASS_TUYA_LIGHT_MODE = "tuya__light_mode" +DEVICE_CLASS_TUYA_RELAY_STATUS = "tuya__relay_status" TUYA_DISCOVERY_NEW = "tuya_discovery_new" TUYA_HA_SIGNAL_UPDATE_ENTITY = "tuya_entry_update" @@ -136,6 +137,7 @@ class DPCode(str, Enum): BRIGHT_VALUE = "bright_value" # Brightness BRIGHT_VALUE_1 = "bright_value_1" BRIGHT_VALUE_2 = "bright_value_2" + BRIGHT_VALUE_3 = "bright_value_3" BRIGHT_VALUE_V2 = "bright_value_v2" C_F = "c_f" # Temperature unit switching CH2O_STATE = "ch2o_state" @@ -168,6 +170,9 @@ class DPCode(str, Enum): HUMIDITY_CURRENT = "humidity_current" # Current humidity HUMIDITY_SET = "humidity_set" # Humidity setting HUMIDITY_VALUE = "humidity_value" # Humidity + LED_TYPE_1 = "led_type_1" + LED_TYPE_2 = "led_type_2" + LED_TYPE_3 = "led_type_3" LIGHT = "light" # Light LIGHT_MODE = "light_mode" LOCK = "lock" # Lock / Child lock @@ -217,6 +222,7 @@ class DPCode(str, Enum): SWITCH_LED = "switch_led" # Switch SWITCH_LED_1 = "switch_led_1" SWITCH_LED_2 = "switch_led_2" + SWITCH_LED_3 = "switch_led_3" SWITCH_NIGHT_LIGHT = "switch_night_light" SWITCH_SPRAY = "switch_spray" # Spraying switch SWITCH_USB1 = "switch_usb1" # USB 1 diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 18aedb9366b..308c0037d0b 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -113,6 +113,25 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { name="Backlight", ), ), + # Dimmer Switch + # https://developer.tuya.com/en/docs/iot/categorytgkg?id=Kaiuz0ktx7m0o + "tgkg": ( + TuyaLightEntityDescription( + key=DPCode.SWITCH_LED_1, + name="Light", + brightness=DPCode.BRIGHT_VALUE_1, + ), + TuyaLightEntityDescription( + key=DPCode.SWITCH_LED_2, + name="Light 2", + brightness=DPCode.BRIGHT_VALUE_2, + ), + TuyaLightEntityDescription( + key=DPCode.SWITCH_LED_3, + name="Light 3", + brightness=DPCode.BRIGHT_VALUE_3, + ), + ), # Dimmer # https://developer.tuya.com/en/docs/iot/tgq?id=Kaof8ke9il4k4 "tgq": ( diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index 17d6a9c91cb..fe6c030f0bc 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -16,6 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantTuyaData from .base import EnumTypeData, TuyaEntity from .const import ( + DEVICE_CLASS_TUYA_LED_TYPE, DEVICE_CLASS_TUYA_LIGHT_MODE, DEVICE_CLASS_TUYA_RELAY_STATUS, DOMAIN, @@ -82,6 +83,40 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { entity_category=ENTITY_CATEGORY_CONFIG, ), ), + # Dimmer Switch + # https://developer.tuya.com/en/docs/iot/categorytgkg?id=Kaiuz0ktx7m0o + "tgkg": ( + SelectEntityDescription( + key=DPCode.RELAY_STATUS, + name="Power on Behavior", + device_class=DEVICE_CLASS_TUYA_RELAY_STATUS, + entity_category=ENTITY_CATEGORY_CONFIG, + ), + SelectEntityDescription( + key=DPCode.LIGHT_MODE, + name="Indicator Light Mode", + device_class=DEVICE_CLASS_TUYA_LIGHT_MODE, + entity_category=ENTITY_CATEGORY_CONFIG, + ), + SelectEntityDescription( + key=DPCode.LED_TYPE_1, + name="Light Source Type", + device_class=DEVICE_CLASS_TUYA_LED_TYPE, + entity_category=ENTITY_CATEGORY_CONFIG, + ), + SelectEntityDescription( + key=DPCode.LED_TYPE_2, + name="Light 2 Source Type", + device_class=DEVICE_CLASS_TUYA_LED_TYPE, + entity_category=ENTITY_CATEGORY_CONFIG, + ), + SelectEntityDescription( + key=DPCode.LED_TYPE_3, + name="Light 3 Source Type", + device_class=DEVICE_CLASS_TUYA_LED_TYPE, + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), } diff --git a/homeassistant/components/tuya/strings.select.json b/homeassistant/components/tuya/strings.select.json index 18450f8d6ff..38d3e7c4a20 100644 --- a/homeassistant/components/tuya/strings.select.json +++ b/homeassistant/components/tuya/strings.select.json @@ -1,5 +1,10 @@ { "state": { + "tuya__led_type": { + "halogen": "Halogen", + "incandescent": "Incandescent", + "led": "LED" + }, "tuya__light_mode": { "none": "[%key:common::state::off%]", "pos": "Indicate switch location", @@ -7,6 +12,9 @@ }, "tuya__relay_status": { "last": "Remember last state", + "memory": "Remember last state", + "off": "[%key:common::state::off%]", + "on": "[%key:common::state::on%]", "power_off": "[%key:common::state::off%]", "power_on": "[%key:common::state::on%]" } diff --git a/homeassistant/components/tuya/translations/select.en.json b/homeassistant/components/tuya/translations/select.en.json index b1e8670f5d4..70ef9486196 100644 --- a/homeassistant/components/tuya/translations/select.en.json +++ b/homeassistant/components/tuya/translations/select.en.json @@ -1,5 +1,10 @@ { "state": { + "tuya__led_type": { + "halogen": "Halogen", + "incandescent": "Incandescent", + "led": "LED" + }, "tuya__light_mode": { "none": "Off", "pos": "Indicate switch location", @@ -7,6 +12,9 @@ }, "tuya__relay_status": { "last": "Remember last state", + "memory": "Remember last state", + "off": "Off", + "on": "On", "power_off": "Off", "power_on": "On" } From 4513ee4ea5146d344d64138b01ddaba6a786ec5f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 20 Oct 2021 23:34:08 +0200 Subject: [PATCH 0596/1038] Use assignment expressions 12 (#57937) --- homeassistant/components/adax/climate.py | 3 +-- homeassistant/components/asuswrt/__init__.py | 3 +-- homeassistant/components/blueprint/models.py | 3 +-- homeassistant/components/enocean/light.py | 3 +-- homeassistant/components/geo_location/trigger.py | 3 +-- homeassistant/components/google_cloud/tts.py | 3 +-- homeassistant/components/isy994/climate.py | 9 +++------ homeassistant/components/isy994/sensor.py | 3 +-- homeassistant/components/octoprint/__init__.py | 3 +-- homeassistant/components/plaato/__init__.py | 3 +-- homeassistant/components/plex/config_flow.py | 3 +-- homeassistant/components/plex/server.py | 3 +-- homeassistant/components/prometheus/__init__.py | 9 +++------ homeassistant/components/remote/reproduce_state.py | 4 +--- homeassistant/components/shelly/__init__.py | 6 ++---- homeassistant/components/shopping_list/__init__.py | 9 +++------ homeassistant/components/sma/__init__.py | 3 +-- homeassistant/components/utility_meter/sensor.py | 3 +-- homeassistant/components/volumio/browse_media.py | 6 ++---- homeassistant/components/water_heater/reproduce_state.py | 4 +--- homeassistant/components/zabbix/sensor.py | 6 ++---- 21 files changed, 30 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/adax/climate.py b/homeassistant/components/adax/climate.py index 7cc23c048fe..783c2a9f2f8 100644 --- a/homeassistant/components/adax/climate.py +++ b/homeassistant/components/adax/climate.py @@ -86,8 +86,7 @@ class AdaxDevice(ClimateEntity): async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return await self._adax_data_handler.set_room_target_temperature( self._device_id, temperature, True diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py index 1305403e1d7..2d067d0e608 100644 --- a/homeassistant/components/asuswrt/__init__.py +++ b/homeassistant/components/asuswrt/__init__.py @@ -73,8 +73,7 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the AsusWrt integration.""" - conf = config.get(DOMAIN) - if conf is None: + if (conf := config.get(DOMAIN)) is None: return True # save the options from config yaml diff --git a/homeassistant/components/blueprint/models.py b/homeassistant/components/blueprint/models.py index 827d37843d9..a8146764710 100644 --- a/homeassistant/components/blueprint/models.py +++ b/homeassistant/components/blueprint/models.py @@ -255,8 +255,7 @@ class DomainBlueprints: def load_from_cache(): """Load blueprint from cache.""" - blueprint = self._blueprints[blueprint_path] - if blueprint is None: + if (blueprint := self._blueprints[blueprint_path]) is None: raise FailedToLoad( self.domain, blueprint_path, diff --git a/homeassistant/components/enocean/light.py b/homeassistant/components/enocean/light.py index b8ea753b8d0..d743b6c3346 100644 --- a/homeassistant/components/enocean/light.py +++ b/homeassistant/components/enocean/light.py @@ -73,8 +73,7 @@ class EnOceanLight(EnOceanEntity, LightEntity): def turn_on(self, **kwargs): """Turn the light source on or sets a specific dimmer value.""" - brightness = kwargs.get(ATTR_BRIGHTNESS) - if brightness is not None: + if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is not None: self._brightness = brightness bval = math.floor(self._brightness / 256.0 * 100.0) diff --git a/homeassistant/components/geo_location/trigger.py b/homeassistant/components/geo_location/trigger.py index b77aecee14c..c030b3d3075 100644 --- a/homeassistant/components/geo_location/trigger.py +++ b/homeassistant/components/geo_location/trigger.py @@ -52,8 +52,7 @@ async def async_attach_trigger(hass, config, action, automation_info): if not source_match(from_state, source) and not source_match(to_state, source): return - zone_state = hass.states.get(zone_entity_id) - if zone_state is None: + 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"], diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index a1cbed2ee55..af4e6771795 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -150,8 +150,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_get_engine(hass, config, discovery_info=None): """Set up Google Cloud TTS component.""" - key_file = config.get(CONF_KEY_FILE) - if key_file: + if key_file := config.get(CONF_KEY_FILE): key_file = hass.config.path(key_file) if not os.path.isfile(key_file): _LOGGER.error("File %s doesn't exist", key_file) diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index 578fbe2bf21..df9598b00ee 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -106,8 +106,7 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): @property def temperature_unit(self) -> str: """Return the unit of measurement.""" - uom = self._node.aux_properties.get(PROP_UOM) - if not uom: + if not (uom := self._node.aux_properties.get(PROP_UOM)): return self.hass.config.units.temperature_unit if uom.value == UOM_ISY_CELSIUS: return TEMP_CELSIUS @@ -117,16 +116,14 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): @property def current_humidity(self) -> int | None: """Return the current humidity.""" - humidity = self._node.aux_properties.get(PROP_HUMIDITY) - if not humidity: + if not (humidity := self._node.aux_properties.get(PROP_HUMIDITY)): return None return int(humidity.value) @property def hvac_mode(self) -> str | None: """Return hvac operation ie. heat, cool mode.""" - hvac_mode = self._node.aux_properties.get(CMD_CLIMATE_MODE) - if not hvac_mode: + if not (hvac_mode := self._node.aux_properties.get(CMD_CLIMATE_MODE)): return None # Which state values used depends on the mode property's UOM: diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index c15da2af0da..6439a0d198f 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -57,8 +57,7 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity): return UOM_FRIENDLY_NAME.get(uom[0], uom[0]) # Special cases for ISY UOM index units: - isy_states = UOM_TO_STATES.get(uom) - if isy_states: + if isy_states := UOM_TO_STATES.get(uom): return isy_states if uom in (UOM_ON_OFF, UOM_INDEX): diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index 396a18318f2..31474611783 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -280,8 +280,7 @@ class OctoPrintAPI: def update(self, sensor_type, end_point, group, tool=None): """Return the value for sensor_type from the provided endpoint.""" - response = self.get(end_point) - if response is not None: + if (response := self.get(end_point)) is not None: return get_value_from_json(response, sensor_type, group, tool) return response diff --git a/homeassistant/components/plaato/__init__.py b/homeassistant/components/plaato/__init__.py index c965c632827..ce764c4fef5 100644 --- a/homeassistant/components/plaato/__init__.py +++ b/homeassistant/components/plaato/__init__.py @@ -85,9 +85,8 @@ WEBHOOK_SCHEMA = vol.Schema( async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Configure based on config entry.""" hass.data.setdefault(DOMAIN, {}) - use_webhook = entry.data[CONF_USE_WEBHOOK] - if use_webhook: + if entry.data[CONF_USE_WEBHOOK]: async_setup_webhook(hass, entry) else: await async_setup_coordinator(hass, entry) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index cffb484ac5a..b2606c6eeaf 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -131,8 +131,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Begin manual configuration.""" if user_input is not None and errors is None: user_input.pop(CONF_URL, None) - host = user_input.get(CONF_HOST) - if host: + if host := user_input.get(CONF_HOST): port = user_input[CONF_PORT] prefix = "https" if user_input.get(CONF_SSL) else "http" user_input[CONF_URL] = f"{prefix}://{host}:{port}" diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 12398edfd59..62fa095b50f 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -259,8 +259,7 @@ class PlexServer: """Process a session payload received from a websocket callback.""" session_payload = payload["PlaySessionStateNotification"][0] - state = session_payload["state"] - if state == "buffering": + if (state := session_payload["state"]) == "buffering": return session_key = int(session_payload["sessionKey"]) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 39ac4c28415..7c531a292b1 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -148,8 +148,7 @@ class PrometheusMetrics: def handle_event(self, event): """Listen for new messages on the bus, and add them to Prometheus.""" - state = event.data.get("new_state") - if state is None: + if (state := event.data.get("new_state")) is None: return entity_id = state.entity_id @@ -318,8 +317,7 @@ class PrometheusMetrics: metric.labels(**self._labels(state)).set(value) def _handle_climate_temp(self, state, attr, metric_name, metric_description): - temp = state.attributes.get(attr) - if temp: + if temp := state.attributes.get(attr): if self._climate_units == TEMP_FAHRENHEIT: temp = fahrenheit_to_celsius(temp) metric = self._metric( @@ -355,8 +353,7 @@ class PrometheusMetrics: "Current temperature in degrees Celsius", ) - current_action = state.attributes.get(ATTR_HVAC_ACTION) - if current_action: + if current_action := state.attributes.get(ATTR_HVAC_ACTION): metric = self._metric( "climate_action", self.prometheus_cli.Gauge, diff --git a/homeassistant/components/remote/reproduce_state.py b/homeassistant/components/remote/reproduce_state.py index cc9685dee2f..064e4a9711a 100644 --- a/homeassistant/components/remote/reproduce_state.py +++ b/homeassistant/components/remote/reproduce_state.py @@ -30,9 +30,7 @@ async def _async_reproduce_state( reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" - cur_state = hass.states.get(state.entity_id) - - if cur_state is None: + if (cur_state := hass.states.get(state.entity_id)) is None: _LOGGER.warning("Unable to find entity %s", state.entity_id) return diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index da1603e3201..f181817e6f1 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -79,8 +79,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Shelly component.""" hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}} - conf = config.get(DOMAIN) - if conf is not None: + if (conf := config.get(DOMAIN)) is not None: hass.data[DOMAIN][CONF_COAP_PORT] = conf[CONF_COAP_PORT] return True @@ -233,9 +232,8 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): ) -> None: """Initialize the Shelly device wrapper.""" self.device_id: str | None = None - sleep_period = entry.data["sleep_period"] - if sleep_period: + if sleep_period := entry.data["sleep_period"]: update_interval = SLEEP_PERIOD_MULTIPLIER * sleep_period else: update_interval = ( diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py index a38720bef59..fd3072ff7f4 100644 --- a/homeassistant/components/shopping_list/__init__.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -82,15 +82,13 @@ async def async_setup_entry(hass, config_entry): async def add_item_service(call): """Add an item with `name`.""" data = hass.data[DOMAIN] - name = call.data.get(ATTR_NAME) - if name is not None: + if (name := call.data.get(ATTR_NAME)) is not None: await data.async_add(name) async def complete_item_service(call): """Mark the item provided via `name` as completed.""" data = hass.data[DOMAIN] - name = call.data.get(ATTR_NAME) - if name is None: + if (name := call.data.get(ATTR_NAME)) is None: return try: item = [item for item in data.items if item["name"] == name][0] @@ -102,8 +100,7 @@ async def async_setup_entry(hass, config_entry): async def incomplete_item_service(call): """Mark the item provided via `name` as incomplete.""" data = hass.data[DOMAIN] - name = call.data.get(ATTR_NAME) - if name is None: + if (name := call.data.get(ATTR_NAME)) is None: return try: item = [item for item in data.items if item["name"] == name][0] diff --git a/homeassistant/components/sma/__init__.py b/homeassistant/components/sma/__init__.py index 2eb0e6760ed..1a48bee7797 100644 --- a/homeassistant/components/sma/__init__.py +++ b/homeassistant/components/sma/__init__.py @@ -61,8 +61,7 @@ def _parse_legacy_options( ) # Parsing of sensors configuration - config_sensors = entry.data.get(CONF_SENSORS) - if not config_sensors: + if not (config_sensors := entry.data.get(CONF_SENSORS)): return [] # Support import of legacy config that should have been removed from 0.99, but was still functional diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 50185461030..29ea478f0b3 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -207,8 +207,7 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): @callback def async_tariff_change(self, event): """Handle tariff changes.""" - new_state = event.data.get("new_state") - if new_state is None: + if (new_state := event.data.get("new_state")) is None: return self._change_status(new_state.state) diff --git a/homeassistant/components/volumio/browse_media.py b/homeassistant/components/volumio/browse_media.py index 25fe929aaf1..ad634392989 100644 --- a/homeassistant/components/volumio/browse_media.py +++ b/homeassistant/components/volumio/browse_media.py @@ -102,8 +102,7 @@ def _list_payload(item, children=None): def _raw_item_payload(entity, item, parent_item=None, title=None, info=None): if "type" in item: - thumbnail = item.get("albumart") - if thumbnail: + if thumbnail := item.get("albumart"): item_hash = str(hash(thumbnail)) entity.thumbnail_cache.setdefault(item_hash, thumbnail) thumbnail = entity.get_browse_image_url(MEDIA_TYPE_MUSIC, item_hash) @@ -156,8 +155,7 @@ async def browse_node(entity, media_library, media_content_type, media_content_i for item in first_list["items"] ] info = navigation.get("info") - title = first_list.get("title") - if not title: + if not (title := first_list.get("title")): if info: title = f"{info.get('album')} ({info.get('artist')})" else: diff --git a/homeassistant/components/water_heater/reproduce_state.py b/homeassistant/components/water_heater/reproduce_state.py index 513b365e67a..5fbe3f935f8 100644 --- a/homeassistant/components/water_heater/reproduce_state.py +++ b/homeassistant/components/water_heater/reproduce_state.py @@ -53,9 +53,7 @@ async def _async_reproduce_state( reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" - cur_state = hass.states.get(state.entity_id) - - if cur_state is None: + if (cur_state := hass.states.get(state.entity_id)) is None: _LOGGER.warning("Unable to find entity %s", state.entity_id) return diff --git a/homeassistant/components/zabbix/sensor.py b/homeassistant/components/zabbix/sensor.py index ff2e2c4d9ba..1b3cf40eedd 100644 --- a/homeassistant/components/zabbix/sensor.py +++ b/homeassistant/components/zabbix/sensor.py @@ -34,16 +34,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Zabbix sensor platform.""" sensors = [] - zapi = hass.data[zabbix.DOMAIN] - if not zapi: + if not (zapi := hass.data[zabbix.DOMAIN]): _LOGGER.error("Zabbix integration hasn't been loaded? zapi is None") return False _LOGGER.info("Connected to Zabbix API Version %s", zapi.api_version()) - trigger_conf = config.get(_CONF_TRIGGERS) # The following code seems overly complex. Need to think about this... - if trigger_conf: + if trigger_conf := config.get(_CONF_TRIGGERS): hostids = trigger_conf.get(_CONF_HOSTIDS) individual = trigger_conf.get(_CONF_INDIVIDUAL) name = trigger_conf.get(CONF_NAME) From c1d671b817c5d6abc81652ebd96d36506294ad02 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 20 Oct 2021 23:53:06 +0200 Subject: [PATCH 0597/1038] Fix template sensor when name template doesn't render (#58088) --- homeassistant/components/template/sensor.py | 1 + tests/components/template/test_sensor.py | 26 +++++++++++++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index 4214323c8ee..ea203bdd879 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -245,6 +245,7 @@ class SensorTemplate(TemplateEntity, SensorEntity): self._friendly_name_template = friendly_name_template + self._attr_name = None # Try to render the name as it can influence the entity ID if friendly_name_template: friendly_name_template.hass = hass diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 5b179957d92..189fe3653f2 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -115,7 +115,7 @@ async def test_entity_picture_template(hass, start_ha): @pytest.mark.parametrize("count,domain", [(1, sensor.DOMAIN)]) @pytest.mark.parametrize( - "attribute,config", + "attribute,config,expected", [ ( "friendly_name", @@ -130,6 +130,22 @@ async def test_entity_picture_template(hass, start_ha): }, }, }, + ("It .", "It Works."), + ), + ( + "friendly_name", + { + "sensor": { + "platform": "template", + "sensors": { + "test_template_sensor": { + "value_template": "{{ states.sensor.test_state.state }}", + "friendly_name_template": "{{ 'It ' + states.sensor.test_state.state + '.'}}", + } + }, + }, + }, + (None, "It Works."), ), ( "friendly_name", @@ -144,6 +160,7 @@ async def test_entity_picture_template(hass, start_ha): }, }, }, + ("It .", "It Works."), ), ( "test_attribute", @@ -160,16 +177,17 @@ async def test_entity_picture_template(hass, start_ha): }, }, }, + ("It .", "It Works."), ), ], ) -async def test_friendly_name_template(hass, attribute, start_ha): +async def test_friendly_name_template(hass, attribute, expected, start_ha): """Test friendly_name template with an unknown value_template.""" - assert hass.states.get(TEST_NAME).attributes.get(attribute) == "It ." + assert hass.states.get(TEST_NAME).attributes.get(attribute) == expected[0] hass.states.async_set("sensor.test_state", "Works") await hass.async_block_till_done() - assert hass.states.get(TEST_NAME).attributes[attribute] == "It Works." + assert hass.states.get(TEST_NAME).attributes[attribute] == expected[1] @pytest.mark.parametrize("count,domain", [(0, sensor.DOMAIN)]) From a824fa9a7b138036de6b1c1daf4666d049e2a270 Mon Sep 17 00:00:00 2001 From: Andrey Kupreychik Date: Thu, 21 Oct 2021 04:53:23 +0700 Subject: [PATCH 0598/1038] Abort keenetic SSDP discovery if the unique id is already setup or ignored (#58009) --- .../components/keenetic_ndms2/config_flow.py | 1 + .../keenetic_ndms2/test_config_flow.py | 50 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/homeassistant/components/keenetic_ndms2/config_flow.py b/homeassistant/components/keenetic_ndms2/config_flow.py index fdb7dafc516..96caea06304 100644 --- a/homeassistant/components/keenetic_ndms2/config_flow.py +++ b/homeassistant/components/keenetic_ndms2/config_flow.py @@ -118,6 +118,7 @@ class KeeneticFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): host = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname await self.async_set_unique_id(discovery_info[ssdp.ATTR_UPNP_UDN]) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) self._async_abort_entries_match({CONF_HOST: host}) diff --git a/tests/components/keenetic_ndms2/test_config_flow.py b/tests/components/keenetic_ndms2/test_config_flow.py index 7e7d4882544..23c1bead25e 100644 --- a/tests/components/keenetic_ndms2/test_config_flow.py +++ b/tests/components/keenetic_ndms2/test_config_flow.py @@ -211,6 +211,56 @@ async def test_ssdp_already_configured(hass: HomeAssistant) -> None: assert result["reason"] == "already_configured" +async def test_ssdp_ignored(hass: HomeAssistant) -> None: + """Test unique ID ignored and discovered.""" + + entry = MockConfigEntry( + domain=keenetic.DOMAIN, + source=config_entries.SOURCE_IGNORE, + unique_id=MOCK_SSDP_DISCOVERY_INFO[ssdp.ATTR_UPNP_UDN], + ) + entry.add_to_hass(hass) + + discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() + result = await hass.config_entries.flow.async_init( + keenetic.DOMAIN, + context={CONF_SOURCE: config_entries.SOURCE_SSDP}, + data=discovery_info, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_ssdp_update_host(hass: HomeAssistant) -> None: + """Test unique ID configured and discovered with the new host.""" + + entry = MockConfigEntry( + domain=keenetic.DOMAIN, + data=MOCK_DATA, + options=MOCK_OPTIONS, + unique_id=MOCK_SSDP_DISCOVERY_INFO[ssdp.ATTR_UPNP_UDN], + ) + entry.add_to_hass(hass) + + new_ip = "10.10.10.10" + + discovery_info = { + **MOCK_SSDP_DISCOVERY_INFO, + ssdp.ATTR_SSDP_LOCATION: f"http://{new_ip}/", + } + + result = await hass.config_entries.flow.async_init( + keenetic.DOMAIN, + context={CONF_SOURCE: config_entries.SOURCE_SSDP}, + data=discovery_info, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_HOST] == new_ip + + async def test_ssdp_reject_no_udn(hass: HomeAssistant) -> None: """Discovered device has no UDN.""" From 745e42621b034821901a337810ef2f89e057d4aa Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 20 Oct 2021 15:57:28 -0600 Subject: [PATCH 0599/1038] Add entity categories for appropriate RainMachine entities (#58107) --- homeassistant/components/rainmachine/binary_sensor.py | 9 +++++++++ homeassistant/components/rainmachine/sensor.py | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index bfd36ecf550..55d009ad6f5 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -7,6 +7,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -50,24 +51,28 @@ BINARY_SENSOR_DESCRIPTIONS = ( key=TYPE_FREEZE, name="Freeze Restrictions", icon="mdi:cancel", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, api_category=DATA_RESTRICTIONS_CURRENT, ), RainMachineBinarySensorDescription( key=TYPE_FREEZE_PROTECTION, name="Freeze Protection", icon="mdi:weather-snowy", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, api_category=DATA_RESTRICTIONS_UNIVERSAL, ), RainMachineBinarySensorDescription( key=TYPE_HOT_DAYS, name="Extra Water on Hot Days", icon="mdi:thermometer-lines", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, api_category=DATA_RESTRICTIONS_UNIVERSAL, ), RainMachineBinarySensorDescription( key=TYPE_HOURLY, name="Hourly Restrictions", icon="mdi:cancel", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, ), @@ -75,6 +80,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( key=TYPE_MONTH, name="Month Restrictions", icon="mdi:cancel", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, ), @@ -82,6 +88,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( key=TYPE_RAINDELAY, name="Rain Delay Restrictions", icon="mdi:cancel", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, ), @@ -89,6 +96,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( key=TYPE_RAINSENSOR, name="Rain Sensor Restrictions", icon="mdi:cancel", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, ), @@ -96,6 +104,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( key=TYPE_WEEKDAY, name="Weekday Restrictions", icon="mdi:cancel", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, ), diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 6a1da223758..6c95552e26f 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -13,6 +13,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, TEMP_CELSIUS, VOLUME_CUBIC_METERS, ) @@ -49,6 +50,7 @@ SENSOR_DESCRIPTIONS = ( name="Flow Sensor Clicks per Cubic Meter", icon="mdi:water-pump", native_unit_of_measurement=f"clicks/{VOLUME_CUBIC_METERS}", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_registry_enabled_default=False, state_class=STATE_CLASS_MEASUREMENT, api_category=DATA_PROVISION_SETTINGS, @@ -57,6 +59,7 @@ SENSOR_DESCRIPTIONS = ( key=TYPE_FLOW_SENSOR_CONSUMED_LITERS, name="Flow Sensor Consumed Liters", icon="mdi:water-pump", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, native_unit_of_measurement="liter", entity_registry_enabled_default=False, state_class=STATE_CLASS_TOTAL_INCREASING, @@ -66,6 +69,7 @@ SENSOR_DESCRIPTIONS = ( key=TYPE_FLOW_SENSOR_START_INDEX, name="Flow Sensor Start Index", icon="mdi:water-pump", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, native_unit_of_measurement="index", entity_registry_enabled_default=False, api_category=DATA_PROVISION_SETTINGS, @@ -74,6 +78,7 @@ SENSOR_DESCRIPTIONS = ( key=TYPE_FLOW_SENSOR_WATERING_CLICKS, name="Flow Sensor Clicks", icon="mdi:water-pump", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, native_unit_of_measurement="clicks", entity_registry_enabled_default=False, state_class=STATE_CLASS_MEASUREMENT, @@ -83,6 +88,7 @@ SENSOR_DESCRIPTIONS = ( key=TYPE_FREEZE_TEMP, name="Freeze Protect Temperature", icon="mdi:thermometer", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, From 03b11501155e293418de6bd6c7282a405fe2a404 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 20 Oct 2021 15:57:45 -0600 Subject: [PATCH 0600/1038] Add entity categories for appropriate Notion entities (#58105) --- homeassistant/components/notion/binary_sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py index c064e492b16..ab3d0775436 100644 --- a/homeassistant/components/notion/binary_sensor.py +++ b/homeassistant/components/notion/binary_sensor.py @@ -16,6 +16,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -56,6 +57,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( key=SENSOR_BATTERY, name="Low Battery", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state="critical", ), NotionBinarySensorDescription( @@ -80,6 +82,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( key=SENSOR_MISSING, name="Missing", device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state="not_missing", ), NotionBinarySensorDescription( From 388761922bde9042d8598573339a2c3797e157ed Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 20 Oct 2021 15:58:09 -0600 Subject: [PATCH 0601/1038] Add entity categories for appropriate Guardian entities (#58104) --- homeassistant/components/guardian/binary_sensor.py | 3 +++ homeassistant/components/guardian/sensor.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py index b420605a0ec..7db91d41885 100644 --- a/homeassistant/components/guardian/binary_sensor.py +++ b/homeassistant/components/guardian/binary_sensor.py @@ -9,6 +9,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -36,6 +37,7 @@ SENSOR_DESCRIPTION_AP_ENABLED = BinarySensorEntityDescription( key=SENSOR_KIND_AP_INFO, name="Onboard AP Enabled", device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ) SENSOR_DESCRIPTION_LEAK_DETECTED = BinarySensorEntityDescription( key=SENSOR_KIND_LEAK_DETECTED, @@ -46,6 +48,7 @@ SENSOR_DESCRIPTION_MOVED = BinarySensorEntityDescription( key=SENSOR_KIND_MOVED, name="Recently Moved", device_class=DEVICE_CLASS_MOVING, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ) PAIRED_SENSOR_DESCRIPTIONS = ( diff --git a/homeassistant/components/guardian/sensor.py b/homeassistant/components/guardian/sensor.py index 16b05e20767..3a014d5cbf4 100644 --- a/homeassistant/components/guardian/sensor.py +++ b/homeassistant/components/guardian/sensor.py @@ -10,6 +10,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, TEMP_FAHRENHEIT, TIME_MINUTES, @@ -38,6 +39,7 @@ SENSOR_DESCRIPTION_BATTERY = SensorEntityDescription( key=SENSOR_KIND_BATTERY, name="Battery", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, ) SENSOR_DESCRIPTION_TEMPERATURE = SensorEntityDescription( @@ -51,6 +53,7 @@ SENSOR_DESCRIPTION_UPTIME = SensorEntityDescription( key=SENSOR_KIND_UPTIME, name="Uptime", icon="mdi:timer", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, native_unit_of_measurement=TIME_MINUTES, ) From 38586d2cf1d4d75655da251569b19db81548afb5 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 20 Oct 2021 16:17:28 -0600 Subject: [PATCH 0602/1038] Add entity categories for appropriate Ambient PWS entities (#58100) --- .../ambient_station/binary_sensor.py | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ambient_station/binary_sensor.py b/homeassistant/components/ambient_station/binary_sensor.py index e513486fb85..79c53246f15 100644 --- a/homeassistant/components/ambient_station/binary_sensor.py +++ b/homeassistant/components/ambient_station/binary_sensor.py @@ -11,7 +11,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_NAME +from homeassistant.const import ATTR_NAME, ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -63,144 +63,168 @@ BINARY_SENSOR_DESCRIPTIONS = ( key=TYPE_BATTOUT, name="Battery", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT1, name="Battery 1", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT2, name="Battery 2", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT3, name="Battery 3", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT4, name="Battery 4", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT5, name="Battery 5", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT6, name="Battery 6", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT7, name="Battery 7", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT8, name="Battery 8", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT9, name="Battery 9", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT10, name="Battery 10", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_CO2, name="CO2 Battery", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_PM25IN_BATT, name="PM25 Indoor Battery", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_PM25_BATT, name="PM25 Battery", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_RELAY1, name="Relay 1", device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY2, name="Relay 2", device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY3, name="Relay 3", device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY4, name="Relay 4", device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY5, name="Relay 5", device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY6, name="Relay 6", device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY7, name="Relay 7", device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY8, name="Relay 8", device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY9, name="Relay 9", device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=1, ), AmbientBinarySensorDescription( key=TYPE_RELAY10, name="Relay 10", device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, on_state=1, ), ) From cca7da77ad9bb8882efca1b71ebae8058fdca648 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 21 Oct 2021 00:22:01 +0200 Subject: [PATCH 0603/1038] Report modbus buffer too small or too big to unpack (#57838) --- .../components/modbus/base_platform.py | 10 ++++- homeassistant/components/modbus/climate.py | 3 +- homeassistant/components/modbus/sensor.py | 5 ++- tests/components/modbus/test_climate.py | 2 + tests/components/modbus/test_sensor.py | 38 +++++++++++++++++++ 5 files changed, 54 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/modbus/base_platform.py b/homeassistant/components/modbus/base_platform.py index 1c4ebe2237d..c962f298586 100644 --- a/homeassistant/components/modbus/base_platform.py +++ b/homeassistant/components/modbus/base_platform.py @@ -160,7 +160,7 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): registers.reverse() return registers - def unpack_structure_result(self, registers: list[int]) -> str: + def unpack_structure_result(self, registers: list[int]) -> str | None: """Convert registers to proper result.""" registers = self._swap_registers(registers) @@ -168,7 +168,13 @@ class BaseStructPlatform(BasePlatform, RestoreEntity): if self._data_type == DataType.STRING: return byte_string.decode() - val = struct.unpack(self._structure, byte_string) + try: + val = struct.unpack(self._structure, byte_string) + except struct.error as err: + recv_size = len(registers) * 2 + msg = f"Received {recv_size} bytes, unpack error {err}" + _LOGGER.error(msg) + return None # Issue: https://github.com/home-assistant/core/issues/41944 # If unpack() returns a tuple greater than 1, don't try to process the value. # Instead, return the values of unpack(...) separated by commas. diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 268d7bfd3df..37ff9504b35 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -166,7 +166,8 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): self._lazy_errors = self._lazy_error_count self._value = self.unpack_structure_result(result.registers) - self._attr_available = True if not self._value: + self._attr_available = False return None + self._attr_available = True return float(self._value) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 4cf642ca44b..6911098dd94 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -74,6 +74,9 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): return self._attr_native_value = self.unpack_structure_result(result.registers) + if self._attr_native_value is None: + self._attr_available = False + else: + self._attr_available = True self._lazy_errors = self._lazy_error_count - self._attr_available = True self.async_write_ha_state() diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index 5fc61fed52d..db471709c10 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -94,11 +94,13 @@ async def test_temperature_climate(hass, expected, mock_do_cycle): { CONF_CLIMATES: [ { + CONF_COUNT: 2, CONF_NAME: TEST_ENTITY_NAME, CONF_TARGET_TEMP: 117, CONF_ADDRESS: 117, CONF_SLAVE: 10, CONF_SCAN_INTERVAL: 0, + CONF_DATA_TYPE: DataType.INT32, } ] }, diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index 4f631b279c1..bf3e8711922 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -559,6 +559,44 @@ async def test_all_sensor(hass, mock_do_cycle, expected): assert hass.states.get(ENTITY_ID).state == expected +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 51, + CONF_SCAN_INTERVAL: 1, + }, + ], + }, + ], +) +@pytest.mark.parametrize( + "config_addon,register_words", + [ + ( + { + CONF_COUNT: 1, + CONF_DATA_TYPE: DataType.INT16, + }, + [7, 9], + ), + ( + { + CONF_COUNT: 2, + CONF_DATA_TYPE: DataType.INT32, + }, + [7], + ), + ], +) +async def test_wrong_unpack(hass, mock_do_cycle): + """Run test for sensor.""" + assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE + + @pytest.mark.parametrize( "do_config", [ From f2a5d92e61c134c6fc2b6a3b5c965a33b38d1848 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 21 Oct 2021 00:22:24 +0200 Subject: [PATCH 0604/1038] Fix connect_fail test and modbus.py 100% coverage (#57894) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joakim Sørensen --- .coveragerc | 1 - homeassistant/components/modbus/modbus.py | 2 +- tests/components/modbus/test_init.py | 15 ++++++--------- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/.coveragerc b/.coveragerc index d86464fa99e..0e5eefeb9a4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -649,7 +649,6 @@ omit = homeassistant/components/mjpeg/camera.py homeassistant/components/mochad/* homeassistant/components/modbus/climate.py - homeassistant/components/modbus/modbus.py homeassistant/components/modem_callerid/sensor.py homeassistant/components/motion_blinds/__init__.py homeassistant/components/motion_blinds/const.py diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index ee5e8306696..9d1c13ffd9b 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -386,7 +386,7 @@ class ModbusHub: return None async with self._lock: if not self._client: - return None + return None # pragma: no cover result = await self.hass.async_add_executor_job( self._pymodbus_call, unit, address, value, use_call ) diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index dae19d4db50..7d9ab3e3471 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -648,7 +648,7 @@ async def test_pymodbus_close_fail(hass, caplog, mock_pymodbus): # Close() is called as part of teardown -async def test_pymodbus_connect_fail(hass, caplog): +async def test_pymodbus_connect_fail(hass, caplog, mock_pymodbus): """Run test for failing pymodbus constructor.""" config = { DOMAIN: [ @@ -660,14 +660,11 @@ async def test_pymodbus_connect_fail(hass, caplog): } ] } - with mock.patch( - "homeassistant.components.modbus.modbus.ModbusTcpClient", autospec=True - ) as mock_pb: - caplog.set_level(logging.ERROR) - exception_message = "test connect exception" - mock_pb.connect.side_effect = ModbusException(exception_message) - - assert await async_setup_component(hass, DOMAIN, config) is True + caplog.set_level(logging.WARNING) + ExceptionMessage = "test connect exception" + mock_pymodbus.connect.side_effect = ModbusException(ExceptionMessage) + assert await async_setup_component(hass, DOMAIN, config) is False + assert ExceptionMessage in caplog.text async def test_delay(hass, mock_pymodbus): From 47678c599577979247ecfa4cb0279d0b68c46248 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 20 Oct 2021 16:22:48 -0600 Subject: [PATCH 0605/1038] Add entity categories for appropriate AirVisual entities (#58102) --- homeassistant/components/airvisual/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index 70d9800e736..1dea2c2e769 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -27,6 +27,7 @@ from homeassistant.const import ( DEVICE_CLASS_PM25, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, TEMP_CELSIUS, ) @@ -103,6 +104,7 @@ NODE_PRO_SENSOR_DESCRIPTIONS = ( key=SENSOR_KIND_BATTERY_LEVEL, name="Battery", device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( From 18ce799f7413374af90bd6015024b851e7159932 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 21 Oct 2021 00:34:42 +0200 Subject: [PATCH 0606/1038] Add `configuration_url` to Denon AVR integration (#58116) --- .../components/denonavr/media_player.py | 52 +++++-------------- 1 file changed, 14 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 68a5b8c71d8..f8cfe75f304 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -35,9 +35,10 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) -from homeassistant.const import ATTR_COMMAND, STATE_PAUSED, STATE_PLAYING +from homeassistant.const import ATTR_COMMAND, CONF_HOST, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.entity import DeviceInfo from . import CONF_RECEIVER from .config_flow import ( @@ -144,9 +145,19 @@ class DenonDevice(MediaPlayerEntity): update_audyssey: bool, ) -> None: """Initialize the device.""" + self._attr_name = receiver.name + self._attr_unique_id = unique_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, config_entry.unique_id)}, + manufacturer=config_entry.data[CONF_MANUFACTURER], + name=config_entry.title, + model=f"{config_entry.data[CONF_MODEL]}-{config_entry.data[CONF_TYPE]}", + configuration_url=f"http://{config_entry.data[CONF_HOST]}/", + ) + self._attr_sound_mode_list = receiver.sound_mode_list + self._attr_source_list = receiver.input_func_list + self._receiver = receiver - self._unique_id = unique_id - self._config_entry = config_entry self._update_audyssey = update_audyssey self._supported_features_base = SUPPORT_DENON @@ -230,31 +241,6 @@ class DenonDevice(MediaPlayerEntity): """Return True if entity is available.""" return self._available - @property - def unique_id(self): - """Return the unique id of the zone.""" - return self._unique_id - - @property - def device_info(self): - """Return the device info of the receiver.""" - if self._config_entry.data[CONF_SERIAL_NUMBER] is None: - return None - - device_info = { - "identifiers": {(DOMAIN, self._config_entry.unique_id)}, - "manufacturer": self._config_entry.data[CONF_MANUFACTURER], - "name": self._config_entry.title, - "model": f"{self._config_entry.data[CONF_MODEL]}-{self._config_entry.data[CONF_TYPE]}", - } - - return device_info - - @property - def name(self): - """Return the name of the device.""" - return self._receiver.name - @property def state(self): """Return the state of the device.""" @@ -279,21 +265,11 @@ class DenonDevice(MediaPlayerEntity): """Return the current input source.""" return self._receiver.input_func - @property - def source_list(self): - """Return a list of available input sources.""" - return self._receiver.input_func_list - @property def sound_mode(self): """Return the current matched sound mode.""" return self._receiver.sound_mode - @property - def sound_mode_list(self): - """Return a list of available sound modes.""" - return self._receiver.sound_mode_list - @property def supported_features(self): """Flag media player features that are supported.""" From 20c35e6032706cadcae101e3f63dc335a8f00a01 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 20 Oct 2021 12:38:21 -1000 Subject: [PATCH 0607/1038] Move Screenlogic lights to the light platform (#55467) Co-authored-by: Kevin Worrel <37058192+dieselrabbit@users.noreply.github.com> --- .coveragerc | 1 + .../components/screenlogic/__init__.py | 61 ++++++++++++++++-- .../components/screenlogic/binary_sensor.py | 2 +- .../components/screenlogic/climate.py | 2 +- homeassistant/components/screenlogic/const.py | 4 +- homeassistant/components/screenlogic/light.py | 29 +++++++++ .../components/screenlogic/sensor.py | 2 +- .../components/screenlogic/services.py | 2 +- .../components/screenlogic/switch.py | 63 +++++-------------- 9 files changed, 107 insertions(+), 59 deletions(-) create mode 100644 homeassistant/components/screenlogic/light.py diff --git a/.coveragerc b/.coveragerc index 0e5eefeb9a4..6690ed5ee20 100644 --- a/.coveragerc +++ b/.coveragerc @@ -900,6 +900,7 @@ omit = homeassistant/components/screenlogic/__init__.py homeassistant/components/screenlogic/binary_sensor.py homeassistant/components/screenlogic/climate.py + homeassistant/components/screenlogic/light.py homeassistant/components/screenlogic/sensor.py homeassistant/components/screenlogic/services.py homeassistant/components/screenlogic/switch.py diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index a9ba02de18d..2178cf7ec2c 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -5,7 +5,9 @@ import logging from screenlogicpy import ScreenLogicError, ScreenLogicGateway from screenlogicpy.const import ( + DATA as SL_DATA, EQUIPMENT, + ON_OFF, SL_GATEWAY_IP, SL_GATEWAY_NAME, SL_GATEWAY_PORT, @@ -16,6 +18,7 @@ from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, CONF_SCAN_INTERVAL from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -29,7 +32,10 @@ from .services import async_load_screenlogic_services, async_unload_screenlogic_ _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["switch", "sensor", "binary_sensor", "climate"] + +REQUEST_REFRESH_DELAY = 1 + +PLATFORMS = ["switch", "sensor", "binary_sensor", "climate", "light"] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -56,10 +62,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.entry_id] = { - "coordinator": coordinator, - "listener": entry.add_update_listener(async_update_listener), - } + entry.async_on_unload(entry.add_update_listener(async_update_listener)) + + hass.data[DOMAIN][entry.entry_id] = coordinator hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -134,6 +139,11 @@ class ScreenlogicDataUpdateCoordinator(DataUpdateCoordinator): _LOGGER, name=DOMAIN, update_interval=interval, + # We don't want an immediate refresh since the device + # takes a moment to reflect the state change + request_refresh_debouncer=Debouncer( + hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False + ), ) def reconnect_gateway(self): @@ -221,3 +231,44 @@ class ScreenlogicEntity(CoordinatorEntity): "manufacturer": "Pentair", "model": equipment_model, } + + +class ScreenLogicCircuitEntity(ScreenlogicEntity): + """ScreenLogic circuit entity.""" + + @property + def name(self): + """Get the name of the switch.""" + return f"{self.gateway_name} {self.circuit['name']}" + + @property + def is_on(self) -> bool: + """Get whether the switch is in on state.""" + return self.circuit["value"] == ON_OFF.ON + + async def async_turn_on(self, **kwargs) -> None: + """Send the ON command.""" + await self._async_set_circuit(ON_OFF.ON) + + async def async_turn_off(self, **kwargs) -> None: + """Send the OFF command.""" + await self._async_set_circuit(ON_OFF.OFF) + + async def _async_set_circuit(self, circuit_value) -> None: + async with self.coordinator.api_lock: + success = await self.hass.async_add_executor_job( + self.gateway.set_circuit, self._data_key, circuit_value + ) + + if success: + _LOGGER.debug("Turn %s %s", self._data_key, circuit_value) + await self.coordinator.async_request_refresh() + else: + _LOGGER.warning( + "Failed to set_circuit %s %s", self._data_key, circuit_value + ) + + @property + def circuit(self): + """Shortcut to access the circuit.""" + return self.coordinator.data[SL_DATA.KEY_CIRCUITS][self._data_key] diff --git a/homeassistant/components/screenlogic/binary_sensor.py b/homeassistant/components/screenlogic/binary_sensor.py index f5d95d03be2..136f74d0d6c 100644 --- a/homeassistant/components/screenlogic/binary_sensor.py +++ b/homeassistant/components/screenlogic/binary_sensor.py @@ -15,7 +15,7 @@ SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS = {DEVICE_TYPE.ALARM: DEVICE_CLASS_PROBLEM} async def async_setup_entry(hass, config_entry, async_add_entities): """Set up entry.""" entities = [] - coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] + coordinator = hass.data[DOMAIN][config_entry.entry_id] # Generic binary sensor entities.append(ScreenLogicBinarySensor(coordinator, "chem_alarm")) diff --git a/homeassistant/components/screenlogic/climate.py b/homeassistant/components/screenlogic/climate.py index b83d2fe03ca..dc9e185ca9f 100644 --- a/homeassistant/components/screenlogic/climate.py +++ b/homeassistant/components/screenlogic/climate.py @@ -37,7 +37,7 @@ SUPPORTED_PRESETS = [ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up entry.""" entities = [] - coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] + coordinator = hass.data[DOMAIN][config_entry.entry_id] for body in coordinator.data[SL_DATA.KEY_BODIES]: entities.append(ScreenLogicClimate(coordinator, body)) diff --git a/homeassistant/components/screenlogic/const.py b/homeassistant/components/screenlogic/const.py index 49a57b8d46e..2a1a3c23d2e 100644 --- a/homeassistant/components/screenlogic/const.py +++ b/homeassistant/components/screenlogic/const.py @@ -1,5 +1,5 @@ """Constants for the ScreenLogic integration.""" -from screenlogicpy.const import COLOR_MODE +from screenlogicpy.const import CIRCUIT_FUNCTION, COLOR_MODE from homeassistant.util import slugify @@ -13,4 +13,6 @@ SUPPORTED_COLOR_MODES = { slugify(name): num for num, name in COLOR_MODE.NAME_FOR_NUM.items() } +LIGHT_CIRCUIT_FUNCTIONS = {CIRCUIT_FUNCTION.INTELLIBRITE, CIRCUIT_FUNCTION.LIGHT} + DISCOVERED_GATEWAYS = "_discovered_gateways" diff --git a/homeassistant/components/screenlogic/light.py b/homeassistant/components/screenlogic/light.py new file mode 100644 index 00000000000..298b8a914a8 --- /dev/null +++ b/homeassistant/components/screenlogic/light.py @@ -0,0 +1,29 @@ +"""Support for a ScreenLogic light 'circuit' switch.""" +import logging + +from screenlogicpy.const import DATA as SL_DATA, GENERIC_CIRCUIT_NAMES + +from homeassistant.components.light import LightEntity + +from . import ScreenLogicCircuitEntity +from .const import DOMAIN, LIGHT_CIRCUIT_FUNCTIONS + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + [ + ScreenLogicLight( + coordinator, circuit_num, circuit["name"] not in GENERIC_CIRCUIT_NAMES + ) + for circuit_num, circuit in coordinator.data[SL_DATA.KEY_CIRCUITS].items() + if circuit["function"] in LIGHT_CIRCUIT_FUNCTIONS + ] + ) + + +class ScreenLogicLight(ScreenLogicCircuitEntity, LightEntity): + """Class to represent a ScreenLogic Light.""" diff --git a/homeassistant/components/screenlogic/sensor.py b/homeassistant/components/screenlogic/sensor.py index 357e771f6be..cd7ba068be0 100644 --- a/homeassistant/components/screenlogic/sensor.py +++ b/homeassistant/components/screenlogic/sensor.py @@ -51,7 +51,7 @@ SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS = { async def async_setup_entry(hass, config_entry, async_add_entities): """Set up entry.""" entities = [] - coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] + coordinator = hass.data[DOMAIN][config_entry.entry_id] equipment_flags = coordinator.data[SL_DATA.KEY_CONFIG]["equipment_flags"] # Generic sensors diff --git a/homeassistant/components/screenlogic/services.py b/homeassistant/components/screenlogic/services.py index 31e35788f44..d5edda12abb 100644 --- a/homeassistant/components/screenlogic/services.py +++ b/homeassistant/components/screenlogic/services.py @@ -51,7 +51,7 @@ def async_load_screenlogic_services(hass: HomeAssistant): ) color_num = SUPPORTED_COLOR_MODES[service_call.data[ATTR_COLOR_MODE]] for entry_id in screenlogic_entry_ids: - coordinator = hass.data[DOMAIN][entry_id]["coordinator"] + coordinator = hass.data[DOMAIN][entry_id] _LOGGER.debug( "Service %s called on %s with mode %s", SERVICE_SET_COLOR_MODE, diff --git a/homeassistant/components/screenlogic/switch.py b/homeassistant/components/screenlogic/switch.py index ff73afebb57..97c95be85ca 100644 --- a/homeassistant/components/screenlogic/switch.py +++ b/homeassistant/components/screenlogic/switch.py @@ -1,64 +1,29 @@ """Support for a ScreenLogic 'circuit' switch.""" import logging -from screenlogicpy.const import DATA as SL_DATA, GENERIC_CIRCUIT_NAMES, ON_OFF +from screenlogicpy.const import DATA as SL_DATA, GENERIC_CIRCUIT_NAMES from homeassistant.components.switch import SwitchEntity -from . import ScreenlogicEntity -from .const import DOMAIN +from . import ScreenLogicCircuitEntity +from .const import DOMAIN, LIGHT_CIRCUIT_FUNCTIONS _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up entry.""" - entities = [] - coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] - - for circuit_num, circuit in coordinator.data[SL_DATA.KEY_CIRCUITS].items(): - enabled = circuit["name"] not in GENERIC_CIRCUIT_NAMES - entities.append(ScreenLogicSwitch(coordinator, circuit_num, enabled)) - - async_add_entities(entities) - - -class ScreenLogicSwitch(ScreenlogicEntity, SwitchEntity): - """ScreenLogic switch entity.""" - - @property - def name(self): - """Get the name of the switch.""" - return f"{self.gateway_name} {self.circuit['name']}" - - @property - def is_on(self) -> bool: - """Get whether the switch is in on state.""" - return self.circuit["value"] == 1 - - async def async_turn_on(self, **kwargs) -> None: - """Send the ON command.""" - return await self._async_set_circuit(ON_OFF.ON) - - async def async_turn_off(self, **kwargs) -> None: - """Send the OFF command.""" - return await self._async_set_circuit(ON_OFF.OFF) - - async def _async_set_circuit(self, circuit_value) -> None: - async with self.coordinator.api_lock: - success = await self.hass.async_add_executor_job( - self.gateway.set_circuit, self._data_key, circuit_value + coordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + [ + ScreenLogicSwitch( + coordinator, circuit_num, circuit["name"] not in GENERIC_CIRCUIT_NAMES ) + for circuit_num, circuit in coordinator.data[SL_DATA.KEY_CIRCUITS].items() + if circuit["function"] not in LIGHT_CIRCUIT_FUNCTIONS + ] + ) - if success: - _LOGGER.debug("Turn %s %s", self._data_key, circuit_value) - await self.coordinator.async_request_refresh() - else: - _LOGGER.warning( - "Failed to set_circuit %s %s", self._data_key, circuit_value - ) - @property - def circuit(self): - """Shortcut to access the circuit.""" - return self.coordinator.data[SL_DATA.KEY_CIRCUITS][self._data_key] +class ScreenLogicSwitch(ScreenLogicCircuitEntity, SwitchEntity): + """Class to represent a ScreenLogic Switch.""" From 840dc2b9311044b0777c314a7c50848ad3765317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 21 Oct 2021 01:43:04 +0300 Subject: [PATCH 0608/1038] Run tests with -X dev and -bb (#58079) --- .github/workflows/ci.yaml | 2 +- tox.ini | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fe291e04c3c..9298400276f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -686,7 +686,7 @@ jobs: - name: Run pytest run: | . venv/bin/activate - pytest \ + python3 -X dev -bb -m pytest \ -qq \ --timeout=9 \ --durations=10 \ diff --git a/tox.ini b/tox.ini index 0c58d2356b9..5def410cb3b 100644 --- a/tox.ini +++ b/tox.ini @@ -8,14 +8,14 @@ basepython = {env:PYTHON3_PATH:python3} # pip version duplicated in homeassistant/package_constraints.txt pip_version = pip>=8.0.3,<20.3 commands = - pytest --timeout=9 --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar {posargs} + {envpython} -X dev -bb -m pytest --timeout=9 --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar {posargs} {toxinidir}/script/check_dirty deps = -r{toxinidir}/requirements_test_all.txt [testenv:cov] commands = - pytest --timeout=9 --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar --cov --cov-report= {posargs} + {envpython} -X dev -bb -m pytest --timeout=9 --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar --cov --cov-report= {posargs} {toxinidir}/script/check_dirty deps = -r{toxinidir}/requirements_test_all.txt From a4964652358ce4439aeeba14343553392d276258 Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Wed, 20 Oct 2021 18:58:40 -0400 Subject: [PATCH 0609/1038] Bump pymazda to 0.2.2 (#58113) --- homeassistant/components/mazda/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mazda/manifest.json b/homeassistant/components/mazda/manifest.json index 7eb85f722ae..27c6f0f5097 100644 --- a/homeassistant/components/mazda/manifest.json +++ b/homeassistant/components/mazda/manifest.json @@ -3,7 +3,7 @@ "name": "Mazda Connected Services", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mazda", - "requirements": ["pymazda==0.2.1"], + "requirements": ["pymazda==0.2.2"], "codeowners": ["@bdr99"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index ed770e42b00..d28ca5c359a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1613,7 +1613,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.2.1 +pymazda==0.2.2 # homeassistant.components.mediaroom pymediaroom==0.6.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 219cd3107b2..fe6a0179587 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -959,7 +959,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.2.1 +pymazda==0.2.2 # homeassistant.components.melcloud pymelcloud==2.5.4 From 4634b659240e8164be2c099ff304378cdf2dfb42 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 21 Oct 2021 00:12:43 +0000 Subject: [PATCH 0610/1038] [ci skip] Translation update --- .../amberelectric/translations/ja.json | 11 ++++++ .../ambient_station/translations/hu.json | 2 +- .../automation/translations/ca.json | 4 +-- .../binary_sensor/translations/ca.json | 4 +-- .../binary_sensor/translations/nl.json | 1 + .../binary_sensor/translations/no.json | 4 +++ .../binary_sensor/translations/pt-BR.json | 10 ++++++ .../components/bond/translations/hu.json | 2 +- .../components/calendar/translations/ca.json | 4 +-- .../components/climate/translations/ca.json | 2 +- .../components/daikin/translations/no.json | 1 + .../components/dlna_dmr/translations/ja.json | 17 +++++++++ .../components/dlna_dmr/translations/no.json | 3 ++ .../components/ecobee/translations/ru.json | 2 +- .../components/efergy/translations/ja.json | 11 ++++++ .../components/efergy/translations/no.json | 21 +++++++++++ .../environment_canada/translations/ja.json | 14 ++++++++ .../environment_canada/translations/no.json | 23 ++++++++++++ .../components/fan/translations/ca.json | 4 +-- .../geonetnz_quakes/translations/hu.json | 2 +- .../components/group/translations/ca.json | 4 +-- .../humidifier/translations/ca.json | 4 +-- .../input_boolean/translations/ca.json | 4 +-- .../components/light/translations/ca.json | 4 +-- .../lutron_caseta/translations/ca.json | 4 +-- .../lutron_caseta/translations/hu.json | 2 +- .../media_player/translations/ca.json | 4 +-- .../motion_blinds/translations/ca.json | 17 +++++++-- .../motion_blinds/translations/nl.json | 17 +++++++-- .../motion_blinds/translations/no.json | 17 +++++++-- .../motion_blinds/translations/pt-BR.json | 26 ++++++++++++++ .../motioneye/translations/pt-BR.json | 12 +++++++ .../components/nest/translations/pt-BR.json | 5 +++ .../components/notion/translations/hu.json | 2 +- .../components/onvif/translations/ru.json | 2 +- .../opengarage/translations/ja.json | 11 ++++++ .../components/openuv/translations/hu.json | 2 +- .../rainmachine/translations/hu.json | 2 +- .../components/remote/translations/ca.json | 4 +-- .../components/rfxtrx/translations/ca.json | 10 +++--- .../components/script/translations/ca.json | 4 +-- .../components/sensor/translations/ca.json | 4 +-- .../simplisafe/translations/ca.json | 11 +++++- .../simplisafe/translations/ja.json | 12 +++++++ .../simplisafe/translations/nl.json | 11 +++++- .../simplisafe/translations/no.json | 11 +++++- .../simplisafe/translations/pt-BR.json | 5 +++ .../simplisafe/translations/ru.json | 11 +++++- .../simplisafe/translations/zh-Hant.json | 11 +++++- .../components/soma/translations/no.json | 8 ++--- .../stookalert/translations/no.json | 14 ++++++++ .../components/switch/translations/ca.json | 4 +-- .../components/tradfri/translations/no.json | 1 + .../components/tuya/translations/hu.json | 2 +- .../components/tuya/translations/ja.json | 21 +++++++++++ .../components/tuya/translations/no.json | 2 +- .../components/tuya/translations/pt-BR.json | 1 + .../uptimerobot/translations/nl.json | 4 +-- .../uptimerobot/translations/no.json | 4 +-- .../components/vacuum/translations/ca.json | 4 +-- .../vlc_telnet/translations/cs.json | 5 ++- .../vlc_telnet/translations/ja.json | 20 +++++++++++ .../vlc_telnet/translations/nl.json | 8 ++++- .../vlc_telnet/translations/no.json | 36 +++++++++++++++++++ .../vlc_telnet/translations/pt-BR.json | 18 ++++++++++ .../water_heater/translations/ca.json | 2 +- .../components/watttime/translations/no.json | 10 +++++- .../watttime/translations/pt-BR.json | 22 ++++++++++++ .../xiaomi_miio/translations/ja.json | 7 ++++ .../xiaomi_miio/translations/no.json | 3 +- .../xiaomi_miio/translations/select.ca.json | 2 +- 71 files changed, 501 insertions(+), 72 deletions(-) create mode 100644 homeassistant/components/amberelectric/translations/ja.json create mode 100644 homeassistant/components/dlna_dmr/translations/ja.json create mode 100644 homeassistant/components/efergy/translations/ja.json create mode 100644 homeassistant/components/efergy/translations/no.json create mode 100644 homeassistant/components/environment_canada/translations/ja.json create mode 100644 homeassistant/components/environment_canada/translations/no.json create mode 100644 homeassistant/components/motion_blinds/translations/pt-BR.json create mode 100644 homeassistant/components/motioneye/translations/pt-BR.json create mode 100644 homeassistant/components/opengarage/translations/ja.json create mode 100644 homeassistant/components/simplisafe/translations/ja.json create mode 100644 homeassistant/components/stookalert/translations/no.json create mode 100644 homeassistant/components/tuya/translations/ja.json create mode 100644 homeassistant/components/vlc_telnet/translations/ja.json create mode 100644 homeassistant/components/vlc_telnet/translations/no.json create mode 100644 homeassistant/components/vlc_telnet/translations/pt-BR.json create mode 100644 homeassistant/components/watttime/translations/pt-BR.json create mode 100644 homeassistant/components/xiaomi_miio/translations/ja.json diff --git a/homeassistant/components/amberelectric/translations/ja.json b/homeassistant/components/amberelectric/translations/ja.json new file mode 100644 index 00000000000..e0a3590c8b4 --- /dev/null +++ b/homeassistant/components/amberelectric/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_token": "API\u30c8\u30fc\u30af\u30f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/translations/hu.json b/homeassistant/components/ambient_station/translations/hu.json index 7c7e3a658b9..6974bda2c20 100644 --- a/homeassistant/components/ambient_station/translations/hu.json +++ b/homeassistant/components/ambient_station/translations/hu.json @@ -13,7 +13,7 @@ "api_key": "API kulcs", "app_key": "Alkalmaz\u00e1skulcs" }, - "title": "T\u00f6ltsd ki az adataid" + "title": "T\u00f6ltse ki az adatait" } } } diff --git a/homeassistant/components/automation/translations/ca.json b/homeassistant/components/automation/translations/ca.json index c1d35331e2b..7d96a6a466d 100644 --- a/homeassistant/components/automation/translations/ca.json +++ b/homeassistant/components/automation/translations/ca.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "OFF", - "on": "ON" + "off": "off", + "on": "on" } }, "title": "Automatitzaci\u00f3" diff --git a/homeassistant/components/binary_sensor/translations/ca.json b/homeassistant/components/binary_sensor/translations/ca.json index f2889315fe5..3f30ef697e8 100644 --- a/homeassistant/components/binary_sensor/translations/ca.json +++ b/homeassistant/components/binary_sensor/translations/ca.json @@ -99,8 +99,8 @@ }, "state": { "_": { - "off": "OFF", - "on": "ON" + "off": "off", + "on": "on" }, "battery": { "off": "Normal", diff --git a/homeassistant/components/binary_sensor/translations/nl.json b/homeassistant/components/binary_sensor/translations/nl.json index f395335c627..1abf0b86bca 100644 --- a/homeassistant/components/binary_sensor/translations/nl.json +++ b/homeassistant/components/binary_sensor/translations/nl.json @@ -52,6 +52,7 @@ "connected": "{entity_name} verbonden", "gas": "{entity_name} begon gas te detecteren", "hot": "{entity_name} werd heet", + "is_tampered": "{entity_name} begonnen met het detecteren van sabotage", "light": "{entity_name} begon licht te detecteren", "locked": "{entity_name} vergrendeld", "moist": "{entity_name} werd vochtig", diff --git a/homeassistant/components/binary_sensor/translations/no.json b/homeassistant/components/binary_sensor/translations/no.json index 041643f9cc3..7dd6243edf8 100644 --- a/homeassistant/components/binary_sensor/translations/no.json +++ b/homeassistant/components/binary_sensor/translations/no.json @@ -31,6 +31,7 @@ "is_not_plugged_in": "{entity_name} er koblet fra", "is_not_powered": "{entity_name} er spenningsl\u00f8s", "is_not_present": "{entity_name} er ikke tilstede", + "is_not_tampered": "{entity_name} oppdager ikke manipulering", "is_not_unsafe": "{entity_name} er trygg", "is_occupied": "{entity_name} er opptatt", "is_off": "{entity_name} er sl\u00e5tt av", @@ -42,6 +43,7 @@ "is_problem": "{entity_name} registrerer et problem", "is_smoke": "{entity_name} registrerer r\u00f8yk", "is_sound": "{entity_name} registrerer lyd", + "is_tampered": "{entity_name} oppdager manipulering", "is_unsafe": "{entity_name} er utrygg", "is_update": "{entity_name} har en tilgjengelig oppdatering", "is_vibration": "{entity_name} registrerer vibrasjon" @@ -52,6 +54,8 @@ "connected": "{entity_name} tilkoblet", "gas": "{entity_name} begynte \u00e5 registrere gass", "hot": "{entity_name} ble varm", + "is_not_tampered": "{entity_name} sluttet \u00e5 oppdage manipulering", + "is_tampered": "{entity_name} begynte \u00e5 oppdage manipulering", "light": "{entity_name} begynte \u00e5 registrere lys", "locked": "{entity_name} l\u00e5st", "moist": "{entity_name} ble fuktig", diff --git a/homeassistant/components/binary_sensor/translations/pt-BR.json b/homeassistant/components/binary_sensor/translations/pt-BR.json index 52671ca0425..385d8620d76 100644 --- a/homeassistant/components/binary_sensor/translations/pt-BR.json +++ b/homeassistant/components/binary_sensor/translations/pt-BR.json @@ -1,4 +1,14 @@ { + "device_automation": { + "condition_type": { + "is_motion": "{entity_name} est\u00e1 detectando movimento", + "is_no_motion": "{entity_name} n\u00e3o est\u00e1 detectando movimento" + }, + "trigger_type": { + "motion": "{entity_name} come\u00e7ou a detectar movimento", + "no_motion": "{entity_name} parou de detectar movimento" + } + }, "state": { "_": { "off": "Desligado", diff --git a/homeassistant/components/bond/translations/hu.json b/homeassistant/components/bond/translations/hu.json index c1bac971f4b..179ec599d9f 100644 --- a/homeassistant/components/bond/translations/hu.json +++ b/homeassistant/components/bond/translations/hu.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "old_firmware": "Nem t\u00e1mogatott r\u00e9gi firmware a Bond eszk\u00f6z\u00f6n - k\u00e9rj\u00fck friss\u00edtsd, miel\u0151tt folytatn\u00e1d", + "old_firmware": "Nem t\u00e1mogatott r\u00e9gi firmware a Bond eszk\u00f6z\u00f6n - k\u00e9rj\u00fck friss\u00edtse, miel\u0151tt folytatn\u00e1", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/calendar/translations/ca.json b/homeassistant/components/calendar/translations/ca.json index f1b3279a4cb..63cffd7063f 100644 --- a/homeassistant/components/calendar/translations/ca.json +++ b/homeassistant/components/calendar/translations/ca.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "OFF", - "on": "ON" + "off": "off", + "on": "on" } }, "title": "Calendari" diff --git a/homeassistant/components/climate/translations/ca.json b/homeassistant/components/climate/translations/ca.json index 3eb99744751..89720be754e 100644 --- a/homeassistant/components/climate/translations/ca.json +++ b/homeassistant/components/climate/translations/ca.json @@ -22,7 +22,7 @@ "fan_only": "Nom\u00e9s ventilador", "heat": "Escalfa", "heat_cool": "Escalfa/Refreda", - "off": "OFF" + "off": "off" } }, "title": "Climatitzaci\u00f3" diff --git a/homeassistant/components/daikin/translations/no.json b/homeassistant/components/daikin/translations/no.json index e63b8eeef0d..45914b51578 100644 --- a/homeassistant/components/daikin/translations/no.json +++ b/homeassistant/components/daikin/translations/no.json @@ -5,6 +5,7 @@ "cannot_connect": "Tilkobling mislyktes" }, "error": { + "api_password": "Ugyldig godkjenning, bruk enten API -n\u00f8kkel eller passord.", "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning", "unknown": "Uventet feil" diff --git a/homeassistant/components/dlna_dmr/translations/ja.json b/homeassistant/components/dlna_dmr/translations/ja.json new file mode 100644 index 00000000000..1a0c1d7fdf4 --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/ja.json @@ -0,0 +1,17 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + }, + "options": { + "error": { + "invalid_url": "\u7121\u52b9\u306aURL" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/no.json b/homeassistant/components/dlna_dmr/translations/no.json index 1ddbfc32afe..83e3ba573b3 100644 --- a/homeassistant/components/dlna_dmr/translations/no.json +++ b/homeassistant/components/dlna_dmr/translations/no.json @@ -17,6 +17,9 @@ "confirm": { "description": "Vil du starte oppsettet?" }, + "import_turn_on": { + "description": "Sl\u00e5 p\u00e5 enheten og klikk p\u00e5 send for \u00e5 fortsette overf\u00f8ringen" + }, "user": { "data": { "url": "URL" diff --git a/homeassistant/components/ecobee/translations/ru.json b/homeassistant/components/ecobee/translations/ru.json index f983a80b389..2bd6ee3ea56 100644 --- a/homeassistant/components/ecobee/translations/ru.json +++ b/homeassistant/components/ecobee/translations/ru.json @@ -9,7 +9,7 @@ }, "step": { "authorize": { - "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443 https://www.ecobee.com/consumerportal/index.html \u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e PIN-\u043a\u043e\u0434\u0430: \n\n {pin} \n \n \u0417\u0430\u0442\u0435\u043c \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u041e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443 https://www.ecobee.com/consumerportal/index.html \u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e PIN-\u043a\u043e\u0434\u0430: \n\n {pin} \n \n \u0417\u0430\u0442\u0435\u043c \u043d\u0430\u0436\u043c\u0438\u0442\u0435 ''\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c''.", "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043d\u0430 ecobee.com" }, "user": { diff --git a/homeassistant/components/efergy/translations/ja.json b/homeassistant/components/efergy/translations/ja.json new file mode 100644 index 00000000000..c2ff6bbb145 --- /dev/null +++ b/homeassistant/components/efergy/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API\u30ad\u30fc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/no.json b/homeassistant/components/efergy/translations/no.json new file mode 100644 index 00000000000..388cbb36f64 --- /dev/null +++ b/homeassistant/components/efergy/translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel" + }, + "title": "Efergy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/ja.json b/homeassistant/components/environment_canada/translations/ja.json new file mode 100644 index 00000000000..0d27b8acbe5 --- /dev/null +++ b/homeassistant/components/environment_canada/translations/ja.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "language": "\u6c17\u8c61\u60c5\u5831\u306e\u8a00\u8a9e", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d4c\u5ea6", + "station": "\u30a6\u30a7\u30b6\u30fc\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/no.json b/homeassistant/components/environment_canada/translations/no.json new file mode 100644 index 00000000000..8d0fb1f201b --- /dev/null +++ b/homeassistant/components/environment_canada/translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "bad_station_id": "Stasjons -ID er ugyldig, mangler eller finnes ikke i stasjons -ID -databasen", + "cannot_connect": "Tilkobling mislyktes", + "error_response": "Svar fra Environment Canada feilaktig", + "too_many_attempts": "Tilkoblinger til milj\u00f8 Canada er takstbegrenset; Pr\u00f8v igjen om 60 sekunder", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "language": "Spr\u00e5k for v\u00e6rinformasjon", + "latitude": "Breddegrad", + "longitude": "Lengdegrad", + "station": "Id for v\u00e6rstasjon" + }, + "description": "Enten en stasjons -ID eller breddegrad/lengdegrad m\u00e5 spesifiseres. Standard breddegrad/lengdegrad som brukes er verdiene som er konfigurert i Home Assistant -installasjonen. Den n\u00e6rmeste v\u00e6rstasjonen til koordinatene vil bli brukt hvis du angir koordinater. Hvis en stasjonskode brukes, m\u00e5 den f\u00f8lge formatet: PP/kode, hvor PP er provinsen p\u00e5 to bokstaver og koden er stasjons-ID. Listen over stasjons -ID -er finner du her: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. V\u00e6rinformasjon kan hentes p\u00e5 enten engelsk eller fransk.", + "title": "Milj\u00f8 Canada: v\u00e6rsted og spr\u00e5k" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/translations/ca.json b/homeassistant/components/fan/translations/ca.json index 7c1789aeb24..da5296b34f0 100644 --- a/homeassistant/components/fan/translations/ca.json +++ b/homeassistant/components/fan/translations/ca.json @@ -15,8 +15,8 @@ }, "state": { "_": { - "off": "OFF", - "on": "ON" + "off": "off", + "on": "on" } }, "title": "Ventilador" diff --git a/homeassistant/components/geonetnz_quakes/translations/hu.json b/homeassistant/components/geonetnz_quakes/translations/hu.json index d6070db4fe7..c67444ec274 100644 --- a/homeassistant/components/geonetnz_quakes/translations/hu.json +++ b/homeassistant/components/geonetnz_quakes/translations/hu.json @@ -9,7 +9,7 @@ "mmi": "MMI", "radius": "Sug\u00e1r" }, - "title": "T\u00f6ltsd ki a sz\u0171r\u0151 adatait." + "title": "T\u00f6ltse ki a sz\u0171r\u0151 adatait." } } } diff --git a/homeassistant/components/group/translations/ca.json b/homeassistant/components/group/translations/ca.json index 552a2c9677e..5cb406727e8 100644 --- a/homeassistant/components/group/translations/ca.json +++ b/homeassistant/components/group/translations/ca.json @@ -5,9 +5,9 @@ "home": "A casa", "locked": "Bloquejat", "not_home": "Fora", - "off": "OFF", + "off": "off", "ok": "OK", - "on": "ON", + "on": "on", "open": "Obert/a", "problem": "Problema", "unlocked": "Desbloquejat" diff --git a/homeassistant/components/humidifier/translations/ca.json b/homeassistant/components/humidifier/translations/ca.json index bf0c1d805f6..5040fd4b419 100644 --- a/homeassistant/components/humidifier/translations/ca.json +++ b/homeassistant/components/humidifier/translations/ca.json @@ -20,8 +20,8 @@ }, "state": { "_": { - "off": "OFF", - "on": "ON" + "off": "off", + "on": "on" } }, "title": "Humidificador" diff --git a/homeassistant/components/input_boolean/translations/ca.json b/homeassistant/components/input_boolean/translations/ca.json index 23600285d58..8e3e86e9166 100644 --- a/homeassistant/components/input_boolean/translations/ca.json +++ b/homeassistant/components/input_boolean/translations/ca.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "OFF", - "on": "ON" + "off": "off", + "on": "on" } }, "title": "Entrada booleana" diff --git a/homeassistant/components/light/translations/ca.json b/homeassistant/components/light/translations/ca.json index 1e91f5005ce..788c4c3aa5c 100644 --- a/homeassistant/components/light/translations/ca.json +++ b/homeassistant/components/light/translations/ca.json @@ -19,8 +19,8 @@ }, "state": { "_": { - "off": "OFF", - "on": "ON" + "off": "off", + "on": "on" } }, "title": "Llum" diff --git a/homeassistant/components/lutron_caseta/translations/ca.json b/homeassistant/components/lutron_caseta/translations/ca.json index de714fea726..b56f0976119 100644 --- a/homeassistant/components/lutron_caseta/translations/ca.json +++ b/homeassistant/components/lutron_caseta/translations/ca.json @@ -48,8 +48,8 @@ "lower_3": "Baixa 3", "lower_4": "Baixa 4", "lower_all": "Baixa-ho tot", - "off": "OFF", - "on": "ON", + "off": "off", + "on": "on", "open_1": "Obre 1", "open_2": "Obre 2", "open_3": "Obre 3", diff --git a/homeassistant/components/lutron_caseta/translations/hu.json b/homeassistant/components/lutron_caseta/translations/hu.json index f3fca2ff705..a891a1b4b72 100644 --- a/homeassistant/components/lutron_caseta/translations/hu.json +++ b/homeassistant/components/lutron_caseta/translations/hu.json @@ -16,7 +16,7 @@ }, "link": { "description": "A(z) {name} ({host}) p\u00e1ros\u00edt\u00e1s\u00e1hoz az \u0171rlap elk\u00fcld\u00e9se ut\u00e1n nyomja meg a h\u00edd h\u00e1tulj\u00e1n tal\u00e1lhat\u00f3 fekete gombot.", - "title": "P\u00e1ros\u00edtsd a h\u00edddal" + "title": "P\u00e1ros\u00edt\u00e1s a h\u00edddal" }, "user": { "data": { diff --git a/homeassistant/components/media_player/translations/ca.json b/homeassistant/components/media_player/translations/ca.json index e1fce334053..5887685e119 100644 --- a/homeassistant/components/media_player/translations/ca.json +++ b/homeassistant/components/media_player/translations/ca.json @@ -18,8 +18,8 @@ "state": { "_": { "idle": "Inactiu", - "off": "OFF", - "on": "ON", + "off": "off", + "on": "on", "paused": "Pausat/ada", "playing": "Reproduint", "standby": "En espera" diff --git a/homeassistant/components/motion_blinds/translations/ca.json b/homeassistant/components/motion_blinds/translations/ca.json index b83746b9ccf..1db1976cbd3 100644 --- a/homeassistant/components/motion_blinds/translations/ca.json +++ b/homeassistant/components/motion_blinds/translations/ca.json @@ -6,13 +6,15 @@ "connection_error": "Ha fallat la connexi\u00f3" }, "error": { - "discovery_error": "No s'ha pogut descobrir cap Motion Gateway" + "discovery_error": "No s'ha pogut descobrir cap Motion Gateway", + "invalid_interface": "Interf\u00edcie de xarxa no v\u00e0lida" }, "flow_title": "Motion Blinds", "step": { "connect": { "data": { - "api_key": "Clau API" + "api_key": "Clau API", + "interface": "Interf\u00edcie de xarxa a utilitzar" }, "description": "Necessitar\u00e0s la clau API de 16 car\u00e0cters, consulta les instruccions a https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key.", "title": "Motion Blinds" @@ -33,5 +35,16 @@ "title": "Motion Blinds" } } + }, + "options": { + "step": { + "init": { + "data": { + "wait_for_push": "Espera l'entrada multidifusi\u00f3 en actualitzar" + }, + "description": "Especifica configuracions opcionals", + "title": "Motion Blinds" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/nl.json b/homeassistant/components/motion_blinds/translations/nl.json index 54baeb9e18d..3d3d078b814 100644 --- a/homeassistant/components/motion_blinds/translations/nl.json +++ b/homeassistant/components/motion_blinds/translations/nl.json @@ -6,13 +6,15 @@ "connection_error": "Kan geen verbinding maken" }, "error": { - "discovery_error": "Kan geen Motion Gateway vinden" + "discovery_error": "Kan geen Motion Gateway vinden", + "invalid_interface": "Ongeldige netwerkinterface" }, "flow_title": "Motion Blinds", "step": { "connect": { "data": { - "api_key": "API-sleutel" + "api_key": "API-sleutel", + "interface": "De te gebruiken netwerkinterface" }, "description": "U hebt de API-sleutel van 16 tekens nodig, zie https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key voor instructies", "title": "Motion Blinds" @@ -33,5 +35,16 @@ "title": "Motion Blinds" } } + }, + "options": { + "step": { + "init": { + "data": { + "wait_for_push": "Wacht op multicast push bij update" + }, + "description": "Optionele instellingen opgeven", + "title": "Motion Blinds" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/no.json b/homeassistant/components/motion_blinds/translations/no.json index e86da7c1fc4..242a6647da9 100644 --- a/homeassistant/components/motion_blinds/translations/no.json +++ b/homeassistant/components/motion_blinds/translations/no.json @@ -6,13 +6,15 @@ "connection_error": "Tilkobling mislyktes" }, "error": { - "discovery_error": "Kunne ikke oppdage en Motion Gateway" + "discovery_error": "Kunne ikke oppdage en Motion Gateway", + "invalid_interface": "Ugyldig nettverksgrensesnitt" }, "flow_title": "Motion Blinds", "step": { "connect": { "data": { - "api_key": "API-n\u00f8kkel" + "api_key": "API-n\u00f8kkel", + "interface": "Nettverksgrensesnittet som skal brukes" }, "description": "Du trenger API-n\u00f8kkelen med 16 tegn, se https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instruksjoner", "title": "" @@ -33,5 +35,16 @@ "title": "Motion Blinds" } } + }, + "options": { + "step": { + "init": { + "data": { + "wait_for_push": "Vent p\u00e5 multicast push p\u00e5 oppdateringen" + }, + "description": "Spesifiser valgfrie innstillinger", + "title": "Bevegelse Persienner" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/pt-BR.json b/homeassistant/components/motion_blinds/translations/pt-BR.json new file mode 100644 index 00000000000..0d9e257feba --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/pt-BR.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "connect": { + "data": { + "interface": "A interface de rede a ser utilizada" + } + }, + "select": { + "data": { + "select_ip": "Endere\u00e7o de IP" + } + } + } + }, + "options": { + "step": { + "init": { + "description": "Especifique as configura\u00e7\u00f5es opcionais" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/pt-BR.json b/homeassistant/components/motioneye/translations/pt-BR.json new file mode 100644 index 00000000000..cbc33e7c1c4 --- /dev/null +++ b/homeassistant/components/motioneye/translations/pt-BR.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + } + } +} \ 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 aab253057dd..6d312fa98c1 100644 --- a/homeassistant/components/nest/translations/pt-BR.json +++ b/homeassistant/components/nest/translations/pt-BR.json @@ -24,5 +24,10 @@ "title": "Link da conta Nest" } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "Movimento detectado" + } } } \ No newline at end of file diff --git a/homeassistant/components/notion/translations/hu.json b/homeassistant/components/notion/translations/hu.json index 43f4f1f914c..1af2d5907fc 100644 --- a/homeassistant/components/notion/translations/hu.json +++ b/homeassistant/components/notion/translations/hu.json @@ -22,7 +22,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "title": "T\u00f6ltsd ki az adataid" + "title": "T\u00f6ltse ki az adatait" } } } diff --git a/homeassistant/components/onvif/translations/ru.json b/homeassistant/components/onvif/translations/ru.json index 55d6549645d..c8643fa9ec3 100644 --- a/homeassistant/components/onvif/translations/ru.json +++ b/homeassistant/components/onvif/translations/ru.json @@ -53,7 +53,7 @@ "data": { "auto": "\u0418\u0441\u043a\u0430\u0442\u044c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438" }, - "description": "\u041a\u043e\u0433\u0434\u0430 \u0412\u044b \u043d\u0430\u0436\u043c\u0451\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u041e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c, \u043d\u0430\u0447\u043d\u0451\u0442\u0441\u044f \u043f\u043e\u0438\u0441\u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 ONVIF, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442 Profile S.\n\n\u041d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u0438 \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u043e\u0442\u043a\u043b\u044e\u0447\u0430\u044e\u0442 ONVIF. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e ONVIF \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u0412\u0430\u0448\u0435\u0439 \u043a\u0430\u043c\u0435\u0440\u044b.", + "description": "\u041a\u043e\u0433\u0434\u0430 \u0412\u044b \u043d\u0430\u0436\u043c\u0451\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 ''\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c'', \u043d\u0430\u0447\u043d\u0451\u0442\u0441\u044f \u043f\u043e\u0438\u0441\u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 ONVIF, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442 Profile S.\n\n\u041d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u0438 \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u043e\u0442\u043a\u043b\u044e\u0447\u0430\u044e\u0442 ONVIF. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e ONVIF \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u0412\u0430\u0448\u0435\u0439 \u043a\u0430\u043c\u0435\u0440\u044b.", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 ONVIF" } } diff --git a/homeassistant/components/opengarage/translations/ja.json b/homeassistant/components/opengarage/translations/ja.json new file mode 100644 index 00000000000..83f7d96e874 --- /dev/null +++ b/homeassistant/components/opengarage/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "device_key": "\u30c7\u30d0\u30a4\u30b9\u30ad\u30fc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/translations/hu.json b/homeassistant/components/openuv/translations/hu.json index 1c3c6387dd3..e94d076782d 100644 --- a/homeassistant/components/openuv/translations/hu.json +++ b/homeassistant/components/openuv/translations/hu.json @@ -14,7 +14,7 @@ "latitude": "Sz\u00e9less\u00e9g", "longitude": "Hossz\u00fas\u00e1g" }, - "title": "T\u00f6ltsd ki az adataid" + "title": "T\u00f6ltse ki az adatait" } } }, diff --git a/homeassistant/components/rainmachine/translations/hu.json b/homeassistant/components/rainmachine/translations/hu.json index c6120797a72..0aa7e0aeeb4 100644 --- a/homeassistant/components/rainmachine/translations/hu.json +++ b/homeassistant/components/rainmachine/translations/hu.json @@ -14,7 +14,7 @@ "password": "Jelsz\u00f3", "port": "Port" }, - "title": "T\u00f6ltsd ki az adataid" + "title": "T\u00f6ltse ki az adatait" } } }, diff --git a/homeassistant/components/remote/translations/ca.json b/homeassistant/components/remote/translations/ca.json index 7e001059f14..b8a317c60d0 100644 --- a/homeassistant/components/remote/translations/ca.json +++ b/homeassistant/components/remote/translations/ca.json @@ -16,8 +16,8 @@ }, "state": { "_": { - "off": "OFF", - "on": "ON" + "off": "off", + "on": "on" } }, "title": "Comandament" diff --git a/homeassistant/components/rfxtrx/translations/ca.json b/homeassistant/components/rfxtrx/translations/ca.json index 477bfa14608..dd1bb23c56a 100644 --- a/homeassistant/components/rfxtrx/translations/ca.json +++ b/homeassistant/components/rfxtrx/translations/ca.json @@ -49,9 +49,9 @@ "error": { "already_configured_device": "El dispositiu ja est\u00e0 configurat", "invalid_event_code": "Codi d'esdeveniment inv\u00e0lid", - "invalid_input_2262_off": "Entrada no v\u00e0lida per a l'ordre OFF", + "invalid_input_2262_off": "Entrada no v\u00e0lida per a l'ordre off", "invalid_input_2262_on": "Entrada no v\u00e0lida per a l'ordre ON", - "invalid_input_off_delay": "Entrada no v\u00e0lida per al retard OFF", + "invalid_input_off_delay": "Entrada no v\u00e0lida per al retard off", "unknown": "Error inesperat" }, "step": { @@ -67,12 +67,12 @@ }, "set_device_options": { "data": { - "command_off": "Valor dels bits de dades per a l'ordre OFF", + "command_off": "Valor dels bits de dades per a l'ordre off", "command_on": "Valor dels bits de dades per a l'ordre ON", "data_bit": "Nombre de bits de dades", "fire_event": "Activa l'esdeveniment de dispositiu", - "off_delay": "Retard OFF", - "off_delay_enabled": "Activa el retard OFF", + "off_delay": "Retard off", + "off_delay_enabled": "Activa el retard off", "replace_device": "Selecciona el dispositiu a substituir", "signal_repetitions": "Nombre de repeticions del senyal", "venetian_blind_mode": "Mode persiana veneciana" diff --git a/homeassistant/components/script/translations/ca.json b/homeassistant/components/script/translations/ca.json index 4369856606f..905805e21fe 100644 --- a/homeassistant/components/script/translations/ca.json +++ b/homeassistant/components/script/translations/ca.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "OFF", - "on": "ON" + "off": "off", + "on": "on" } }, "title": "Programa (script)" diff --git a/homeassistant/components/sensor/translations/ca.json b/homeassistant/components/sensor/translations/ca.json index c90ed273a67..ddf8f50a010 100644 --- a/homeassistant/components/sensor/translations/ca.json +++ b/homeassistant/components/sensor/translations/ca.json @@ -55,8 +55,8 @@ }, "state": { "_": { - "off": "OFF", - "on": "ON" + "off": "off", + "on": "on" } }, "title": "Sensor" diff --git a/homeassistant/components/simplisafe/translations/ca.json b/homeassistant/components/simplisafe/translations/ca.json index 9eb1390466c..66dd5c6ddf9 100644 --- a/homeassistant/components/simplisafe/translations/ca.json +++ b/homeassistant/components/simplisafe/translations/ca.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Aquest compte SimpliSafe ja est\u00e0 en \u00fas.", - "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "wrong_account": "Les credencials d'usuari proporcionades no coincideixen amb les d'aquest compte SimpliSafe." }, "error": { "identifier_exists": "Aquest compte ja est\u00e0 registrat", @@ -11,6 +12,13 @@ "unknown": "Error inesperat" }, "step": { + "input_auth_code": { + "data": { + "auth_code": "Codi d'autoritzaci\u00f3" + }, + "description": "Introdueix el codi d'autoritzaci\u00f3 de l'URL de l'aplicaci\u00f3 web SimpliSafe:", + "title": "Acabament d'autoritzaci\u00f3" + }, "mfa": { "description": "Consulta el correu i busca-hi un missatge amb un enlla\u00e7 de SimpliSafe. Despr\u00e9s de verificar l'enlla\u00e7, torna aqu\u00ed per completar la instal\u00b7laci\u00f3 de la integraci\u00f3.", "title": "Autenticaci\u00f3 multi-factor SimpliSafe" @@ -28,6 +36,7 @@ "password": "Contrasenya", "username": "Correu electr\u00f2nic" }, + "description": "A partir del 2021, SimpliSafe ha passat a un nou mecanisme d'autenticaci\u00f3 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 estiguis a punt, fes clic a [aqu\u00ed]({url}) per obrir l'aplicaci\u00f3 web SimpliSafe i introduir les credencials. Quan el proc\u00e9s s'hagi completat, torna aqu\u00ed i fes clic a Envia.", "title": "Introdueix la teva informaci\u00f3" } } diff --git a/homeassistant/components/simplisafe/translations/ja.json b/homeassistant/components/simplisafe/translations/ja.json new file mode 100644 index 00000000000..70f15e85dd5 --- /dev/null +++ b/homeassistant/components/simplisafe/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "input_auth_code": { + "data": { + "auth_code": "\u8a8d\u8a3c\u30b3\u30fc\u30c9" + }, + "title": "\u627f\u8a8d\u7d42\u4e86" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/nl.json b/homeassistant/components/simplisafe/translations/nl.json index 8fa91994aca..4d6df03f890 100644 --- a/homeassistant/components/simplisafe/translations/nl.json +++ b/homeassistant/components/simplisafe/translations/nl.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Dit SimpliSafe-account is al in gebruik.", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie was succesvol", + "wrong_account": "De opgegeven gebruikersgegevens komen niet overeen met deze SimpliSafe-account." }, "error": { "identifier_exists": "Account bestaat al", @@ -11,6 +12,13 @@ "unknown": "Onverwachte fout" }, "step": { + "input_auth_code": { + "data": { + "auth_code": "Autorisatie Code" + }, + "description": "Voer de autorisatiecode in van de SimpliSafe web app URL:", + "title": "Autorisatie voltooien" + }, "mfa": { "description": "Controleer uw e-mail voor een link van SimpliSafe. Nadat u de link hebt geverifieerd, kom hier terug om de installatie van de integratie te voltooien.", "title": "SimpliSafe Multi-Factor Authenticatie" @@ -28,6 +36,7 @@ "password": "Wachtwoord", "username": "E-mail" }, + "description": "Met ingang van 2021 is SimpliSafe overgestapt op een nieuw authenticatiemechanisme via de webapp. Vanwege technische beperkingen is er een handmatige stap aan het einde van dit proces; zorg ervoor dat u de [documentatie](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) leest voordat u begint.\n\nWanneer u er klaar voor bent, klikt u op [hier]({url}) om de SimpliSafe web app te openen en uw inloggegevens in te voeren. Wanneer het proces is voltooid, gaat u hier terug en klikt u op Verzenden.", "title": "Vul uw gegevens in" } } diff --git a/homeassistant/components/simplisafe/translations/no.json b/homeassistant/components/simplisafe/translations/no.json index acd8adf0792..0b4d11029d7 100644 --- a/homeassistant/components/simplisafe/translations/no.json +++ b/homeassistant/components/simplisafe/translations/no.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Denne SimpliSafe-kontoen er allerede i bruk.", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "wrong_account": "Brukerlegitimasjonen som er oppgitt, samsvarer ikke med denne SimpliSafe -kontoen." }, "error": { "identifier_exists": "Konto er allerede registrert", @@ -11,6 +12,13 @@ "unknown": "Uventet feil" }, "step": { + "input_auth_code": { + "data": { + "auth_code": "Autorisasjonskode" + }, + "description": "Skriv inn autorisasjonskoden fra SimpliSafe -webappens URL:", + "title": "Fullf\u00f8r autorisasjon" + }, "mfa": { "description": "Sjekk e-posten din for en lenke fra SimpliSafe. Etter \u00e5 ha bekreftet lenken, g\u00e5 tilbake hit for \u00e5 fullf\u00f8re installasjonen av integrasjonen.", "title": "SimpliSafe flertrinnsbekreftelse" @@ -28,6 +36,7 @@ "password": "Passord", "username": "E-post" }, + "description": "Fra og med 2021 har SimpliSafe flyttet til en ny godkjenningsmekanisme via nettappen. 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, klikker du [her] ( {url} ) for \u00e5 \u00e5pne SimpliSafe -webappen og legge inn legitimasjonen din. N\u00e5r prosessen er fullf\u00f8rt, g\u00e5r du tilbake hit og klikker Send.", "title": "Fyll ut informasjonen din." } } diff --git a/homeassistant/components/simplisafe/translations/pt-BR.json b/homeassistant/components/simplisafe/translations/pt-BR.json index 832af325d4b..0e5b2151e20 100644 --- a/homeassistant/components/simplisafe/translations/pt-BR.json +++ b/homeassistant/components/simplisafe/translations/pt-BR.json @@ -4,6 +4,11 @@ "identifier_exists": "Conta j\u00e1 cadastrada" }, "step": { + "input_auth_code": { + "data": { + "auth_code": "C\u00f3digo de Autoriza\u00e7\u00e3o" + } + }, "user": { "data": { "password": "Senha", diff --git a/homeassistant/components/simplisafe/translations/ru.json b/homeassistant/components/simplisafe/translations/ru.json index 5fc3fce065e..099637e422c 100644 --- a/homeassistant/components/simplisafe/translations/ru.json +++ b/homeassistant/components/simplisafe/translations/ru.json @@ -2,7 +2,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.", - "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.", + "wrong_account": "\u0423\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0442 \u044d\u0442\u043e\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 SimpliSafe." }, "error": { "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", @@ -11,6 +12,13 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { + "input_auth_code": { + "data": { + "auth_code": "\u041a\u043e\u0434 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0438\u0437 \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f SimpliSafe:", + "title": "\u0417\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438" + }, "mfa": { "description": "\u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0441\u0432\u043e\u044e \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0443\u044e \u043f\u043e\u0447\u0442\u0443 \u043d\u0430 \u043d\u0430\u043b\u0438\u0447\u0438\u0435 \u0441\u0441\u044b\u043b\u043a\u0438 \u043e\u0442 SimpliSafe. \u041f\u043e\u0441\u043b\u0435 \u0442\u043e\u0433\u043e \u043a\u0430\u043a \u043e\u0442\u043a\u0440\u043e\u0435\u0442\u0435 \u0441\u0441\u044b\u043b\u043a\u0443, \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0443 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438.", "title": "\u0414\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f SimpliSafe" @@ -28,6 +36,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" }, + "description": "\u041d\u0430\u0447\u0438\u043d\u0430\u044f \u0441 2021 \u0433\u043e\u0434\u0430 SimpliSafe \u043f\u0435\u0440\u0435\u0448\u043b\u0430 \u043d\u0430 \u043d\u043e\u0432\u044b\u0439 \u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0447\u0435\u0440\u0435\u0437 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\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 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 ''\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c''.", "title": "SimpliSafe" } } diff --git a/homeassistant/components/simplisafe/translations/zh-Hant.json b/homeassistant/components/simplisafe/translations/zh-Hant.json index 517d48321a8..59931520202 100644 --- a/homeassistant/components/simplisafe/translations/zh-Hant.json +++ b/homeassistant/components/simplisafe/translations/zh-Hant.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "\u6b64 SimpliSafe \u5e33\u865f\u5df2\u88ab\u4f7f\u7528\u3002", - "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "wrong_account": "\u6240\u4ee5\u63d0\u4f9b\u7684\u6191\u8b49\u8207 Simplisafe \u5e33\u865f\u4e0d\u7b26\u3002" }, "error": { "identifier_exists": "\u5e33\u865f\u5df2\u8a3b\u518a", @@ -11,6 +12,13 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "input_auth_code": { + "data": { + "auth_code": "\u8a8d\u8b49\u78bc" + }, + "description": "\u8f38\u5165\u7531 SimpliSafe \u7db2\u9801 App URL \u6240\u53d6\u5f97\u4e4b\u8a8d\u8b49\u78bc\uff1a", + "title": "\u5b8c\u6210\u8a8d\u8b49" + }, "mfa": { "description": "\u8acb\u6aa2\u67e5\u4f86\u81ea SimpliSafe \u7684\u90f5\u4ef6\u4ee5\u53d6\u5f97\u9023\u7d50\u3002\u78ba\u8a8d\u9023\u7d50\u5f8c\uff0c\u518d\u56de\u5230\u6b64\u8655\u4ee5\u5b8c\u6210\u6574\u5408\u5b89\u88dd\u3002", "title": "SimpliSafe \u591a\u6b65\u9a5f\u9a57\u8b49" @@ -28,6 +36,7 @@ "password": "\u5bc6\u78bc", "username": "\u96fb\u5b50\u90f5\u4ef6" }, + "description": "SimpliSafe \u81ea 2021 \u958b\u59cb\u8b8a\u66f4\u70ba\u900f\u904e Web App \u65b9\u5f0f\u7684\u8a8d\u8b49\u6a5f\u5236\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\u9ede\u9078\u50b3\u9001\u3002", "title": "\u586b\u5beb\u8cc7\u8a0a\u3002" } } diff --git a/homeassistant/components/soma/translations/no.json b/homeassistant/components/soma/translations/no.json index a399f430329..e07d382cae1 100644 --- a/homeassistant/components/soma/translations/no.json +++ b/homeassistant/components/soma/translations/no.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "already_setup": "Du kan bare konfigurere \u00e9n Soma-konto.", - "authorize_url_timeout": "Tidsavbrudd genererer godkjennelses-URL.", - "connection_error": "Kunne ikke koble til SOMA Connect.", + "already_setup": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", + "connection_error": "Tilkobling mislyktes", "missing_configuration": "Soma-komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", "result_error": "SOMA Connect svarte med feilstatus." }, "create_entry": { - "default": "Vellykket godkjenning med Somfy." + "default": "Vellykket godkjenning" }, "step": { "user": { diff --git a/homeassistant/components/stookalert/translations/no.json b/homeassistant/components/stookalert/translations/no.json new file mode 100644 index 00000000000..f964d23ce39 --- /dev/null +++ b/homeassistant/components/stookalert/translations/no.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert" + }, + "step": { + "user": { + "data": { + "province": "Provins" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/translations/ca.json b/homeassistant/components/switch/translations/ca.json index e39386a680f..999373799f0 100644 --- a/homeassistant/components/switch/translations/ca.json +++ b/homeassistant/components/switch/translations/ca.json @@ -16,8 +16,8 @@ }, "state": { "_": { - "off": "OFF", - "on": "ON" + "off": "off", + "on": "on" } }, "title": "Interruptor" diff --git a/homeassistant/components/tradfri/translations/no.json b/homeassistant/components/tradfri/translations/no.json index abdf0a26b12..c3db70b1255 100644 --- a/homeassistant/components/tradfri/translations/no.json +++ b/homeassistant/components/tradfri/translations/no.json @@ -5,6 +5,7 @@ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede" }, "error": { + "cannot_authenticate": "Kan ikke autentisere, er Gateway paret med en annen server som f.eks. Homekit?", "cannot_connect": "Tilkobling mislyktes", "invalid_key": "Kunne ikke registrere med gitt n\u00f8kkel. Hvis dette fortsetter, pr\u00f8v \u00e5 starte gatewayen p\u00e5 nytt.", "timeout": "Tidsavbrudd ved validering av kode." diff --git a/homeassistant/components/tuya/translations/hu.json b/homeassistant/components/tuya/translations/hu.json index 6f2ae6e4219..9b19ea34d54 100644 --- a/homeassistant/components/tuya/translations/hu.json +++ b/homeassistant/components/tuya/translations/hu.json @@ -75,7 +75,7 @@ "query_device": "V\u00e1lassza ki azt az eszk\u00f6zt, amely a lek\u00e9rdez\u00e9si m\u00f3dszert haszn\u00e1lja a gyorsabb \u00e1llapotfriss\u00edt\u00e9shez", "query_interval": "Eszk\u00f6z lek\u00e9rdez\u00e9si id\u0151k\u00f6ze m\u00e1sodpercben" }, - "description": "Ne \u00e1ll\u00edtsd t\u00fal alacsonyra a lek\u00e9rdez\u00e9si intervallum \u00e9rt\u00e9keit, k\u00fcl\u00f6nben a h\u00edv\u00e1sok nem fognak hiba\u00fczenetet gener\u00e1lni a napl\u00f3ban", + "description": "Ne \u00e1ll\u00edtsa t\u00fal alacsonyra a lek\u00e9rdez\u00e9si intervallum \u00e9rt\u00e9keit, k\u00fcl\u00f6nben a h\u00edv\u00e1sok nem fognak hiba\u00fczenetet gener\u00e1lni a napl\u00f3ban", "title": "Tuya be\u00e1ll\u00edt\u00e1sok konfigur\u00e1l\u00e1sa" } } diff --git a/homeassistant/components/tuya/translations/ja.json b/homeassistant/components/tuya/translations/ja.json new file mode 100644 index 00000000000..6454194b1c7 --- /dev/null +++ b/homeassistant/components/tuya/translations/ja.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "login": { + "data": { + "access_id": "\u30a2\u30af\u30bb\u30b9ID", + "country_code": "\u56fd\u5225\u30b3\u30fc\u30c9", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "tuya_app_type": "\u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea", + "username": "\u30a2\u30ab\u30a6\u30f3\u30c8" + }, + "title": "Tuya" + }, + "user": { + "data": { + "region": "\u30ea\u30fc\u30b8\u30e7\u30f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/no.json b/homeassistant/components/tuya/translations/no.json index 254eb1c1230..16657972322 100644 --- a/homeassistant/components/tuya/translations/no.json +++ b/homeassistant/components/tuya/translations/no.json @@ -28,7 +28,7 @@ "data": { "access_id": "Tuya IoT Access ID", "access_secret": "Tuya IoT Access Secret", - "country_code": "Din landskode for kontoen din (f.eks. 1 for USA eller 86 for Kina)", + "country_code": "Land", "password": "Passord", "platform": "Appen der kontoen din er registrert", "region": "Region", diff --git a/homeassistant/components/tuya/translations/pt-BR.json b/homeassistant/components/tuya/translations/pt-BR.json index 8dc537e7549..34c91bfe0fe 100644 --- a/homeassistant/components/tuya/translations/pt-BR.json +++ b/homeassistant/components/tuya/translations/pt-BR.json @@ -7,6 +7,7 @@ "country_code": "O c\u00f3digo do pa\u00eds da sua conta (por exemplo, 1 para os EUA ou 86 para a China)", "password": "Senha", "platform": "O aplicativo onde sua conta \u00e9 registrada", + "region": "Regi\u00e3o", "username": "Nome de usu\u00e1rio" }, "description": "Digite sua credencial Tuya.", diff --git a/homeassistant/components/uptimerobot/translations/nl.json b/homeassistant/components/uptimerobot/translations/nl.json index 7e0ad6a3cd0..3431a9cbfa5 100644 --- a/homeassistant/components/uptimerobot/translations/nl.json +++ b/homeassistant/components/uptimerobot/translations/nl.json @@ -17,14 +17,14 @@ "data": { "api_key": "API-sleutel" }, - "description": "U moet een alleen-lezen API-sleutel van Uptime Robot opgeven", + "description": "U moet een nieuwe alleen-lezen API-sleutel van UptimeRobot opgeven", "title": "Verifieer de integratie opnieuw" }, "user": { "data": { "api_key": "API-sleutel" }, - "description": "U moet een alleen-lezen API-sleutel van Uptime Robot opgeven" + "description": "U moet een alleen-lezen API-sleutel van UptimeRobot opgeven" } } } diff --git a/homeassistant/components/uptimerobot/translations/no.json b/homeassistant/components/uptimerobot/translations/no.json index 8c6351d78c4..cbb5066a747 100644 --- a/homeassistant/components/uptimerobot/translations/no.json +++ b/homeassistant/components/uptimerobot/translations/no.json @@ -17,14 +17,14 @@ "data": { "api_key": "API-n\u00f8kkel" }, - "description": "Du m\u00e5 angi en ny skrivebeskyttet API-n\u00f8kkel fra Uptime Robot", + "description": "Du m\u00e5 levere en ny skrivebeskyttet API-n\u00f8kkel fra UptimeRobot", "title": "Godkjenne integrering p\u00e5 nytt" }, "user": { "data": { "api_key": "API-n\u00f8kkel" }, - "description": "Du m\u00e5 angi en skrivebeskyttet API-n\u00f8kkel fra Uptime Robot" + "description": "Du m\u00e5 levere en skrivebeskyttet API-n\u00f8kkel fra UptimeRobot" } } } diff --git a/homeassistant/components/vacuum/translations/ca.json b/homeassistant/components/vacuum/translations/ca.json index d98a51a5363..11d431a1810 100644 --- a/homeassistant/components/vacuum/translations/ca.json +++ b/homeassistant/components/vacuum/translations/ca.json @@ -19,8 +19,8 @@ "docked": "Aparcat", "error": "Error", "idle": "Inactiu", - "off": "OFF", - "on": "ON", + "off": "off", + "on": "on", "paused": "Pausat/ada", "returning": "Retornant a base" } diff --git a/homeassistant/components/vlc_telnet/translations/cs.json b/homeassistant/components/vlc_telnet/translations/cs.json index a06a73ff569..d23f42bdc76 100644 --- a/homeassistant/components/vlc_telnet/translations/cs.json +++ b/homeassistant/components/vlc_telnet/translations/cs.json @@ -2,7 +2,10 @@ "config": { "abort": { "already_configured": "Slu\u017eba je ji\u017e nastavena", - "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", diff --git a/homeassistant/components/vlc_telnet/translations/ja.json b/homeassistant/components/vlc_telnet/translations/ja.json new file mode 100644 index 00000000000..4aa4b490324 --- /dev/null +++ b/homeassistant/components/vlc_telnet/translations/ja.json @@ -0,0 +1,20 @@ +{ + "config": { + "flow_title": "{host}", + "step": { + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + }, + "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "name": "\u540d\u524d", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vlc_telnet/translations/nl.json b/homeassistant/components/vlc_telnet/translations/nl.json index ad9fc759014..538421263c9 100644 --- a/homeassistant/components/vlc_telnet/translations/nl.json +++ b/homeassistant/components/vlc_telnet/translations/nl.json @@ -2,7 +2,10 @@ "config": { "abort": { "already_configured": "Deze service is al geconfigureerd", - "reauth_successful": "Het opnieuw verifi\u00ebren is geslaagd" + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "reauth_successful": "Het opnieuw verifi\u00ebren is geslaagd", + "unknown": "Onverwachte fout" }, "error": { "cannot_connect": "Verbinding tot stand brengen is mislukt", @@ -11,6 +14,9 @@ }, "flow_title": "{host}", "step": { + "hassio_confirm": { + "description": "Wilt u verbinding maken met add-on {addon}?" + }, "reauth_confirm": { "data": { "password": "Wachtwoord" diff --git a/homeassistant/components/vlc_telnet/translations/no.json b/homeassistant/components/vlc_telnet/translations/no.json new file mode 100644 index 00000000000..9becf574700 --- /dev/null +++ b/homeassistant/components/vlc_telnet/translations/no.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "unknown": "Uventet feil" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "flow_title": "{host}", + "step": { + "hassio_confirm": { + "description": "Vil du koble til tillegg {addon} ?" + }, + "reauth_confirm": { + "data": { + "password": "Passord" + }, + "description": "Skriv inn riktig passord for verten: {host}" + }, + "user": { + "data": { + "host": "Vert", + "name": "Navn", + "password": "Passord", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vlc_telnet/translations/pt-BR.json b/homeassistant/components/vlc_telnet/translations/pt-BR.json new file mode 100644 index 00000000000..f9028aae002 --- /dev/null +++ b/homeassistant/components/vlc_telnet/translations/pt-BR.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "Senha" + } + }, + "user": { + "data": { + "name": "Nome", + "password": "Senha", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/water_heater/translations/ca.json b/homeassistant/components/water_heater/translations/ca.json index 022b5e887d8..6033868ccaa 100644 --- a/homeassistant/components/water_heater/translations/ca.json +++ b/homeassistant/components/water_heater/translations/ca.json @@ -12,7 +12,7 @@ "gas": "Gas", "heat_pump": "Bomba de calor", "high_demand": "Alta demanda", - "off": "OFF", + "off": "off", "performance": "Rendiment" } } diff --git a/homeassistant/components/watttime/translations/no.json b/homeassistant/components/watttime/translations/no.json index a58a29ff052..bef57982658 100644 --- a/homeassistant/components/watttime/translations/no.json +++ b/homeassistant/components/watttime/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", @@ -22,6 +23,13 @@ }, "description": "Velg et sted \u00e5 overv\u00e5ke:" }, + "reauth_confirm": { + "data": { + "password": "Passord" + }, + "description": "Skriv inn passordet for {username} :", + "title": "Godkjenne integrering p\u00e5 nytt" + }, "user": { "data": { "password": "Passord", diff --git a/homeassistant/components/watttime/translations/pt-BR.json b/homeassistant/components/watttime/translations/pt-BR.json new file mode 100644 index 00000000000..a522da7febd --- /dev/null +++ b/homeassistant/components/watttime/translations/pt-BR.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "location": { + "data": { + "location_type": "Localiza\u00e7\u00e3o" + } + }, + "reauth_confirm": { + "data": { + "password": "Senha" + } + }, + "user": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/ja.json b/homeassistant/components/xiaomi_miio/translations/ja.json new file mode 100644 index 00000000000..a138f746baf --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/ja.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "wrong_token": "\u30c1\u30a7\u30c3\u30af\u30b5\u30e0\u30a8\u30e9\u30fc\u3001\u9593\u9055\u3063\u305f\u30c8\u30fc\u30af\u30f3" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/no.json b/homeassistant/components/xiaomi_miio/translations/no.json index a296dd7aa08..9094cce023a 100644 --- a/homeassistant/components/xiaomi_miio/translations/no.json +++ b/homeassistant/components/xiaomi_miio/translations/no.json @@ -13,7 +13,8 @@ "cloud_login_error": "Kunne ikke logge inn p\u00e5 Xiaomi Miio Cloud, sjekk legitimasjonen.", "cloud_no_devices": "Ingen enheter funnet i denne Xiaomi Miio-skykontoen.", "no_device_selected": "Ingen enhet valgt, vennligst velg en enhet.", - "unknown_device": "Enhetsmodellen er ikke kjent, kan ikke konfigurere enheten ved hjelp av konfigurasjonsflyt." + "unknown_device": "Enhetsmodellen er ikke kjent, kan ikke konfigurere enheten ved hjelp av konfigurasjonsflyt.", + "wrong_token": "Kontrollsumfeil, feil token" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/xiaomi_miio/translations/select.ca.json b/homeassistant/components/xiaomi_miio/translations/select.ca.json index bc96de04645..eb54883ffa6 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.ca.json +++ b/homeassistant/components/xiaomi_miio/translations/select.ca.json @@ -3,7 +3,7 @@ "xiaomi_miio__led_brightness": { "bright": "Brillant", "dim": "Atenua", - "off": "OFF" + "off": "off" } } } \ No newline at end of file From 3bdc637d1b80706c6fba8e9f44ca6f1ae3ed649c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 20 Oct 2021 20:26:25 -0700 Subject: [PATCH 0611/1038] Bump frontend to 20211020.0 (#58139) --- 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 114f68ba720..0c614dbbc7e 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20211014.0" + "home-assistant-frontend==20211020.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 857b74a9870..0508619a1e4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==3.4.8 emoji==1.5.0 hass-nabucasa==0.50.0 -home-assistant-frontend==20211014.0 +home-assistant-frontend==20211020.0 httpx==0.19.0 ifaddr==0.1.7 jinja2==3.0.2 diff --git a/requirements_all.txt b/requirements_all.txt index d28ca5c359a..942c942ad7b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -810,7 +810,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211014.0 +home-assistant-frontend==20211020.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fe6a0179587..fc5034c6404 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -494,7 +494,7 @@ hole==0.5.1 holidays==0.11.3.1 # homeassistant.components.frontend -home-assistant-frontend==20211014.0 +home-assistant-frontend==20211020.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From dcaa68902326c76834373a185b0b4ad051a59198 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 21 Oct 2021 07:20:39 +0200 Subject: [PATCH 0612/1038] Add auto slider/box mode to number entity (#57737) --- homeassistant/components/demo/number.py | 29 ++++++++++++++++++++- homeassistant/components/number/__init__.py | 11 +++++++- homeassistant/components/number/const.py | 6 +++++ tests/components/demo/test_number.py | 21 ++++++++++++++- 4 files changed, 64 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/demo/number.py b/homeassistant/components/demo/number.py index cad2255806e..8f3ccf47230 100644 --- a/homeassistant/components/demo/number.py +++ b/homeassistant/components/demo/number.py @@ -1,7 +1,10 @@ """Demo platform that offers a fake Number entity.""" from __future__ import annotations +from typing import Literal + from homeassistant.components.number import NumberEntity +from homeassistant.components.number.const import MODE_AUTO, MODE_BOX, MODE_SLIDER from homeassistant.const import DEVICE_DEFAULT_NAME from . import DOMAIN @@ -17,6 +20,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= 42.0, "mdi:volume-high", False, + mode=MODE_SLIDER, ), DemoNumber( "pwm1", @@ -27,6 +31,27 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= 0.0, 1.0, 0.01, + MODE_BOX, + ), + DemoNumber( + "large_range", + "Large Range", + 500, + "mdi:square-wave", + False, + 1, + 1000, + 1, + ), + DemoNumber( + "small_range", + "Small Range", + 128, + "mdi:square-wave", + False, + 1, + 255, + 1, ), ] ) @@ -51,7 +76,8 @@ class DemoNumber(NumberEntity): assumed: bool, min_value: float | None = None, max_value: float | None = None, - step=None, + step: float | None = None, + mode: Literal["auto", "box", "slider"] = MODE_AUTO, ) -> None: """Initialize the Demo Number entity.""" self._attr_assumed_state = assumed @@ -59,6 +85,7 @@ class DemoNumber(NumberEntity): self._attr_name = name or DEVICE_DEFAULT_NAME self._attr_unique_id = unique_id self._attr_value = state + self._attr_mode = mode if min_value is not None: self._attr_min_value = min_value diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index ac727288b07..89ad7d8b8c2 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -4,11 +4,12 @@ from __future__ import annotations from dataclasses import dataclass from datetime import timedelta import logging -from typing import Any, final +from typing import Any, Literal, final import voluptuous as vol from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_MODE from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -27,6 +28,7 @@ from .const import ( DEFAULT_MIN_VALUE, DEFAULT_STEP, DOMAIN, + MODE_AUTO, SERVICE_SET_VALUE, ) @@ -90,6 +92,7 @@ class NumberEntity(Entity): _attr_min_value: float = DEFAULT_MIN_VALUE _attr_state: None = None _attr_step: float + _attr_mode: Literal["auto", "slider", "box"] = MODE_AUTO _attr_value: float @property @@ -99,6 +102,7 @@ class NumberEntity(Entity): ATTR_MIN: self.min_value, ATTR_MAX: self.max_value, ATTR_STEP: self.step, + ATTR_MODE: self.mode, } @property @@ -123,6 +127,11 @@ class NumberEntity(Entity): step /= 10.0 return step + @property + def mode(self) -> Literal["auto", "slider", "box"]: + """Return the mode of the entity.""" + return self._attr_mode + @property @final def state(self) -> float | None: diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py index 2aa8075cba3..749463b11e5 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/const.py @@ -1,10 +1,16 @@ """Provides the constants needed for the component.""" +from typing import Final + ATTR_VALUE = "value" ATTR_MIN = "min" ATTR_MAX = "max" ATTR_STEP = "step" +MODE_AUTO: Final = "auto" +MODE_BOX: Final = "box" +MODE_SLIDER: Final = "slider" + DEFAULT_MIN_VALUE = 0.0 DEFAULT_MAX_VALUE = 100.0 DEFAULT_STEP = 1.0 diff --git a/tests/components/demo/test_number.py b/tests/components/demo/test_number.py index 82536b0d2f8..88e46f5c66d 100644 --- a/tests/components/demo/test_number.py +++ b/tests/components/demo/test_number.py @@ -9,13 +9,18 @@ from homeassistant.components.number.const import ( ATTR_STEP, ATTR_VALUE, DOMAIN, + MODE_AUTO, + MODE_BOX, + MODE_SLIDER, SERVICE_SET_VALUE, ) -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE from homeassistant.setup import async_setup_component ENTITY_VOLUME = "number.volume" ENTITY_PWM = "number.pwm_1" +ENTITY_LARGE_RANGE = "number.large_range" +ENTITY_SMALL_RANGE = "number.small_range" @pytest.fixture(autouse=True) @@ -37,11 +42,25 @@ def test_default_setup_params(hass): assert state.attributes.get(ATTR_MIN) == 0.0 assert state.attributes.get(ATTR_MAX) == 100.0 assert state.attributes.get(ATTR_STEP) == 1.0 + assert state.attributes.get(ATTR_MODE) == MODE_SLIDER state = hass.states.get(ENTITY_PWM) assert state.attributes.get(ATTR_MIN) == 0.0 assert state.attributes.get(ATTR_MAX) == 1.0 assert state.attributes.get(ATTR_STEP) == 0.01 + assert state.attributes.get(ATTR_MODE) == MODE_BOX + + state = hass.states.get(ENTITY_LARGE_RANGE) + assert state.attributes.get(ATTR_MIN) == 1.0 + assert state.attributes.get(ATTR_MAX) == 1000.0 + assert state.attributes.get(ATTR_STEP) == 1.0 + assert state.attributes.get(ATTR_MODE) == MODE_AUTO + + state = hass.states.get(ENTITY_SMALL_RANGE) + assert state.attributes.get(ATTR_MIN) == 1.0 + assert state.attributes.get(ATTR_MAX) == 255.0 + assert state.attributes.get(ATTR_STEP) == 1.0 + assert state.attributes.get(ATTR_MODE) == MODE_AUTO async def test_set_value_bad_attr(hass): From 379a0e2b530ccb2741dfab19f554d59c695e1318 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 21 Oct 2021 01:00:34 -0500 Subject: [PATCH 0613/1038] Add `configuration_url` to Sonos devices (#58148) --- homeassistant/components/sonos/entity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py index 5730679dbd9..0d53c848ae9 100644 --- a/homeassistant/components/sonos/entity.py +++ b/homeassistant/components/sonos/entity.py @@ -101,6 +101,7 @@ class SonosEntity(Entity): }, "manufacturer": "Sonos", "suggested_area": self.speaker.zone_name, + "configuration_url": f"http://{self.soco.ip_address}:1400/support/review", } @property From 3ffe0b36253121897f53c8017b5bbbf6c4a9e2e2 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 21 Oct 2021 01:01:00 -0500 Subject: [PATCH 0614/1038] Add `configuration_url` to Plex integration (#58149) --- homeassistant/components/plex/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index db2ce15d395..6ca5f50c4ad 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -113,6 +113,7 @@ class PlexSensor(SensorEntity): "model": "Plex Media Server", "name": self._server.friendly_name, "sw_version": self._server.version, + "configuration_url": f"{self._server.url_in_use}/web", } @@ -203,4 +204,5 @@ class PlexLibrarySectionSensor(SensorEntity): "model": "Plex Media Server", "name": self.server_name, "sw_version": self._server.version, + "configuration_url": f"{self._server.url_in_use}/web", } From 548b87222943fdf48974ae6bedb537d62abd68ed Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Thu, 21 Oct 2021 08:20:37 +0200 Subject: [PATCH 0615/1038] Add missing names for notify service fields (#58154) --- homeassistant/components/notify/services.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/notify/services.yaml b/homeassistant/components/notify/services.yaml index a4ac45e9df8..7284cb68eb6 100644 --- a/homeassistant/components/notify/services.yaml +++ b/homeassistant/components/notify/services.yaml @@ -18,6 +18,7 @@ notify: selector: text: target: + name: Target description: An array of targets to send the notification to. Optional depending on the platform. @@ -38,12 +39,14 @@ persistent_notification: description: Sends a notification to the visible in the front-end. fields: message: + name: Message description: Message body of the notification. required: true example: The garage door has been open for 10 minutes. selector: text: title: + name: Title description: Title for your notification. example: "Your Garage Door Friend" selector: From 8f1ba96d0b5bfb422676809ebda28b5e7abb9a80 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Thu, 21 Oct 2021 08:21:04 +0200 Subject: [PATCH 0616/1038] Remove accidental blanks from Shelly trigger type translations (#58151) --- homeassistant/components/shelly/strings.json | 4 ++-- homeassistant/helpers/reload.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index 43cae79f94a..19a464fe7ce 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -40,14 +40,14 @@ "single": "{subtype} single clicked", "double": "{subtype} double clicked", "triple": "{subtype} triple clicked", - "long": " {subtype} long clicked", + "long": "{subtype} long clicked", "single_long": "{subtype} single clicked and then long clicked", "long_single": "{subtype} long clicked and then single clicked", "btn_down": "{subtype} button down", "btn_up": "{subtype} button up", "single_push": "{subtype} single push", "double_push": "{subtype} double push", - "long_push": " {subtype} long push" + "long_push": "{subtype} long push" } } } diff --git a/homeassistant/helpers/reload.py b/homeassistant/helpers/reload.py index cedd07676ba..8f9ce48a59b 100644 --- a/homeassistant/helpers/reload.py +++ b/homeassistant/helpers/reload.py @@ -82,7 +82,7 @@ async def _resetup_platform( await component.async_setup(hass, root_config) # type: ignore return - # If its an entity platform, we use the entity_platform + # If it's an entity platform, we use the entity_platform # async_reset method platform = async_get_platform_without_config_entry( hass, integration_name, integration_platform @@ -93,7 +93,7 @@ async def _resetup_platform( if not root_config[integration_platform]: # No config for this platform - # and its not loaded. Nothing to do + # and it's not loaded. Nothing to do. return await _async_setup_platform( From acb24788d009342fc224d9ed55c91885c8b8e515 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Thu, 21 Oct 2021 08:21:48 +0200 Subject: [PATCH 0617/1038] Update pyhomematic to 0.1.76 (#58136) --- homeassistant/components/homematic/const.py | 2 ++ homeassistant/components/homematic/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematic/const.py b/homeassistant/components/homematic/const.py index 03b3f55e505..8aaa3ea21ac 100644 --- a/homeassistant/components/homematic/const.py +++ b/homeassistant/components/homematic/const.py @@ -64,6 +64,7 @@ HM_DEVICE_TYPES = { "IPSwitchBattery", "IPMultiIOPCB", "IPGarageSwitch", + "IPWHS2", ], DISCOVER_LIGHTS: [ "Dimmer", @@ -178,6 +179,7 @@ HM_DEVICE_TYPES = { "IPLanRouter", "IPMultiIOPCB", "IPLockDLD", + "IPWHS2", ], DISCOVER_COVER: [ "Blind", diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json index 34015426d78..896470f5a42 100644 --- a/homeassistant/components/homematic/manifest.json +++ b/homeassistant/components/homematic/manifest.json @@ -2,7 +2,7 @@ "domain": "homematic", "name": "Homematic", "documentation": "https://www.home-assistant.io/integrations/homematic", - "requirements": ["pyhomematic==0.1.75"], + "requirements": ["pyhomematic==0.1.76"], "codeowners": ["@pvizeli", "@danielperna84"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 942c942ad7b..15d664ba697 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1520,7 +1520,7 @@ pyhik==0.2.8 pyhiveapi==0.4.2 # homeassistant.components.homematic -pyhomematic==0.1.75 +pyhomematic==0.1.76 # homeassistant.components.homeworks pyhomeworks==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fc5034c6404..0f58342b8e5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -899,7 +899,7 @@ pyheos==0.7.2 pyhiveapi==0.4.2 # homeassistant.components.homematic -pyhomematic==0.1.75 +pyhomematic==0.1.76 # homeassistant.components.ialarm pyialarm==1.9.0 From 4eea618cc464052e1a341998aa218528f2ba048e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 20 Oct 2021 23:22:34 -0700 Subject: [PATCH 0618/1038] input_datetime: Move has_date, has_time to capability_attributes (#58138) --- homeassistant/components/input_datetime/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index 13063910e2b..de767d69ba8 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -341,13 +341,19 @@ class InputDatetime(RestoreEntity): return self._current_datetime.strftime(FMT_TIME) + @property + def capability_attributes(self) -> dict: + """Return the capability attributes.""" + return { + CONF_HAS_DATE: self.has_date, + CONF_HAS_TIME: self.has_time, + } + @property def extra_state_attributes(self): """Return the state attributes.""" attrs = { ATTR_EDITABLE: self.editable, - CONF_HAS_DATE: self.has_date, - CONF_HAS_TIME: self.has_time, } if self._current_datetime is None: From c979e89b70ac05f6a7a5a91af0fe11f68ec2e836 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 21 Oct 2021 08:26:01 +0200 Subject: [PATCH 0619/1038] Use assignment expressions 14 (#57939) --- homeassistant/components/ddwrt/device_tracker.py | 4 +--- homeassistant/components/denonavr/config_flow.py | 3 +-- homeassistant/components/digital_ocean/binary_sensor.py | 3 +-- homeassistant/components/digital_ocean/switch.py | 3 +-- homeassistant/components/dovado/notify.py | 4 +--- homeassistant/components/econet/water_heater.py | 3 +-- homeassistant/components/greeneye_monitor/__init__.py | 3 +-- homeassistant/components/habitica/sensor.py | 3 +-- homeassistant/components/icloud/__init__.py | 8 ++------ homeassistant/components/intesishome/climate.py | 7 ++----- homeassistant/components/lcn/climate.py | 3 +-- homeassistant/components/lcn/helpers.py | 3 +-- homeassistant/components/lcn/services.py | 6 ++---- homeassistant/components/lock/reproduce_state.py | 4 +--- homeassistant/components/opnsense/device_tracker.py | 3 +-- homeassistant/components/owntracks/__init__.py | 4 +--- homeassistant/components/panasonic_viera/__init__.py | 3 +-- homeassistant/components/risco/alarm_control_panel.py | 3 +-- homeassistant/components/squeezebox/browse_media.py | 3 +-- homeassistant/components/squeezebox/media_player.py | 3 +-- homeassistant/components/telegram_bot/__init__.py | 3 +-- homeassistant/components/xiaomi_aqara/__init__.py | 6 ++---- homeassistant/components/xiaomi_aqara/config_flow.py | 4 +--- homeassistant/components/xiaomi_aqara/lock.py | 9 +++------ homeassistant/components/xiaomi_aqara/sensor.py | 3 +-- 25 files changed, 31 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/ddwrt/device_tracker.py b/homeassistant/components/ddwrt/device_tracker.py index bc52e7712b6..bc4ef7f8f82 100644 --- a/homeassistant/components/ddwrt/device_tracker.py +++ b/homeassistant/components/ddwrt/device_tracker.py @@ -88,9 +88,7 @@ class DdWrtDeviceScanner(DeviceScanner): if not data: return None - dhcp_leases = data.get("dhcp_leases") - - if not dhcp_leases: + if not (dhcp_leases := data.get("dhcp_leases")): return None # Remove leading and trailing quotes and spaces diff --git a/homeassistant/components/denonavr/config_flow.py b/homeassistant/components/denonavr/config_flow.py index 1005858e729..ffb73327d31 100644 --- a/homeassistant/components/denonavr/config_flow.py +++ b/homeassistant/components/denonavr/config_flow.py @@ -111,8 +111,7 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: # check if IP address is set manually - host = user_input.get(CONF_HOST) - if host: + if host := user_input.get(CONF_HOST): self.host = host return await self.async_step_connect() diff --git a/homeassistant/components/digital_ocean/binary_sensor.py b/homeassistant/components/digital_ocean/binary_sensor.py index 9a9f82c36d2..b5188092862 100644 --- a/homeassistant/components/digital_ocean/binary_sensor.py +++ b/homeassistant/components/digital_ocean/binary_sensor.py @@ -36,8 +36,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Digital Ocean droplet sensor.""" - digital = hass.data.get(DATA_DIGITAL_OCEAN) - if not digital: + if not (digital := hass.data.get(DATA_DIGITAL_OCEAN)): return False droplets = config[CONF_DROPLETS] diff --git a/homeassistant/components/digital_ocean/switch.py b/homeassistant/components/digital_ocean/switch.py index 0678b9ab1a1..d52c223c866 100644 --- a/homeassistant/components/digital_ocean/switch.py +++ b/homeassistant/components/digital_ocean/switch.py @@ -33,8 +33,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Digital Ocean droplet switch.""" - digital = hass.data.get(DATA_DIGITAL_OCEAN) - if not digital: + if not (digital := hass.data.get(DATA_DIGITAL_OCEAN)): return False droplets = config[CONF_DROPLETS] diff --git a/homeassistant/components/dovado/notify.py b/homeassistant/components/dovado/notify.py index 02ce994b1df..c599ad918e8 100644 --- a/homeassistant/components/dovado/notify.py +++ b/homeassistant/components/dovado/notify.py @@ -22,9 +22,7 @@ class DovadoSMSNotificationService(BaseNotificationService): def send_message(self, message, **kwargs): """Send SMS to the specified target phone number.""" - target = kwargs.get(ATTR_TARGET) - - if not target: + if not (target := kwargs.get(ATTR_TARGET)): _LOGGER.error("One target is required") return diff --git a/homeassistant/components/econet/water_heater.py b/homeassistant/components/econet/water_heater.py index ed31e78af7c..7ea4d7740a5 100644 --- a/homeassistant/components/econet/water_heater.py +++ b/homeassistant/components/econet/water_heater.py @@ -113,8 +113,7 @@ class EcoNetWaterHeater(EcoNetEntity, WaterHeaterEntity): def set_temperature(self, **kwargs): """Set new target temperature.""" - target_temp = kwargs.get(ATTR_TEMPERATURE) - if target_temp is not None: + 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") diff --git a/homeassistant/components/greeneye_monitor/__init__.py b/homeassistant/components/greeneye_monitor/__init__.py index 51471739e98..cc7b8955756 100644 --- a/homeassistant/components/greeneye_monitor/__init__.py +++ b/homeassistant/components/greeneye_monitor/__init__.py @@ -157,8 +157,7 @@ async def async_setup(hass, config): } ) - sensor_configs = monitor_config[CONF_TEMPERATURE_SENSORS] - if sensor_configs: + if sensor_configs := monitor_config[CONF_TEMPERATURE_SENSORS]: temperature_unit = { CONF_TEMPERATURE_UNIT: sensor_configs[CONF_TEMPERATURE_UNIT] } diff --git a/homeassistant/components/habitica/sensor.py b/homeassistant/components/habitica/sensor.py index ae27e0a51fc..64494fb9694 100644 --- a/homeassistant/components/habitica/sensor.py +++ b/homeassistant/components/habitica/sensor.py @@ -213,8 +213,7 @@ class HabitipyTaskSensor(SensorEntity): task_id = received_task[TASKS_MAP_ID] task = {} for map_key, map_value in TASKS_MAP.items(): - value = received_task.get(map_value) - if value: + if value := received_task.get(map_value): task[map_key] = value attrs[task_id] = task return attrs diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index 865b77b4ddf..7054a0f0ad1 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -88,9 +88,7 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up iCloud from legacy config file.""" - - conf = config.get(DOMAIN) - if conf is None: + if (conf := config.get(DOMAIN)) is None: return True for account_conf in conf: @@ -169,9 +167,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def update_account(service: ServiceDataType) -> None: """Call the update function of an iCloud account.""" - account = service.data.get(ATTR_ACCOUNT) - - if account is None: + if (account := service.data.get(ATTR_ACCOUNT)) is None: for account in hass.data[DOMAIN].values(): account.keep_alive() else: diff --git a/homeassistant/components/intesishome/climate.py b/homeassistant/components/intesishome/climate.py index b93babf534e..afe0fb519a8 100644 --- a/homeassistant/components/intesishome/climate.py +++ b/homeassistant/components/intesishome/climate.py @@ -259,13 +259,10 @@ class IntesisAC(ClimateEntity): async def async_set_temperature(self, **kwargs): """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - hvac_mode = kwargs.get(ATTR_HVAC_MODE) - - if hvac_mode: + if hvac_mode := kwargs.get(ATTR_HVAC_MODE): await self.async_set_hvac_mode(hvac_mode) - if temperature: + if temperature := kwargs.get(ATTR_TEMPERATURE): _LOGGER.debug("Setting %s to %s degrees", self._device_type, temperature) await self._controller.set_temperature(self._device_id, temperature) self._target_temp = temperature diff --git a/homeassistant/components/lcn/climate.py b/homeassistant/components/lcn/climate.py index 4254a5e5480..c3882033caf 100644 --- a/homeassistant/components/lcn/climate.py +++ b/homeassistant/components/lcn/climate.py @@ -176,8 +176,7 @@ class LcnClimate(LcnEntity, ClimateEntity): async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return if not await self.device_connection.var_abs( diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py index 5aaede430c5..b62f8474470 100644 --- a/homeassistant/components/lcn/helpers.py +++ b/homeassistant/components/lcn/helpers.py @@ -208,8 +208,7 @@ def has_unique_host_names(hosts: list[ConfigType]) -> list[ConfigType]: """ suffix = 0 for host in hosts: - host_name = host.get(CONF_NAME) - if host_name is None: + if host.get(CONF_NAME) is None: if suffix == 0: host[CONF_NAME] = DEFAULT_NAME else: diff --git a/homeassistant/components/lcn/services.py b/homeassistant/components/lcn/services.py index 8c305d68403..c55e6586edf 100644 --- a/homeassistant/components/lcn/services.py +++ b/homeassistant/components/lcn/services.py @@ -307,8 +307,7 @@ class SendKeys(LcnServiceCall): key_id = int(key) - 1 keys[table_id][key_id] = True - delay_time = service.data[CONF_TIME] - if delay_time != 0: + if (delay_time := service.data[CONF_TIME]) != 0: hit = pypck.lcn_defs.SendKeyCommand.HIT if pypck.lcn_defs.SendKeyCommand[service.data[CONF_STATE]] != hit: raise ValueError( @@ -347,8 +346,7 @@ class LockKeys(LcnServiceCall): ] table_id = ord(service.data[CONF_TABLE]) - 65 - delay_time = service.data[CONF_TIME] - if delay_time != 0: + if (delay_time := service.data[CONF_TIME]) != 0: if table_id != 0: raise ValueError( "Only table A is allowed when locking keys for a specific time." diff --git a/homeassistant/components/lock/reproduce_state.py b/homeassistant/components/lock/reproduce_state.py index cdd538c88be..b6eefcfac63 100644 --- a/homeassistant/components/lock/reproduce_state.py +++ b/homeassistant/components/lock/reproduce_state.py @@ -32,9 +32,7 @@ async def _async_reproduce_state( reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" - cur_state = hass.states.get(state.entity_id) - - if cur_state is None: + if (cur_state := hass.states.get(state.entity_id)) is None: _LOGGER.warning("Unable to find entity %s", state.entity_id) return diff --git a/homeassistant/components/opnsense/device_tracker.py b/homeassistant/components/opnsense/device_tracker.py index 936872f7a86..0bce00a8e82 100644 --- a/homeassistant/components/opnsense/device_tracker.py +++ b/homeassistant/components/opnsense/device_tracker.py @@ -56,7 +56,6 @@ class OPNSenseDeviceScanner(DeviceScanner): """Return the extra attrs of the given device.""" if device not in self.last_results: return None - mfg = self.last_results[device].get("manufacturer") - if not mfg: + if not (mfg := self.last_results[device].get("manufacturer")): return {} return {"manufacturer": mfg} diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index d51566718d6..24c7cc74d52 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -242,9 +242,7 @@ class OwnTracksContext: @callback def async_valid_accuracy(self, message): """Check if we should ignore this message.""" - acc = message.get("acc") - - if acc is None: + if (acc := message.get("acc")) is None: return False try: diff --git a/homeassistant/components/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py index ab63b535e80..419181da2d9 100644 --- a/homeassistant/components/panasonic_viera/__init__.py +++ b/homeassistant/components/panasonic_viera/__init__.py @@ -73,8 +73,7 @@ async def async_setup_entry(hass, config_entry): host = config[CONF_HOST] port = config[CONF_PORT] - on_action = config[CONF_ON_ACTION] - if on_action is not None: + if (on_action := config[CONF_ON_ACTION]) is not None: on_action = Script(hass, on_action, config[CONF_NAME], DOMAIN) params = {} diff --git a/homeassistant/components/risco/alarm_control_panel.py b/homeassistant/components/risco/alarm_control_panel.py index 705ec50db28..9b2f603ce8a 100644 --- a/homeassistant/components/risco/alarm_control_panel.py +++ b/homeassistant/components/risco/alarm_control_panel.py @@ -164,8 +164,7 @@ class RiscoAlarm(AlarmControlPanelEntity, RiscoEntity): _LOGGER.warning("Wrong code entered for %s", mode) return - risco_state = self._ha_to_risco[mode] - if not risco_state: + if not (risco_state := self._ha_to_risco[mode]): _LOGGER.warning("No mapping for mode %s", mode) return diff --git a/homeassistant/components/squeezebox/browse_media.py b/homeassistant/components/squeezebox/browse_media.py index 294a1105a71..81d0231ae70 100644 --- a/homeassistant/components/squeezebox/browse_media.py +++ b/homeassistant/components/squeezebox/browse_media.py @@ -97,8 +97,7 @@ async def build_item_response(entity, player, payload): item_id = str(item["id"]) item_thumbnail = None - artwork_track_id = item.get("artwork_track_id") - if artwork_track_id: + if artwork_track_id := item.get("artwork_track_id"): if internal_request: item_thumbnail = player.generate_image_url_from_track_id( artwork_track_id diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 1ba406097d7..a0904c3178d 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -561,8 +561,7 @@ class SqueezeBoxEntity(MediaPlayerEntity): player_ids = { p.entity_id: p.unique_id for p in self.hass.data[DOMAIN][KNOWN_PLAYERS] } - other_player_id = player_ids.get(other_player) - if other_player_id: + if other_player_id := player_ids.get(other_player): await self._player.async_sync(other_player_id) else: _LOGGER.info("Could not find player_id for %s. Not syncing", other_player) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 84b7249d203..7fd83141b7d 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -329,8 +329,7 @@ async def async_setup(hass, config): """Handle sending Telegram Bot message service calls.""" def _render_template_attr(data, attribute): - attribute_templ = data.get(attribute) - if attribute_templ: + if attribute_templ := data.get(attribute): if any( isinstance(attribute_templ, vtype) for vtype in (float, int, str) ): diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index d78398fb46f..44637c530ef 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -76,8 +76,7 @@ def setup(hass, config): kwargs = {"mid": ring_id} - ring_vol = call.data.get(ATTR_RINGTONE_VOL) - if ring_vol is not None: + if (ring_vol := call.data.get(ATTR_RINGTONE_VOL)) is not None: kwargs["vol"] = ring_vol gateway.write_to_hub(gateway.sid, **kwargs) @@ -379,8 +378,7 @@ def _add_gateway_to_schema(hass, schema): raise vol.Invalid(f"Unknown gateway sid {sid}") kwargs = {} - xiaomi_data = hass.data.get(DOMAIN) - if xiaomi_data is not None: + if (xiaomi_data := hass.data.get(DOMAIN)) is not None: gateways = list(xiaomi_data[GATEWAYS_KEY].values()) # If the user has only 1 gateway, make it the default for services. diff --git a/homeassistant/components/xiaomi_aqara/config_flow.py b/homeassistant/components/xiaomi_aqara/config_flow.py index 68c688d3eb1..3fbe7a71496 100644 --- a/homeassistant/components/xiaomi_aqara/config_flow.py +++ b/homeassistant/components/xiaomi_aqara/config_flow.py @@ -76,10 +76,8 @@ class XiaomiAqaraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if self.host is None: self.host = user_input.get(CONF_HOST) if self.sid is None: - mac_address = user_input.get(CONF_MAC) - # format sid from mac_address - if mac_address is not None: + if (mac_address := user_input.get(CONF_MAC)) is not None: self.sid = format_mac(mac_address).replace(":", "") # if host is already known by zeroconf discovery or manual optional settings diff --git a/homeassistant/components/xiaomi_aqara/lock.py b/homeassistant/components/xiaomi_aqara/lock.py index 5afb1701e33..b7167452b65 100644 --- a/homeassistant/components/xiaomi_aqara/lock.py +++ b/homeassistant/components/xiaomi_aqara/lock.py @@ -22,8 +22,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] gateway = hass.data[DOMAIN][GATEWAYS_KEY][config_entry.entry_id] for device in gateway.devices["lock"]: - model = device["model"] - if model == "lock.aq1": + if device["model"] == "lock.aq1": entities.append(XiaomiAqaraLock(device, "Lock", gateway, config_entry)) async_add_entities(entities) @@ -63,14 +62,12 @@ class XiaomiAqaraLock(LockEntity, XiaomiDevice): def parse_data(self, data, raw_data): """Parse data sent by gateway.""" - value = data.get(VERIFIED_WRONG_KEY) - if value is not None: + if (value := data.get(VERIFIED_WRONG_KEY)) is not None: self._verified_wrong_times = int(value) return True for key in (FINGER_KEY, PASSWORD_KEY, CARD_KEY): - value = data.get(key) - if value is not None: + if (value := data.get(key)) is not None: self._changed_by = int(value) self._verified_wrong_times = 0 self._state = STATE_UNLOCKED diff --git a/homeassistant/components/xiaomi_aqara/sensor.py b/homeassistant/components/xiaomi_aqara/sensor.py index 3935f4fdc57..c3f5cf4dacc 100644 --- a/homeassistant/components/xiaomi_aqara/sensor.py +++ b/homeassistant/components/xiaomi_aqara/sensor.py @@ -158,8 +158,7 @@ class XiaomiSensor(XiaomiDevice, SensorEntity): def parse_data(self, data, raw_data): """Parse data sent by gateway.""" - value = data.get(self._data_key) - if value is None: + if (value := data.get(self._data_key)) is None: return False if self._data_key in ("coordination", "status"): self._attr_native_value = value From 1bcf39517a0b9581b017526bbccaa066bacca914 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 21 Oct 2021 08:27:42 +0200 Subject: [PATCH 0620/1038] Use assignment expressions 13 (#57938) --- homeassistant/components/avion/light.py | 4 +--- homeassistant/components/aws/__init__.py | 7 ++----- homeassistant/components/aws/notify.py | 3 +-- .../components/forked_daapd/media_player.py | 8 +++----- homeassistant/components/here_travel_time/sensor.py | 7 ++----- homeassistant/components/izone/__init__.py | 3 +-- homeassistant/components/izone/climate.py | 6 ++---- homeassistant/components/izone/discovery.py | 6 ++---- homeassistant/components/kaiterra/api_data.py | 4 +--- homeassistant/components/logentries/__init__.py | 3 +-- homeassistant/components/motioneye/__init__.py | 3 +-- homeassistant/components/netgear_lte/__init__.py | 7 ++----- homeassistant/components/openhome/media_player.py | 3 +-- homeassistant/components/pilight/binary_sensor.py | 3 +-- homeassistant/components/proliphix/climate.py | 3 +-- homeassistant/components/rocketchat/notify.py | 3 +-- homeassistant/components/smartthings/__init__.py | 3 +-- homeassistant/components/smartthings/climate.py | 12 ++++-------- homeassistant/components/smartthings/lock.py | 3 +-- homeassistant/components/smartthings/smartapp.py | 6 ++---- 20 files changed, 31 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/avion/light.py b/homeassistant/components/avion/light.py index fcc780f77bc..0bf1787aac7 100644 --- a/homeassistant/components/avion/light.py +++ b/homeassistant/components/avion/light.py @@ -95,9 +95,7 @@ class AvionLight(LightEntity): def turn_on(self, **kwargs): """Turn the specified or all lights on.""" - brightness = kwargs.get(ATTR_BRIGHTNESS) - - if brightness is not None: + if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is not None: self._attr_brightness = brightness self.set_state(self.brightness) diff --git a/homeassistant/components/aws/__init__.py b/homeassistant/components/aws/__init__.py index da8c27d7445..6dcbad748cc 100644 --- a/homeassistant/components/aws/__init__.py +++ b/homeassistant/components/aws/__init__.py @@ -85,8 +85,7 @@ async def async_setup(hass, config): """Set up AWS component.""" hass.data[DATA_HASS_CONFIG] = config - conf = config.get(DOMAIN) - if conf is None: + if (conf := config.get(DOMAIN)) is None: # create a default conf using default profile conf = CONFIG_SCHEMA({ATTR_CREDENTIALS: DEFAULT_CREDENTIAL}) @@ -159,9 +158,7 @@ async def _validate_aws_credentials(hass, credential): del aws_config[CONF_NAME] del aws_config[CONF_VALIDATE] - profile = aws_config.get(CONF_PROFILE_NAME) - - if profile is not None: + if (profile := aws_config.get(CONF_PROFILE_NAME)) is not None: session = aiobotocore.AioSession(profile=profile) del aws_config[CONF_PROFILE_NAME] if CONF_ACCESS_KEY_ID in aws_config: diff --git a/homeassistant/components/aws/notify.py b/homeassistant/components/aws/notify.py index c9d6ca2faa7..b271a2a8786 100644 --- a/homeassistant/components/aws/notify.py +++ b/homeassistant/components/aws/notify.py @@ -82,8 +82,7 @@ async def async_get_service(hass, config, discovery_info=None): del aws_config[CONF_CREDENTIAL_NAME] if session is None: - profile = aws_config.get(CONF_PROFILE_NAME) - if profile is not None: + if (profile := aws_config.get(CONF_PROFILE_NAME)) is not None: session = aiobotocore.AioSession(profile=profile) del aws_config[CONF_PROFILE_NAME] else: diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index 724db80fabd..aeb2350ce22 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -621,8 +621,7 @@ class ForkedDaapdMaster(MediaPlayerEntity): @property def media_image_url(self): """Image url of current playing media.""" - url = self._track_info.get("artwork_url") - if url: + if url := self._track_info.get("artwork_url"): url = self._api.full_url(url) return url @@ -769,11 +768,10 @@ class ForkedDaapdUpdater: async def async_init(self): """Perform async portion of class initialization.""" server_config = await self._api.get_request("config") - websocket_port = server_config.get("websocket_port") - if websocket_port: + if websocket_port := server_config.get("websocket_port"): self.websocket_handler = asyncio.create_task( self._api.start_websocket_handler( - server_config["websocket_port"], + websocket_port, WS_NOTIFY_EVENT_TYPES, self._update, WEBSOCKET_RECONNECT_TIME, diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index a47e28179b3..1d47bbaf89f 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -326,9 +326,7 @@ class HERETravelTimeSensor(SensorEntity): async def _get_location_from_entity(self, entity_id: str) -> str | None: """Get the location from the entity state or attributes.""" - entity = self.hass.states.get(entity_id) - - if entity is None: + if (entity := self.hass.states.get(entity_id)) is None: _LOGGER.error("Unable to find entity %s", entity_id) return None @@ -484,8 +482,7 @@ class HERETravelTimeData: if suppliers is not None: supplier_titles = [] for supplier in suppliers: - title = supplier.get("title") - if title is not None: + if (title := supplier.get("title")) is not None: supplier_titles.append(title) joined_supplier_titles = ",".join(supplier_titles) attribution = f"With the support of {joined_supplier_titles}. All information is provided without warranty of any kind." diff --git a/homeassistant/components/izone/__init__.py b/homeassistant/components/izone/__init__.py index e3f4b62af63..2ac66963638 100644 --- a/homeassistant/components/izone/__init__.py +++ b/homeassistant/components/izone/__init__.py @@ -28,8 +28,7 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Register the iZone component config.""" - conf = config.get(IZONE) - if not conf: + if not (conf := config.get(IZONE)): return True hass.data[DATA_CONFIG] = conf diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index 06dfee0e7fb..bd1de0f935e 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -429,8 +429,7 @@ class ControllerDevice(ClimateEntity): if not self.supported_features & SUPPORT_TARGET_TEMPERATURE: self.async_schedule_update_ha_state(True) return - temp = kwargs.get(ATTR_TEMPERATURE) - if temp is not None: + if (temp := kwargs.get(ATTR_TEMPERATURE)) is not None: await self.wrap_and_catch(self._controller.set_temp_setpoint(temp)) async def async_set_fan_mode(self, fan_mode: str) -> None: @@ -627,8 +626,7 @@ class ZoneDevice(ClimateEntity): """Set new target temperature.""" if self._zone.mode != Zone.Mode.AUTO: return - temp = kwargs.get(ATTR_TEMPERATURE) - if temp is not None: + if (temp := kwargs.get(ATTR_TEMPERATURE)) is not None: await self._controller.wrap_and_catch(self._zone.set_temp_setpoint(temp)) async def async_set_hvac_mode(self, hvac_mode: str) -> None: diff --git a/homeassistant/components/izone/discovery.py b/homeassistant/components/izone/discovery.py index 715c87bc7a8..04b0760261e 100644 --- a/homeassistant/components/izone/discovery.py +++ b/homeassistant/components/izone/discovery.py @@ -49,8 +49,7 @@ class DiscoveryService(pizone.Listener): async def async_start_discovery_service(hass: HomeAssistant): """Set up the pizone internal discovery.""" - disco = hass.data.get(DATA_DISCOVERY_SERVICE) - if disco: + if disco := hass.data.get(DATA_DISCOVERY_SERVICE): # Already started return disco @@ -75,8 +74,7 @@ async def async_start_discovery_service(hass: HomeAssistant): async def async_stop_discovery_service(hass: HomeAssistant): """Stop the discovery service.""" - disco = hass.data.get(DATA_DISCOVERY_SERVICE) - if not disco: + if not (disco := hass.data.get(DATA_DISCOVERY_SERVICE)): return await disco.pi_disco.close() diff --git a/homeassistant/components/kaiterra/api_data.py b/homeassistant/components/kaiterra/api_data.py index e0f4d817e03..b426a298ddb 100644 --- a/homeassistant/components/kaiterra/api_data.py +++ b/homeassistant/components/kaiterra/api_data.py @@ -72,9 +72,7 @@ class KaiterraApiData: aqi, main_pollutant = None, None for sensor_name, sensor in device.items(): - points = sensor.get("points") - - if not points: + if not (points := sensor.get("points")): continue point = points[0] diff --git a/homeassistant/components/logentries/__init__.py b/homeassistant/components/logentries/__init__.py index 55d1ab7aae6..9390bde240a 100644 --- a/homeassistant/components/logentries/__init__.py +++ b/homeassistant/components/logentries/__init__.py @@ -28,8 +28,7 @@ def setup(hass, config): def logentries_event_listener(event): """Listen for new messages on the bus and sends them to Logentries.""" - state = event.data.get("new_state") - if state is None: + if (state := event.data.get("new_state")) is None: return try: _state = state_helper.state_as_number(state) diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index 51f1e316a87..b0ee5241ec7 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -420,9 +420,8 @@ async def handle_webhook( event_type = data[ATTR_EVENT_TYPE] device_registry = dr.async_get(hass) device_id = data[ATTR_DEVICE_ID] - device = device_registry.async_get(device_id) - if not device: + if not (device := device_registry.async_get(device_id)): return Response( text=f"Device not found: {device_id}", status=HTTPStatus.BAD_REQUEST, diff --git a/homeassistant/components/netgear_lte/__init__.py b/homeassistant/components/netgear_lte/__init__.py index ed4733d6696..3ee4e14d5e1 100644 --- a/homeassistant/components/netgear_lte/__init__.py +++ b/homeassistant/components/netgear_lte/__init__.py @@ -193,12 +193,9 @@ async def async_setup(hass, config): for sms_id in service.data[ATTR_SMS_ID]: await modem_data.modem.delete_sms(sms_id) elif service.service == SERVICE_SET_OPTION: - failover = service.data.get(ATTR_FAILOVER) - if failover: + if failover := service.data.get(ATTR_FAILOVER): await modem_data.modem.set_failover_mode(failover) - - autoconnect = service.data.get(ATTR_AUTOCONNECT) - if autoconnect: + if autoconnect := service.data.get(ATTR_AUTOCONNECT): await modem_data.modem.set_autoconnect_mode(autoconnect) elif service.service == SERVICE_CONNECT_LTE: await modem_data.modem.connect_lte() diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index 26393dad179..d369eeffd8b 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -263,8 +263,7 @@ class OpenhomeDevice(MediaPlayerEntity): @property def media_artist(self): """Artist of current playing media, music track only.""" - artists = self._track_information.get("artist") - if artists: + if artists := self._track_information.get("artist"): return artists[0] @property diff --git a/homeassistant/components/pilight/binary_sensor.py b/homeassistant/components/pilight/binary_sensor.py index e380d39bef4..d964739212a 100644 --- a/homeassistant/components/pilight/binary_sensor.py +++ b/homeassistant/components/pilight/binary_sensor.py @@ -39,8 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Pilight Binary Sensor.""" - disarm = config.get(CONF_DISARM_AFTER_TRIGGER) - if disarm: + if config.get(CONF_DISARM_AFTER_TRIGGER): add_entities( [ PilightTriggerSensor( diff --git a/homeassistant/components/proliphix/climate.py b/homeassistant/components/proliphix/climate.py index a293642038e..da21410f1fc 100644 --- a/homeassistant/components/proliphix/climate.py +++ b/homeassistant/components/proliphix/climate.py @@ -131,7 +131,6 @@ class ProliphixThermostat(ClimateEntity): def set_temperature(self, **kwargs): """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return self._pdp.setback = temperature diff --git a/homeassistant/components/rocketchat/notify.py b/homeassistant/components/rocketchat/notify.py index efb2288f4d8..90495cc6eb2 100644 --- a/homeassistant/components/rocketchat/notify.py +++ b/homeassistant/components/rocketchat/notify.py @@ -69,8 +69,7 @@ class RocketChatNotificationService(BaseNotificationService): data = kwargs.get(ATTR_DATA) or {} resp = self._server.chat_post_message(message, channel=self._room, **data) if resp.status_code == HTTP_OK: - success = resp.json()["success"] - if not success: + if not resp.json()["success"]: _LOGGER.error("Unable to post Rocket.Chat message") else: _LOGGER.error( diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 1913751ba78..44a360a2e55 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -367,8 +367,7 @@ class DeviceBroker: for evt in req.events: if evt.event_type != EVENT_TYPE_DEVICE: continue - device = self.devices.get(evt.device_id) - if not device: + if not (device := self.devices.get(evt.device_id)): continue device.status.apply_attribute_update( evt.component_id, diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index da9e0fd090a..baa9ddbc118 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -184,8 +184,7 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateEntity): async def async_set_temperature(self, **kwargs): """Set new operation mode and target temperatures.""" # Operation state - operation_state = kwargs.get(ATTR_HVAC_MODE) - if operation_state: + if operation_state := kwargs.get(ATTR_HVAC_MODE): mode = STATE_TO_MODE[operation_state] await self._device.set_thermostat_mode(mode, set_status=True) await self.async_update() @@ -235,8 +234,7 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateEntity): supported_modes = self._device.status.supported_thermostat_modes if isinstance(supported_modes, Iterable): for mode in supported_modes: - state = MODE_TO_STATE.get(mode) - if state is not None: + if (state := MODE_TO_STATE.get(mode)) is not None: modes.add(state) else: _LOGGER.debug( @@ -363,8 +361,7 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity): """Set new target temperature.""" tasks = [] # operation mode - operation_mode = kwargs.get(ATTR_HVAC_MODE) - if operation_mode: + if operation_mode := kwargs.get(ATTR_HVAC_MODE): if operation_mode == HVAC_MODE_OFF: tasks.append(self._device.switch_off(set_status=True)) else: @@ -398,8 +395,7 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity): """Update the calculated fields of the AC.""" modes = {HVAC_MODE_OFF} for mode in self._device.status.supported_ac_modes: - state = AC_MODE_TO_STATE.get(mode) - if state is not None: + if (state := AC_MODE_TO_STATE.get(mode)) is not None: modes.add(state) else: _LOGGER.debug( diff --git a/homeassistant/components/smartthings/lock.py b/homeassistant/components/smartthings/lock.py index 601e207a6f5..4d4c48b1d9b 100644 --- a/homeassistant/components/smartthings/lock.py +++ b/homeassistant/components/smartthings/lock.py @@ -67,7 +67,6 @@ class SmartThingsLock(SmartThingsEntity, LockEntity): state_attrs["lock_state"] = status.value if isinstance(status.data, dict): for st_attr, ha_attr in ST_LOCK_ATTR_MAP.items(): - data_val = status.data.get(st_attr) - if data_val is not None: + if (data_val := status.data.get(st_attr)) is not None: state_attrs[ha_attr] = data_val return state_attrs diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index 0225a17a62c..36f2610b981 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -188,8 +188,7 @@ def setup_smartapp(hass, app): for each SmartThings account that is configured in hass. """ manager = hass.data[DOMAIN][DATA_MANAGER] - smartapp = manager.smartapps.get(app.app_id) - if smartapp: + if smartapp := manager.smartapps.get(app.app_id): # already setup return smartapp smartapp = manager.register(app.app_id, app.webhook_public_key) @@ -206,8 +205,7 @@ async def setup_smartapp_endpoint(hass: HomeAssistant): SmartApps are an extension point within the SmartThings ecosystem and is used to receive push updates (i.e. device updates) from the cloud. """ - data = hass.data.get(DOMAIN) - if data: + if hass.data.get(DOMAIN): # already setup return From ea2bc3fde161e7925ee4aea5a007c28861b052f8 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 21 Oct 2021 02:31:32 -0400 Subject: [PATCH 0621/1038] Add entity categories to goalzero (#57906) --- homeassistant/components/goalzero/binary_sensor.py | 3 ++- homeassistant/components/goalzero/sensor.py | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/goalzero/binary_sensor.py b/homeassistant/components/goalzero/binary_sensor.py index ddde8c80e96..56bbe1e2261 100644 --- a/homeassistant/components/goalzero/binary_sensor.py +++ b/homeassistant/components/goalzero/binary_sensor.py @@ -11,7 +11,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -31,6 +31,7 @@ BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( key="app_online", name="App Online", device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), BinarySensorEntityDescription( key="isCharging", diff --git a/homeassistant/components/goalzero/sensor.py b/homeassistant/components/goalzero/sensor.py index bd775ab82bb..6677936b35a 100644 --- a/homeassistant/components/goalzero/sensor.py +++ b/homeassistant/components/goalzero/sensor.py @@ -22,6 +22,7 @@ from homeassistant.const import ( ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_WATT_HOUR, + ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, POWER_WATT, SIGNAL_STRENGTH_DECIBELS, @@ -107,6 +108,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Temperature", device_class=DEVICE_CLASS_TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key="wifiStrength", @@ -114,22 +116,26 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( device_class=DEVICE_CLASS_SIGNAL_STRENGTH, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key="timestamp", name="Total Run Time", native_unit_of_measurement=TIME_SECONDS, entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key="ssid", name="Wi-Fi SSID", entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), SensorEntityDescription( key="ipAddr", name="IP Address", entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), ) From 62c20860ac8a9a0f03321b64d4c7c0cf3412318d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 21 Oct 2021 08:33:10 +0200 Subject: [PATCH 0622/1038] Use assignment expressions 22 (#57971) --- .../components/alexa/smart_home_http.py | 3 +- .../components/august/config_flow.py | 10 ++----- .../components/azure_service_bus/notify.py | 3 +- .../components/bluesound/media_player.py | 30 +++++++------------ .../components/environment_canada/sensor.py | 3 +- .../components/fritzbox/config_flow.py | 3 +- homeassistant/components/hangouts/__init__.py | 3 +- .../components/life360/device_tracker.py | 3 +- homeassistant/components/onvif/event.py | 3 +- .../components/proximity/__init__.py | 4 +-- .../components/radiotherm/climate.py | 6 ++-- homeassistant/components/roku/__init__.py | 3 +- homeassistant/components/script/__init__.py | 3 +- homeassistant/components/tibber/sensor.py | 3 +- .../components/tplink/config_flow.py | 3 +- .../components/trend/binary_sensor.py | 3 +- homeassistant/components/xmpp/notify.py | 3 +- homeassistant/components/yeelight/light.py | 4 +-- 18 files changed, 29 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/alexa/smart_home_http.py b/homeassistant/components/alexa/smart_home_http.py index 6d6b9f54533..df4f95f12f2 100644 --- a/homeassistant/components/alexa/smart_home_http.py +++ b/homeassistant/components/alexa/smart_home_http.py @@ -70,8 +70,7 @@ class AlexaConfig(AbstractConfig): return self._config[CONF_FILTER](entity_id) entity_registry = er.async_get(self.hass) - registry_entry = entity_registry.async_get(entity_id) - if registry_entry: + if registry_entry := entity_registry.async_get(entity_id): auxiliary_entity = registry_entry.entity_category in ( ENTITY_CATEGORY_CONFIG, ENTITY_CATEGORY_DIAGNOSTIC, diff --git a/homeassistant/components/august/config_flow.py b/homeassistant/components/august/config_flow.py index af048f9dc46..eb7bac9ae1a 100644 --- a/homeassistant/components/august/config_flow.py +++ b/homeassistant/components/august/config_flow.py @@ -14,20 +14,14 @@ from .gateway import AugustGateway _LOGGER = logging.getLogger(__name__) -async def async_validate_input( - data, - august_gateway, -): +async def async_validate_input(data, august_gateway): """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. Request configuration steps from the user. """ - - code = data.get(VERIFICATION_CODE_KEY) - - if code is not None: + if (code := data.get(VERIFICATION_CODE_KEY)) is not None: result = await august_gateway.authenticator.async_validate_verification_code( code ) diff --git a/homeassistant/components/azure_service_bus/notify.py b/homeassistant/components/azure_service_bus/notify.py index e7c85adede8..0d48ff6b2d6 100644 --- a/homeassistant/components/azure_service_bus/notify.py +++ b/homeassistant/components/azure_service_bus/notify.py @@ -90,8 +90,7 @@ class ServiceBusNotificationService(BaseNotificationService): if ATTR_TARGET in kwargs: dto[ATTR_ASB_TARGET] = kwargs[ATTR_TARGET] - data = kwargs.get(ATTR_DATA) - if data: + if data := kwargs.get(ATTR_DATA): dto.update(data) queue_message = Message(json.dumps(dto)) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 86d0be72bdc..04261a4137c 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -160,8 +160,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) return - hosts = config.get(CONF_HOSTS) - if hosts: + if hosts := config.get(CONF_HOSTS): for host in hosts: _add_player( hass, @@ -173,15 +172,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_service_handler(service): """Map services to method of Bluesound devices.""" - method = SERVICE_TO_METHOD.get(service.service) - if not method: + if not (method := SERVICE_TO_METHOD.get(service.service)): return params = { key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID } - entity_ids = service.data.get(ATTR_ENTITY_ID) - if entity_ids: + if entity_ids := service.data.get(ATTR_ENTITY_ID): target_players = [ player for player in hass.data[DATA_BLUESOUND] @@ -259,8 +256,7 @@ class BluesoundPlayer(MediaPlayerEntity): if not self._icon: self._icon = self._sync_status.get("@icon", self.host) - master = self._sync_status.get("master") - if master is not None: + if (master := self._sync_status.get("master")) is not None: self._is_master = False master_host = master.get("#text") master_device = [ @@ -580,8 +576,7 @@ class BluesoundPlayer(MediaPlayerEntity): if self.is_grouped and not self.is_master: return self._group_name - artist = self._status.get("artist") - if not artist: + if not (artist := self._status.get("artist")): artist = self._status.get("title2") return artist @@ -591,8 +586,7 @@ class BluesoundPlayer(MediaPlayerEntity): if self._status is None or (self.is_grouped and not self.is_master): return None - album = self._status.get("album") - if not album: + if not (album := self._status.get("album")): album = self._status.get("title3") return album @@ -602,8 +596,7 @@ class BluesoundPlayer(MediaPlayerEntity): if self._status is None or (self.is_grouped and not self.is_master): return None - url = self._status.get("image") - if not url: + if not (url := self._status.get("image")): return if url[0] == "/": url = f"http://{self.host}:{self.port}{url}" @@ -620,8 +613,7 @@ class BluesoundPlayer(MediaPlayerEntity): if self._last_status_update is None or mediastate == STATE_IDLE: return None - position = self._status.get("secs") - if position is None: + if (position := self._status.get("secs")) is None: return None position = float(position) @@ -636,8 +628,7 @@ class BluesoundPlayer(MediaPlayerEntity): if self._status is None or (self.is_grouped and not self.is_master): return None - duration = self._status.get("totlen") - if duration is None: + if (duration := self._status.get("totlen")) is None: return None return float(duration) @@ -712,8 +703,7 @@ class BluesoundPlayer(MediaPlayerEntity): if self._status is None or (self.is_grouped and not self.is_master): return None - current_service = self._status.get("service", "") - if current_service == "": + if (current_service := self._status.get("service", "")) == "": return "" stream_url = self._status.get("streamUrl", "") diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 8e4c1483261..48c9ed57dee 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -140,8 +140,7 @@ class ECSensor(SensorEntity): else: self._unit = sensor_data.get("unit") - timestamp = metadata.get("timestamp") - if timestamp: + if timestamp := metadata.get("timestamp"): updated_utc = datetime.strptime(timestamp, "%Y%m%d%H%M%S").isoformat() else: updated_utc = None diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index 3ae3368f4ae..bcf17a1a958 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -125,8 +125,7 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): assert isinstance(host, str) self.context[CONF_HOST] = host - uuid = discovery_info.get(ATTR_UPNP_UDN) - if uuid: + if uuid := discovery_info.get(ATTR_UPNP_UDN): if uuid.startswith("uuid:"): uuid = uuid[5:] await self.async_set_unique_id(uuid) diff --git a/homeassistant/components/hangouts/__init__.py b/homeassistant/components/hangouts/__init__.py index 04814a9c3e9..820aab8cb73 100644 --- a/homeassistant/components/hangouts/__init__.py +++ b/homeassistant/components/hangouts/__init__.py @@ -57,8 +57,7 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the Hangouts bot component.""" - config = config.get(DOMAIN) - if config is None: + if (config := config.get(DOMAIN)) is None: hass.data[DOMAIN] = { CONF_INTENTS: {}, CONF_DEFAULT_CONVERSATIONS: [], diff --git a/homeassistant/components/life360/device_tracker.py b/homeassistant/components/life360/device_tracker.py index 6697dd50893..f293ab2aca3 100644 --- a/homeassistant/components/life360/device_tracker.py +++ b/homeassistant/components/life360/device_tracker.py @@ -211,8 +211,7 @@ class Life360Scanner: prev_seen = self._prev_seen(dev_id, last_seen) if not loc: - err_msg = member["issues"]["title"] - if err_msg: + if err_msg := member["issues"]["title"]: if member["issues"]["dialog"]: err_msg += f": {member['issues']['dialog']}" else: diff --git a/homeassistant/components/onvif/event.py b/homeassistant/components/onvif/event.py index f76efb2bc8e..fcb8e39cf54 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -208,8 +208,7 @@ class EventManager: continue topic = msg.Topic._value_1 - parser = PARSERS.get(topic) - if not parser: + if not (parser := PARSERS.get(topic)): if topic not in UNHANDLED_TOPICS: LOGGER.info( "No registered handler for event from %s: %s", diff --git a/homeassistant/components/proximity/__init__.py b/homeassistant/components/proximity/__init__.py index 1840162f896..eb64de56df5 100644 --- a/homeassistant/components/proximity/__init__.py +++ b/homeassistant/components/proximity/__init__.py @@ -164,9 +164,7 @@ class Proximity(Entity): # Check for devices in the monitored zone. for device in self.proximity_devices: - device_state = self.hass.states.get(device) - - if device_state is None: + if (device_state := self.hass.states.get(device)) is None: devices_to_calculate = True continue diff --git a/homeassistant/components/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py index 112e9f3a76d..d68072229ac 100644 --- a/homeassistant/components/radiotherm/climate.py +++ b/homeassistant/components/radiotherm/climate.py @@ -201,8 +201,7 @@ class RadioThermostat(ClimateEntity): def set_fan_mode(self, fan_mode): """Turn fan on/off.""" - code = FAN_MODE_TO_CODE.get(fan_mode) - if code is not None: + if (code := FAN_MODE_TO_CODE.get(fan_mode)) is not None: self.device.fmode = code @property @@ -322,8 +321,7 @@ class RadioThermostat(ClimateEntity): def set_temperature(self, **kwargs): """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return temperature = round_temp(temperature) diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index 55da7484aba..f1c30ec2af7 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -24,8 +24,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Roku from a config entry.""" hass.data.setdefault(DOMAIN, {}) - coordinator = hass.data[DOMAIN].get(entry.entry_id) - if not coordinator: + if not (coordinator := hass.data[DOMAIN].get(entry.entry_id)): coordinator = RokuDataUpdateCoordinator(hass, host=entry.data[CONF_HOST]) hass.data[DOMAIN][entry.entry_id] = coordinator diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index f3f34a0ad53..b07cc2325b1 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -404,8 +404,7 @@ class ScriptEntity(ToggleEntity, RestoreEntity): script_trace.set_trace(trace_get()) with trace_path("sequence"): this = None - state = self.hass.states.get(self.entity_id) - if state: + if state := self.hass.states.get(self.entity_id): this = state.as_dict() script_vars = {"this": this, **(variables or {})} return await self.script.async_run(script_vars, context) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 120432e0608..9a19e5a7602 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -446,8 +446,7 @@ class TibberRtDataCoordinator(update_coordinator.DataUpdateCoordinator): def get_live_measurement(self): """Get live measurement data.""" - errors = self.data.get("errors") - if errors: + if errors := self.data.get("errors"): _LOGGER.error(errors[0]) return None return self.data.get("data", {}).get("liveMeasurement") diff --git a/homeassistant/components/tplink/config_flow.py b/homeassistant/components/tplink/config_flow.py index d9bed10ee42..8abc00d7fdb 100644 --- a/homeassistant/components/tplink/config_flow.py +++ b/homeassistant/components/tplink/config_flow.py @@ -89,8 +89,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle the initial step.""" errors = {} if user_input is not None: - host = user_input[CONF_HOST] - if not host: + if not (host := user_input[CONF_HOST]): return await self.async_step_pick_device() try: device = await self._async_try_connect(host, raise_on_progress=False) diff --git a/homeassistant/components/trend/binary_sensor.py b/homeassistant/components/trend/binary_sensor.py index 52a72eca8c9..302201b5516 100644 --- a/homeassistant/components/trend/binary_sensor.py +++ b/homeassistant/components/trend/binary_sensor.py @@ -169,8 +169,7 @@ class SensorTrend(BinarySensorEntity): @callback def trend_sensor_state_listener(event): """Handle state changes on the observed device.""" - new_state = event.data.get("new_state") - if new_state is None: + if (new_state := event.data.get("new_state")) is None: return try: if self._attribute: diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py index 29624f37ffa..1e022c9e72f 100644 --- a/homeassistant/components/xmpp/notify.py +++ b/homeassistant/components/xmpp/notify.py @@ -313,8 +313,7 @@ async def async_send_message( # noqa: C901 filesize = len(input_file) _LOGGER.debug("Filesize is %s bytes", filesize) - content_type = mimetypes.guess_type(path)[0] - if content_type is None: + if (content_type := mimetypes.guess_type(path)[0]) is None: content_type = DEFAULT_CONTENT_TYPE _LOGGER.debug("Content type is %s", content_type) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index abe1285f609..67f55c7f737 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -1075,9 +1075,7 @@ class YeelightAmbientLight(YeelightColorLightWithoutNightlightSwitch): return "bright" def _get_property(self, prop, default=None): - bg_prop = self.PROPERTIES_MAPPING.get(prop) - - if not bg_prop: + if not (bg_prop := self.PROPERTIES_MAPPING.get(prop)): bg_prop = f"bg_{prop}" return super()._get_property(bg_prop, default) From 50686bd06d380bc5a51549a73a4713e85357da32 Mon Sep 17 00:00:00 2001 From: Brig Lamoreaux Date: Wed, 20 Oct 2021 23:35:59 -0700 Subject: [PATCH 0623/1038] Add Device Type Energy to Srp Energy Sensor (#58147) --- homeassistant/components/srp_energy/sensor.py | 18 ++++++++++++++++-- tests/components/srp_energy/test_sensor.py | 9 ++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/srp_energy/sensor.py b/homeassistant/components/srp_energy/sensor.py index 97b65840e83..4a5e3c33748 100644 --- a/homeassistant/components/srp_energy/sensor.py +++ b/homeassistant/components/srp_energy/sensor.py @@ -5,8 +5,12 @@ import logging import async_timeout from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ATTR_ATTRIBUTION, ENERGY_KILO_WATT_HOUR +from homeassistant.components.sensor import STATE_CLASS_TOTAL_INCREASING, SensorEntity +from homeassistant.const import ( + ATTR_ATTRIBUTION, + DEVICE_CLASS_ENERGY, + ENERGY_KILO_WATT_HOUR, +) from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( @@ -137,6 +141,16 @@ class SrpEntity(SensorEntity): """Return if entity is available.""" return self.coordinator.last_update_success + @property + def device_class(self): + """Return the device class.""" + return DEVICE_CLASS_ENERGY + + @property + def state_class(self): + """Return the state class.""" + return STATE_CLASS_TOTAL_INCREASING + async def async_added_to_hass(self): """When entity is added to hass.""" self.async_on_remove( diff --git a/tests/components/srp_energy/test_sensor.py b/tests/components/srp_energy/test_sensor.py index 3e830b7fc93..5a4585cc8e5 100644 --- a/tests/components/srp_energy/test_sensor.py +++ b/tests/components/srp_energy/test_sensor.py @@ -1,6 +1,7 @@ """Tests for the srp_energy sensor platform.""" from unittest.mock import MagicMock +from homeassistant.components.sensor import STATE_CLASS_TOTAL_INCREASING from homeassistant.components.srp_energy.const import ( ATTRIBUTION, DEFAULT_NAME, @@ -10,7 +11,11 @@ from homeassistant.components.srp_energy.const import ( SRP_ENERGY_DOMAIN, ) from homeassistant.components.srp_energy.sensor import SrpEntity, async_setup_entry -from homeassistant.const import ATTR_ATTRIBUTION, ENERGY_KILO_WATT_HOUR +from homeassistant.const import ( + ATTR_ATTRIBUTION, + DEVICE_CLASS_ENERGY, + ENERGY_KILO_WATT_HOUR, +) async def test_async_setup_entry(hass): @@ -94,6 +99,8 @@ async def test_srp_entity(hass): assert srp_entity.should_poll is False assert srp_entity.extra_state_attributes[ATTR_ATTRIBUTION] == ATTRIBUTION assert srp_entity.available is not None + assert srp_entity.device_class == DEVICE_CLASS_ENERGY + assert srp_entity.state_class == STATE_CLASS_TOTAL_INCREASING await srp_entity.async_added_to_hass() assert srp_entity.state is not None From c7ff6eb5eee0659fb8b4b4d11ee204963ad3edc5 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Thu, 21 Oct 2021 08:36:52 +0200 Subject: [PATCH 0624/1038] Address late review for Fritz entity_category (#58141) --- homeassistant/components/fritz/sensor.py | 5 ++--- homeassistant/components/fritz/switch.py | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fritz/sensor.py b/homeassistant/components/fritz/sensor.py index 42a682cf1e0..809af534f0e 100644 --- a/homeassistant/components/fritz/sensor.py +++ b/homeassistant/components/fritz/sensor.py @@ -27,7 +27,6 @@ from homeassistant.const import ( DATA_RATE_KILOBITS_PER_SECOND, DATA_RATE_KILOBYTES_PER_SECOND, DEVICE_CLASS_TIMESTAMP, - ENTITY_CATEGORY_CONFIG, ENTITY_CATEGORY_DIAGNOSTIC, SIGNAL_STRENGTH_DECIBELS, ) @@ -198,7 +197,7 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = ( name="Max Connection Upload Throughput", native_unit_of_measurement=DATA_RATE_KILOBITS_PER_SECOND, icon="mdi:upload", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, value_fn=_retrieve_max_kb_s_sent_state, ), FritzSensorEntityDescription( @@ -206,7 +205,7 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = ( name="Max Connection Download Throughput", native_unit_of_measurement=DATA_RATE_KILOBITS_PER_SECOND, icon="mdi:download", - entity_category=ENTITY_CATEGORY_CONFIG, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, value_fn=_retrieve_max_kb_s_received_state, ), FritzSensorEntityDescription( diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index 072cca6a1b0..969f8cf8f9e 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -652,6 +652,7 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch, SwitchEntity): self._fritzbox_tools = fritzbox_tools self._attributes = {} + self._attr_entity_category = ENTITY_CATEGORY_CONFIG self._network_num = network_num switch_info = SwitchInfo( From 2ff356393ca9bdf2969b7bd802517c38aab7dadb Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 21 Oct 2021 04:54:50 -0600 Subject: [PATCH 0625/1038] Clean up SimpliSafe entity inheritance structure (#58063) * Migrate SimpliSafe to new web-based authentication * Ensure we're storing data correctly * Re-organize SimpliSafe device structure * Constants * More work * Code review --- .../components/simplisafe/__init__.py | 60 +++++++------------ .../simplisafe/alarm_control_panel.py | 2 +- .../components/simplisafe/binary_sensor.py | 28 ++++----- homeassistant/components/simplisafe/lock.py | 20 +++---- homeassistant/components/simplisafe/sensor.py | 19 ++++-- 5 files changed, 57 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 28ece7d45cd..d41d7b03eda 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -7,8 +7,7 @@ from datetime import timedelta from typing import TYPE_CHECKING, cast from simplipy import API -from simplipy.device.sensor.v2 import SensorV2 -from simplipy.device.sensor.v3 import SensorV3 +from simplipy.device import Device from simplipy.errors import ( EndpointUnavailableError, InvalidCredentialsError, @@ -62,6 +61,8 @@ from .const import ( EVENT_SIMPLISAFE_NOTIFICATION = "SIMPLISAFE_NOTIFICATION" +DEFAULT_ENTITY_MODEL = "alarm_control_panel" +DEFAULT_ENTITY_NAME = "Alarm Control Panel" DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) DEFAULT_SOCKET_MIN_RETRY = 15 @@ -159,7 +160,7 @@ async def async_register_base_station( device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, - identifiers={(DOMAIN, system.serial)}, + identifiers={(DOMAIN, system.system_id)}, manufacturer="SimpliSafe", model=system.version, name=system.address, @@ -424,29 +425,34 @@ class SimpliSafeEntity(CoordinatorEntity): self, simplisafe: SimpliSafe, system: SystemV2 | SystemV3, - name: str, *, - serial: str | None = None, + device: Device | None = None, ) -> None: """Initialize.""" assert simplisafe.coordinator super().__init__(simplisafe.coordinator) - if serial: - self._serial = serial + if device: + model = device.type.name + device_name = device.name + serial = device.serial else: - self._serial = system.serial + model = DEFAULT_ENTITY_MODEL + device_name = DEFAULT_ENTITY_NAME + serial = system.serial self._attr_extra_state_attributes = {ATTR_SYSTEM_ID: system.system_id} self._attr_device_info = { - "identifiers": {(DOMAIN, system.system_id)}, + "identifiers": {(DOMAIN, serial)}, "manufacturer": "SimpliSafe", - "model": str(system.version), - "name": name, - "via_device": (DOMAIN, system.serial), + "model": model, + "name": device_name, + "via_device": (DOMAIN, system.system_id), } - self._attr_name = f"{system.address} {name}" - self._attr_unique_id = self._serial + + self._attr_name = f"{system.address} {device_name} {' '.join([w.title() for w in model.split('_')])}" + self._attr_unique_id = serial + self._device = device self._online = True self._simplisafe = simplisafe self._system = system @@ -481,29 +487,3 @@ class SimpliSafeEntity(CoordinatorEntity): def async_update_from_rest_api(self) -> None: """Update the entity with the provided REST API data.""" raise NotImplementedError() - - -class SimpliSafeBaseSensor(SimpliSafeEntity): - """Define a SimpliSafe base (binary) sensor.""" - - def __init__( - self, - simplisafe: SimpliSafe, - system: SystemV2 | SystemV3, - sensor: SensorV2 | SensorV3, - ) -> None: - """Initialize.""" - super().__init__(simplisafe, system, sensor.name, serial=sensor.serial) - - self._attr_device_info = { - "identifiers": {(DOMAIN, sensor.serial)}, - "manufacturer": "SimpliSafe", - "model": sensor.type.name, - "name": sensor.name, - "via_device": (DOMAIN, system.serial), - } - - human_friendly_name = " ".join([w.title() for w in sensor.type.name.split("_")]) - self._attr_name = f"{super().name} {human_friendly_name}" - - self._sensor = sensor diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 278d7579edf..1355669129d 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -80,7 +80,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): def __init__(self, simplisafe: SimpliSafe, system: SystemV2 | SystemV3) -> None: """Initialize the SimpliSafe alarm.""" - super().__init__(simplisafe, system, "Alarm Control Panel") + super().__init__(simplisafe, system) if code := self._simplisafe.entry.options.get(CONF_CODE): if code.isdigit(): diff --git a/homeassistant/components/simplisafe/binary_sensor.py b/homeassistant/components/simplisafe/binary_sensor.py index e7b156a5b1c..f276a5fea66 100644 --- a/homeassistant/components/simplisafe/binary_sensor.py +++ b/homeassistant/components/simplisafe/binary_sensor.py @@ -2,9 +2,7 @@ from __future__ import annotations from simplipy.device import DeviceTypes -from simplipy.device.sensor.v2 import SensorV2 from simplipy.device.sensor.v3 import SensorV3 -from simplipy.system.v2 import SystemV2 from simplipy.system.v3 import SystemV3 from homeassistant.components.binary_sensor import ( @@ -22,7 +20,7 @@ from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import SimpliSafe, SimpliSafeBaseSensor +from . import SimpliSafe, SimpliSafeEntity from .const import DATA_CLIENT, DOMAIN, LOGGER SUPPORTED_BATTERY_SENSOR_TYPES = [ @@ -77,45 +75,45 @@ async def async_setup_entry( async_add_entities(sensors) -class TriggeredBinarySensor(SimpliSafeBaseSensor, BinarySensorEntity): +class TriggeredBinarySensor(SimpliSafeEntity, BinarySensorEntity): """Define a binary sensor related to whether an entity has been triggered.""" def __init__( self, simplisafe: SimpliSafe, - system: SystemV2 | SystemV3, - sensor: SensorV2 | SensorV3, + system: SystemV3, + sensor: SensorV3, device_class: str, ) -> None: """Initialize.""" - super().__init__(simplisafe, system, sensor) + super().__init__(simplisafe, system, device=sensor) self._attr_device_class = device_class + self._device: SensorV3 @callback def async_update_from_rest_api(self) -> None: """Update the entity with the provided REST API data.""" - self._attr_is_on = self._sensor.triggered + self._attr_is_on = self._device.triggered -class BatteryBinarySensor(SimpliSafeBaseSensor, BinarySensorEntity): +class BatteryBinarySensor(SimpliSafeEntity, BinarySensorEntity): """Define a SimpliSafe battery binary sensor entity.""" _attr_device_class = DEVICE_CLASS_BATTERY _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__( - self, - simplisafe: SimpliSafe, - system: SystemV2 | SystemV3, - sensor: SensorV2 | SensorV3, + self, simplisafe: SimpliSafe, system: SystemV3, sensor: SensorV3 ) -> None: """Initialize.""" - super().__init__(simplisafe, system, sensor) + super().__init__(simplisafe, system, device=sensor) + self._attr_name = f"{super().name} Battery" self._attr_unique_id = f"{super().unique_id}-battery" + self._device: SensorV3 @callback def async_update_from_rest_api(self) -> None: """Update the entity with the provided REST API data.""" - self._attr_is_on = self._sensor.low_battery + self._attr_is_on = self._device.low_battery diff --git a/homeassistant/components/simplisafe/lock.py b/homeassistant/components/simplisafe/lock.py index 34fa141745b..3375a413edb 100644 --- a/homeassistant/components/simplisafe/lock.py +++ b/homeassistant/components/simplisafe/lock.py @@ -42,16 +42,16 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity): def __init__(self, simplisafe: SimpliSafe, system: SystemV3, lock: Lock) -> None: """Initialize.""" - super().__init__(simplisafe, system, lock.name, serial=lock.serial) + super().__init__(simplisafe, system, device=lock) - self._lock = lock + self._device: Lock async def async_lock(self, **kwargs: Any) -> None: """Lock the lock.""" try: - await self._lock.async_lock() + await self._device.async_lock() except SimplipyError as err: - LOGGER.error('Error while locking "%s": %s', self._lock.name, err) + LOGGER.error('Error while locking "%s": %s', self._device.name, err) return self._attr_is_locked = True @@ -60,9 +60,9 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity): async def async_unlock(self, **kwargs: Any) -> None: """Unlock the lock.""" try: - await self._lock.async_unlock() + await self._device.async_unlock() except SimplipyError as err: - LOGGER.error('Error while unlocking "%s": %s', self._lock.name, err) + LOGGER.error('Error while unlocking "%s": %s', self._device.name, err) return self._attr_is_locked = False @@ -73,10 +73,10 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity): """Update the entity with the provided REST API data.""" self._attr_extra_state_attributes.update( { - ATTR_LOCK_LOW_BATTERY: self._lock.lock_low_battery, - ATTR_PIN_PAD_LOW_BATTERY: self._lock.pin_pad_low_battery, + ATTR_LOCK_LOW_BATTERY: self._device.lock_low_battery, + ATTR_PIN_PAD_LOW_BATTERY: self._device.pin_pad_low_battery, } ) - self._attr_is_jammed = self._lock.state == LockStates.jammed - self._attr_is_locked = self._lock.state == LockStates.locked + self._attr_is_jammed = self._device.state == LockStates.jammed + self._attr_is_locked = self._device.state == LockStates.locked diff --git a/homeassistant/components/simplisafe/sensor.py b/homeassistant/components/simplisafe/sensor.py index 96aed33979d..97edd3008dd 100644 --- a/homeassistant/components/simplisafe/sensor.py +++ b/homeassistant/components/simplisafe/sensor.py @@ -1,8 +1,9 @@ """Support for SimpliSafe freeze sensor.""" -from typing import TYPE_CHECKING +from __future__ import annotations from simplipy.device import DeviceTypes from simplipy.device.sensor.v3 import SensorV3 +from simplipy.system.v3 import SystemV3 from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.config_entries import ConfigEntry @@ -10,7 +11,7 @@ from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import SimpliSafeBaseSensor +from . import SimpliSafe, SimpliSafeEntity from .const import DATA_CLIENT, DOMAIN, LOGGER @@ -33,16 +34,22 @@ async def async_setup_entry( async_add_entities(sensors) -class SimplisafeFreezeSensor(SimpliSafeBaseSensor, SensorEntity): +class SimplisafeFreezeSensor(SimpliSafeEntity, SensorEntity): """Define a SimpliSafe freeze sensor entity.""" _attr_device_class = DEVICE_CLASS_TEMPERATURE _attr_native_unit_of_measurement = TEMP_FAHRENHEIT _attr_state_class = STATE_CLASS_MEASUREMENT + def __init__( + self, simplisafe: SimpliSafe, system: SystemV3, sensor: SensorV3 + ) -> None: + """Initialize.""" + super().__init__(simplisafe, system, device=sensor) + + self._device: SensorV3 + @callback def async_update_from_rest_api(self) -> None: """Update the entity with the provided REST API data.""" - if TYPE_CHECKING: - assert isinstance(self._sensor, SensorV3) - self._attr_native_value = self._sensor.temperature + self._attr_native_value = self._device.temperature From df1154395e0f5101e74e66927b037131ad0938b1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 21 Oct 2021 15:03:33 +0200 Subject: [PATCH 0626/1038] Revert "Swap order of int template helper kwargs (#57729)" (#58015) This reverts commit f8dbcb953c8abec020da44bc1b581832405a17ba. --- homeassistant/helpers/template.py | 4 ++-- tests/helpers/test_template.py | 14 ++------------ 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 8a72f7bdda4..1c6fcce9e5e 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1460,7 +1460,7 @@ def forgiving_float_filter(value, default=_SENTINEL): return default -def forgiving_int(value, base=10, default=_SENTINEL): +def forgiving_int(value, default=_SENTINEL, base=10): """Try to convert value to an int, and warn if it fails.""" result = jinja2.filters.do_int(value, default=default, base=base) if result is _SENTINEL: @@ -1469,7 +1469,7 @@ def forgiving_int(value, base=10, default=_SENTINEL): return result -def forgiving_int_filter(value, base=10, default=_SENTINEL): +def forgiving_int_filter(value, default=_SENTINEL, base=10): """Try to convert value to an int, and warn if it fails.""" result = jinja2.filters.do_int(value, default=default, base=base) if result is _SENTINEL: diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index d6a25496bbf..4be9d527d31 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -248,14 +248,9 @@ def test_int_filter(hass): hass.states.async_set("sensor.temperature", "0x10") assert render(hass, "{{ states.sensor.temperature.state | int(base=16) }}") == 16 - assert render(hass, "{{ states.sensor.temperature.state | int(16) }}") == 16 - - hass.states.async_set("sensor.temperature", "1111") - assert render(hass, "{{ states.sensor.temperature.state | int(base=2) }}") == 15 - assert render(hass, "{{ states.sensor.temperature.state | int(2) }}") == 15 assert render(hass, "{{ 'bad' | int }}") == 0 - assert render(hass, "{{ 'bad' | int(10, 1) }}") == 1 + assert render(hass, "{{ 'bad' | int(1) }}") == 1 assert render(hass, "{{ 'bad' | int(default=1) }}") == 1 @@ -267,14 +262,9 @@ def test_int_function(hass): hass.states.async_set("sensor.temperature", "0x10") assert render(hass, "{{ int(states.sensor.temperature.state, base=16) }}") == 16 - assert render(hass, "{{ int(states.sensor.temperature.state, 16) }}") == 16 - - hass.states.async_set("sensor.temperature", "1111") - assert render(hass, "{{ int(states.sensor.temperature.state, base=2) }}") == 15 - assert render(hass, "{{ int(states.sensor.temperature.state, 2) }}") == 15 assert render(hass, "{{ int('bad') }}") == "bad" - assert render(hass, "{{ int('bad', 10, 1) }}") == 1 + assert render(hass, "{{ int('bad', 1) }}") == 1 assert render(hass, "{{ int('bad', default=1) }}") == 1 From 946df49ca89fe4d7e342c6aff426a070c8b7adcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Thu, 21 Oct 2021 18:03:37 +0200 Subject: [PATCH 0627/1038] Add long-term statistics for AEMET sensors (#57844) --- homeassistant/components/aemet/const.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/aemet/const.py b/homeassistant/components/aemet/const.py index e84060b444d..ba37c66da64 100644 --- a/homeassistant/components/aemet/const.py +++ b/homeassistant/components/aemet/const.py @@ -1,7 +1,10 @@ """Constant values for the AEMET OpenData component.""" from __future__ import annotations -from homeassistant.components.sensor import SensorEntityDescription +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntityDescription, +) from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, @@ -252,12 +255,14 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Humidity", native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_PRESSURE, name="Pressure", native_unit_of_measurement=PRESSURE_HPA, device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_RAIN, @@ -268,6 +273,7 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=ATTR_API_RAIN_PROB, name="Rain probability", native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_SNOW, @@ -278,6 +284,7 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=ATTR_API_SNOW_PROB, name="Snow probability", native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_STATION_ID, @@ -296,18 +303,21 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=ATTR_API_STORM_PROB, name="Storm probability", native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_TEMPERATURE, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_TEMPERATURE_FEELING, name="Temperature feeling", native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_TOWN_ID, @@ -326,6 +336,7 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=ATTR_API_WIND_BEARING, name="Wind bearing", native_unit_of_measurement=DEGREE, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_WIND_MAX_SPEED, @@ -336,6 +347,7 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=ATTR_API_WIND_SPEED, name="Wind speed", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + state_class=STATE_CLASS_MEASUREMENT, ), ) From 626bd251299efa2aeaa71e3961f280a5473c97ce Mon Sep 17 00:00:00 2001 From: ANMalko Date: Thu, 21 Oct 2021 20:10:23 +0300 Subject: [PATCH 0628/1038] Add LOOKin integration (#58125) Co-authored-by: J. Nick Koston --- .coveragerc | 4 + CODEOWNERS | 1 + homeassistant/components/lookin/__init__.py | 68 +++++++ .../components/lookin/config_flow.py | 108 ++++++++++ homeassistant/components/lookin/const.py | 9 + homeassistant/components/lookin/entity.py | 84 ++++++++ homeassistant/components/lookin/manifest.json | 10 + homeassistant/components/lookin/models.py | 20 ++ homeassistant/components/lookin/sensor.py | 98 ++++++++++ homeassistant/components/lookin/strings.json | 31 +++ .../components/lookin/translations/en.json | 31 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/zeroconf.py | 5 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/lookin/__init__.py | 53 +++++ tests/components/lookin/test_config_flow.py | 185 ++++++++++++++++++ 17 files changed, 714 insertions(+) create mode 100644 homeassistant/components/lookin/__init__.py create mode 100644 homeassistant/components/lookin/config_flow.py create mode 100644 homeassistant/components/lookin/const.py create mode 100644 homeassistant/components/lookin/entity.py create mode 100644 homeassistant/components/lookin/manifest.json create mode 100644 homeassistant/components/lookin/models.py create mode 100644 homeassistant/components/lookin/sensor.py create mode 100644 homeassistant/components/lookin/strings.json create mode 100644 homeassistant/components/lookin/translations/en.json create mode 100644 tests/components/lookin/__init__.py create mode 100644 tests/components/lookin/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 6690ed5ee20..7a1f53347b2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -585,6 +585,10 @@ omit = homeassistant/components/logi_circle/const.py homeassistant/components/logi_circle/sensor.py homeassistant/components/london_underground/sensor.py + homeassistant/components/lookin/__init__.py + homeassistant/components/lookin/entity.py + homeassistant/components/lookin/models.py + homeassistant/components/lookin/sensor.py homeassistant/components/loopenergy/sensor.py homeassistant/components/luci/device_tracker.py homeassistant/components/luftdaten/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index d25abecb8ed..936fa50cb9f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -287,6 +287,7 @@ homeassistant/components/litterrobot/* @natekspencer homeassistant/components/local_ip/* @issacg homeassistant/components/logger/* @home-assistant/core homeassistant/components/logi_circle/* @evanjd +homeassistant/components/lookin/* @ANMalko homeassistant/components/loopenergy/* @pavoni homeassistant/components/lovelace/* @home-assistant/frontend homeassistant/components/luci/* @mzdrale diff --git a/homeassistant/components/lookin/__init__.py b/homeassistant/components/lookin/__init__.py new file mode 100644 index 00000000000..a096c08dfeb --- /dev/null +++ b/homeassistant/components/lookin/__init__.py @@ -0,0 +1,68 @@ +"""The lookin integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +import aiohttp +from aiolookin import LookInHttpProtocol, LookinUDPSubscriptions, start_lookin_udp + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST +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, PLATFORMS +from .models import LookinData + +LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up lookin from a config entry.""" + + host = entry.data[CONF_HOST] + lookin_protocol = LookInHttpProtocol( + api_uri=f"http://{host}", session=async_get_clientsession(hass) + ) + + try: + lookin_device = await lookin_protocol.get_info() + devices = await lookin_protocol.get_devices() + except aiohttp.ClientError as ex: + raise ConfigEntryNotReady from ex + + meteo_coordinator: DataUpdateCoordinator = DataUpdateCoordinator( + hass, + LOGGER, + name=entry.title, + update_method=lookin_protocol.get_meteo_sensor, + update_interval=timedelta( + minutes=5 + ), # Updates are pushed (fallback is polling) + ) + await meteo_coordinator.async_config_entry_first_refresh() + + lookin_udp_subs = LookinUDPSubscriptions() + entry.async_on_unload(await start_lookin_udp(lookin_udp_subs)) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = LookinData( + lookin_udp_subs=lookin_udp_subs, + lookin_device=lookin_device, + meteo_coordinator=meteo_coordinator, + devices=devices, + lookin_protocol=lookin_protocol, + ) + + 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/lookin/config_flow.py b/homeassistant/components/lookin/config_flow.py new file mode 100644 index 00000000000..f4fcece1303 --- /dev/null +++ b/homeassistant/components/lookin/config_flow.py @@ -0,0 +1,108 @@ +"""The lookin integration config_flow.""" +from __future__ import annotations + +import logging +from typing import Any + +import aiohttp +from aiolookin import Device, LookInHttpProtocol, NoUsableService +import voluptuous as vol + +from homeassistant import config_entries +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.typing import DiscoveryInfoType + +from .const import DOMAIN + +LOGGER = logging.getLogger(__name__) + + +class LookinFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for lookin.""" + + def __init__(self) -> None: + """Init the lookin flow.""" + self._host: str | None = None + self._name: str | None = None + + async def async_step_zeroconf( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: + """Start a discovery flow from zeroconf.""" + uid: str = discovery_info["hostname"][: -len(".local.")] + host: str = discovery_info["host"] + await self.async_set_unique_id(uid.upper()) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + + try: + device: Device = await self._validate_device(host=host) + except (aiohttp.ClientError, NoUsableService): + return self.async_abort(reason="cannot_connect") + except Exception: # pylint: disable=broad-except + LOGGER.exception("Unexpected exception") + return self.async_abort(reason="unknown") + else: + self._name = device.name + + self._host = host + self._set_confirm_only() + self.context["title_placeholders"] = {"name": self._name, "host": host} + return await self.async_step_discovery_confirm() + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """User initiated discover flow.""" + errors: dict[str, str] = {} + + if user_input is not None: + host = user_input[CONF_HOST] + try: + device = await self._validate_device(host=host) + except (aiohttp.ClientError, NoUsableService): + errors[CONF_HOST] = "cannot_connect" + except Exception: # pylint: disable=broad-except + LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + device_id = device.id.upper() + await self.async_set_unique_id(device_id, raise_on_progress=False) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + return self.async_create_entry( + title=device.name or host, + data={CONF_HOST: host}, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_HOST): str}), + errors=errors, + ) + + async def _validate_device(self, host: str) -> Device: + """Validate we can connect to the device.""" + session = async_get_clientsession(self.hass) + lookin_protocol = LookInHttpProtocol(host, session) + return await lookin_protocol.get_info() + + async def async_step_discovery_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm the discover flow.""" + assert self._host is not None + if user_input is None: + self.context["title_placeholders"] = { + "name": self._name, + "host": self._host, + } + return self.async_show_form( + step_id="discovery_confirm", + description_placeholders={"name": self._name, "host": self._host}, + ) + + return self.async_create_entry( + title=self._name or self._host, + data={CONF_HOST: self._host}, + ) diff --git a/homeassistant/components/lookin/const.py b/homeassistant/components/lookin/const.py new file mode 100644 index 00000000000..a478b24df3b --- /dev/null +++ b/homeassistant/components/lookin/const.py @@ -0,0 +1,9 @@ +"""The lookin integration constants.""" +from __future__ import annotations + +from typing import Final + +DOMAIN: Final = "lookin" +PLATFORMS: Final = [ + "sensor", +] diff --git a/homeassistant/components/lookin/entity.py b/homeassistant/components/lookin/entity.py new file mode 100644 index 00000000000..fd2ee5e4a6c --- /dev/null +++ b/homeassistant/components/lookin/entity.py @@ -0,0 +1,84 @@ +"""The lookin integration entity.""" +from __future__ import annotations + +from aiolookin import POWER_CMD, POWER_OFF_CMD, POWER_ON_CMD, Climate, Remote + +from homeassistant.helpers.entity import DeviceInfo, Entity + +from .const import DOMAIN +from .models import LookinData + + +class LookinDeviceEntity(Entity): + """A lookin device entity on the device itself.""" + + _attr_should_poll = False + + def __init__(self, lookin_data: LookinData) -> None: + """Init the lookin device entity.""" + super().__init__() + self._lookin_device = lookin_data.lookin_device + self._lookin_protocol = lookin_data.lookin_protocol + self._lookin_udp_subs = lookin_data.lookin_udp_subs + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._lookin_device.id)}, + name=self._lookin_device.name, + manufacturer="LOOKin", + model="LOOKin Remote2", + sw_version=self._lookin_device.firmware, + ) + + +class LookinEntity(Entity): + """A base class for lookin entities.""" + + _attr_should_poll = False + _attr_assumed_state = True + + def __init__( + self, + uuid: str, + device: Remote | Climate, + lookin_data: LookinData, + ) -> None: + """Init the base entity.""" + self._device = device + self._uuid = uuid + self._lookin_device = lookin_data.lookin_device + self._lookin_protocol = lookin_data.lookin_protocol + self._lookin_udp_subs = lookin_data.lookin_udp_subs + self._meteo_coordinator = lookin_data.meteo_coordinator + self._function_names = {function.name for function in self._device.functions} + self._attr_unique_id = uuid + self._attr_name = self._device.name + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._uuid)}, + name=self._device.name, + model=self._device.device_type, + via_device=(DOMAIN, self._lookin_device.id), + ) + + async def _async_send_command(self, command: str) -> None: + """Send command from saved IR device.""" + await self._lookin_protocol.send_command( + uuid=self._uuid, command=command, signal="FF" + ) + + +class LookinPowerEntity(LookinEntity): + """A Lookin entity that has a power on and power off command.""" + + def __init__( + self, + uuid: str, + device: Remote | Climate, + lookin_data: LookinData, + ) -> None: + """Init the power entity.""" + super().__init__(uuid, device, lookin_data) + self._power_on_command: str = POWER_CMD + self._power_off_command: str = POWER_CMD + if POWER_ON_CMD in self._function_names: + self._power_on_command = POWER_ON_CMD + if POWER_OFF_CMD in self._function_names: + self._power_off_command = POWER_OFF_CMD diff --git a/homeassistant/components/lookin/manifest.json b/homeassistant/components/lookin/manifest.json new file mode 100644 index 00000000000..2307c89f3aa --- /dev/null +++ b/homeassistant/components/lookin/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "lookin", + "name": "LOOKin", + "documentation": "https://www.home-assistant.io/integrations/lookin/", + "codeowners": ["@ANMalko"], + "requirements": ["aiolookin==0.0.2"], + "zeroconf": ["_lookin._tcp.local."], + "config_flow": true, + "iot_class": "local_push" +} diff --git a/homeassistant/components/lookin/models.py b/homeassistant/components/lookin/models.py new file mode 100644 index 00000000000..6fd812133c0 --- /dev/null +++ b/homeassistant/components/lookin/models.py @@ -0,0 +1,20 @@ +"""The lookin integration models.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from aiolookin import Device, LookInHttpProtocol, LookinUDPSubscriptions + +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + + +@dataclass +class LookinData: + """Data for the lookin integration.""" + + lookin_udp_subs: LookinUDPSubscriptions + lookin_device: Device + meteo_coordinator: DataUpdateCoordinator + devices: list[dict[str, Any]] + lookin_protocol: LookInHttpProtocol diff --git a/homeassistant/components/lookin/sensor.py b/homeassistant/components/lookin/sensor.py new file mode 100644 index 00000000000..34a3859c7ec --- /dev/null +++ b/homeassistant/components/lookin/sensor.py @@ -0,0 +1,98 @@ +"""The lookin integration sensor platform.""" +from __future__ import annotations + +import logging + +from aiolookin import MeteoSensor, SensorID + +from homeassistant.components.sensor import ( + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .entity import LookinDeviceEntity +from .models import LookinData + +LOGGER = logging.getLogger(__name__) + + +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="temperature", + name="Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="humidity", + name="Humidity", + native_unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up lookin sensors from the config entry.""" + lookin_data: LookinData = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + [LookinSensorEntity(description, lookin_data) for description in SENSOR_TYPES] + ) + + +class LookinSensorEntity(CoordinatorEntity, LookinDeviceEntity, SensorEntity, Entity): + """A lookin device sensor entity.""" + + def __init__( + self, description: SensorEntityDescription, lookin_data: LookinData + ) -> None: + """Init the lookin sensor entity.""" + super().__init__(lookin_data.meteo_coordinator) + LookinDeviceEntity.__init__(self, lookin_data) + self.entity_description = description + self._attr_name = f"{self._lookin_device.name} {description.name}" + self._attr_native_value = getattr(self.coordinator.data, description.key) + self._attr_unique_id = f"{self._lookin_device.id}-{description.key}" + + def _handle_coordinator_update(self) -> None: + """Update the state of the entity.""" + self._attr_native_value = getattr( + self.coordinator.data, self.entity_description.key + ) + super()._handle_coordinator_update() + + @callback + def _async_push_update(self, msg: dict[str, str]) -> None: + """Process an update pushed via UDP.""" + if int(msg["event_id"]): + return + LOGGER.debug("Processing push message for meteo sensor: %s", msg) + meteo: MeteoSensor = self.coordinator.data + meteo.update_from_value(msg["value"]) + self.coordinator.async_set_updated_data(meteo) + + async def async_added_to_hass(self) -> None: + """Call when the entity is added to hass.""" + self.async_on_remove( + self._lookin_udp_subs.subscribe_sensor( + self._lookin_device.id, SensorID.Meteo, None, self._async_push_update + ) + ) + return await super().async_added_to_hass() diff --git a/homeassistant/components/lookin/strings.json b/homeassistant/components/lookin/strings.json new file mode 100644 index 00000000000..1285be4abf0 --- /dev/null +++ b/homeassistant/components/lookin/strings.json @@ -0,0 +1,31 @@ +{ + "config": { + "flow_title": "{name} ({host})", + "step": { + "user": { + "data": { + "ip_address": "[%key:common::config_flow::data::ip%]" + } + }, + "device_name": { + "data": { + "name": "[%key:common::config_flow::data::name%]" + } + }, + "discovery_confirm": { + "description": "Do you want to setup {name} ({host})?" + } + }, + "error": { + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lookin/translations/en.json b/homeassistant/components/lookin/translations/en.json new file mode 100644 index 00000000000..ddb8c310408 --- /dev/null +++ b/homeassistant/components/lookin/translations/en.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "cannot_connect": "Failed to connect", + "no_devices_found": "No devices found on the network" + }, + "error": { + "cannot_connect": "Failed to connect", + "no_devices_found": "No devices found on the network", + "unknown": "Unexpected error" + }, + "flow_title": "{name} ({host})", + "step": { + "device_name": { + "data": { + "name": "Name" + } + }, + "discovery_confirm": { + "description": "Do you want to setup {name} ({host})?" + }, + "user": { + "data": { + "ip_address": "IP Address" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 39719402239..042babb6313 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -159,6 +159,7 @@ FLOWS = [ "local_ip", "locative", "logi_circle", + "lookin", "luftdaten", "lutron_caseta", "lyric", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 6a1a7f03be1..041406af50a 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -157,6 +157,11 @@ ZEROCONF = { "domain": "lutron_caseta" } ], + "_lookin._tcp.local.": [ + { + "domain": "lookin" + } + ], "_mediaremotetv._tcp.local.": [ { "domain": "apple_tv" diff --git a/requirements_all.txt b/requirements_all.txt index 15d664ba697..34bf5a9764c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -206,6 +206,9 @@ aiolifx_effects==0.2.2 # homeassistant.components.lutron_caseta aiolip==1.1.6 +# homeassistant.components.lookin +aiolookin==0.0.2 + # homeassistant.components.lyric aiolyric==1.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0f58342b8e5..c78662c7351 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -136,6 +136,9 @@ aiokafka==0.6.0 # homeassistant.components.lutron_caseta aiolip==1.1.6 +# homeassistant.components.lookin +aiolookin==0.0.2 + # homeassistant.components.lyric aiolyric==1.0.7 diff --git a/tests/components/lookin/__init__.py b/tests/components/lookin/__init__.py new file mode 100644 index 00000000000..c2821fafab8 --- /dev/null +++ b/tests/components/lookin/__init__.py @@ -0,0 +1,53 @@ +"""Tests for the lookin integration.""" +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +from aiolookin import Climate, Device, Remote + +from homeassistant.components.zeroconf import HaServiceInfo + +DEVICE_ID = "98F33163" +MODULE = "homeassistant.components.lookin" +MODULE_CONFIG_FLOW = "homeassistant.components.lookin.config_flow" +IP_ADDRESS = "127.0.0.1" + +DEVICE_NAME = "Living Room" +DEFAULT_ENTRY_TITLE = DEVICE_NAME + +ZC_NAME = f"LOOKin_{DEVICE_ID}" +ZC_TYPE = "_lookin._tcp." +ZEROCONF_DATA: HaServiceInfo = { + "host": IP_ADDRESS, + "hostname": f"{ZC_NAME.lower()}.local.", + "port": 80, + "type": ZC_TYPE, + "name": ZC_NAME, + "properties": {}, +} + + +def _mocked_climate() -> Climate: + climate = MagicMock(auto_spec=Climate) + return climate + + +def _mocked_remote() -> Remote: + remote = MagicMock(auto_spec=Remote) + return remote + + +def _mocked_device() -> Device: + device = MagicMock(auto_spec=Device) + device.name = DEVICE_NAME + device.id = DEVICE_ID + return device + + +def _patch_get_info(device=None, exception=None): + async def _get_info(*args, **kwargs): + if exception: + raise exception + return device if device else _mocked_device() + + return patch(f"{MODULE_CONFIG_FLOW}.LookInHttpProtocol.get_info", new=_get_info) diff --git a/tests/components/lookin/test_config_flow.py b/tests/components/lookin/test_config_flow.py new file mode 100644 index 00000000000..92f6e500045 --- /dev/null +++ b/tests/components/lookin/test_config_flow.py @@ -0,0 +1,185 @@ +"""Define tests for the lookin config flow.""" +from __future__ import annotations + +from unittest.mock import patch + +from aiolookin import NoUsableService + +from homeassistant import config_entries +from homeassistant.components.lookin.const import DOMAIN +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from . import ( + DEFAULT_ENTRY_TITLE, + DEVICE_ID, + IP_ADDRESS, + MODULE, + ZEROCONF_DATA, + _patch_get_info, +) + +from tests.common import MockConfigEntry + + +async def test_manual_setup(hass: HomeAssistant): + """Test manually setting up.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + with _patch_get_info(), patch( + f"{MODULE}.async_setup_entry", return_value=True + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: IP_ADDRESS} + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == {CONF_HOST: IP_ADDRESS} + assert result["title"] == DEFAULT_ENTRY_TITLE + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_manual_setup_already_exists(hass: HomeAssistant): + """Test manually setting up and the device already exists.""" + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=DEVICE_ID + ) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + with _patch_get_info(): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: IP_ADDRESS} + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_manual_setup_device_offline(hass: HomeAssistant): + """Test manually setting up, device offline.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + with _patch_get_info(exception=NoUsableService): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: IP_ADDRESS} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert result["errors"] == {CONF_HOST: "cannot_connect"} + + +async def test_manual_setup_unknown_exception(hass: HomeAssistant): + """Test manually setting up, unknown exception.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + with _patch_get_info(exception=Exception): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: IP_ADDRESS} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert result["errors"] == {"base": "unknown"} + + +async def test_discovered_zeroconf(hass): + """Test we can setup when discovered from zeroconf.""" + + with _patch_get_info(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=ZEROCONF_DATA, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with _patch_get_info(), patch( + f"{MODULE}.async_setup_entry", return_value=True + ) as mock_async_setup_entry: + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["data"] == {CONF_HOST: IP_ADDRESS} + assert result2["title"] == DEFAULT_ENTRY_TITLE + assert mock_async_setup_entry.called + + entry = hass.config_entries.async_entries(DOMAIN)[0] + zc_data_new_ip = ZEROCONF_DATA.copy() + zc_data_new_ip["host"] = "127.0.0.2" + + with _patch_get_info(), patch( + f"{MODULE}.async_setup_entry", return_value=True + ) as mock_async_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zc_data_new_ip, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_HOST] == "127.0.0.2" + + +async def test_discovered_zeroconf_cannot_connect(hass): + """Test we abort if we cannot connect when discovered from zeroconf.""" + + with _patch_get_info(exception=NoUsableService): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=ZEROCONF_DATA, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + +async def test_discovered_zeroconf_unknown_exception(hass): + """Test we abort if we get an unknown exception when discovered from zeroconf.""" + + with _patch_get_info(exception=Exception): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=ZEROCONF_DATA, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unknown" From dcf60b54cefc118791308665d69c00e34c249500 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Thu, 21 Oct 2021 22:57:34 +0200 Subject: [PATCH 0629/1038] Improve SSDP discovery compatibility when device was discovery through an SSDP advertisement (#58133) --- homeassistant/components/ssdp/__init__.py | 4 ++ tests/components/ssdp/test_init.py | 50 +++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index da46fc565d2..af3f09f560d 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -477,6 +477,10 @@ def discovery_info_from_headers_and_description( if udn := _udn_from_usn(info[ATTR_SSDP_USN]): info[ATTR_UPNP_UDN] = udn + # Increase compatibility. + if ATTR_SSDP_ST not in info and ATTR_SSDP_NT in info: + info[ATTR_SSDP_ST] = info[ATTR_SSDP_NT] + return info diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 0304f8f067b..e72b25715fe 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -311,6 +311,56 @@ async def test_flow_start_only_alive( ) +@pytest.mark.usefixtures("mock_get_source_ip") +@patch( + "homeassistant.components.ssdp.async_get_ssdp", + return_value={}, +) +async def test_discovery_from_advertisement_sets_ssdp_st( + mock_get_ssdp, hass, aioclient_mock, mock_flow_init +): + """Test discovery from advertisement sets `ssdp_st` for more compatibility.""" + aioclient_mock.get( + "http://1.1.1.1", + text=""" + + + Paulus + + + """, + ) + ssdp_listener = await init_ssdp_component(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + mock_ssdp_advertisement = _ssdp_headers( + { + "nt": "mock-st", + "nts": "ssdp:alive", + "location": "http://1.1.1.1", + "usn": "uuid:mock-udn::mock-st", + } + ) + await ssdp_listener._on_alive(mock_ssdp_advertisement) + await hass.async_block_till_done() + + discovery_info = await ssdp.async_get_discovery_info_by_udn(hass, "uuid:mock-udn") + assert discovery_info == [ + { + ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1", + ssdp.ATTR_SSDP_NT: "mock-st", + ssdp.ATTR_SSDP_ST: "mock-st", # Set by ssdp component, not in original advertisement. + ssdp.ATTR_SSDP_USN: "uuid:mock-udn::mock-st", + ssdp.ATTR_UPNP_UDN: "uuid:mock-udn", + ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", + ssdp.ATTR_SSDP_UDN: ANY, + "nts": "ssdp:alive", + "_timestamp": ANY, + } + ] + + @patch( # XXX TODO: Isn't this duplicate with mock_get_source_ip? "homeassistant.components.ssdp.Scanner._async_build_source_set", return_value={IPv4Address("192.168.1.1")}, From 4a9209ebc818f68ca5816df4375131240e7fd6a8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 21 Oct 2021 10:58:34 -1000 Subject: [PATCH 0630/1038] Bump async-upnp-client to 0.22.9 (#58185) Co-authored-by: Steven Looman --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 1a54bd6d4ec..18e50ba1035 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Renderer", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.22.8"], + "requirements": ["async-upnp-client==0.22.9"], "dependencies": ["ssdp"], "codeowners": ["@StevenLooman", "@chishm"], "iot_class": "local_push" diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 85e489a72dd..facb93efdad 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,7 +2,7 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", - "requirements": ["async-upnp-client==0.22.8"], + "requirements": ["async-upnp-client==0.22.9"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 4029ff5c3bb..20e90fec751 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -3,7 +3,7 @@ "name": "UPnP/IGD", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": ["async-upnp-client==0.22.8"], + "requirements": ["async-upnp-client==0.22.9"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman","@ehendrix23"], "ssdp": [ diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 4682215092b..944ca80b105 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,7 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.7.8", "async-upnp-client==0.22.8"], + "requirements": ["yeelight==0.7.8", "async-upnp-client==0.22.9"], "codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0508619a1e4..82a19e54b6f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.5 aiohttp==3.7.4.post0 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.22.8 +async-upnp-client==0.22.9 async_timeout==3.0.1 attrs==21.2.0 awesomeversion==21.8.1 diff --git a/requirements_all.txt b/requirements_all.txt index 34bf5a9764c..bbfec519206 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -333,7 +333,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.22.8 +async-upnp-client==0.22.9 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c78662c7351..2cc9d5526a6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -230,7 +230,7 @@ arcam-fmj==0.7.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.22.8 +async-upnp-client==0.22.9 # homeassistant.components.aurora auroranoaa==0.0.2 From 749258a05d4466f0f60a352f525d1d97ca7af09a Mon Sep 17 00:00:00 2001 From: ANMalko Date: Fri, 22 Oct 2021 01:00:48 +0300 Subject: [PATCH 0631/1038] Add climate platform to lookin (#58175) Co-authored-by: J. Nick Koston --- .coveragerc | 1 + homeassistant/components/lookin/climate.py | 210 +++++++++++++++++++++ homeassistant/components/lookin/const.py | 1 + 3 files changed, 212 insertions(+) create mode 100644 homeassistant/components/lookin/climate.py diff --git a/.coveragerc b/.coveragerc index 7a1f53347b2..eac2dbf54a8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -589,6 +589,7 @@ omit = homeassistant/components/lookin/entity.py homeassistant/components/lookin/models.py homeassistant/components/lookin/sensor.py + homeassistant/components/lookin/climate.py homeassistant/components/loopenergy/sensor.py homeassistant/components/luci/device_tracker.py homeassistant/components/luftdaten/__init__.py diff --git a/homeassistant/components/lookin/climate.py b/homeassistant/components/lookin/climate.py new file mode 100644 index 00000000000..4cba00121d5 --- /dev/null +++ b/homeassistant/components/lookin/climate.py @@ -0,0 +1,210 @@ +"""The lookin integration climate platform.""" +from __future__ import annotations + +from collections.abc import Coroutine +from datetime import timedelta +import logging +from typing import Any, Callable, Final, cast + +from aiolookin import Climate, MeteoSensor, SensorID + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MIDDLE, + HVAC_MODE_AUTO, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + SUPPORT_FAN_MODE, + SUPPORT_SWING_MODE, + SUPPORT_TARGET_TEMPERATURE, + SWING_BOTH, + SWING_OFF, +) +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.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import DOMAIN +from .entity import LookinEntity +from .models import LookinData + +SUPPORT_FLAGS: int = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | SUPPORT_SWING_MODE + +LOOKIN_FAN_MODE_IDX_TO_HASS: Final = [FAN_AUTO, FAN_LOW, FAN_MIDDLE, FAN_HIGH] +LOOKIN_SWING_MODE_IDX_TO_HASS: Final = [SWING_OFF, SWING_BOTH] +LOOKIN_HVAC_MODE_IDX_TO_HASS: Final = [ + HVAC_MODE_OFF, + HVAC_MODE_AUTO, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, +] + +HASS_TO_LOOKIN_HVAC_MODE: dict[str, int] = { + mode: idx for idx, mode in enumerate(LOOKIN_HVAC_MODE_IDX_TO_HASS) +} +HASS_TO_LOOKIN_FAN_MODE: dict[str, int] = { + mode: idx for idx, mode in enumerate(LOOKIN_FAN_MODE_IDX_TO_HASS) +} +HASS_TO_LOOKIN_SWING_MODE: dict[str, int] = { + mode: idx for idx, mode in enumerate(LOOKIN_SWING_MODE_IDX_TO_HASS) +} + + +MIN_TEMP: Final = 16 +MAX_TEMP: Final = 30 +LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the climate platform for lookin from a config entry.""" + lookin_data: LookinData = hass.data[DOMAIN][config_entry.entry_id] + entities = [] + + for remote in lookin_data.devices: + if remote["Type"] != "EF": + continue + uuid = remote["UUID"] + + def _wrap_async_update(uuid) -> Callable[[], Coroutine[None, Any, Climate]]: + """Create a function to capture the uuid cell variable.""" + + async def _async_update() -> Climate: + return await lookin_data.lookin_protocol.get_conditioner(uuid) + + return _async_update + + coordinator = DataUpdateCoordinator( + hass, + LOGGER, + name=f"{config_entry.title} {uuid}", + update_method=_wrap_async_update(uuid), + update_interval=timedelta( + seconds=60 + ), # Updates are pushed (fallback is polling) + ) + await coordinator.async_refresh() + device: Climate = coordinator.data + entities.append( + ConditionerEntity( + uuid=uuid, + device=device, + lookin_data=lookin_data, + coordinator=coordinator, + ) + ) + + async_add_entities(entities) + + +class ConditionerEntity(LookinEntity, CoordinatorEntity, ClimateEntity): + """An aircon or heat pump.""" + + _attr_temperature_unit = TEMP_CELSIUS + _attr_supported_features: int = SUPPORT_FLAGS + _attr_fan_modes: list[str] = LOOKIN_FAN_MODE_IDX_TO_HASS + _attr_swing_modes: list[str] = LOOKIN_SWING_MODE_IDX_TO_HASS + _attr_hvac_modes: list[str] = LOOKIN_HVAC_MODE_IDX_TO_HASS + _attr_min_temp = MIN_TEMP + _attr_max_temp = MAX_TEMP + _attr_target_temperature_step = PRECISION_WHOLE + + def __init__( + self, + uuid: str, + device: Climate, + lookin_data: LookinData, + coordinator: DataUpdateCoordinator, + ) -> None: + """Init the ConditionerEntity.""" + CoordinatorEntity.__init__(self, coordinator) + super().__init__(uuid, device, lookin_data) + self._async_update_from_data() + + @property + def _climate(self) -> Climate: + return cast(Climate, self.coordinator.data) + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set the hvac mode of the device.""" + if (mode := HASS_TO_LOOKIN_HVAC_MODE.get(hvac_mode)) is None: + return + self._climate.hvac_mode = mode + await self._async_update_conditioner() + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set the temperature of the device.""" + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: + return + self._climate.temp_celsius = int(temperature) + await self._async_update_conditioner() + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set the fan mode of the device.""" + if (mode := HASS_TO_LOOKIN_FAN_MODE.get(fan_mode)) is None: + return + self._climate.fan_mode = mode + await self._async_update_conditioner() + + async def async_set_swing_mode(self, swing_mode: str) -> None: + """Set the swing mode of the device.""" + if (mode := HASS_TO_LOOKIN_SWING_MODE.get(swing_mode)) is None: + return + self._climate.swing_mode = mode + await self._async_update_conditioner() + + async def _async_update_conditioner(self) -> None: + """Update the conditioner state from the climate data.""" + self.coordinator.async_set_updated_data(self._climate) + await self._lookin_protocol.update_conditioner(climate=self._climate) + + def _async_update_from_data(self) -> None: + """Update attrs from data.""" + meteo_data: MeteoSensor = self._meteo_coordinator.data + self._attr_current_temperature = meteo_data.temperature + self._attr_current_humidity = int(meteo_data.humidity) + self._attr_target_temperature = self._climate.temp_celsius + self._attr_fan_mode = LOOKIN_FAN_MODE_IDX_TO_HASS[self._climate.fan_mode] + self._attr_swing_mode = LOOKIN_SWING_MODE_IDX_TO_HASS[self._climate.swing_mode] + self._attr_hvac_mode = LOOKIN_HVAC_MODE_IDX_TO_HASS[self._climate.hvac_mode] + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._async_update_from_data() + super()._handle_coordinator_update() + + @callback + def _async_push_update(self, msg: dict[str, str]) -> None: + """Process an update pushed via UDP.""" + LOGGER.debug("Processing push message for %s: %s", self.entity_id, msg) + self._climate.update_from_status(msg["value"]) + self.coordinator.async_set_updated_data(self._climate) + + async def async_added_to_hass(self) -> None: + """Call when the entity is added to hass.""" + self.async_on_remove( + self._lookin_udp_subs.subscribe_sensor( + self._lookin_device.id, SensorID.IR, self._uuid, self._async_push_update + ) + ) + self.async_on_remove( + self._meteo_coordinator.async_add_listener(self._handle_coordinator_update) + ) + return await super().async_added_to_hass() diff --git a/homeassistant/components/lookin/const.py b/homeassistant/components/lookin/const.py index a478b24df3b..1605a169592 100644 --- a/homeassistant/components/lookin/const.py +++ b/homeassistant/components/lookin/const.py @@ -6,4 +6,5 @@ from typing import Final DOMAIN: Final = "lookin" PLATFORMS: Final = [ "sensor", + "climate", ] From cc4241836e1c7e6201ad36b9aedebf3359c29ddb Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 22 Oct 2021 01:09:08 +0200 Subject: [PATCH 0632/1038] Move notify setup to legacy (#58033) --- homeassistant/components/notify/__init__.py | 358 ++------------------ homeassistant/components/notify/const.py | 40 +++ homeassistant/components/notify/legacy.py | 315 +++++++++++++++++ 3 files changed, 380 insertions(+), 333 deletions(-) create mode 100644 homeassistant/components/notify/const.py create mode 100644 homeassistant/components/notify/legacy.py diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index ef78676027d..bc8a7bffa95 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -1,354 +1,60 @@ """Provides functionality to notify people.""" from __future__ import annotations -import asyncio -from functools import partial -import logging -from typing import Any, cast - import voluptuous as vol import homeassistant.components.persistent_notification as pn -from homeassistant.const import CONF_DESCRIPTION, CONF_NAME, CONF_PLATFORM -from homeassistant.core import HomeAssistant, ServiceCall, callback -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_per_platform, discovery, template +from homeassistant.const import CONF_NAME, CONF_PLATFORM +from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.service import async_set_service_schema -from homeassistant.loader import async_get_integration, bind_hass -from homeassistant.setup import async_prepare_setup_platform, async_start_setup -from homeassistant.util import slugify -from homeassistant.util.yaml import load_yaml +from homeassistant.helpers.typing import ConfigType -# mypy: allow-untyped-defs, no-check-untyped-defs - -_LOGGER = logging.getLogger(__name__) +from .const import ( # noqa: F401 + ATTR_DATA, + ATTR_MESSAGE, + ATTR_TARGET, + ATTR_TITLE, + DOMAIN, + NOTIFY_SERVICE_SCHEMA, + PERSISTENT_NOTIFICATION_SERVICE_SCHEMA, + SERVICE_NOTIFY, + SERVICE_PERSISTENT_NOTIFICATION, +) +from .legacy import ( # noqa: F401 + BaseNotificationService, + async_reload, + async_reset_platform, + async_setup_legacy, + check_templates_warn, +) # Platform specific data -ATTR_DATA = "data" - -# Text to notify user of -ATTR_MESSAGE = "message" - -# Target of the notification (user, device, etc) -ATTR_TARGET = "target" - -# Title of notification -ATTR_TITLE = "title" ATTR_TITLE_DEFAULT = "Home Assistant" -DOMAIN = "notify" - -SERVICE_NOTIFY = "notify" -SERVICE_PERSISTENT_NOTIFICATION = "persistent_notification" - -NOTIFY_SERVICES = "notify_services" - -CONF_FIELDS = "fields" - PLATFORM_SCHEMA = vol.Schema( {vol.Required(CONF_PLATFORM): cv.string, vol.Optional(CONF_NAME): cv.string}, extra=vol.ALLOW_EXTRA, ) -NOTIFY_SERVICE_SCHEMA = vol.Schema( - { - vol.Required(ATTR_MESSAGE): cv.template, - vol.Optional(ATTR_TITLE): cv.template, - vol.Optional(ATTR_TARGET): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_DATA): dict, - } -) -PERSISTENT_NOTIFICATION_SERVICE_SCHEMA = vol.Schema( - { - vol.Required(ATTR_MESSAGE): cv.template, - vol.Optional(ATTR_TITLE): cv.template, - } -) - - -@callback -def _check_templates_warn(hass: HomeAssistant, tpl: template.Template) -> None: - """Warn user that passing templates to notify service is deprecated.""" - if tpl.is_static or hass.data.get("notify_template_warned"): - return - - hass.data["notify_template_warned"] = True - _LOGGER.warning( - "Passing templates to notify service is deprecated and will be removed in 2021.12. " - "Automations and scripts handle templates automatically" - ) - - -@bind_hass -async def async_reload(hass: HomeAssistant, integration_name: str) -> None: - """Register notify services for an integration.""" - if not _async_integration_has_notify_services(hass, integration_name): - return - - tasks = [ - notify_service.async_register_services() - for notify_service in hass.data[NOTIFY_SERVICES][integration_name] - ] - - await asyncio.gather(*tasks) - - -@bind_hass -async def async_reset_platform(hass: HomeAssistant, integration_name: str) -> None: - """Unregister notify services for an integration.""" - if not _async_integration_has_notify_services(hass, integration_name): - return - - tasks = [ - notify_service.async_unregister_services() - for notify_service in hass.data[NOTIFY_SERVICES][integration_name] - ] - - await asyncio.gather(*tasks) - - del hass.data[NOTIFY_SERVICES][integration_name] - - -def _async_integration_has_notify_services( - hass: HomeAssistant, integration_name: str -) -> bool: - """Determine if an integration has notify services registered.""" - if ( - NOTIFY_SERVICES not in hass.data - or integration_name not in hass.data[NOTIFY_SERVICES] - ): - return False - - return True - - -class BaseNotificationService: - """An abstract class for notification services.""" - - # While not purely typed, it makes typehinting more useful for us - # and removes the need for constant None checks or asserts. - hass: HomeAssistant = None # type: ignore - - # Name => target - registered_targets: dict[str, str] - - def send_message(self, message, **kwargs): - """Send a message. - - kwargs can contain ATTR_TITLE to specify a title. - """ - raise NotImplementedError() - - async def async_send_message(self, message: Any, **kwargs: Any) -> None: - """Send a message. - - kwargs can contain ATTR_TITLE to specify a title. - """ - await self.hass.async_add_executor_job( - partial(self.send_message, message, **kwargs) - ) - - async def _async_notify_message_service(self, service: ServiceCall) -> None: - """Handle sending notification message service calls.""" - kwargs = {} - message = service.data[ATTR_MESSAGE] - title = service.data.get(ATTR_TITLE) - - if title: - _check_templates_warn(self.hass, title) - title.hass = self.hass - kwargs[ATTR_TITLE] = title.async_render(parse_result=False) - - if self.registered_targets.get(service.service) is not None: - kwargs[ATTR_TARGET] = [self.registered_targets[service.service]] - elif service.data.get(ATTR_TARGET) is not None: - kwargs[ATTR_TARGET] = service.data.get(ATTR_TARGET) - - _check_templates_warn(self.hass, message) - message.hass = self.hass - kwargs[ATTR_MESSAGE] = message.async_render(parse_result=False) - kwargs[ATTR_DATA] = service.data.get(ATTR_DATA) - - await self.async_send_message(**kwargs) - - async def async_setup( - self, - hass: HomeAssistant, - service_name: str, - target_service_name_prefix: str, - ) -> None: - """Store the data for the notify service.""" - # pylint: disable=attribute-defined-outside-init - self.hass = hass - self._service_name = service_name - self._target_service_name_prefix = target_service_name_prefix - self.registered_targets = {} - - # Load service descriptions from notify/services.yaml - integration = await async_get_integration(hass, DOMAIN) - services_yaml = integration.file_path / "services.yaml" - self.services_dict = cast( - dict, await hass.async_add_executor_job(load_yaml, str(services_yaml)) - ) - - async def async_register_services(self) -> None: - """Create or update the notify services.""" - if hasattr(self, "targets"): - stale_targets = set(self.registered_targets) - - for name, target in self.targets.items(): # type: ignore - target_name = slugify(f"{self._target_service_name_prefix}_{name}") - if target_name in stale_targets: - stale_targets.remove(target_name) - if ( - target_name in self.registered_targets - and target == self.registered_targets[target_name] - ): - continue - self.registered_targets[target_name] = target - self.hass.services.async_register( - DOMAIN, - target_name, - self._async_notify_message_service, - schema=NOTIFY_SERVICE_SCHEMA, - ) - # Register the service description - service_desc = { - CONF_NAME: f"Send a notification via {target_name}", - CONF_DESCRIPTION: f"Sends a notification message using the {target_name} integration.", - CONF_FIELDS: self.services_dict[SERVICE_NOTIFY][CONF_FIELDS], - } - async_set_service_schema(self.hass, DOMAIN, target_name, service_desc) - - for stale_target_name in stale_targets: - del self.registered_targets[stale_target_name] - self.hass.services.async_remove( - DOMAIN, - stale_target_name, - ) - - if self.hass.services.has_service(DOMAIN, self._service_name): - return - - self.hass.services.async_register( - DOMAIN, - self._service_name, - self._async_notify_message_service, - schema=NOTIFY_SERVICE_SCHEMA, - ) - - # Register the service description - service_desc = { - CONF_NAME: f"Send a notification with {self._service_name}", - CONF_DESCRIPTION: f"Sends a notification message using the {self._service_name} service.", - CONF_FIELDS: self.services_dict[SERVICE_NOTIFY][CONF_FIELDS], - } - async_set_service_schema(self.hass, DOMAIN, self._service_name, service_desc) - - async def async_unregister_services(self) -> None: - """Unregister the notify services.""" - if self.registered_targets: - remove_targets = set(self.registered_targets) - for remove_target_name in remove_targets: - del self.registered_targets[remove_target_name] - self.hass.services.async_remove( - DOMAIN, - remove_target_name, - ) - - if not self.hass.services.has_service(DOMAIN, self._service_name): - return - - self.hass.services.async_remove( - DOMAIN, - self._service_name, - ) - - -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the notify services.""" - hass.data.setdefault(NOTIFY_SERVICES, {}) + await async_setup_legacy(hass, config) async def persistent_notification(service: ServiceCall) -> None: """Send notification via the built-in persistsent_notify integration.""" message = service.data[ATTR_MESSAGE] message.hass = hass - _check_templates_warn(hass, message) + check_templates_warn(hass, message) title = None if title_tpl := service.data.get(ATTR_TITLE): - _check_templates_warn(hass, title_tpl) + check_templates_warn(hass, title_tpl) title_tpl.hass = hass title = title_tpl.async_render(parse_result=False) pn.async_create(hass, message.async_render(parse_result=False), title) - async def async_setup_platform( - integration_name, p_config=None, discovery_info=None - ): - """Set up a notify platform.""" - if p_config is None: - p_config = {} - - platform = await async_prepare_setup_platform( - hass, config, DOMAIN, integration_name - ) - - if platform is None: - _LOGGER.error("Unknown notification service specified") - return - - full_name = f"{DOMAIN}.{integration_name}" - _LOGGER.info("Setting up %s", full_name) - with async_start_setup(hass, [full_name]): - notify_service = None - try: - if hasattr(platform, "async_get_service"): - notify_service = await platform.async_get_service( - hass, p_config, discovery_info - ) - elif hasattr(platform, "get_service"): - notify_service = await hass.async_add_executor_job( - platform.get_service, hass, p_config, discovery_info - ) - else: - raise HomeAssistantError("Invalid notify platform.") - - if notify_service is None: - # Platforms can decide not to create a service based - # on discovery data. - if discovery_info is None: - _LOGGER.error( - "Failed to initialize notification service %s", - integration_name, - ) - return - - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error setting up platform %s", integration_name) - return - - if discovery_info is None: - discovery_info = {} - - conf_name = p_config.get(CONF_NAME) or discovery_info.get(CONF_NAME) - target_service_name_prefix = conf_name or integration_name - service_name = slugify(conf_name or SERVICE_NOTIFY) - - await notify_service.async_setup( - hass, service_name, target_service_name_prefix - ) - await notify_service.async_register_services() - - hass.data[NOTIFY_SERVICES].setdefault(integration_name, []).append( - notify_service - ) - hass.config.components.add(f"{DOMAIN}.{integration_name}") - - return True - hass.services.async_register( DOMAIN, SERVICE_PERSISTENT_NOTIFICATION, @@ -356,18 +62,4 @@ async def async_setup(hass, config): schema=PERSISTENT_NOTIFICATION_SERVICE_SCHEMA, ) - setup_tasks = [ - asyncio.create_task(async_setup_platform(integration_name, p_config)) - for integration_name, p_config in config_per_platform(config, DOMAIN) - ] - - if setup_tasks: - await asyncio.wait(setup_tasks) - - async def async_platform_discovered(platform, info): - """Handle for discovered platform.""" - await async_setup_platform(platform, discovery_info=info) - - discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered) - return True diff --git a/homeassistant/components/notify/const.py b/homeassistant/components/notify/const.py new file mode 100644 index 00000000000..d30702915d9 --- /dev/null +++ b/homeassistant/components/notify/const.py @@ -0,0 +1,40 @@ +"""Provide common notify constants.""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv + +ATTR_DATA = "data" + +# Text to notify user of +ATTR_MESSAGE = "message" + +# Target of the notification (user, device, etc) +ATTR_TARGET = "target" + +# Title of notification +ATTR_TITLE = "title" + +DOMAIN = "notify" + +LOGGER = logging.getLogger(__package__) + +SERVICE_NOTIFY = "notify" +SERVICE_PERSISTENT_NOTIFICATION = "persistent_notification" + +NOTIFY_SERVICE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_MESSAGE): cv.template, + vol.Optional(ATTR_TITLE): cv.template, + vol.Optional(ATTR_TARGET): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ATTR_DATA): dict, + } +) + +PERSISTENT_NOTIFICATION_SERVICE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_MESSAGE): cv.template, + vol.Optional(ATTR_TITLE): cv.template, + } +) diff --git a/homeassistant/components/notify/legacy.py b/homeassistant/components/notify/legacy.py new file mode 100644 index 00000000000..a2bf9c0c173 --- /dev/null +++ b/homeassistant/components/notify/legacy.py @@ -0,0 +1,315 @@ +"""Handle legacy notification platforms.""" +from __future__ import annotations + +import asyncio +from functools import partial +from typing import Any, cast + +from homeassistant.const import CONF_DESCRIPTION, CONF_NAME +from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_per_platform, discovery, template +from homeassistant.helpers.service import async_set_service_schema +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.loader import async_get_integration, bind_hass +from homeassistant.setup import async_prepare_setup_platform, async_start_setup +from homeassistant.util import slugify +from homeassistant.util.yaml import load_yaml + +from .const import ( + ATTR_DATA, + ATTR_MESSAGE, + ATTR_TARGET, + ATTR_TITLE, + DOMAIN, + LOGGER, + NOTIFY_SERVICE_SCHEMA, + SERVICE_NOTIFY, +) + +CONF_FIELDS = "fields" +NOTIFY_SERVICES = "notify_services" + + +async def async_setup_legacy(hass: HomeAssistant, config: ConfigType) -> None: + """Set up legacy notify services.""" + hass.data.setdefault(NOTIFY_SERVICES, {}) + + async def async_setup_platform( + integration_name: str, + p_config: ConfigType | None = None, + discovery_info: DiscoveryInfoType | None = None, + ) -> None: + """Set up a notify platform.""" + if p_config is None: + p_config = {} + + platform = await async_prepare_setup_platform( + hass, config, DOMAIN, integration_name + ) + + if platform is None: + LOGGER.error("Unknown notification service specified") + return + + full_name = f"{DOMAIN}.{integration_name}" + LOGGER.info("Setting up %s", full_name) + with async_start_setup(hass, [full_name]): + notify_service = None + try: + if hasattr(platform, "async_get_service"): + notify_service = await platform.async_get_service( # type: ignore + hass, p_config, discovery_info + ) + elif hasattr(platform, "get_service"): + notify_service = await hass.async_add_executor_job( + platform.get_service, hass, p_config, discovery_info # type: ignore + ) + else: + raise HomeAssistantError("Invalid notify platform.") + + if notify_service is None: + # Platforms can decide not to create a service based + # on discovery data. + if discovery_info is None: + LOGGER.error( + "Failed to initialize notification service %s", + integration_name, + ) + return + + except Exception: # pylint: disable=broad-except + LOGGER.exception("Error setting up platform %s", integration_name) + return + + if discovery_info is None: + discovery_info = {} + + conf_name = p_config.get(CONF_NAME) or discovery_info.get(CONF_NAME) + target_service_name_prefix = conf_name or integration_name + service_name = slugify(conf_name or SERVICE_NOTIFY) + + await notify_service.async_setup( + hass, service_name, target_service_name_prefix + ) + await notify_service.async_register_services() + + hass.data[NOTIFY_SERVICES].setdefault(integration_name, []).append( + notify_service + ) + 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 setup_tasks: + await asyncio.wait(setup_tasks) + + async def async_platform_discovered( + platform: str, info: DiscoveryInfoType | None + ) -> None: + """Handle for discovered platform.""" + await async_setup_platform(platform, discovery_info=info) + + discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered) + + +@callback +def check_templates_warn(hass: HomeAssistant, tpl: template.Template) -> None: + """Warn user that passing templates to notify service is deprecated.""" + if tpl.is_static or hass.data.get("notify_template_warned"): + return + + hass.data["notify_template_warned"] = True + LOGGER.warning( + "Passing templates to notify service is deprecated and will be removed in 2021.12. " + "Automations and scripts handle templates automatically" + ) + + +@bind_hass +async def async_reload(hass: HomeAssistant, integration_name: str) -> None: + """Register notify services for an integration.""" + if not _async_integration_has_notify_services(hass, integration_name): + return + + tasks = [ + notify_service.async_register_services() + for notify_service in hass.data[NOTIFY_SERVICES][integration_name] + ] + + await asyncio.gather(*tasks) + + +@bind_hass +async def async_reset_platform(hass: HomeAssistant, integration_name: str) -> None: + """Unregister notify services for an integration.""" + if not _async_integration_has_notify_services(hass, integration_name): + return + + tasks = [ + notify_service.async_unregister_services() + for notify_service in hass.data[NOTIFY_SERVICES][integration_name] + ] + + await asyncio.gather(*tasks) + + del hass.data[NOTIFY_SERVICES][integration_name] + + +def _async_integration_has_notify_services( + hass: HomeAssistant, integration_name: str +) -> bool: + """Determine if an integration has notify services registered.""" + if ( + NOTIFY_SERVICES not in hass.data + or integration_name not in hass.data[NOTIFY_SERVICES] + ): + return False + + return True + + +class BaseNotificationService: + """An abstract class for notification services.""" + + # While not purely typed, it makes typehinting more useful for us + # and removes the need for constant None checks or asserts. + hass: HomeAssistant = None # type: ignore + + # Name => target + registered_targets: dict[str, str] + + def send_message(self, message: str, **kwargs: Any) -> None: + """Send a message. + + kwargs can contain ATTR_TITLE to specify a title. + """ + raise NotImplementedError() + + async def async_send_message(self, message: str, **kwargs: Any) -> None: + """Send a message. + + kwargs can contain ATTR_TITLE to specify a title. + """ + await self.hass.async_add_executor_job( + partial(self.send_message, message, **kwargs) + ) + + async def _async_notify_message_service(self, service: ServiceCall) -> None: + """Handle sending notification message service calls.""" + kwargs = {} + message = service.data[ATTR_MESSAGE] + title = service.data.get(ATTR_TITLE) + + if title: + check_templates_warn(self.hass, title) + title.hass = self.hass + kwargs[ATTR_TITLE] = title.async_render(parse_result=False) + + if self.registered_targets.get(service.service) is not None: + kwargs[ATTR_TARGET] = [self.registered_targets[service.service]] + elif service.data.get(ATTR_TARGET) is not None: + kwargs[ATTR_TARGET] = service.data.get(ATTR_TARGET) + + check_templates_warn(self.hass, message) + message.hass = self.hass + kwargs[ATTR_MESSAGE] = message.async_render(parse_result=False) + kwargs[ATTR_DATA] = service.data.get(ATTR_DATA) + + await self.async_send_message(**kwargs) + + async def async_setup( + self, + hass: HomeAssistant, + service_name: str, + target_service_name_prefix: str, + ) -> None: + """Store the data for the notify service.""" + # pylint: disable=attribute-defined-outside-init + self.hass = hass + self._service_name = service_name + self._target_service_name_prefix = target_service_name_prefix + self.registered_targets = {} + + # Load service descriptions from notify/services.yaml + integration = await async_get_integration(hass, DOMAIN) + services_yaml = integration.file_path / "services.yaml" + self.services_dict = cast( + dict, await hass.async_add_executor_job(load_yaml, str(services_yaml)) + ) + + async def async_register_services(self) -> None: + """Create or update the notify services.""" + if hasattr(self, "targets"): + stale_targets = set(self.registered_targets) + + for name, target in self.targets.items(): # type: ignore + target_name = slugify(f"{self._target_service_name_prefix}_{name}") + if target_name in stale_targets: + stale_targets.remove(target_name) + if ( + target_name in self.registered_targets + and target == self.registered_targets[target_name] + ): + continue + self.registered_targets[target_name] = target + self.hass.services.async_register( + DOMAIN, + target_name, + self._async_notify_message_service, + schema=NOTIFY_SERVICE_SCHEMA, + ) + # Register the service description + service_desc = { + CONF_NAME: f"Send a notification via {target_name}", + CONF_DESCRIPTION: f"Sends a notification message using the {target_name} integration.", + CONF_FIELDS: self.services_dict[SERVICE_NOTIFY][CONF_FIELDS], + } + async_set_service_schema(self.hass, DOMAIN, target_name, service_desc) + + for stale_target_name in stale_targets: + del self.registered_targets[stale_target_name] + self.hass.services.async_remove( + DOMAIN, + stale_target_name, + ) + + if self.hass.services.has_service(DOMAIN, self._service_name): + return + + self.hass.services.async_register( + DOMAIN, + self._service_name, + self._async_notify_message_service, + schema=NOTIFY_SERVICE_SCHEMA, + ) + + # Register the service description + service_desc = { + CONF_NAME: f"Send a notification with {self._service_name}", + CONF_DESCRIPTION: f"Sends a notification message using the {self._service_name} service.", + CONF_FIELDS: self.services_dict[SERVICE_NOTIFY][CONF_FIELDS], + } + async_set_service_schema(self.hass, DOMAIN, self._service_name, service_desc) + + async def async_unregister_services(self) -> None: + """Unregister the notify services.""" + if self.registered_targets: + remove_targets = set(self.registered_targets) + for remove_target_name in remove_targets: + del self.registered_targets[remove_target_name] + self.hass.services.async_remove( + DOMAIN, + remove_target_name, + ) + + if not self.hass.services.has_service(DOMAIN, self._service_name): + return + + self.hass.services.async_remove( + DOMAIN, + self._service_name, + ) From 7cc924e83c3d6c890edf4a00327006f95549ca65 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 21 Oct 2021 13:45:16 -1000 Subject: [PATCH 0633/1038] Remove unreachable code in data_entry_flow.py (#58193) - bc1daf1802d2ef3ec6afe64882eb21ecf28baa49 removed the need for this guard --- homeassistant/data_entry_flow.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 791fd9d21c5..c82ec3acfd7 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -341,17 +341,11 @@ class FlowHandler: @property def source(self) -> str | None: """Source that initialized the flow.""" - if not hasattr(self, "context"): - return None - return self.context.get("source", None) @property def show_advanced_options(self) -> bool: """If we should show advanced options.""" - if not hasattr(self, "context"): - return False - return self.context.get("show_advanced_options", False) @callback From f9d985553caf3861042c303fa59c0647bbe7a7ac Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 22 Oct 2021 00:14:10 +0000 Subject: [PATCH 0634/1038] [ci skip] Translation update --- .../components/adax/translations/bg.json | 19 ++++++++++++ .../airvisual/translations/sensor.bg.json | 9 ++++++ .../components/ambee/translations/bg.json | 26 ++++++++++++++++ .../amberelectric/translations/bg.json | 1 + .../components/auth/translations/ja.json | 7 +++++ .../binary_sensor/translations/ja.json | 8 ++--- .../components/bosch_shc/translations/bg.json | 3 ++ .../cloudflare/translations/bg.json | 1 + .../components/co2signal/translations/bg.json | 30 ++++++++++++++++++ .../components/coinbase/translations/bg.json | 24 ++++++++++++++ .../components/deconz/translations/ja.json | 8 +++++ .../devolo_home_control/translations/bg.json | 3 +- .../components/dlna_dmr/translations/bg.json | 1 + .../components/dsmr/translations/bg.json | 28 +++++++++++++++-- .../forecast_solar/translations/bg.json | 14 +++++++++ .../freedompro/translations/bg.json | 10 +++++- .../components/fritz/translations/bg.json | 8 ++++- .../garages_amsterdam/translations/bg.json | 10 ++++++ .../components/goalzero/translations/bg.json | 4 ++- .../growatt_server/translations/bg.json | 11 +++++++ .../components/hangouts/translations/ja.json | 24 ++++++++++++++ .../homematicip_cloud/translations/ja.json | 12 ++++++- .../components/honeywell/translations/bg.json | 15 +++++++++ .../components/hue/translations/ja.json | 12 ++++++- .../translations/bg.json | 1 + .../keenetic_ndms2/translations/bg.json | 1 + .../components/kraken/translations/bg.json | 7 +++++ .../components/lookin/translations/bg.json | 30 ++++++++++++++++++ .../components/lookin/translations/ca.json | 31 +++++++++++++++++++ .../components/lookin/translations/de.json | 31 +++++++++++++++++++ .../components/lookin/translations/it.json | 31 +++++++++++++++++++ .../components/lookin/translations/ru.json | 31 +++++++++++++++++++ .../meteoclimatic/translations/bg.json | 16 ++++++++++ .../modern_forms/translations/bg.json | 20 ++++++++++++ .../motion_blinds/translations/bg.json | 3 ++ .../components/mqtt/translations/ja.json | 18 +++++++++++ .../components/nest/translations/ja.json | 21 +++++++++++++ .../components/netgear/translations/bg.json | 4 ++- .../components/nexia/translations/bg.json | 11 +++++++ .../nfandroidtv/translations/bg.json | 19 ++++++++++++ .../nmap_tracker/translations/bg.json | 17 ++++++++++ .../components/openuv/translations/bg.json | 7 +++++ .../components/openuv/translations/ja.json | 16 ++++++++++ .../components/samsungtv/translations/bg.json | 4 ++- .../components/shelly/translations/ca.json | 2 +- .../components/shelly/translations/de.json | 4 +-- .../components/shelly/translations/en.json | 4 +-- .../components/shelly/translations/it.json | 2 +- .../shelly/translations/zh-Hant.json | 2 +- .../components/sia/translations/bg.json | 15 ++++++++- .../simplisafe/translations/bg.json | 5 +++ .../components/sonos/translations/ja.json | 9 ++++++ .../components/sun/translations/ja.json | 2 +- .../switcher_kis/translations/bg.json | 13 ++++++++ .../synology_dsm/translations/bg.json | 8 +++++ .../totalconnect/translations/bg.json | 5 +++ .../tuya/translations/select.bg.json | 20 ++++++++++++ .../tuya/translations/select.ca.json | 22 +++++++++++++ .../tuya/translations/select.de.json | 22 +++++++++++++ .../tuya/translations/select.et.json | 22 +++++++++++++ .../tuya/translations/select.it.json | 22 +++++++++++++ .../tuya/translations/select.ru.json | 22 +++++++++++++ .../tuya/translations/select.zh-Hant.json | 22 +++++++++++++ .../components/wallbox/translations/bg.json | 22 +++++++++++++ .../xiaomi_miio/translations/bg.json | 21 ++++++++++++- .../yamaha_musiccast/translations/bg.json | 14 +++++++++ .../components/zha/translations/bg.json | 1 + .../components/zone/translations/ja.json | 14 +++++++-- .../components/zwave_js/translations/bg.json | 16 ++++++++++ .../components/zwave_js/translations/ja.json | 2 +- 70 files changed, 891 insertions(+), 29 deletions(-) create mode 100644 homeassistant/components/adax/translations/bg.json create mode 100644 homeassistant/components/airvisual/translations/sensor.bg.json create mode 100644 homeassistant/components/ambee/translations/bg.json create mode 100644 homeassistant/components/auth/translations/ja.json create mode 100644 homeassistant/components/bosch_shc/translations/bg.json create mode 100644 homeassistant/components/co2signal/translations/bg.json create mode 100644 homeassistant/components/coinbase/translations/bg.json create mode 100644 homeassistant/components/garages_amsterdam/translations/bg.json create mode 100644 homeassistant/components/growatt_server/translations/bg.json create mode 100644 homeassistant/components/hangouts/translations/ja.json create mode 100644 homeassistant/components/honeywell/translations/bg.json create mode 100644 homeassistant/components/kraken/translations/bg.json create mode 100644 homeassistant/components/lookin/translations/bg.json create mode 100644 homeassistant/components/lookin/translations/ca.json create mode 100644 homeassistant/components/lookin/translations/de.json create mode 100644 homeassistant/components/lookin/translations/it.json create mode 100644 homeassistant/components/lookin/translations/ru.json create mode 100644 homeassistant/components/meteoclimatic/translations/bg.json create mode 100644 homeassistant/components/modern_forms/translations/bg.json create mode 100644 homeassistant/components/mqtt/translations/ja.json create mode 100644 homeassistant/components/nest/translations/ja.json create mode 100644 homeassistant/components/nexia/translations/bg.json create mode 100644 homeassistant/components/nfandroidtv/translations/bg.json create mode 100644 homeassistant/components/nmap_tracker/translations/bg.json create mode 100644 homeassistant/components/openuv/translations/ja.json create mode 100644 homeassistant/components/sonos/translations/ja.json create mode 100644 homeassistant/components/switcher_kis/translations/bg.json create mode 100644 homeassistant/components/tuya/translations/select.bg.json create mode 100644 homeassistant/components/tuya/translations/select.ca.json create mode 100644 homeassistant/components/tuya/translations/select.de.json create mode 100644 homeassistant/components/tuya/translations/select.et.json create mode 100644 homeassistant/components/tuya/translations/select.it.json create mode 100644 homeassistant/components/tuya/translations/select.ru.json create mode 100644 homeassistant/components/tuya/translations/select.zh-Hant.json create mode 100644 homeassistant/components/wallbox/translations/bg.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/bg.json diff --git a/homeassistant/components/adax/translations/bg.json b/homeassistant/components/adax/translations/bg.json new file mode 100644 index 00000000000..329b8fd8399 --- /dev/null +++ b/homeassistant/components/adax/translations/bg.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "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" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.bg.json b/homeassistant/components/airvisual/translations/sensor.bg.json new file mode 100644 index 00000000000..311df560225 --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.bg.json @@ -0,0 +1,9 @@ +{ + "state": { + "airvisual__pollutant_label": { + "o3": "\u041e\u0437\u043e\u043d", + "p1": "PM10", + "p2": "PM2.5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/bg.json b/homeassistant/components/ambee/translations/bg.json new file mode 100644 index 00000000000..c72dc5227ca --- /dev/null +++ b/homeassistant/components/ambee/translations/bg.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "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\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_api_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + } + }, + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447", + "latitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0448\u0438\u0440\u0438\u043d\u0430", + "longitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0434\u044a\u043b\u0436\u0438\u043d\u0430", + "name": "\u0418\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/amberelectric/translations/bg.json b/homeassistant/components/amberelectric/translations/bg.json index 1e3ce9e6025..5cfcc05b133 100644 --- a/homeassistant/components/amberelectric/translations/bg.json +++ b/homeassistant/components/amberelectric/translations/bg.json @@ -5,6 +5,7 @@ "title": "Amber Electric" }, "user": { + "description": "\u041e\u0442\u0438\u0434\u0435\u0442\u0435 \u043d\u0430 {api_url}, \u0437\u0430 \u0434\u0430 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u0442\u0435 API \u043a\u043b\u044e\u0447", "title": "Amber Electric" } } diff --git a/homeassistant/components/auth/translations/ja.json b/homeassistant/components/auth/translations/ja.json new file mode 100644 index 00000000000..1ef902e6fe2 --- /dev/null +++ b/homeassistant/components/auth/translations/ja.json @@ -0,0 +1,7 @@ +{ + "mfa_setup": { + "totp": { + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/ja.json b/homeassistant/components/binary_sensor/translations/ja.json index 5434f8687bf..54280a5334a 100644 --- a/homeassistant/components/binary_sensor/translations/ja.json +++ b/homeassistant/components/binary_sensor/translations/ja.json @@ -17,12 +17,12 @@ "on": "\u63a5\u7d9a\u6e08" }, "door": { - "off": "\u9589\u9396", - "on": "\u958b\u653e" + "off": "\u9589", + "on": "\u958b" }, "garage_door": { - "off": "\u9589\u9396", - "on": "\u958b\u653e" + "off": "\u9589", + "on": "\u958b" }, "gas": { "off": "\u672a\u691c\u51fa", diff --git a/homeassistant/components/bosch_shc/translations/bg.json b/homeassistant/components/bosch_shc/translations/bg.json new file mode 100644 index 00000000000..80f917a9793 --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/bg.json @@ -0,0 +1,3 @@ +{ + "title": "Bosch SHC" +} \ No newline at end of file diff --git a/homeassistant/components/cloudflare/translations/bg.json b/homeassistant/components/cloudflare/translations/bg.json index 4716bbfb615..a34f51c1828 100644 --- a/homeassistant/components/cloudflare/translations/bg.json +++ b/homeassistant/components/cloudflare/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "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", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "error": { diff --git a/homeassistant/components/co2signal/translations/bg.json b/homeassistant/components/co2signal/translations/bg.json new file mode 100644 index 00000000000..bb253fb6e6b --- /dev/null +++ b/homeassistant/components/co2signal/translations/bg.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "coordinates": { + "data": { + "latitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0448\u0438\u0440\u0438\u043d\u0430", + "longitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0434\u044a\u043b\u0436\u0438\u043d\u0430" + } + }, + "country": { + "data": { + "country_code": "\u041a\u043e\u0434 \u043d\u0430 \u0434\u044a\u0440\u0436\u0430\u0432\u0430\u0442\u0430" + } + }, + "user": { + "data": { + "location": "\u041f\u043e\u043b\u0443\u0447\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u0434\u0430\u043d\u043d\u0438 \u0437\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/bg.json b/homeassistant/components/coinbase/translations/bg.json new file mode 100644 index 00000000000..6888f4ddf35 --- /dev/null +++ b/homeassistant/components/coinbase/translations/bg.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + } + } + } + }, + "options": { + "error": { + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/ja.json b/homeassistant/components/deconz/translations/ja.json index 240e04262e4..be03f3b2036 100644 --- a/homeassistant/components/deconz/translations/ja.json +++ b/homeassistant/components/deconz/translations/ja.json @@ -1,7 +1,15 @@ { "config": { + "abort": { + "already_configured": "\u30d6\u30ea\u30c3\u30b8\u306f\u3059\u3067\u306b\u69cb\u6210\u3055\u308c\u3066\u3044\u307e\u3059" + }, "error": { "no_key": "API\u30ad\u30fc\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" + }, + "step": { + "link": { + "title": "deCONZ\u3068\u30ea\u30f3\u30af\u3059\u308b" + } } } } \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/bg.json b/homeassistant/components/devolo_home_control/translations/bg.json index b3b4632e98d..d5f922c14ff 100644 --- a/homeassistant/components/devolo_home_control/translations/bg.json +++ b/homeassistant/components/devolo_home_control/translations/bg.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u041f\u0440\u043e\u0444\u0438\u043b\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + "already_configured": "\u041f\u0440\u043e\u0444\u0438\u043b\u044a\u0442 \u0432\u0435\u0447\u0435 \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" }, "step": { "user": { diff --git a/homeassistant/components/dlna_dmr/translations/bg.json b/homeassistant/components/dlna_dmr/translations/bg.json index 3c266fff82b..0d6f344a263 100644 --- a/homeassistant/components/dlna_dmr/translations/bg.json +++ b/homeassistant/components/dlna_dmr/translations/bg.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "could_not_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 DLNA \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", + "incomplete_config": "\u0412 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043b\u0438\u043f\u0441\u0432\u0430 \u0437\u0430\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u0430 \u043f\u0440\u043e\u043c\u0435\u043d\u043b\u0438\u0432\u0430", "non_unique_id": "\u041d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0441\u0430 \u043d\u044f\u043a\u043e\u043b\u043a\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u0435\u0434\u0438\u043d \u0438 \u0441\u044a\u0449 \u0443\u043d\u0438\u043a\u0430\u043b\u0435\u043d \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440" }, "error": { diff --git a/homeassistant/components/dsmr/translations/bg.json b/homeassistant/components/dsmr/translations/bg.json index 73e82d9f048..439b8d63d8d 100644 --- a/homeassistant/components/dsmr/translations/bg.json +++ b/homeassistant/components/dsmr/translations/bg.json @@ -2,16 +2,38 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "cannot_communicate": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430 \u043a\u043e\u043c\u0443\u043d\u0438\u043a\u0430\u0446\u0438\u044f" + "cannot_communicate": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430 \u043a\u043e\u043c\u0443\u043d\u0438\u043a\u0430\u0446\u0438\u044f", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "error": { - "cannot_communicate": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430 \u043a\u043e\u043c\u0443\u043d\u0438\u043a\u0430\u0446\u0438\u044f" + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "cannot_communicate": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430 \u043a\u043e\u043c\u0443\u043d\u0438\u043a\u0430\u0446\u0438\u044f", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "step": { "setup_network": { "data": { + "dsmr_version": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 DSMR \u0432\u0435\u0440\u0441\u0438\u044f", + "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" - } + }, + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "setup_serial": { + "data": { + "dsmr_version": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 DSMR \u0432\u0435\u0440\u0441\u0438\u044f", + "port": "\u0418\u0437\u0431\u043e\u0440 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "title": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "setup_serial_manual_path": { + "title": "\u041f\u044a\u0442" + }, + "user": { + "data": { + "type": "\u0422\u0438\u043f \u0432\u0440\u044a\u0437\u043a\u0430" + }, + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u043d\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" } } } diff --git a/homeassistant/components/forecast_solar/translations/bg.json b/homeassistant/components/forecast_solar/translations/bg.json index 35cfa0ad1d7..289146783a4 100644 --- a/homeassistant/components/forecast_solar/translations/bg.json +++ b/homeassistant/components/forecast_solar/translations/bg.json @@ -3,9 +3,23 @@ "step": { "user": { "data": { + "azimuth": "\u0410\u0437\u0438\u043c\u0443\u0442 (360 \u0433\u0440\u0430\u0434\u0443\u0441\u0430, 0 = \u0421\u0435\u0432\u0435\u0440, 90 = \u0418\u0437\u0442\u043e\u043a, 180 = \u042e\u0433, 270 = \u0417\u0430\u043f\u0430\u0434)", + "declination": "\u0414\u0435\u043a\u043b\u0438\u043d\u0430\u0446\u0438\u044f (0 = \u0445\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u043d\u043e, 90 = \u0432\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u043d\u043e)", + "latitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0448\u0438\u0440\u0438\u043d\u0430", + "longitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0434\u044a\u043b\u0436\u0438\u043d\u0430", "name": "\u0418\u043c\u0435" } } } + }, + "options": { + "step": { + "init": { + "data": { + "azimuth": "\u0410\u0437\u0438\u043c\u0443\u0442 (360 \u0433\u0440\u0430\u0434\u0443\u0441\u0430, 0 = \u0421\u0435\u0432\u0435\u0440, 90 = \u0418\u0437\u0442\u043e\u043a, 180 = \u042e\u0433, 270 = \u0417\u0430\u043f\u0430\u0434)", + "declination": "\u0414\u0435\u043a\u043b\u0438\u043d\u0430\u0446\u0438\u044f (0 = \u0445\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u043d\u043e, 90 = \u0432\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u043d\u043e)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/freedompro/translations/bg.json b/homeassistant/components/freedompro/translations/bg.json index fdbdc5b1cdf..1e5b299d96b 100644 --- a/homeassistant/components/freedompro/translations/bg.json +++ b/homeassistant/components/freedompro/translations/bg.json @@ -1,10 +1,18 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "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" + }, "step": { "user": { "data": { "api_key": "API \u043a\u043b\u044e\u0447" - } + }, + "title": "Freedompro API \u043a\u043b\u044e\u0447" } } } diff --git a/homeassistant/components/fritz/translations/bg.json b/homeassistant/components/fritz/translations/bg.json index 5502d41d8a2..b1ea395f077 100644 --- a/homeassistant/components/fritz/translations/bg.json +++ b/homeassistant/components/fritz/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "start_config": { "data": { @@ -8,7 +11,10 @@ }, "user": { "data": { - "port": "\u041f\u043e\u0440\u0442" + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } } } diff --git a/homeassistant/components/garages_amsterdam/translations/bg.json b/homeassistant/components/garages_amsterdam/translations/bg.json new file mode 100644 index 00000000000..3348117ce6b --- /dev/null +++ b/homeassistant/components/garages_amsterdam/translations/bg.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + } + }, + "title": "Garages Amsterdam" +} \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/bg.json b/homeassistant/components/goalzero/translations/bg.json index 7a50cb7d9cc..2461fa173ae 100644 --- a/homeassistant/components/goalzero/translations/bg.json +++ b/homeassistant/components/goalzero/translations/bg.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "invalid_host": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0438\u043c\u0435 \u043d\u0430 \u0445\u043e\u0441\u0442 \u0438\u043b\u0438 IP \u0430\u0434\u0440\u0435\u0441", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/growatt_server/translations/bg.json b/homeassistant/components/growatt_server/translations/bg.json new file mode 100644 index 00000000000..02c83a6e916 --- /dev/null +++ b/homeassistant/components/growatt_server/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/ja.json b/homeassistant/components/hangouts/translations/ja.json new file mode 100644 index 00000000000..751e1ae41a1 --- /dev/null +++ b/homeassistant/components/hangouts/translations/ja.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "invalid_login": "\u30ed\u30b0\u30a4\u30f3\u304c\u7121\u52b9\u3067\u3059\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002" + }, + "step": { + "2fa": { + "data": { + "2fa": "2\u8981\u7d20 PIN" + }, + "description": "\u7a7a", + "title": "2\u8981\u7d20\u8a8d\u8a3c" + }, + "user": { + "data": { + "email": "Email", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "\u7a7a", + "title": "Google \u30cf\u30f3\u30b0\u30a2\u30a6\u30c8 \u30ed\u30b0\u30a4\u30f3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/translations/ja.json b/homeassistant/components/homematicip_cloud/translations/ja.json index b26b247a66c..5b5d0d62ab9 100644 --- a/homeassistant/components/homematicip_cloud/translations/ja.json +++ b/homeassistant/components/homematicip_cloud/translations/ja.json @@ -6,7 +6,17 @@ }, "error": { "invalid_sgtin_or_pin": "PIN\u304c\u7121\u52b9\u3067\u3059\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", - "press_the_button": "\u9752\u3044\u30dc\u30bf\u30f3\u3092\u62bc\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "press_the_button": "\u9752\u3044\u30dc\u30bf\u30f3\u3092\u62bc\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "register_failed": "\u767b\u9332\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", + "timeout_button": "\u9752\u3044\u30dc\u30bf\u30f3\u3092\u62bc\u3059\u3068\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3059\u3002\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002" + }, + "step": { + "init": { + "data": { + "hapid": "\u30a2\u30af\u30bb\u30b9\u30dd\u30a4\u30f3\u30c8ID (SGTIN)", + "pin": "PIN\u30b3\u30fc\u30c9" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/honeywell/translations/bg.json b/homeassistant/components/honeywell/translations/bg.json new file mode 100644 index 00000000000..e7020268311 --- /dev/null +++ b/homeassistant/components/honeywell/translations/bg.json @@ -0,0 +1,15 @@ +{ + "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": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/translations/ja.json b/homeassistant/components/hue/translations/ja.json index 6c4290140dc..0ce555c1f29 100644 --- a/homeassistant/components/hue/translations/ja.json +++ b/homeassistant/components/hue/translations/ja.json @@ -1,13 +1,23 @@ { "config": { "abort": { + "all_configured": "\u3059\u3079\u3066\u306e\u3001Philips Hue\u30d6\u30ea\u30c3\u30b8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "discover_timeout": "Hue\u30d6\u30ea\u30c3\u30b8\u3092\u767a\u898b(\u63a2\u308a\u5f53\u3066)\u3067\u304d\u307e\u305b\u3093", + "no_bridges": "Philips Hue\u30d6\u30ea\u30c3\u30b8\u306f\u767a\u898b\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f", "unknown": "\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f" }, + "error": { + "register_failed": "\u767b\u9332\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3001\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044" + }, "step": { "init": { "data": { "host": "\u30db\u30b9\u30c8" - } + }, + "title": "Philips Hue\u30d6\u30ea\u30c3\u30b8\u3092\u30d4\u30c3\u30af\u30a2\u30c3\u30d7" + }, + "link": { + "title": "\u30ea\u30f3\u30af\u30cf\u30d6" } } } diff --git a/homeassistant/components/hunterdouglas_powerview/translations/bg.json b/homeassistant/components/hunterdouglas_powerview/translations/bg.json index 4baee58f6bd..17ef87d7d8b 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/bg.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/bg.json @@ -7,6 +7,7 @@ "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/keenetic_ndms2/translations/bg.json b/homeassistant/components/keenetic_ndms2/translations/bg.json index fc2115d9ca0..3bebf2d185e 100644 --- a/homeassistant/components/keenetic_ndms2/translations/bg.json +++ b/homeassistant/components/keenetic_ndms2/translations/bg.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/kraken/translations/bg.json b/homeassistant/components/kraken/translations/bg.json new file mode 100644 index 00000000000..3ac8e39cb8d --- /dev/null +++ b/homeassistant/components/kraken/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lookin/translations/bg.json b/homeassistant/components/lookin/translations/bg.json new file mode 100644 index 00000000000..a87e472c794 --- /dev/null +++ b/homeassistant/components/lookin/translations/bg.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "flow_title": "{name} ({host})", + "step": { + "device_name": { + "data": { + "name": "\u0418\u043c\u0435" + } + }, + "discovery_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name} ({host})?" + }, + "user": { + "data": { + "ip_address": "IP \u0430\u0434\u0440\u0435\u0441" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lookin/translations/ca.json b/homeassistant/components/lookin/translations/ca.json new file mode 100644 index 00000000000..acf26bf6a00 --- /dev/null +++ b/homeassistant/components/lookin/translations/ca.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "cannot_connect": "Ha fallat la connexi\u00f3", + "no_devices_found": "No s'han trobat dispositius a la xarxa" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "no_devices_found": "No s'han trobat dispositius a la xarxa", + "unknown": "Error inesperat" + }, + "flow_title": "{name} ({host})", + "step": { + "device_name": { + "data": { + "name": "Nom" + } + }, + "discovery_confirm": { + "description": "Vols configurar {name} ({host})?" + }, + "user": { + "data": { + "ip_address": "Adre\u00e7a IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lookin/translations/de.json b/homeassistant/components/lookin/translations/de.json new file mode 100644 index 00000000000..866b7bda840 --- /dev/null +++ b/homeassistant/components/lookin/translations/de.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "cannot_connect": "Verbindung fehlgeschlagen", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "unknown": "Unerwarteter Fehler" + }, + "flow_title": "{name} ({host})", + "step": { + "device_name": { + "data": { + "name": "Name" + } + }, + "discovery_confirm": { + "description": "M\u00f6chtest du {name} ({host}) einrichten?" + }, + "user": { + "data": { + "ip_address": "IP-Adresse" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lookin/translations/it.json b/homeassistant/components/lookin/translations/it.json new file mode 100644 index 00000000000..4286e1a45c2 --- /dev/null +++ b/homeassistant/components/lookin/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", + "cannot_connect": "Impossibile connettersi", + "no_devices_found": "Nessun dispositivo trovato sulla rete" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "no_devices_found": "Nessun dispositivo trovato sulla rete", + "unknown": "Errore imprevisto" + }, + "flow_title": "{name} ({host})", + "step": { + "device_name": { + "data": { + "name": "Nome" + } + }, + "discovery_confirm": { + "description": "Vuoi configurare {name} ({host})?" + }, + "user": { + "data": { + "ip_address": "Indirizzo IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lookin/translations/ru.json b/homeassistant/components/lookin/translations/ru.json new file mode 100644 index 00000000000..756dd25475a --- /dev/null +++ b/homeassistant/components/lookin/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.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\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." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "{name} ({host})", + "step": { + "device_name": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + } + }, + "discovery_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} ({host})?" + }, + "user": { + "data": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteoclimatic/translations/bg.json b/homeassistant/components/meteoclimatic/translations/bg.json new file mode 100644 index 00000000000..63b0e7be8b7 --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/bg.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "error": { + "not_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430" + }, + "step": { + "user": { + "title": "Meteoclimatic" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/bg.json b/homeassistant/components/modern_forms/translations/bg.json new file mode 100644 index 00000000000..a6e2f383b1a --- /dev/null +++ b/homeassistant/components/modern_forms/translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + }, + "title": "Modern Forms" +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/bg.json b/homeassistant/components/motion_blinds/translations/bg.json index 53a8814ba2d..dad41cb1999 100644 --- a/homeassistant/components/motion_blinds/translations/bg.json +++ b/homeassistant/components/motion_blinds/translations/bg.json @@ -4,6 +4,9 @@ "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "connection_error": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, + "error": { + "invalid_interface": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u043c\u0440\u0435\u0436\u043e\u0432 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441" + }, "step": { "connect": { "data": { diff --git a/homeassistant/components/mqtt/translations/ja.json b/homeassistant/components/mqtt/translations/ja.json new file mode 100644 index 00000000000..0ec3c953a00 --- /dev/null +++ b/homeassistant/components/mqtt/translations/ja.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "broker": { + "data": { + "broker": "\u30d6\u30ed\u30fc\u30ab\u30fc", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8" + } + }, + "hassio_confirm": { + "data": { + "discovery": "\u691c\u51fa\u3092\u6709\u52b9\u306b\u3059\u308b" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/ja.json b/homeassistant/components/nest/translations/ja.json new file mode 100644 index 00000000000..bb80db0af5e --- /dev/null +++ b/homeassistant/components/nest/translations/ja.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "internal_error": "\u30b3\u30fc\u30c9\u306e\u691c\u8a3c\u4e2d\u306b\u5185\u90e8\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f", + "timeout": "\u30b3\u30fc\u30c9\u306e\u691c\u8a3c\u3092\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3059" + }, + "step": { + "init": { + "data": { + "flow_impl": "\u30d7\u30ed\u30d0\u30a4\u30c0\u30fc" + }, + "title": "\u8a8d\u8a3c\u30d7\u30ed\u30d0\u30a4\u30c0\u30fc" + }, + "link": { + "data": { + "code": "PIN\u30b3\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netgear/translations/bg.json b/homeassistant/components/netgear/translations/bg.json index d90e23ebf92..5ab9a57dd27 100644 --- a/homeassistant/components/netgear/translations/bg.json +++ b/homeassistant/components/netgear/translations/bg.json @@ -6,8 +6,10 @@ "step": { "user": { "data": { + "host": "\u0425\u043e\u0441\u0442 (\u043f\u043e \u0438\u0437\u0431\u043e\u0440)", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", - "port": "\u041f\u043e\u0440\u0442 (\u043f\u043e \u0438\u0437\u0431\u043e\u0440)" + "port": "\u041f\u043e\u0440\u0442 (\u043f\u043e \u0438\u0437\u0431\u043e\u0440)", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 (\u043f\u043e \u0438\u0437\u0431\u043e\u0440)" }, "title": "Netgear" } diff --git a/homeassistant/components/nexia/translations/bg.json b/homeassistant/components/nexia/translations/bg.json new file mode 100644 index 00000000000..78264e2adbd --- /dev/null +++ b/homeassistant/components/nexia/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "brand": "\u041c\u0430\u0440\u043a\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/translations/bg.json b/homeassistant/components/nfandroidtv/translations/bg.json new file mode 100644 index 00000000000..78978419e43 --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/bg.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u0418\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/bg.json b/homeassistant/components/nmap_tracker/translations/bg.json new file mode 100644 index 00000000000..1ac82f0ab95 --- /dev/null +++ b/homeassistant/components/nmap_tracker/translations/bg.json @@ -0,0 +1,17 @@ +{ + "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" + } + }, + "options": { + "step": { + "init": { + "data": { + "interval_seconds": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043d\u0430 \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u043d\u0435" + } + } + } + }, + "title": "Nmap Tracker" +} \ No newline at end of file diff --git a/homeassistant/components/openuv/translations/bg.json b/homeassistant/components/openuv/translations/bg.json index 59cbf3d1c55..9541f00f7f4 100644 --- a/homeassistant/components/openuv/translations/bg.json +++ b/homeassistant/components/openuv/translations/bg.json @@ -14,5 +14,12 @@ "title": "\u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f\u0442\u0430 \u0441\u0438" } } + }, + "options": { + "step": { + "init": { + "title": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 OpenUV" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/openuv/translations/ja.json b/homeassistant/components/openuv/translations/ja.json new file mode 100644 index 00000000000..db717442b5e --- /dev/null +++ b/homeassistant/components/openuv/translations/ja.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc" + }, + "step": { + "user": { + "data": { + "api_key": "API\u30ad\u30fc", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d4c\u5ea6" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/bg.json b/homeassistant/components/samsungtv/translations/bg.json index a8133e96d15..1432000c6b9 100644 --- a/homeassistant/components/samsungtv/translations/bg.json +++ b/homeassistant/components/samsungtv/translations/bg.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "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", + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "flow_title": "{device}" } diff --git a/homeassistant/components/shelly/translations/ca.json b/homeassistant/components/shelly/translations/ca.json index c485d955ff2..6430bdcf6a6 100644 --- a/homeassistant/components/shelly/translations/ca.json +++ b/homeassistant/components/shelly/translations/ca.json @@ -42,7 +42,7 @@ "double": "{subtype} clicat dues vegades", "double_push": "{subtype} clicat dues vegades", "long": "{subtype} clicat durant una estona", - "long_push": "{subtype} clicat durant una estona", + "long_push": "{subtype} premut durant una estona", "long_single": "{subtype} clicat durant una estona i despr\u00e9s r\u00e0pid", "single": "{subtype} clicat una vegada", "single_long": "{subtype} clicat r\u00e0pid i, despr\u00e9s, durant una estona", diff --git a/homeassistant/components/shelly/translations/de.json b/homeassistant/components/shelly/translations/de.json index 3a943507284..75487b5047a 100644 --- a/homeassistant/components/shelly/translations/de.json +++ b/homeassistant/components/shelly/translations/de.json @@ -41,8 +41,8 @@ "btn_up": "{subtype} Taste nach oben", "double": "{subtype} zweifach bet\u00e4tigt", "double_push": "{subtype} Doppelter Push", - "long": "{subtype} gehalten", - "long_push": "{subtype} langer Push", + "long": "{subtype} lange angeklickt", + "long_push": "{subtype} langer Druck", "long_single": "{subtype} gehalten und dann einfach bet\u00e4tigt", "single": "{subtype} einfach bet\u00e4tigt", "single_long": "{subtype} einfach bet\u00e4tigt und dann gehalten", diff --git a/homeassistant/components/shelly/translations/en.json b/homeassistant/components/shelly/translations/en.json index b48eb630024..c23eb13840c 100644 --- a/homeassistant/components/shelly/translations/en.json +++ b/homeassistant/components/shelly/translations/en.json @@ -41,8 +41,8 @@ "btn_up": "{subtype} button up", "double": "{subtype} double clicked", "double_push": "{subtype} double push", - "long": " {subtype} long clicked", - "long_push": " {subtype} long push", + "long": "{subtype} long clicked", + "long_push": "{subtype} long push", "long_single": "{subtype} long clicked and then single clicked", "single": "{subtype} single clicked", "single_long": "{subtype} single clicked and then long clicked", diff --git a/homeassistant/components/shelly/translations/it.json b/homeassistant/components/shelly/translations/it.json index c004141cac4..ec20d3a7b26 100644 --- a/homeassistant/components/shelly/translations/it.json +++ b/homeassistant/components/shelly/translations/it.json @@ -41,7 +41,7 @@ "btn_up": "{subtype} pulsante in su", "double": "{subtype} premuto due volte", "double_push": "{subtype} doppia pressione", - "long": "{subtype} premuto a lungo", + "long": "{subtype} cliccato a lungo", "long_push": "{subtype} pressione prolungata", "long_single": "{subtype} premuto a lungo e poi singolarmente", "single": "{subtype} premuto singolarmente", diff --git a/homeassistant/components/shelly/translations/zh-Hant.json b/homeassistant/components/shelly/translations/zh-Hant.json index bc746ccac2a..62d1bd18850 100644 --- a/homeassistant/components/shelly/translations/zh-Hant.json +++ b/homeassistant/components/shelly/translations/zh-Hant.json @@ -41,7 +41,7 @@ "btn_up": "\"{subtype}\" \u6309\u9215\u91cb\u653e", "double": "{subtype} \u96d9\u64ca", "double_push": "{subtype} \u96d9\u6309", - "long": "{subtype} \u9577\u6309", + "long": "{subtype} \u9577\u9ede\u64ca", "long_push": "{subtype} \u9577\u6309", "long_single": "{subtype} \u9577\u6309\u5f8c\u55ae\u64ca", "single": "{subtype} \u55ae\u64ca", diff --git a/homeassistant/components/sia/translations/bg.json b/homeassistant/components/sia/translations/bg.json index 4983c9a14b2..20d8511d195 100644 --- a/homeassistant/components/sia/translations/bg.json +++ b/homeassistant/components/sia/translations/bg.json @@ -1,9 +1,22 @@ { "config": { + "error": { + "invalid_zones": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0435 \u0434\u0430 \u0438\u043c\u0430 \u043f\u043e\u043d\u0435 1 \u0437\u043e\u043d\u0430.", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, "step": { + "additional_account": { + "data": { + "additional_account": "\u0414\u043e\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b\u043d\u0438 \u0430\u043a\u0430\u0443\u043d\u0442\u0438", + "encryption_key": "\u041a\u043b\u044e\u0447 \u0437\u0430 \u043a\u0440\u0438\u043f\u0442\u0438\u0440\u0430\u043d\u0435" + } + }, "user": { "data": { - "port": "\u041f\u043e\u0440\u0442" + "additional_account": "\u0414\u043e\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b\u043d\u0438 \u0430\u043a\u0430\u0443\u043d\u0442\u0438", + "encryption_key": "\u041a\u043b\u044e\u0447 \u0437\u0430 \u043a\u0440\u0438\u043f\u0442\u0438\u0440\u0430\u043d\u0435", + "port": "\u041f\u043e\u0440\u0442", + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b" } } } diff --git a/homeassistant/components/simplisafe/translations/bg.json b/homeassistant/components/simplisafe/translations/bg.json index 8d93bacf69b..4013449a082 100644 --- a/homeassistant/components/simplisafe/translations/bg.json +++ b/homeassistant/components/simplisafe/translations/bg.json @@ -5,6 +5,11 @@ "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": { + "input_auth_code": { + "data": { + "auth_code": "\u041a\u043e\u0434 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f" + } + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", diff --git a/homeassistant/components/sonos/translations/ja.json b/homeassistant/components/sonos/translations/ja.json new file mode 100644 index 00000000000..7867cbcbf98 --- /dev/null +++ b/homeassistant/components/sonos/translations/ja.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "confirm": { + "description": "Sonos\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sun/translations/ja.json b/homeassistant/components/sun/translations/ja.json index 579f1b5817f..758beba9dbf 100644 --- a/homeassistant/components/sun/translations/ja.json +++ b/homeassistant/components/sun/translations/ja.json @@ -1,7 +1,7 @@ { "state": { "_": { - "above_horizon": "\u5730\u5e73\u7dda\u306e\u4e0a", + "above_horizon": "\u5730\u5e73\u7dda\u3088\u308a\u4e0a", "below_horizon": "\u5730\u5e73\u7dda\u3088\u308a\u4e0b" } }, diff --git a/homeassistant/components/switcher_kis/translations/bg.json b/homeassistant/components/switcher_kis/translations/bg.json new file mode 100644 index 00000000000..e7ed81d36f5 --- /dev/null +++ b/homeassistant/components/switcher_kis/translations/bg.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, + "step": { + "confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0442\u0430?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/bg.json b/homeassistant/components/synology_dsm/translations/bg.json index c750e92869c..46e2b6d88f4 100644 --- a/homeassistant/components/synology_dsm/translations/bg.json +++ b/homeassistant/components/synology_dsm/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "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", "reconfigure_successful": "\u041f\u0440\u0435\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { @@ -19,6 +20,13 @@ "port": "\u041f\u043e\u0440\u0442" } }, + "reauth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + }, + "description": "\u041f\u0440\u0438\u0447\u0438\u043d\u0430: {details}" + }, "reauth_confirm": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", diff --git a/homeassistant/components/totalconnect/translations/bg.json b/homeassistant/components/totalconnect/translations/bg.json index 0f1f04fa894..1858bd74b7b 100644 --- a/homeassistant/components/totalconnect/translations/bg.json +++ b/homeassistant/components/totalconnect/translations/bg.json @@ -4,6 +4,11 @@ "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" }, "step": { + "locations": { + "data": { + "usercode": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438 \u043a\u043e\u0434" + } + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", diff --git a/homeassistant/components/tuya/translations/select.bg.json b/homeassistant/components/tuya/translations/select.bg.json new file mode 100644 index 00000000000..0d9652389ea --- /dev/null +++ b/homeassistant/components/tuya/translations/select.bg.json @@ -0,0 +1,20 @@ +{ + "state": { + "tuya__led_type": { + "halogen": "\u0425\u0430\u043b\u043e\u0433\u0435\u043d\u043d\u0438", + "incandescent": "\u0421 \u043d\u0430\u0436\u0435\u0436\u0430\u0435\u043c\u0430 \u0436\u0438\u0447\u043a\u0430", + "led": "LED" + }, + "tuya__light_mode": { + "none": "\u0418\u0437\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + }, + "tuya__relay_status": { + "last": "\u0417\u0430\u043f\u043e\u043c\u043d\u044f\u043d\u0435 \u043d\u0430 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u043e\u0442\u043e \u0441\u044a\u0441\u0442\u043e\u044f\u043d\u0438\u0435", + "memory": "\u0417\u0430\u043f\u043e\u043c\u043d\u044f\u043d\u0435 \u043d\u0430 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u043e\u0442\u043e \u0441\u044a\u0441\u0442\u043e\u044f\u043d\u0438\u0435", + "off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "power_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "power_on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.ca.json b/homeassistant/components/tuya/translations/select.ca.json new file mode 100644 index 00000000000..c5eec67dd3a --- /dev/null +++ b/homeassistant/components/tuya/translations/select.ca.json @@ -0,0 +1,22 @@ +{ + "state": { + "tuya__led_type": { + "halogen": "Halogen", + "incandescent": "Incandescent", + "led": "LED" + }, + "tuya__light_mode": { + "none": "off", + "pos": "Indica la ubicaci\u00f3 de l'interruptor", + "relay": "Indiqueu l'estat, activat/desactivat" + }, + "tuya__relay_status": { + "last": "Recorda l'\u00faltim estat", + "memory": "Recorda l'\u00faltim estat", + "off": "off", + "on": "on", + "power_off": "off", + "power_on": "on" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.de.json b/homeassistant/components/tuya/translations/select.de.json new file mode 100644 index 00000000000..bebed009b54 --- /dev/null +++ b/homeassistant/components/tuya/translations/select.de.json @@ -0,0 +1,22 @@ +{ + "state": { + "tuya__led_type": { + "halogen": "Halogen", + "incandescent": "Gl\u00fchlampe", + "led": "LED" + }, + "tuya__light_mode": { + "none": "Aus", + "pos": "Schalterposition anzeigen", + "relay": "Ein-/Ausschaltzustand anzeigen" + }, + "tuya__relay_status": { + "last": "Letzten Zustand merken", + "memory": "Letzten Zustand merken", + "off": "Aus", + "on": "An", + "power_off": "Aus", + "power_on": "An" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.et.json b/homeassistant/components/tuya/translations/select.et.json new file mode 100644 index 00000000000..d2556dc8277 --- /dev/null +++ b/homeassistant/components/tuya/translations/select.et.json @@ -0,0 +1,22 @@ +{ + "state": { + "tuya__led_type": { + "halogen": "Halogeenlamp", + "incandescent": "H\u00f5\u00f5glamp", + "led": "LED" + }, + "tuya__light_mode": { + "none": "V\u00e4ljas", + "pos": "Kuva l\u00fcliti olekut", + "relay": "Kuva l\u00fcliti sees/v\u00e4ljas olekut" + }, + "tuya__relay_status": { + "last": "J\u00e4ta viimane olek meelde", + "memory": "J\u00e4ta viimane olek meelde", + "off": "V\u00e4ljas", + "on": "Sees", + "power_off": "V\u00e4ljas", + "power_on": "Sees" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.it.json b/homeassistant/components/tuya/translations/select.it.json new file mode 100644 index 00000000000..a7bed12090c --- /dev/null +++ b/homeassistant/components/tuya/translations/select.it.json @@ -0,0 +1,22 @@ +{ + "state": { + "tuya__led_type": { + "halogen": "Alogena", + "incandescent": "Incandescenza", + "led": "LED" + }, + "tuya__light_mode": { + "none": "Spento", + "pos": "Indica la posizione dell'interruttore", + "relay": "Indica lo stato di accensione/spegnimento dell'interruttore" + }, + "tuya__relay_status": { + "last": "Ricorda l'ultimo stato", + "memory": "Ricorda l'ultimo stato", + "off": "Spento", + "on": "Acceso", + "power_off": "Spento", + "power_on": "Acceso" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.ru.json b/homeassistant/components/tuya/translations/select.ru.json new file mode 100644 index 00000000000..b2a31d128d6 --- /dev/null +++ b/homeassistant/components/tuya/translations/select.ru.json @@ -0,0 +1,22 @@ +{ + "state": { + "tuya__led_type": { + "halogen": "\u0413\u0430\u043b\u043e\u0433\u0435\u043d", + "incandescent": "\u041b\u0430\u043c\u043f\u0430 \u043d\u0430\u043a\u0430\u043b\u0438\u0432\u0430\u043d\u0438\u044f", + "led": "\u0421\u0432\u0435\u0442\u043e\u0434\u0438\u043e\u0434" + }, + "tuya__light_mode": { + "none": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "pos": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044f", + "relay": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f/\u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" + }, + "tuya__relay_status": { + "last": "\u0417\u0430\u043f\u043e\u043c\u043d\u0438\u0442\u044c \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435", + "memory": "\u0417\u0430\u043f\u043e\u043c\u043d\u0438\u0442\u044c \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435", + "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "power_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "power_on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.zh-Hant.json b/homeassistant/components/tuya/translations/select.zh-Hant.json new file mode 100644 index 00000000000..142619d255f --- /dev/null +++ b/homeassistant/components/tuya/translations/select.zh-Hant.json @@ -0,0 +1,22 @@ +{ + "state": { + "tuya__led_type": { + "halogen": "\u9e75\u7d20\u71c8", + "incandescent": "\u767d\u71be\u71c8", + "led": "LED" + }, + "tuya__light_mode": { + "none": "\u95dc\u9589", + "pos": "\u6307\u793a\u958b\u95dc\u4f4d\u7f6e", + "relay": "\u6307\u793a\u958b\u95dc\u958b\u555f/\u95dc\u9589\u72c0\u614b" + }, + "tuya__relay_status": { + "last": "\u8a18\u4f4f\u6700\u5f8c\u72c0\u614b", + "memory": "\u8a18\u4f4f\u6700\u5f8c\u72c0\u614b", + "off": "\u95dc\u9589", + "on": "\u958b\u555f", + "power_off": "\u95dc\u9589", + "power_on": "\u958b\u555f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/bg.json b/homeassistant/components/wallbox/translations/bg.json new file mode 100644 index 00000000000..648be54571d --- /dev/null +++ b/homeassistant/components/wallbox/translations/bg.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "station": "\u0421\u0435\u0440\u0438\u0435\u043d \u043d\u043e\u043c\u0435\u0440 \u043d\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f\u0442\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + }, + "title": "Wallbox" +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/bg.json b/homeassistant/components/xiaomi_miio/translations/bg.json index 3721d0a27c1..b692bd4bb9f 100644 --- a/homeassistant/components/xiaomi_miio/translations/bg.json +++ b/homeassistant/components/xiaomi_miio/translations/bg.json @@ -1,13 +1,24 @@ { "config": { "abort": { - "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "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\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "no_device_selected": "\u041d\u0435 \u0435 \u0438\u0437\u0431\u0440\u0430\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u043c\u043e\u043b\u044f, \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0435\u0434\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e." }, "step": { + "cloud": { + "data": { + "manual": "\u0420\u044a\u0447\u043d\u043e \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 (\u043d\u0435 \u0441\u0435 \u043f\u0440\u0435\u043f\u043e\u0440\u044a\u0447\u0432\u0430)" + } + }, + "connect": { + "data": { + "model": "\u041c\u043e\u0434\u0435\u043b \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e" + } + }, "device": { "data": { "host": "IP \u0430\u0434\u0440\u0435\u0441" @@ -18,6 +29,14 @@ "host": "IP \u0430\u0434\u0440\u0435\u0441", "name": "\u0418\u043c\u0435 \u043d\u0430 \u0448\u043b\u044e\u0437\u0430" } + }, + "manual": { + "data": { + "host": "IP \u0430\u0434\u0440\u0435\u0441" + } + }, + "reauth_confirm": { + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" } } } diff --git a/homeassistant/components/yamaha_musiccast/translations/bg.json b/homeassistant/components/yamaha_musiccast/translations/bg.json new file mode 100644 index 00000000000..6814831ecff --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/bg.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/bg.json b/homeassistant/components/zha/translations/bg.json index feed380b03a..6196428fca6 100644 --- a/homeassistant/components/zha/translations/bg.json +++ b/homeassistant/components/zha/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "not_zha_device": "\u0422\u043e\u0432\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0435 zha \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", "single_instance_allowed": "\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 ZHA." }, "error": { diff --git a/homeassistant/components/zone/translations/ja.json b/homeassistant/components/zone/translations/ja.json index 093f5ad9938..7461836070b 100644 --- a/homeassistant/components/zone/translations/ja.json +++ b/homeassistant/components/zone/translations/ja.json @@ -1,13 +1,21 @@ { "config": { + "error": { + "name_exists": "\u540d\u524d\u306f\u3059\u3067\u306b\u5b58\u5728\u3057\u307e\u3059" + }, "step": { "init": { "data": { + "icon": "\u30a2\u30a4\u30b3\u30f3", "latitude": "\u7def\u5ea6", "longitude": "\u7d4c\u5ea6", - "name": "\u540d\u524d" - } + "name": "\u540d\u524d", + "passive": "\u30d1\u30c3\u30b7\u30d6", + "radius": "\u534a\u5f84" + }, + "title": "\u30be\u30fc\u30f3\u30d1\u30e9\u30e1\u30fc\u30bf\u3092\u5b9a\u7fa9\u3059\u308b" } - } + }, + "title": "\u30be\u30fc\u30f3" } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/bg.json b/homeassistant/components/zwave_js/translations/bg.json index 222c9fd34a1..afde8dc1336 100644 --- a/homeassistant/components/zwave_js/translations/bg.json +++ b/homeassistant/components/zwave_js/translations/bg.json @@ -21,6 +21,14 @@ } }, "options": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, "step": { "configure_addon": { "data": { @@ -29,6 +37,14 @@ "s2_authenticated_key": "S2 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u0435\u043d \u043a\u043b\u044e\u0447", "s2_unauthenticated_key": "S2 \u043d\u0435\u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u0435\u043d \u043a\u043b\u044e\u0447" } + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u043d\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" } } } diff --git a/homeassistant/components/zwave_js/translations/ja.json b/homeassistant/components/zwave_js/translations/ja.json index f7f882aa078..78714cd5a7c 100644 --- a/homeassistant/components/zwave_js/translations/ja.json +++ b/homeassistant/components/zwave_js/translations/ja.json @@ -19,7 +19,7 @@ "title": "\u63a5\u7d9a\u65b9\u6cd5\u306e\u9078\u629e" }, "start_addon": { - "title": "Z-Wave JS\u306e\u30a2\u30c9\u30aa\u30f3\u304c\u59cb\u307e\u308a\u307e\u3059\u3002" + "title": "Z-Wave JS \u30a2\u30c9\u30aa\u30f3\u304c\u8d77\u52d5\u3057\u3066\u3044\u307e\u3059\u3002" } } } From d27ee4c1a4f1bd28271dbee6c5429ec5eb1fa8f8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 21 Oct 2021 18:25:25 -1000 Subject: [PATCH 0635/1038] Allow setting the nexia run mode with the hvac mode (#57940) --- homeassistant/components/nexia/climate.py | 30 ++++++++++++++++++++ homeassistant/components/nexia/const.py | 2 ++ homeassistant/components/nexia/services.yaml | 28 ++++++++++++++++++ 3 files changed, 60 insertions(+) diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index 68fafab1718..2c1fbf5a3f4 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -1,5 +1,7 @@ """Support for Nexia / Trane XL thermostats.""" from nexia.const import ( + HOLD_PERMANENT, + HOLD_RESUME_SCHEDULE, OPERATION_MODE_AUTO, OPERATION_MODE_COOL, OPERATION_MODE_HEAT, @@ -14,6 +16,7 @@ import voluptuous as vol from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( ATTR_HUMIDITY, + ATTR_HVAC_MODE, ATTR_MAX_HUMIDITY, ATTR_MIN_HUMIDITY, ATTR_TARGET_TEMP_HIGH, @@ -45,6 +48,7 @@ from .const import ( ATTR_DEHUMIDIFY_SUPPORTED, ATTR_HUMIDIFY_SETPOINT, ATTR_HUMIDIFY_SUPPORTED, + ATTR_RUN_MODE, ATTR_ZONE_STATUS, DOMAIN, SIGNAL_THERMOSTAT_UPDATE, @@ -56,6 +60,7 @@ from .util import percent_conv SERVICE_SET_AIRCLEANER_MODE = "set_aircleaner_mode" SERVICE_SET_HUMIDIFY_SETPOINT = "set_humidify_setpoint" +SERVICE_SET_HVAC_RUN_MODE = "set_hvac_run_mode" SET_AIRCLEANER_SCHEMA = { vol.Required(ATTR_AIRCLEANER_MODE): cv.string, @@ -65,6 +70,17 @@ SET_HUMIDITY_SCHEMA = { vol.Required(ATTR_HUMIDITY): vol.All(vol.Coerce(int), vol.Range(min=35, max=65)), } +SET_HVAC_RUN_MODE_SCHEMA = vol.All( + cv.has_at_least_one_key(ATTR_RUN_MODE, ATTR_HVAC_MODE), + cv.make_entity_service_schema( + { + vol.Optional(ATTR_RUN_MODE): vol.In([HOLD_PERMANENT, HOLD_RESUME_SCHEDULE]), + vol.Optional(ATTR_HVAC_MODE): vol.In( + [HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_AUTO] + ), + } + ), +) # # Nexia has two bits to determine hvac mode @@ -102,6 +118,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): platform.async_register_entity_service( SERVICE_SET_AIRCLEANER_MODE, SET_AIRCLEANER_SCHEMA, SERVICE_SET_AIRCLEANER_MODE ) + platform.async_register_entity_service( + SERVICE_SET_HVAC_RUN_MODE, SET_HVAC_RUN_MODE_SCHEMA, SERVICE_SET_HVAC_RUN_MODE + ) entities = [] for thermostat_id in nexia_home.get_thermostat_ids(): @@ -188,6 +207,17 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): self._thermostat.set_fan_mode(fan_mode) self._signal_thermostat_update() + def set_hvac_run_mode(self, run_mode, hvac_mode): + """Set the hvac run mode.""" + if run_mode is not None: + if run_mode == HOLD_PERMANENT: + self._zone.call_permanent_hold() + else: + self._zone.call_return_to_schedule() + if hvac_mode is not None: + self._zone.set_mode(mode=HA_TO_NEXIA_HVAC_MODE_MAP[hvac_mode]) + self._signal_thermostat_update() + @property def preset_mode(self): """Preset that is active.""" diff --git a/homeassistant/components/nexia/const.py b/homeassistant/components/nexia/const.py index 4b076805e71..ada75ed580f 100644 --- a/homeassistant/components/nexia/const.py +++ b/homeassistant/components/nexia/const.py @@ -18,6 +18,8 @@ ATTR_DESCRIPTION = "description" ATTR_AIRCLEANER_MODE = "aircleaner_mode" +ATTR_RUN_MODE = "run_mode" + ATTR_ZONE_STATUS = "zone_status" ATTR_HUMIDIFY_SUPPORTED = "humidify_supported" ATTR_DEHUMIDIFY_SUPPORTED = "dehumidify_supported" diff --git a/homeassistant/components/nexia/services.yaml b/homeassistant/components/nexia/services.yaml index 740c865e274..78cf889f978 100644 --- a/homeassistant/components/nexia/services.yaml +++ b/homeassistant/components/nexia/services.yaml @@ -34,3 +34,31 @@ set_humidify_setpoint: min: 35 max: 65 unit_of_measurement: '%' + +set_hvac_run_mode: + name: Set hvac run mode + description: "The hvac run mode." + target: + entity: + integration: nexia + domain: climate + fields: + run_mode: + name: Run mode + description: 'Run the schedule or hold. If not specified, the current run mode will be used.' + required: false + selector: + select: + options: + - 'permanent_hold' + - 'run_schedule' + hvac_mode: + name: Hvac mode + description: 'The hvac mode to use for the schedule or hold. If not specified, the current hvac mode will be used.' + required: false + selector: + select: + options: + - 'auto' + - 'cool' + - 'heat' From d67c1118dc1d6d9232de36c845c009777d994684 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 21 Oct 2021 20:22:51 -1000 Subject: [PATCH 0636/1038] Cleanup lookin entity MRO and inheritance (#58194) --- homeassistant/components/lookin/climate.py | 12 +- homeassistant/components/lookin/entity.py | 134 +++++++++++++++++---- homeassistant/components/lookin/sensor.py | 9 +- 3 files changed, 115 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/lookin/climate.py b/homeassistant/components/lookin/climate.py index 4cba00121d5..536d4ca4016 100644 --- a/homeassistant/components/lookin/climate.py +++ b/homeassistant/components/lookin/climate.py @@ -30,13 +30,10 @@ 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.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN -from .entity import LookinEntity +from .entity import LookinCoordinatorEntity from .models import LookinData SUPPORT_FLAGS: int = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | SUPPORT_SWING_MODE @@ -113,7 +110,7 @@ async def async_setup_entry( async_add_entities(entities) -class ConditionerEntity(LookinEntity, CoordinatorEntity, ClimateEntity): +class ConditionerEntity(LookinCoordinatorEntity, ClimateEntity): """An aircon or heat pump.""" _attr_temperature_unit = TEMP_CELSIUS @@ -133,8 +130,7 @@ class ConditionerEntity(LookinEntity, CoordinatorEntity, ClimateEntity): coordinator: DataUpdateCoordinator, ) -> None: """Init the ConditionerEntity.""" - CoordinatorEntity.__init__(self, coordinator) - super().__init__(uuid, device, lookin_data) + super().__init__(coordinator, uuid, device, lookin_data) self._async_update_from_data() @property diff --git a/homeassistant/components/lookin/entity.py b/homeassistant/components/lookin/entity.py index fd2ee5e4a6c..228d69f8341 100644 --- a/homeassistant/components/lookin/entity.py +++ b/homeassistant/components/lookin/entity.py @@ -2,34 +2,94 @@ from __future__ import annotations from aiolookin import POWER_CMD, POWER_OFF_CMD, POWER_ON_CMD, Climate, Remote +from aiolookin.models import Device from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from .const import DOMAIN from .models import LookinData -class LookinDeviceEntity(Entity): +def _lookin_device_to_device_info(lookin_device: Device) -> DeviceInfo: + """Convert a lookin device into DeviceInfo.""" + return DeviceInfo( + identifiers={(DOMAIN, lookin_device.id)}, + name=lookin_device.name, + manufacturer="LOOKin", + model="LOOKin Remote2", + sw_version=lookin_device.firmware, + ) + + +def _lookin_controlled_device_to_device_info( + lookin_device: Device, uuid: str, device: Device +) -> DeviceInfo: + return DeviceInfo( + identifiers={(DOMAIN, uuid)}, + name=device.name, + model=device.device_type, + via_device=(DOMAIN, lookin_device.id), + ) + + +class LookinDeviceMixIn: + """A mix in to set lookin attributes for the lookin device.""" + + def _set_lookin_device_attrs(self, lookin_data: LookinData) -> None: + """Set attrs for the lookin device.""" + self._lookin_device = lookin_data.lookin_device + self._lookin_protocol = lookin_data.lookin_protocol + self._lookin_udp_subs = lookin_data.lookin_udp_subs + + +class LookinDeviceEntity(LookinDeviceMixIn, Entity): """A lookin device entity on the device itself.""" _attr_should_poll = False def __init__(self, lookin_data: LookinData) -> None: """Init the lookin device entity.""" - super().__init__() - self._lookin_device = lookin_data.lookin_device - self._lookin_protocol = lookin_data.lookin_protocol - self._lookin_udp_subs = lookin_data.lookin_udp_subs - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self._lookin_device.id)}, - name=self._lookin_device.name, - manufacturer="LOOKin", - model="LOOKin Remote2", - sw_version=self._lookin_device.firmware, + self._set_lookin_device_attrs(lookin_data) + self._attr_device_info = _lookin_device_to_device_info( + lookin_data.lookin_device ) -class LookinEntity(Entity): +class LookinDeviceCoordinatorEntity(LookinDeviceMixIn, CoordinatorEntity): + """A lookin device entity on the device itself that uses the coordinator.""" + + _attr_should_poll = False + + def __init__(self, lookin_data: LookinData) -> None: + """Init the lookin device entity.""" + super().__init__(lookin_data.meteo_coordinator) + self._set_lookin_device_attrs(lookin_data) + self._attr_device_info = _lookin_device_to_device_info( + lookin_data.lookin_device + ) + + +class LookinEntityMixIn: + """A mix in to set attributes for a lookin entity.""" + + def _set_lookin_entity_attrs( + self, + uuid: str, + device: Remote | Climate, + lookin_data: LookinData, + ) -> None: + """Set attrs for the device controlled via the lookin device.""" + self._device = device + self._uuid = uuid + self._meteo_coordinator = lookin_data.meteo_coordinator + self._function_names = {function.name for function in self._device.functions} + + +class LookinEntity(LookinDeviceMixIn, LookinEntityMixIn, Entity): """A base class for lookin entities.""" _attr_should_poll = False @@ -42,21 +102,43 @@ class LookinEntity(Entity): lookin_data: LookinData, ) -> None: """Init the base entity.""" - self._device = device - self._uuid = uuid - self._lookin_device = lookin_data.lookin_device - self._lookin_protocol = lookin_data.lookin_protocol - self._lookin_udp_subs = lookin_data.lookin_udp_subs - self._meteo_coordinator = lookin_data.meteo_coordinator - self._function_names = {function.name for function in self._device.functions} - self._attr_unique_id = uuid - self._attr_name = self._device.name - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self._uuid)}, - name=self._device.name, - model=self._device.device_type, - via_device=(DOMAIN, self._lookin_device.id), + self._set_lookin_device_attrs(lookin_data) + self._set_lookin_entity_attrs(uuid, device, lookin_data) + self._attr_device_info = _lookin_controlled_device_to_device_info( + self._lookin_device, uuid, device ) + self._attr_unique_id = uuid + self._attr_name = device.name + + async def _async_send_command(self, command: str) -> None: + """Send command from saved IR device.""" + await self._lookin_protocol.send_command( + uuid=self._uuid, command=command, signal="FF" + ) + + +class LookinCoordinatorEntity(LookinDeviceMixIn, LookinEntityMixIn, CoordinatorEntity): + """A lookin device entity for an external device that uses the coordinator.""" + + _attr_should_poll = False + _attr_assumed_state = True + + def __init__( + self, + coordinator: DataUpdateCoordinator, + uuid: str, + device: Remote | Climate, + lookin_data: LookinData, + ) -> None: + """Init the base entity.""" + super().__init__(coordinator) + self._set_lookin_device_attrs(lookin_data) + self._set_lookin_entity_attrs(uuid, device, lookin_data) + self._attr_device_info = _lookin_controlled_device_to_device_info( + self._lookin_device, uuid, device + ) + self._attr_unique_id = uuid + self._attr_name = device.name async def _async_send_command(self, command: str) -> None: """Send command from saved IR device.""" diff --git a/homeassistant/components/lookin/sensor.py b/homeassistant/components/lookin/sensor.py index 34a3859c7ec..d7d4a7a937a 100644 --- a/homeassistant/components/lookin/sensor.py +++ b/homeassistant/components/lookin/sensor.py @@ -15,12 +15,10 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, TEMP_CELSIUS from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .entity import LookinDeviceEntity +from .entity import LookinDeviceCoordinatorEntity from .models import LookinData LOGGER = logging.getLogger(__name__) @@ -57,15 +55,14 @@ async def async_setup_entry( ) -class LookinSensorEntity(CoordinatorEntity, LookinDeviceEntity, SensorEntity, Entity): +class LookinSensorEntity(LookinDeviceCoordinatorEntity, SensorEntity): """A lookin device sensor entity.""" def __init__( self, description: SensorEntityDescription, lookin_data: LookinData ) -> None: """Init the lookin sensor entity.""" - super().__init__(lookin_data.meteo_coordinator) - LookinDeviceEntity.__init__(self, lookin_data) + super().__init__(lookin_data) self.entity_description = description self._attr_name = f"{self._lookin_device.name} {description.name}" self._attr_native_value = getattr(self.coordinator.data, description.key) From 547e36ae94f314e392b14ca7302c4082a0c47955 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 22 Oct 2021 10:38:04 +0200 Subject: [PATCH 0637/1038] Tweak energy validator (#58018) * Tweak energy validator * Update code and tests * Tweak implementation * Update tests * Update after rebase --- homeassistant/components/energy/sensor.py | 18 +- homeassistant/components/energy/validate.py | 123 +++++----- tests/components/energy/test_validate.py | 244 ++++++++++++++++++-- 3 files changed, 311 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index 462c3f3215d..1d919e07737 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -23,7 +23,13 @@ from homeassistant.const import ( ENERGY_WATT_HOUR, VOLUME_CUBIC_METERS, ) -from homeassistant.core import HomeAssistant, State, callback, split_entity_id +from homeassistant.core import ( + HomeAssistant, + State, + callback, + split_entity_id, + valid_entity_id, +) from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -177,9 +183,13 @@ class SensorManager: # Make sure the right data is there # If the entity existed, we don't pop it from to_remove so it's removed - if config.get(adapter.entity_energy_key) is None or ( - config.get("entity_energy_price") is None - and config.get("number_energy_price") is None + if ( + config.get(adapter.entity_energy_key) is None + or not valid_entity_id(config[adapter.entity_energy_key]) + or ( + config.get("entity_energy_price") is None + and config.get("number_energy_price") is None + ) ): return diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py index 24d060b4352..c7f6c46aa1c 100644 --- a/homeassistant/components/energy/validate.py +++ b/homeassistant/components/energy/validate.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Mapping, Sequence import dataclasses +import functools from typing import Any from homeassistant.components import recorder, sensor @@ -66,56 +67,68 @@ class EnergyPreferencesValidation: return dataclasses.asdict(self) -@callback -def _async_validate_usage_stat( +async def _async_validate_usage_stat( hass: HomeAssistant, - stat_value: str, + stat_id: str, allowed_device_classes: Sequence[str], allowed_units: Mapping[str, Sequence[str]], unit_error: str, result: list[ValidationIssue], ) -> None: """Validate a statistic.""" - has_entity_source = valid_entity_id(stat_value) + metadata = await hass.async_add_executor_job( + functools.partial( + recorder.statistics.get_metadata, + hass, + statistic_ids=(stat_id,), + ) + ) + + if stat_id not in metadata: + result.append(ValidationIssue("statistics_not_defined", stat_id)) + + has_entity_source = valid_entity_id(stat_id) if not has_entity_source: return - if not recorder.is_entity_recorded(hass, stat_value): + entity_id = stat_id + + if not recorder.is_entity_recorded(hass, entity_id): result.append( ValidationIssue( "recorder_untracked", - stat_value, + entity_id, ) ) return - state = hass.states.get(stat_value) + state = hass.states.get(entity_id) if state is None: result.append( ValidationIssue( "entity_not_defined", - stat_value, + entity_id, ) ) return if state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN): - result.append(ValidationIssue("entity_unavailable", stat_value, state.state)) + result.append(ValidationIssue("entity_unavailable", entity_id, state.state)) return try: current_value: float | None = float(state.state) except ValueError: result.append( - ValidationIssue("entity_state_non_numeric", stat_value, state.state) + ValidationIssue("entity_state_non_numeric", entity_id, state.state) ) return if current_value is not None and current_value < 0: result.append( - ValidationIssue("entity_negative_state", stat_value, current_value) + ValidationIssue("entity_negative_state", entity_id, current_value) ) device_class = state.attributes.get(ATTR_DEVICE_CLASS) @@ -123,7 +136,7 @@ def _async_validate_usage_stat( result.append( ValidationIssue( "entity_unexpected_device_class", - stat_value, + entity_id, device_class, ) ) @@ -131,7 +144,7 @@ def _async_validate_usage_stat( unit = state.attributes.get("unit_of_measurement") if device_class and unit not in allowed_units.get(device_class, []): - result.append(ValidationIssue(unit_error, stat_value, unit)) + result.append(ValidationIssue(unit_error, entity_id, unit)) state_class = state.attributes.get(sensor.ATTR_STATE_CLASS) @@ -144,7 +157,7 @@ def _async_validate_usage_stat( result.append( ValidationIssue( "entity_unexpected_state_class", - stat_value, + entity_id, state_class, ) ) @@ -154,7 +167,7 @@ def _async_validate_usage_stat( and sensor.ATTR_LAST_RESET not in state.attributes ): result.append( - ValidationIssue("entity_state_class_measurement_no_last_reset", stat_value) + ValidationIssue("entity_state_class_measurement_no_last_reset", entity_id) ) @@ -192,33 +205,33 @@ def _async_validate_price_entity( result.append(ValidationIssue(unit_error, entity_id, unit)) -@callback -def _async_validate_cost_stat( +async def _async_validate_cost_stat( hass: HomeAssistant, stat_id: str, result: list[ValidationIssue] ) -> None: """Validate that the cost stat is correct.""" + metadata = await hass.async_add_executor_job( + functools.partial( + recorder.statistics.get_metadata, + hass, + statistic_ids=(stat_id,), + ) + ) + + if stat_id not in metadata: + result.append(ValidationIssue("statistics_not_defined", stat_id)) + has_entity = valid_entity_id(stat_id) if not has_entity: return if not recorder.is_entity_recorded(hass, stat_id): - result.append( - ValidationIssue( - "recorder_untracked", - stat_id, - ) - ) + result.append(ValidationIssue("recorder_untracked", stat_id)) state = hass.states.get(stat_id) if state is None: - result.append( - ValidationIssue( - "entity_not_defined", - stat_id, - ) - ) + result.append(ValidationIssue("entity_not_defined", stat_id)) return state_class = state.attributes.get("state_class") @@ -244,16 +257,16 @@ def _async_validate_cost_stat( @callback def _async_validate_auto_generated_cost_entity( - hass: HomeAssistant, entity_id: str, result: list[ValidationIssue] + hass: HomeAssistant, energy_entity_id: str, result: list[ValidationIssue] ) -> None: """Validate that the auto generated cost entity is correct.""" - if not recorder.is_entity_recorded(hass, entity_id): - result.append( - ValidationIssue( - "recorder_untracked", - entity_id, - ) - ) + if energy_entity_id not in hass.data[DOMAIN]["cost_sensors"]: + # The cost entity has not been setup + return + + cost_entity_id = hass.data[DOMAIN]["cost_sensors"][energy_entity_id] + if not recorder.is_entity_recorded(hass, cost_entity_id): + result.append(ValidationIssue("recorder_untracked", cost_entity_id)) async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: @@ -271,7 +284,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: if source["type"] == "grid": for flow in source["flow_from"]: - _async_validate_usage_stat( + await _async_validate_usage_stat( hass, flow["stat_energy_from"], ENERGY_USAGE_DEVICE_CLASSES, @@ -281,7 +294,9 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ) if flow.get("stat_cost") is not None: - _async_validate_cost_stat(hass, flow["stat_cost"], source_result) + await _async_validate_cost_stat( + hass, flow["stat_cost"], source_result + ) elif flow.get("entity_energy_price") is not None: _async_validate_price_entity( hass, @@ -291,18 +306,18 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ENERGY_PRICE_UNIT_ERROR, ) - if ( + if flow.get("entity_energy_from") is not None and ( flow.get("entity_energy_price") is not None or flow.get("number_energy_price") is not None ): _async_validate_auto_generated_cost_entity( hass, - hass.data[DOMAIN]["cost_sensors"][flow["stat_energy_from"]], + flow["entity_energy_from"], source_result, ) for flow in source["flow_to"]: - _async_validate_usage_stat( + await _async_validate_usage_stat( hass, flow["stat_energy_to"], ENERGY_USAGE_DEVICE_CLASSES, @@ -312,7 +327,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ) if flow.get("stat_compensation") is not None: - _async_validate_cost_stat( + await _async_validate_cost_stat( hass, flow["stat_compensation"], source_result ) elif flow.get("entity_energy_price") is not None: @@ -324,18 +339,18 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ENERGY_PRICE_UNIT_ERROR, ) - if ( + if flow.get("entity_energy_to") is not None and ( flow.get("entity_energy_price") is not None or flow.get("number_energy_price") is not None ): _async_validate_auto_generated_cost_entity( hass, - hass.data[DOMAIN]["cost_sensors"][flow["stat_energy_to"]], + flow["entity_energy_to"], source_result, ) elif source["type"] == "gas": - _async_validate_usage_stat( + await _async_validate_usage_stat( hass, source["stat_energy_from"], GAS_USAGE_DEVICE_CLASSES, @@ -345,7 +360,9 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ) if source.get("stat_cost") is not None: - _async_validate_cost_stat(hass, source["stat_cost"], source_result) + await _async_validate_cost_stat( + hass, source["stat_cost"], source_result + ) elif source.get("entity_energy_price") is not None: _async_validate_price_entity( hass, @@ -355,18 +372,18 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: GAS_PRICE_UNIT_ERROR, ) - if ( + if source.get("entity_energy_from") is not None and ( source.get("entity_energy_price") is not None or source.get("number_energy_price") is not None ): _async_validate_auto_generated_cost_entity( hass, - hass.data[DOMAIN]["cost_sensors"][source["stat_energy_from"]], + source["entity_energy_from"], source_result, ) elif source["type"] == "solar": - _async_validate_usage_stat( + await _async_validate_usage_stat( hass, source["stat_energy_from"], ENERGY_USAGE_DEVICE_CLASSES, @@ -376,7 +393,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ) elif source["type"] == "battery": - _async_validate_usage_stat( + await _async_validate_usage_stat( hass, source["stat_energy_from"], ENERGY_USAGE_DEVICE_CLASSES, @@ -384,7 +401,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ENERGY_UNIT_ERROR, source_result, ) - _async_validate_usage_stat( + await _async_validate_usage_stat( hass, source["stat_energy_to"], ENERGY_USAGE_DEVICE_CLASSES, @@ -396,7 +413,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: for device in manager.data["device_consumption"]: device_result: list[ValidationIssue] = [] result.device_consumption.append(device_result) - _async_validate_usage_stat( + await _async_validate_usage_stat( hass, device["stat_consumption"], ENERGY_USAGE_DEVICE_CLASSES, diff --git a/tests/components/energy/test_validate.py b/tests/components/energy/test_validate.py index 668f3113fea..5e3ad5c4aff 100644 --- a/tests/components/energy/test_validate.py +++ b/tests/components/energy/test_validate.py @@ -21,6 +21,20 @@ def mock_is_entity_recorded(): yield mocks +@pytest.fixture +def mock_get_metadata(): + """Mock recorder.statistics.get_metadata.""" + mocks = {} + + with patch( + "homeassistant.components.recorder.statistics.get_metadata", + side_effect=lambda hass, statistic_ids: mocks.get( + statistic_ids[0], {statistic_ids[0]: (1, {})} + ), + ): + yield mocks + + @pytest.fixture(autouse=True) async def mock_energy_manager(hass): """Set up energy.""" @@ -48,7 +62,9 @@ async def test_validation_empty_config(hass): ("measurement", {"last_reset": "abc"}), ], ) -async def test_validation(hass, mock_energy_manager, state_class, extra): +async def test_validation( + hass, mock_energy_manager, mock_get_metadata, state_class, extra +): """Test validating success.""" for key in ("device_cons", "battery_import", "battery_export", "solar_production"): hass.states.async_set( @@ -82,7 +98,7 @@ async def test_validation(hass, mock_energy_manager, state_class, extra): async def test_validation_device_consumption_entity_missing(hass, mock_energy_manager): - """Test validating missing stat for device.""" + """Test validating missing entity for device.""" await mock_energy_manager.async_update( {"device_consumption": [{"stat_consumption": "sensor.not_exist"}]} ) @@ -90,10 +106,34 @@ async def test_validation_device_consumption_entity_missing(hass, mock_energy_ma "energy_sources": [], "device_consumption": [ [ + { + "type": "statistics_not_defined", + "identifier": "sensor.not_exist", + "value": None, + }, { "type": "entity_not_defined", "identifier": "sensor.not_exist", "value": None, + }, + ] + ], + } + + +async def test_validation_device_consumption_stat_missing(hass, mock_energy_manager): + """Test validating missing statistic for device with non entity stats.""" + await mock_energy_manager.async_update( + {"device_consumption": [{"stat_consumption": "external:not_exist"}]} + ) + assert (await validate.async_validate(hass)).as_dict() == { + "energy_sources": [], + "device_consumption": [ + [ + { + "type": "statistics_not_defined", + "identifier": "external:not_exist", + "value": None, } ] ], @@ -101,7 +141,7 @@ async def test_validation_device_consumption_entity_missing(hass, mock_energy_ma async def test_validation_device_consumption_entity_unavailable( - hass, mock_energy_manager + hass, mock_energy_manager, mock_get_metadata ): """Test validating missing stat for device.""" await mock_energy_manager.async_update( @@ -124,7 +164,7 @@ async def test_validation_device_consumption_entity_unavailable( async def test_validation_device_consumption_entity_non_numeric( - hass, mock_energy_manager + hass, mock_energy_manager, mock_get_metadata ): """Test validating missing stat for device.""" await mock_energy_manager.async_update( @@ -147,7 +187,7 @@ async def test_validation_device_consumption_entity_non_numeric( async def test_validation_device_consumption_entity_unexpected_unit( - hass, mock_energy_manager + hass, mock_energy_manager, mock_get_metadata ): """Test validating missing stat for device.""" await mock_energy_manager.async_update( @@ -178,7 +218,7 @@ async def test_validation_device_consumption_entity_unexpected_unit( async def test_validation_device_consumption_recorder_not_tracked( - hass, mock_energy_manager, mock_is_entity_recorded + hass, mock_energy_manager, mock_is_entity_recorded, mock_get_metadata ): """Test validating device based on untracked entity.""" mock_is_entity_recorded["sensor.not_recorded"] = False @@ -200,7 +240,9 @@ async def test_validation_device_consumption_recorder_not_tracked( } -async def test_validation_device_consumption_no_last_reset(hass, mock_energy_manager): +async def test_validation_device_consumption_no_last_reset( + hass, mock_energy_manager, mock_get_metadata +): """Test validating device based on untracked entity.""" await mock_energy_manager.async_update( {"device_consumption": [{"stat_consumption": "sensor.no_last_reset"}]} @@ -229,7 +271,7 @@ async def test_validation_device_consumption_no_last_reset(hass, mock_energy_man } -async def test_validation_solar(hass, mock_energy_manager): +async def test_validation_solar(hass, mock_energy_manager, mock_get_metadata): """Test validating missing stat for device.""" await mock_energy_manager.async_update( { @@ -262,7 +304,7 @@ async def test_validation_solar(hass, mock_energy_manager): } -async def test_validation_battery(hass, mock_energy_manager): +async def test_validation_battery(hass, mock_energy_manager, mock_get_metadata): """Test validating missing stat for device.""" await mock_energy_manager.async_update( { @@ -313,10 +355,14 @@ async def test_validation_battery(hass, mock_energy_manager): } -async def test_validation_grid(hass, mock_energy_manager, mock_is_entity_recorded): +async def test_validation_grid( + hass, mock_energy_manager, mock_is_entity_recorded, mock_get_metadata +): """Test validating grid with sensors for energy and cost/compensation.""" mock_is_entity_recorded["sensor.grid_cost_1"] = False mock_is_entity_recorded["sensor.grid_compensation_1"] = False + mock_get_metadata["sensor.grid_cost_1"] = {} + mock_get_metadata["sensor.grid_compensation_1"] = {} await mock_energy_manager.async_update( { "energy_sources": [ @@ -365,6 +411,11 @@ async def test_validation_grid(hass, mock_energy_manager, mock_is_entity_recorde "identifier": "sensor.grid_consumption_1", "value": "beers", }, + { + "type": "statistics_not_defined", + "identifier": "sensor.grid_cost_1", + "value": None, + }, { "type": "recorder_untracked", "identifier": "sensor.grid_cost_1", @@ -380,6 +431,11 @@ async def test_validation_grid(hass, mock_energy_manager, mock_is_entity_recorde "identifier": "sensor.grid_production_1", "value": "beers", }, + { + "type": "statistics_not_defined", + "identifier": "sensor.grid_compensation_1", + "value": None, + }, { "type": "recorder_untracked", "identifier": "sensor.grid_compensation_1", @@ -396,8 +452,91 @@ async def test_validation_grid(hass, mock_energy_manager, mock_is_entity_recorde } -async def test_validation_grid_price_not_exist(hass, mock_energy_manager): - """Test validating grid with price entity that does not exist.""" +async def test_validation_grid_external_cost_compensation( + hass, mock_energy_manager, mock_is_entity_recorded, mock_get_metadata +): + """Test validating grid with non entity stats for energy and cost/compensation.""" + mock_get_metadata["external:grid_cost_1"] = {} + mock_get_metadata["external:grid_compensation_1"] = {} + await mock_energy_manager.async_update( + { + "energy_sources": [ + { + "type": "grid", + "flow_from": [ + { + "stat_energy_from": "sensor.grid_consumption_1", + "stat_cost": "external:grid_cost_1", + } + ], + "flow_to": [ + { + "stat_energy_to": "sensor.grid_production_1", + "stat_compensation": "external:grid_compensation_1", + } + ], + } + ] + } + ) + hass.states.async_set( + "sensor.grid_consumption_1", + "10.10", + { + "device_class": "energy", + "unit_of_measurement": "beers", + "state_class": "total_increasing", + }, + ) + hass.states.async_set( + "sensor.grid_production_1", + "10.10", + { + "device_class": "energy", + "unit_of_measurement": "beers", + "state_class": "total_increasing", + }, + ) + + assert (await validate.async_validate(hass)).as_dict() == { + "energy_sources": [ + [ + { + "type": "entity_unexpected_unit_energy", + "identifier": "sensor.grid_consumption_1", + "value": "beers", + }, + { + "type": "statistics_not_defined", + "identifier": "external:grid_cost_1", + "value": None, + }, + { + "type": "entity_unexpected_unit_energy", + "identifier": "sensor.grid_production_1", + "value": "beers", + }, + { + "type": "statistics_not_defined", + "identifier": "external:grid_compensation_1", + "value": None, + }, + ] + ], + "device_consumption": [], + } + + +async def test_validation_grid_price_not_exist( + hass, mock_energy_manager, mock_get_metadata, mock_is_entity_recorded +): + """Test validating grid with errors. + + - The price entity for the auto generated cost entity does not exist. + - The auto generated cost entities are not recorded. + """ + mock_is_entity_recorded["sensor.grid_consumption_1_cost"] = False + mock_is_entity_recorded["sensor.grid_production_1_compensation"] = False hass.states.async_set( "sensor.grid_consumption_1", "10.10", @@ -450,13 +589,82 @@ async def test_validation_grid_price_not_exist(hass, mock_energy_manager): "type": "entity_not_defined", "identifier": "sensor.grid_price_1", "value": None, - } + }, + { + "type": "recorder_untracked", + "identifier": "sensor.grid_consumption_1_cost", + "value": None, + }, + { + "type": "recorder_untracked", + "identifier": "sensor.grid_production_1_compensation", + "value": None, + }, ] ], "device_consumption": [], } +async def test_validation_grid_auto_cost_entity_errors( + hass, mock_energy_manager, mock_get_metadata, mock_is_entity_recorded, caplog +): + """Test validating grid when the auto generated cost entity config is incorrect. + + The intention of the test is to make sure the validation does not throw due to the + bad config. + """ + hass.states.async_set( + "sensor.grid_consumption_1", + "10.10", + { + "device_class": "energy", + "unit_of_measurement": "kWh", + "state_class": "total_increasing", + }, + ) + hass.states.async_set( + "sensor.grid_production_1", + "10.10", + { + "device_class": "energy", + "unit_of_measurement": "kWh", + "state_class": "total_increasing", + }, + ) + await mock_energy_manager.async_update( + { + "energy_sources": [ + { + "type": "grid", + "flow_from": [ + { + "stat_energy_from": "sensor.grid_consumption_1", + "entity_energy_from": None, + "entity_energy_price": None, + "number_energy_price": 0.20, + } + ], + "flow_to": [ + { + "stat_energy_to": "sensor.grid_production_1", + "entity_energy_to": "invalid", + "entity_energy_price": None, + "number_energy_price": 0.10, + } + ], + } + ] + } + ) + await hass.async_block_till_done() + + assert (await validate.async_validate(hass)).as_dict() == { + "energy_sources": [[]], + "device_consumption": [], + } + + @pytest.mark.parametrize( "state, unit, expected", ( @@ -481,7 +689,7 @@ async def test_validation_grid_price_not_exist(hass, mock_energy_manager): ), ) async def test_validation_grid_price_errors( - hass, mock_energy_manager, state, unit, expected + hass, mock_energy_manager, mock_get_metadata, state, unit, expected ): """Test validating grid with price data that gives errors.""" hass.states.async_set( @@ -526,7 +734,9 @@ async def test_validation_grid_price_errors( } -async def test_validation_gas(hass, mock_energy_manager, mock_is_entity_recorded): +async def test_validation_gas( + hass, mock_energy_manager, mock_is_entity_recorded, mock_get_metadata +): """Test validating gas with sensors for energy and cost/compensation.""" mock_is_entity_recorded["sensor.gas_cost_1"] = False mock_is_entity_recorded["sensor.gas_compensation_1"] = False @@ -653,7 +863,7 @@ async def test_validation_gas(hass, mock_energy_manager, mock_is_entity_recorded async def test_validation_gas_no_costs_tracking( - hass, mock_energy_manager, mock_is_entity_recorded + hass, mock_energy_manager, mock_is_entity_recorded, mock_get_metadata ): """Test validating gas with sensors without cost tracking.""" await mock_energy_manager.async_update( @@ -687,7 +897,7 @@ async def test_validation_gas_no_costs_tracking( async def test_validation_grid_no_costs_tracking( - hass, mock_energy_manager, mock_is_entity_recorded + hass, mock_energy_manager, mock_is_entity_recorded, mock_get_metadata ): """Test validating grid with sensors for energy without cost tracking.""" await mock_energy_manager.async_update( From c0934ce03c53eae744f3fabf155a8c96b954393c Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 22 Oct 2021 10:50:21 +0200 Subject: [PATCH 0638/1038] Fjaraskupan entity categories (#57846) --- homeassistant/components/fjaraskupan/sensor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fjaraskupan/sensor.py b/homeassistant/components/fjaraskupan/sensor.py index 4252828c633..1821008a1d7 100644 --- a/homeassistant/components/fjaraskupan/sensor.py +++ b/homeassistant/components/fjaraskupan/sensor.py @@ -9,7 +9,10 @@ from homeassistant.components.sensor import ( SensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import SIGNAL_STRENGTH_DECIBELS_MILLIWATT +from homeassistant.const import ( + ENTITY_CATEGORY_DIAGNOSTIC, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -57,6 +60,7 @@ class RssiSensor(CoordinatorEntity[State], SensorEntity): self._attr_state_class = STATE_CLASS_MEASUREMENT self._attr_native_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT self._attr_entity_registry_enabled_default = False + self._attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC @property def native_value(self) -> StateType: From f740302287be05e8b19dd82690b5ef4e26e8fcd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Fri, 22 Oct 2021 11:07:33 +0200 Subject: [PATCH 0639/1038] Add long-term statistics for Tado sensors (#58111) --- homeassistant/components/tado/sensor.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index 044241f2be0..872e2cbb42e 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -1,7 +1,7 @@ """Support for Tado sensors for each zone.""" import logging -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, @@ -153,6 +153,13 @@ class TadoHomeSensor(TadoHomeEntity, SensorEntity): return DEVICE_CLASS_TEMPERATURE return None + @property + def state_class(self): + """Return the state class.""" + if self.home_variable in ["outdoor temperature", "solar percentage"]: + return STATE_CLASS_MEASUREMENT + return None + @callback def _async_update_callback(self): """Update and write state.""" @@ -259,6 +266,13 @@ class TadoZoneSensor(TadoZoneEntity, SensorEntity): return DEVICE_CLASS_TEMPERATURE return None + @property + def state_class(self): + """Return the state class.""" + if self.zone_variable in ["ac", "heating", "humidity", "temperature"]: + return STATE_CLASS_MEASUREMENT + return None + @callback def _async_update_callback(self): """Update and write state.""" From 038158508c6f58d45cfcee658be651902c0cd98b Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 22 Oct 2021 11:08:46 +0200 Subject: [PATCH 0640/1038] Add `configuration_url` to AsusWrt integration (#58172) --- homeassistant/components/asuswrt/router.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index 7e89ea07dbd..03a15f80110 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -386,13 +386,14 @@ class AsusWrtRouter: @property def device_info(self) -> DeviceInfo: """Return the device information.""" - return { - "identifiers": {(DOMAIN, "AsusWRT")}, - "name": self._host, - "model": self._model, - "manufacturer": "Asus", - "sw_version": self._sw_v, - } + return DeviceInfo( + identifiers={(DOMAIN, "AsusWRT")}, + name=self._host, + model=self._model, + manufacturer="Asus", + sw_version=self._sw_v, + configuration_url=f"http://{self._host}", + ) @property def signal_device_new(self) -> str: From 281adfe3c993217ac027f63db297f6dc7ff5b68d Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 22 Oct 2021 11:09:50 +0200 Subject: [PATCH 0641/1038] Add support for device configuration URL to Axis devices (#58176) --- homeassistant/components/axis/device.py | 1 + tests/components/axis/test_device.py | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index 93d90e93fe2..823593ecacb 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -172,6 +172,7 @@ class AxisNetworkDevice: device_registry = dr.async_get(self.hass) device_registry.async_get_or_create( config_entry_id=self.config_entry.entry_id, + configuration_url=self.api.config.url, connections={(CONNECTION_NETWORK_MAC, self.unique_id)}, identifiers={(AXIS_DOMAIN, self.unique_id)}, manufacturer=ATTR_MANUFACTURER, diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index a68fa50a093..2d2ba83f633 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -26,6 +26,7 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) +from homeassistant.helpers import device_registry as dr from tests.common import MockConfigEntry, async_fire_mqtt_message @@ -323,6 +324,13 @@ async def test_device_setup(hass): assert device.name == ENTRY_CONFIG[CONF_NAME] assert device.unique_id == FORMATTED_MAC + device_registry = dr.async_get(hass) + device_entry = device_registry.async_get_device( + identifiers={(AXIS_DOMAIN, device.unique_id)} + ) + + assert device_entry.configuration_url == device.api.config.url + async def test_device_info(hass): """Verify other path of device information works.""" From 70469e0979ec6a8032389088c745484067e2d9c6 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 22 Oct 2021 11:13:05 +0200 Subject: [PATCH 0642/1038] Use assignment expressions 23 (#58180) --- homeassistant/components/apns/notify.py | 4 +--- .../components/asuswrt/config_flow.py | 3 +-- homeassistant/components/fints/sensor.py | 3 +-- .../components/homekit/aidmanager.py | 3 +-- .../components/homematicip_cloud/climate.py | 3 +-- .../components/homematicip_cloud/services.py | 20 ++++++------------- homeassistant/components/nest/__init__.py | 3 +-- .../components/nest/device_trigger.py | 16 +++++++-------- .../components/netgear/device_tracker.py | 3 +-- .../components/samsungtv/config_flow.py | 3 +-- homeassistant/components/sentry/__init__.py | 3 +-- homeassistant/components/spaceapi/__init__.py | 6 ++---- .../components/thinkingcleaner/sensor.py | 4 +--- .../components/thinkingcleaner/switch.py | 3 +-- homeassistant/components/trace/__init__.py | 3 +-- homeassistant/components/tuya/cover.py | 6 ++---- 16 files changed, 29 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/apns/notify.py b/homeassistant/components/apns/notify.py index a87cae09b1a..e0287897e96 100644 --- a/homeassistant/components/apns/notify.py +++ b/homeassistant/components/apns/notify.py @@ -220,9 +220,7 @@ class ApnsNotificationService(BaseNotificationService): ) device_state = kwargs.get(ATTR_TARGET) - message_data = kwargs.get(ATTR_DATA) - - if message_data is None: + if (message_data := kwargs.get(ATTR_DATA)) is None: message_data = {} if isinstance(message, str): diff --git a/homeassistant/components/asuswrt/config_flow.py b/homeassistant/components/asuswrt/config_flow.py index c48ea4d57fe..5a20880b4b0 100644 --- a/homeassistant/components/asuswrt/config_flow.py +++ b/homeassistant/components/asuswrt/config_flow.py @@ -217,8 +217,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): } ) - conf_mode = self.config_entry.data[CONF_MODE] - if conf_mode == MODE_AP: + if self.config_entry.data[CONF_MODE] == MODE_AP: data_schema = data_schema.extend( { vol.Optional( diff --git a/homeassistant/components/fints/sensor.py b/homeassistant/components/fints/sensor.py index d584bbed4bb..a7bec82cee0 100644 --- a/homeassistant/components/fints/sensor.py +++ b/homeassistant/components/fints/sensor.py @@ -79,8 +79,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.info("Skipping account %s for bank %s", account.iban, fints_name) continue - account_name = account_config.get(account.iban) - if not account_name: + if not (account_name := account_config.get(account.iban)): account_name = f"{fints_name} - {account.iban}" accounts.append(FinTsAccount(client, account, account_name)) _LOGGER.debug("Creating account %s for bank %s", account.iban, fints_name) diff --git a/homeassistant/components/homekit/aidmanager.py b/homeassistant/components/homekit/aidmanager.py index ddf3c7c564e..0f5e29426a8 100644 --- a/homeassistant/components/homekit/aidmanager.py +++ b/homeassistant/components/homekit/aidmanager.py @@ -92,8 +92,7 @@ class AccessoryAidStorage: def get_or_allocate_aid_for_entity_id(self, entity_id: str): """Generate a stable aid for an entity id.""" - entity = self._entity_registry.async_get(entity_id) - if not entity: + if not (entity := self._entity_registry.async_get(entity_id)): return self.get_or_allocate_aid(None, entity_id) sys_unique_id = get_system_unique_id(entity) diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 1b6c2491e2e..060d265c62a 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -207,8 +207,7 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return if self.min_temp <= temperature <= self.max_temp: diff --git a/homeassistant/components/homematicip_cloud/services.py b/homeassistant/components/homematicip_cloud/services.py index 45795f8858e..45b47b40efa 100644 --- a/homeassistant/components/homematicip_cloud/services.py +++ b/homeassistant/components/homematicip_cloud/services.py @@ -208,9 +208,8 @@ async def _async_activate_eco_mode_with_duration( ) -> None: """Service to activate eco mode with duration.""" duration = service.data[ATTR_DURATION] - hapid = service.data.get(ATTR_ACCESSPOINT_ID) - if hapid: + if hapid := service.data.get(ATTR_ACCESSPOINT_ID): home = _get_home(hass, hapid) if home: await home.activate_absence_with_duration(duration) @@ -224,9 +223,8 @@ async def _async_activate_eco_mode_with_period( ) -> None: """Service to activate eco mode with period.""" endtime = service.data[ATTR_ENDTIME] - hapid = service.data.get(ATTR_ACCESSPOINT_ID) - if hapid: + if hapid := service.data.get(ATTR_ACCESSPOINT_ID): home = _get_home(hass, hapid) if home: await home.activate_absence_with_period(endtime) @@ -239,9 +237,8 @@ async def _async_activate_vacation(hass: HomeAssistant, service: ServiceCall) -> """Service to activate vacation.""" endtime = service.data[ATTR_ENDTIME] temperature = service.data[ATTR_TEMPERATURE] - hapid = service.data.get(ATTR_ACCESSPOINT_ID) - if hapid: + if hapid := service.data.get(ATTR_ACCESSPOINT_ID): home = _get_home(hass, hapid) if home: await home.activate_vacation(endtime, temperature) @@ -252,9 +249,7 @@ async def _async_activate_vacation(hass: HomeAssistant, service: ServiceCall) -> async def _async_deactivate_eco_mode(hass: HomeAssistant, service: ServiceCall) -> None: """Service to deactivate eco mode.""" - hapid = service.data.get(ATTR_ACCESSPOINT_ID) - - if hapid: + if hapid := service.data.get(ATTR_ACCESSPOINT_ID): home = _get_home(hass, hapid) if home: await home.deactivate_absence() @@ -265,9 +260,7 @@ async def _async_deactivate_eco_mode(hass: HomeAssistant, service: ServiceCall) async def _async_deactivate_vacation(hass: HomeAssistant, service: ServiceCall) -> None: """Service to deactivate vacation.""" - hapid = service.data.get(ATTR_ACCESSPOINT_ID) - - if hapid: + if hapid := service.data.get(ATTR_ACCESSPOINT_ID): home = _get_home(hass, hapid) if home: await home.deactivate_vacation() @@ -337,8 +330,7 @@ async def _async_reset_energy_counter(hass: HomeAssistant, service: ServiceCall) def _get_home(hass: HomeAssistant, hapid: str) -> AsyncHome | None: """Return a HmIP home.""" - hap = hass.data[HMIPC_DOMAIN].get(hapid) - if hap: + if hap := hass.data[HMIPC_DOMAIN].get(hapid): return hap.home _LOGGER.info("No matching access point found for access point id %s", hapid) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 9ca7d530f79..344bdb74970 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -123,8 +123,7 @@ class SignalUpdateCallback: if not device_entry: return for event in events: - event_type = EVENT_NAME_MAP.get(event) - if not event_type: + if not (event_type := EVENT_NAME_MAP.get(event)): continue message = { "device_id": device_entry.id, diff --git a/homeassistant/components/nest/device_trigger.py b/homeassistant/components/nest/device_trigger.py index bcd5b6b96b3..28eb444b91a 100644 --- a/homeassistant/components/nest/device_trigger.py +++ b/homeassistant/components/nest/device_trigger.py @@ -38,8 +38,7 @@ async def async_get_nest_device_id(hass: HomeAssistant, device_id: str) -> str | device_registry: DeviceRegistry = ( await hass.helpers.device_registry.async_get_registry() ) - device = device_registry.async_get(device_id) - if device: + if device := device_registry.async_get(device_id): for (domain, unique_id) in device.identifiers: if domain == DOMAIN: return unique_id @@ -54,16 +53,15 @@ async def async_get_device_trigger_types( # "shouldn't happen" cases subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER] device_manager = await subscriber.async_get_device_manager() - nest_device = device_manager.devices.get(nest_device_id) - if not nest_device: + if not (nest_device := device_manager.devices.get(nest_device_id)): raise InvalidDeviceAutomationConfig(f"Nest device not found {nest_device_id}") # Determine the set of event types based on the supported device traits - trigger_types = [] - for trait in nest_device.traits: - trigger_type = DEVICE_TRAIT_TRIGGER_MAP.get(trait) - if trigger_type: - trigger_types.append(trigger_type) + trigger_types = [ + trigger_type + for trait in nest_device.traits + if (trigger_type := DEVICE_TRAIT_TRIGGER_MAP.get(trait)) + ] return trigger_types diff --git a/homeassistant/components/netgear/device_tracker.py b/homeassistant/components/netgear/device_tracker.py index 0d3a1098f0c..f7c92a271b9 100644 --- a/homeassistant/components/netgear/device_tracker.py +++ b/homeassistant/components/netgear/device_tracker.py @@ -86,8 +86,7 @@ class NetgearScannerEntity(NetgearDeviceEntity, ScannerEntity): def get_hostname(self): """Return the hostname of the given device or None if we don't know.""" - hostname = self._device["name"] - if hostname == "--": + if (hostname := self._device["name"]) == "--": return None return hostname diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index bcce5eec5ed..c75086322da 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -159,8 +159,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) return False dev_info = info.get("device", {}) - device_type = dev_info.get("type") - if device_type != "Samsung SmartTV": + if (device_type := dev_info.get("type")) != "Samsung SmartTV": raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) self._model = dev_info.get("modelName") name = dev_info.get("name") diff --git a/homeassistant/components/sentry/__init__.py b/homeassistant/components/sentry/__init__.py index c34bc2b350a..350ccfa5b8d 100644 --- a/homeassistant/components/sentry/__init__.py +++ b/homeassistant/components/sentry/__init__.py @@ -154,8 +154,7 @@ def process_before_send( ] # Add additional tags based on what caused the event. - platform = entity_platform.current_platform.get() - if platform is not None: + if (platform := entity_platform.current_platform.get()) is not None: # This event happened in a platform additional_tags["custom_component"] = "no" additional_tags["integration"] = platform.platform_name diff --git a/homeassistant/components/spaceapi/__init__.py b/homeassistant/components/spaceapi/__init__.py index 66583050b20..df4f6617ee0 100644 --- a/homeassistant/components/spaceapi/__init__.py +++ b/homeassistant/components/spaceapi/__init__.py @@ -249,8 +249,7 @@ class APISpaceApiView(HomeAssistantView): @staticmethod def get_sensor_data(hass, spaceapi, sensor): """Get data from a sensor.""" - sensor_state = hass.states.get(sensor) - if not sensor_state: + if not (sensor_state := hass.states.get(sensor)): return None sensor_data = {ATTR_NAME: sensor_state.name, ATTR_VALUE: sensor_state.state} if ATTR_SENSOR_LOCATION in sensor_state.attributes: @@ -279,9 +278,8 @@ class APISpaceApiView(HomeAssistantView): pass state_entity = spaceapi["state"][ATTR_ENTITY_ID] - space_state = hass.states.get(state_entity) - if space_state is not None: + if (space_state := hass.states.get(state_entity)) is not None: state = { ATTR_OPEN: space_state.state != "off", ATTR_LASTCHANGE: dt_util.as_timestamp(space_state.last_updated), diff --git a/homeassistant/components/thinkingcleaner/sensor.py b/homeassistant/components/thinkingcleaner/sensor.py index 588fc212138..76ad6578ae2 100644 --- a/homeassistant/components/thinkingcleaner/sensor.py +++ b/homeassistant/components/thinkingcleaner/sensor.py @@ -67,9 +67,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Optional(CONF_HOST): cv.string}) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ThinkingCleaner platform.""" - - host = config.get(CONF_HOST) - if host: + if host := config.get(CONF_HOST): devices = [ThinkingCleaner(host, "unknown")] else: discovery = Discovery() diff --git a/homeassistant/components/thinkingcleaner/switch.py b/homeassistant/components/thinkingcleaner/switch.py index cad94b72023..0f24ba6e755 100644 --- a/homeassistant/components/thinkingcleaner/switch.py +++ b/homeassistant/components/thinkingcleaner/switch.py @@ -42,8 +42,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Optional(CONF_HOST): cv.string}) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ThinkingCleaner platform.""" - host = config.get(CONF_HOST) - if host: + if host := config.get(CONF_HOST): devices = [ThinkingCleaner(host, "unknown")] else: discovery = Discovery() diff --git a/homeassistant/components/trace/__init__.py b/homeassistant/components/trace/__init__.py index 2f41365cb2f..29ed9f4d062 100644 --- a/homeassistant/components/trace/__init__.py +++ b/homeassistant/components/trace/__init__.py @@ -132,8 +132,7 @@ async def async_list_traces(hass, wanted_domain, wanted_key): def async_store_trace(hass, trace, stored_traces): """Store a trace if its key is valid.""" - key = trace.key - if key: + if key := trace.key: traces = hass.data[DATA_TRACE] if key not in traces: traces[key] = LimitedSizeDict(size_limit=stored_traces) diff --git a/homeassistant/components/tuya/cover.py b/homeassistant/components/tuya/cover.py index bc5a5b85772..0b8a658fd7c 100644 --- a/homeassistant/components/tuya/cover.py +++ b/homeassistant/components/tuya/cover.py @@ -239,8 +239,7 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity): ): return None - position = self.device.status.get(dpcode) - if position is None: + if (position := self.device.status.get(dpcode)) is None: return None return round( @@ -256,8 +255,7 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity): if self._tilt_dpcode is None or self._tilt_type is None: return None - angle = self.device.status.get(self._tilt_dpcode) - if angle is None: + if (angle := self.device.status.get(self._tilt_dpcode)) is None: return None return round(self._tilt_type.remap_value_to(angle, 0, 100)) From 83e45300c2c28de3992c6e988cbfe8df7873639c Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 22 Oct 2021 11:14:28 +0200 Subject: [PATCH 0643/1038] Add `configuration_url` to Netatmo devices (#58160) --- homeassistant/components/netatmo/camera.py | 2 ++ homeassistant/components/netatmo/climate.py | 3 +++ homeassistant/components/netatmo/const.py | 4 ++++ homeassistant/components/netatmo/light.py | 2 ++ .../components/netatmo/netatmo_entity_base.py | 14 ++++++++------ homeassistant/components/netatmo/select.py | 2 ++ homeassistant/components/netatmo/sensor.py | 10 +++++++++- 7 files changed, 30 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 5e63c56788b..7e2ea494604 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -36,6 +36,7 @@ from .const import ( SERVICE_SET_PERSON_AWAY, SERVICE_SET_PERSONS_HOME, SIGNAL_NAME, + TYPE_SECURITY, WEBHOOK_LIGHT_MODE, WEBHOOK_NACAMERA_CONNECTION, WEBHOOK_PUSH_TYPE, @@ -135,6 +136,7 @@ class NetatmoCamera(NetatmoBase, Camera): self._device_name = self._data.get_camera(camera_id=camera_id)["name"] self._attr_name = f"{MANUFACTURER} {self._device_name}" self._model = camera_type + self._netatmo_type = TYPE_SECURITY self._attr_unique_id = f"{self._id}-{self._model}" self._quality = quality self._vpnurl: str | None = None diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index e71b6939982..6b0f9cf5124 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -52,6 +52,7 @@ from .const import ( MANUFACTURER, SERVICE_SET_SCHEDULE, SIGNAL_NAME, + TYPE_ENERGY, ) from .data_handler import ( HOMEDATA_DATA_CLASS_NAME, @@ -209,6 +210,8 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self._model = NA_THERM break + self._netatmo_type = TYPE_ENERGY + self._device_name = self._data.rooms[home_id][room_id]["name"] self._attr_name = f"{MANUFACTURER} {self._device_name}" self._current_temperature: float | None = None diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index ea0f486b6cc..07651d982a5 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -45,6 +45,10 @@ MODELS = { "public": MODEL_PUBLIC, } +TYPE_SECURITY = "security" +TYPE_ENERGY = "energy" +TYPE_WEATHER = "weather" + AUTH = "netatmo_auth" CONF_PUBLIC = "public_sensor_config" CAMERA_DATA = "netatmo_camera" diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index 34c0d023edc..cb52271dbf5 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -19,6 +19,7 @@ from .const import ( EVENT_TYPE_LIGHT_MODE, MANUFACTURER, SIGNAL_NAME, + TYPE_SECURITY, WEBHOOK_LIGHT_MODE, WEBHOOK_PUSH_TYPE, ) @@ -88,6 +89,7 @@ class NetatmoLight(NetatmoBase, LightEntity): self._id = camera_id self._home_id = home_id self._model = camera_type + self._netatmo_type = TYPE_SECURITY self._device_name: str = self._data.get_camera(camera_id)["name"] self._attr_name = f"{MANUFACTURER} {self._device_name}" self._is_on = False diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py index f276fb3d947..1704fadedca 100644 --- a/homeassistant/components/netatmo/netatmo_entity_base.py +++ b/homeassistant/components/netatmo/netatmo_entity_base.py @@ -28,6 +28,7 @@ class NetatmoBase(Entity): self._device_name: str = "" self._id: str = "" self._model: str = "" + self._netatmo_type: str = "" self._attr_name = None self._attr_unique_id = None self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} @@ -91,9 +92,10 @@ class NetatmoBase(Entity): @property def device_info(self) -> DeviceInfo: """Return the device info for the sensor.""" - return { - "identifiers": {(DOMAIN, self._id)}, - "name": self._device_name, - "manufacturer": MANUFACTURER, - "model": MODELS[self._model], - } + return DeviceInfo( + configuration_url=f"https://my.netatmo.com/app/{self._netatmo_type}", + identifiers={(DOMAIN, self._id)}, + name=self._device_name, + manufacturer=MANUFACTURER, + model=MODELS[self._model], + ) diff --git a/homeassistant/components/netatmo/select.py b/homeassistant/components/netatmo/select.py index 387fb8f0acc..1f4c60b9dbc 100644 --- a/homeassistant/components/netatmo/select.py +++ b/homeassistant/components/netatmo/select.py @@ -20,6 +20,7 @@ from .const import ( EVENT_TYPE_SCHEDULE, MANUFACTURER, SIGNAL_NAME, + TYPE_ENERGY, ) from .data_handler import HOMEDATA_DATA_CLASS_NAME, NetatmoDataHandler from .helper import get_all_home_ids, update_climate_schedules @@ -87,6 +88,7 @@ class NetatmoScheduleSelect(NetatmoBase, SelectEntity): self._attr_name = f"{MANUFACTURER} {self._device_name}" self._model: str = "NATherm1" + self._netatmo_type = TYPE_ENERGY self._attr_unique_id = f"{self._home_id}-schedule-select" diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 8a4078d3d22..108ed3b2cea 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -42,7 +42,14 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import CONF_WEATHER_AREAS, DATA_HANDLER, DOMAIN, MANUFACTURER, SIGNAL_NAME +from .const import ( + CONF_WEATHER_AREAS, + DATA_HANDLER, + DOMAIN, + MANUFACTURER, + SIGNAL_NAME, + TYPE_WEATHER, +) from .data_handler import ( HOMECOACH_DATA_CLASS_NAME, PUBLICDATA_DATA_CLASS_NAME, @@ -492,6 +499,7 @@ class NetatmoSensor(NetatmoBase, SensorEntity): self._attr_name = f"{MANUFACTURER} {self._device_name} {description.name}" self._model = device["type"] + self._netatmo_type = TYPE_WEATHER self._attr_unique_id = f"{self._id}-{description.key}" @property From be201e3ebe0cfb294e1e385a33476d4a769ca445 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 22 Oct 2021 11:29:21 +0200 Subject: [PATCH 0644/1038] Use assignment expressions 27 (#58188) --- .../alarm_control_panel/reproduce_state.py | 4 +--- homeassistant/components/climacell/sensor.py | 3 +-- homeassistant/components/emulated_kasa/__init__.py | 12 ++++-------- homeassistant/components/evohome/climate.py | 3 +-- homeassistant/components/frontend/storage.py | 3 +-- homeassistant/components/google/__init__.py | 3 +-- homeassistant/components/homematic/climate.py | 3 +-- .../components/image_processing/__init__.py | 3 +-- homeassistant/components/ipp/__init__.py | 3 +-- homeassistant/components/mpd/media_player.py | 10 +++------- homeassistant/components/pushover/notify.py | 3 +-- homeassistant/components/raspihats/__init__.py | 3 +-- homeassistant/components/sabnzbd/__init__.py | 3 +-- homeassistant/components/scrape/sensor.py | 4 ++-- homeassistant/components/serial/sensor.py | 3 +-- homeassistant/components/songpal/__init__.py | 3 +-- homeassistant/components/supla/__init__.py | 3 +-- homeassistant/components/supla/cover.py | 3 +-- homeassistant/components/supla/switch.py | 3 +-- homeassistant/components/watson_iot/__init__.py | 4 +--- homeassistant/components/whirlpool/climate.py | 6 ++---- homeassistant/components/wirelesstag/__init__.py | 3 +-- 22 files changed, 29 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/reproduce_state.py b/homeassistant/components/alarm_control_panel/reproduce_state.py index 3fcc540d04b..ad992012c04 100644 --- a/homeassistant/components/alarm_control_panel/reproduce_state.py +++ b/homeassistant/components/alarm_control_panel/reproduce_state.py @@ -48,9 +48,7 @@ async def _async_reproduce_state( reproduce_options: dict[str, Any] | None = None, ) -> None: """Reproduce a single state.""" - cur_state = hass.states.get(state.entity_id) - - if cur_state is None: + if (cur_state := hass.states.get(state.entity_id)) is None: _LOGGER.warning("Unable to find entity %s", state.entity_id) return diff --git a/homeassistant/components/climacell/sensor.py b/homeassistant/components/climacell/sensor.py index 07bc4790b5f..f934449fdb0 100644 --- a/homeassistant/components/climacell/sensor.py +++ b/homeassistant/components/climacell/sensor.py @@ -28,9 +28,8 @@ async def async_setup_entry( ) -> None: """Set up a config entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] - api_version = config_entry.data[CONF_API_VERSION] - if api_version == 3: + if (api_version := config_entry.data[CONF_API_VERSION]) == 3: api_class = ClimaCellV3SensorEntity sensor_types = CC_V3_SENSOR_TYPES else: diff --git a/homeassistant/components/emulated_kasa/__init__.py b/homeassistant/components/emulated_kasa/__init__.py index d513669cd00..967edc8d157 100644 --- a/homeassistant/components/emulated_kasa/__init__.py +++ b/homeassistant/components/emulated_kasa/__init__.py @@ -51,8 +51,7 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the emulated_kasa component.""" - conf = config.get(DOMAIN) - if not conf: + if not (conf := config.get(DOMAIN)): return True entity_configs = conf[CONF_ENTITIES] @@ -83,13 +82,11 @@ async def validate_configs(hass, entity_configs): """Validate that entities exist and ensure templates are ready to use.""" entity_registry = await hass.helpers.entity_registry.async_get_registry() for entity_id, entity_config in entity_configs.items(): - state = hass.states.get(entity_id) - if state is None: + if (state := hass.states.get(entity_id)) is None: _LOGGER.debug("Entity not found: %s", entity_id) continue - entity = entity_registry.async_get(entity_id) - if entity: + if entity := entity_registry.async_get(entity_id): entity_config[CONF_UNIQUE_ID] = get_system_unique_id(entity) else: entity_config[CONF_UNIQUE_ID] = entity_id @@ -122,8 +119,7 @@ def get_system_unique_id(entity: RegistryEntry): def get_plug_devices(hass, entity_configs): """Produce list of plug devices from config entities.""" for entity_id, entity_config in entity_configs.items(): - state = hass.states.get(entity_id) - if state is None: + if (state := hass.states.get(entity_id)) is None: continue name = entity_config.get(CONF_NAME, state.name) diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 6dc2809630d..c293034e05a 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -230,9 +230,8 @@ class EvoZone(EvoChild, EvoClimateEntity): async def async_set_temperature(self, **kwargs) -> None: """Set a new target temperature.""" temperature = kwargs["temperature"] - until = kwargs.get("until") - if until is None: + if (until := kwargs.get("until")) is None: if self._evo_device.setpointStatus["setpointMode"] == EVO_FOLLOW: await self._update_schedule() until = dt_util.parse_datetime(self.setpoints.get("next_sp_from", "")) diff --git a/homeassistant/components/frontend/storage.py b/homeassistant/components/frontend/storage.py index 0b04655bd86..d7aabbec9b8 100644 --- a/homeassistant/components/frontend/storage.py +++ b/homeassistant/components/frontend/storage.py @@ -33,9 +33,8 @@ def with_store(orig_func: Callable) -> Callable: """Provide user specific data and store to function.""" stores, data = hass.data[DATA_STORAGE] user_id = connection.user.id - store = stores.get(user_id) - if store is None: + if (store := stores.get(user_id)) is None: store = stores[user_id] = hass.helpers.storage.Store( STORAGE_VERSION_USER_DATA, f"frontend.user_data_{connection.user.id}" ) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 7e157d238d5..08663a297d2 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -233,8 +233,7 @@ def setup(hass, config): if DATA_INDEX not in hass.data: hass.data[DATA_INDEX] = {} - conf = config.get(DOMAIN, {}) - if not conf: + if not (conf := config.get(DOMAIN, {})): # component is set up by tts platform return True diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index aa5fb4a8e44..84c722db1df 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -133,8 +133,7 @@ class HMThermostat(HMDevice, ClimateEntity): def set_temperature(self, **kwargs): """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return None self._hmdevice.writeNodeData(self._state, float(temperature)) diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index d1220a84cdd..5c09e0b0bc7 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -161,8 +161,7 @@ class ImageProcessingFaceEntity(ImageProcessingEntity): if ATTR_CONFIDENCE not in face: continue - f_co = face[ATTR_CONFIDENCE] - if f_co > confidence: + if (f_co := face[ATTR_CONFIDENCE]) > confidence: confidence = f_co for attr in (ATTR_NAME, ATTR_MOTION): if attr in face: diff --git a/homeassistant/components/ipp/__init__.py b/homeassistant/components/ipp/__init__.py index 242390b55b8..d5930ac5f56 100644 --- a/homeassistant/components/ipp/__init__.py +++ b/homeassistant/components/ipp/__init__.py @@ -15,8 +15,7 @@ PLATFORMS = [SENSOR_DOMAIN] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up IPP from a config entry.""" hass.data.setdefault(DOMAIN, {}) - coordinator = hass.data[DOMAIN].get(entry.entry_id) - if not coordinator: + if not (coordinator := hass.data[DOMAIN].get(entry.entry_id)): # Create IPP instance for this entry coordinator = IPPDataUpdateCoordinator( hass, diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index baf57844eaf..c89378f20de 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -140,9 +140,7 @@ class MpdDevice(MediaPlayerEntity): self._status = await self._client.status() self._currentsong = await self._client.currentsong() - position = self._status.get("elapsed") - - if position is None: + if (position := self._status.get("elapsed")) is None: position = self._status.get("time") if isinstance(position, str) and ":" in position: @@ -257,16 +255,14 @@ class MpdDevice(MediaPlayerEntity): @property def media_image_hash(self): """Hash value for media image.""" - file = self._currentsong.get("file") - if file: + if file := self._currentsong.get("file"): return hashlib.sha256(file.encode("utf-8")).hexdigest()[:16] return None async def async_get_media_image(self): """Fetch media image of current playing track.""" - file = self._currentsong.get("file") - if not file: + if not (file := self._currentsong.get("file")): return None, None # not all MPD implementations and versions support the `albumart` and `fetchpicture` commands diff --git a/homeassistant/components/pushover/notify.py b/homeassistant/components/pushover/notify.py index 3f599ac2d8a..8a18597c2ca 100644 --- a/homeassistant/components/pushover/notify.py +++ b/homeassistant/components/pushover/notify.py @@ -68,9 +68,8 @@ class PushoverNotificationService(BaseNotificationService): sound = data.get(ATTR_SOUND) html = 1 if data.get(ATTR_HTML, False) else 0 - image = data.get(ATTR_ATTACHMENT) # Check for attachment - if image is not None: + if (image := data.get(ATTR_ATTACHMENT)) is not None: # Only allow attachments from whitelisted paths, check valid path if self._hass.config.is_allowed_path(data[ATTR_ATTACHMENT]): # try to open it as a normal file. diff --git a/homeassistant/components/raspihats/__init__.py b/homeassistant/components/raspihats/__init__.py index c9e862a0672..478bd9147ef 100644 --- a/homeassistant/components/raspihats/__init__.py +++ b/homeassistant/components/raspihats/__init__.py @@ -118,8 +118,7 @@ class I2CHatsManager(threading.Thread): def register_board(self, board, address): """Register I2C-HAT.""" with self._lock: - i2c_hat = self._i2c_hats.get(address) - if i2c_hat is None: + if (i2c_hat := self._i2c_hats.get(address)) is None: # This is a Pi module and can't be installed in CI without # breaking the build. # pylint: disable=import-outside-toplevel,import-error diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index f6e15bd074c..f2b1ce882c7 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -206,8 +206,7 @@ async def async_setup(hass, config): discovery.async_listen(hass, SERVICE_SABNZBD, sabnzbd_discovered) - conf = config.get(DOMAIN) - if conf is not None: + if (conf := config.get(DOMAIN)) is not None: use_ssl = conf[CONF_SSL] name = conf.get(CONF_NAME) api_key = conf.get(CONF_API_KEY) diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index 1f5b543f9ee..2e118bf744f 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -66,8 +66,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= unit = config.get(CONF_UNIT_OF_MEASUREMENT) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) - value_template = config.get(CONF_VALUE_TEMPLATE) - if value_template is not None: + + if (value_template := config.get(CONF_VALUE_TEMPLATE)) is not None: value_template.hass = hass if username and password: diff --git a/homeassistant/components/serial/sensor.py b/homeassistant/components/serial/sensor.py index cbdba50b6b6..37d9c689ce7 100644 --- a/homeassistant/components/serial/sensor.py +++ b/homeassistant/components/serial/sensor.py @@ -81,8 +81,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= rtscts = config.get(CONF_RTSCTS) dsrdtr = config.get(CONF_DSRDTR) - value_template = config.get(CONF_VALUE_TEMPLATE) - if value_template is not None: + if (value_template := config.get(CONF_VALUE_TEMPLATE)) is not None: value_template.hass = hass sensor = SerialSensor( diff --git a/homeassistant/components/songpal/__init__.py b/homeassistant/components/songpal/__init__.py index 2053d2857c2..eb93dafb123 100644 --- a/homeassistant/components/songpal/__init__.py +++ b/homeassistant/components/songpal/__init__.py @@ -24,8 +24,7 @@ PLATFORMS = ["media_player"] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up songpal environment.""" - conf = config.get(DOMAIN) - if conf is None: + if (conf := config.get(DOMAIN)) is None: return True for config_entry in conf: hass.async_create_task( diff --git a/homeassistant/components/supla/__init__.py b/homeassistant/components/supla/__init__.py index c8862a37b61..51e6c377bdc 100644 --- a/homeassistant/components/supla/__init__.py +++ b/homeassistant/components/supla/__init__.py @@ -190,8 +190,7 @@ class SuplaChannel(CoordinatorEntity): """Return True if entity is available.""" if self.channel_data is None: return False - state = self.channel_data.get("state") - if state is None: + if (state := self.channel_data.get("state")) is None: return False return state.get("connected") diff --git a/homeassistant/components/supla/cover.py b/homeassistant/components/supla/cover.py index ac71bc4ea8f..a4c2fc95792 100644 --- a/homeassistant/components/supla/cover.py +++ b/homeassistant/components/supla/cover.py @@ -59,8 +59,7 @@ class SuplaCover(SuplaChannel, CoverEntity): @property def current_cover_position(self): """Return current position of cover. 0 is closed, 100 is open.""" - state = self.channel_data.get("state") - if state: + if state := self.channel_data.get("state"): return 100 - state["shut"] return None diff --git a/homeassistant/components/supla/switch.py b/homeassistant/components/supla/switch.py index 9122d9d1970..32ff208f10e 100644 --- a/homeassistant/components/supla/switch.py +++ b/homeassistant/components/supla/switch.py @@ -49,7 +49,6 @@ class SuplaSwitch(SuplaChannel, SwitchEntity): @property def is_on(self): """Return true if switch is on.""" - state = self.channel_data.get("state") - if state: + if state := self.channel_data.get("state"): return state["on"] return False diff --git a/homeassistant/components/watson_iot/__init__.py b/homeassistant/components/watson_iot/__init__.py index 99f6b63ec90..8b81a9d741b 100644 --- a/homeassistant/components/watson_iot/__init__.py +++ b/homeassistant/components/watson_iot/__init__.py @@ -177,9 +177,7 @@ class WatsonIOTThread(threading.Thread): events = [] try: - item = self.queue.get() - - if item is None: + if (item := self.queue.get()) is None: self.shutdown = True else: event_json = self.event_to_json(item[1]) diff --git a/homeassistant/components/whirlpool/climate.py b/homeassistant/components/whirlpool/climate.py index ccfa11b3ee4..eb1b88dcc13 100644 --- a/homeassistant/components/whirlpool/climate.py +++ b/homeassistant/components/whirlpool/climate.py @@ -156,8 +156,7 @@ class AirConEntity(ClimateEntity): await self._aircon.set_power_on(False) return - mode = HVAC_MODE_TO_AIRCON_MODE.get(hvac_mode) - if not mode: + if not (mode := HVAC_MODE_TO_AIRCON_MODE.get(hvac_mode)): raise ValueError(f"Invalid hvac mode {hvac_mode}") await self._aircon.set_mode(mode) @@ -172,8 +171,7 @@ class AirConEntity(ClimateEntity): async def async_set_fan_mode(self, fan_mode): """Set fan mode.""" - fanspeed = FAN_MODE_TO_AIRCON_FANSPEED.get(fan_mode) - if not fanspeed: + 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) diff --git a/homeassistant/components/wirelesstag/__init__.py b/homeassistant/components/wirelesstag/__init__.py index 519663d1261..24afb6b0465 100644 --- a/homeassistant/components/wirelesstag/__init__.py +++ b/homeassistant/components/wirelesstag/__init__.py @@ -199,8 +199,7 @@ class WirelessTagBaseSensor(Entity): return updated_tags = self._api.load_tags() - updated_tag = updated_tags[self._uuid] - if updated_tag is None: + if (updated_tag := updated_tags[self._uuid]) is None: _LOGGER.error('Unable to update tag: "%s"', self.name) return From 184e0d7fdfc8075bd990c88dec57e3b750cb6ca2 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 22 Oct 2021 11:31:17 +0200 Subject: [PATCH 0645/1038] Use assignment expressions 26 (#58187) --- homeassistant/components/cloud/alexa_config.py | 3 +-- homeassistant/components/cloud/google_config.py | 3 +-- homeassistant/components/dlna_dmr/config_flow.py | 3 +-- homeassistant/components/eq3btsmart/climate.py | 3 +-- homeassistant/components/fitbit/sensor.py | 13 +++++++------ .../components/generic_thermostat/climate.py | 3 +-- homeassistant/components/honeywell/climate.py | 9 +++------ homeassistant/components/html5/notify.py | 14 ++++---------- homeassistant/components/knx/climate.py | 3 +-- .../components/mobile_app/device_tracker.py | 11 +++-------- homeassistant/components/mobile_app/notify.py | 4 +--- homeassistant/components/mobile_app/webhook.py | 4 +--- .../components/rss_feed_template/__init__.py | 3 +-- homeassistant/components/sensibo/climate.py | 6 ++---- homeassistant/components/universal/media_player.py | 4 +--- 15 files changed, 29 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index f578a114dfc..3e82e662fe9 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -134,8 +134,7 @@ class AlexaConfig(alexa_config.AbstractConfig): return entity_expose entity_registry = er.async_get(self.hass) - registry_entry = entity_registry.async_get(entity_id) - if registry_entry: + if registry_entry := entity_registry.async_get(entity_id): auxiliary_entity = registry_entry.entity_category in ( ENTITY_CATEGORY_CONFIG, ENTITY_CATEGORY_DIAGNOSTIC, diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index bfb6510fcda..4f71aeeb9a0 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -132,8 +132,7 @@ class CloudGoogleConfig(AbstractConfig): return entity_expose entity_registry = er.async_get(self.hass) - registry_entry = entity_registry.async_get(entity_id) - if registry_entry: + if registry_entry := entity_registry.async_get(entity_id): auxiliary_entity = registry_entry.entity_category in ( ENTITY_CATEGORY_CONFIG, ENTITY_CATEGORY_DIAGNOSTIC, diff --git a/homeassistant/components/dlna_dmr/config_flow.py b/homeassistant/components/dlna_dmr/config_flow.py index 551faf2815f..3958ca9e3e5 100644 --- a/homeassistant/components/dlna_dmr/config_flow.py +++ b/homeassistant/components/dlna_dmr/config_flow.py @@ -352,8 +352,7 @@ class DlnaDmrOptionsFlowHandler(config_entries.OptionsFlow): def _add_with_suggestion(key: str, validator: Callable) -> None: """Add a field to with a suggested, not default, value.""" - suggested_value = options.get(key) - if suggested_value is None: + if (suggested_value := options.get(key)) is None: fields[vol.Optional(key)] = validator else: fields[ diff --git a/homeassistant/components/eq3btsmart/climate.py b/homeassistant/components/eq3btsmart/climate.py index 3c3d41d090c..b7c39ec996c 100644 --- a/homeassistant/components/eq3btsmart/climate.py +++ b/homeassistant/components/eq3btsmart/climate.py @@ -123,8 +123,7 @@ class EQ3BTSmartThermostat(ClimateEntity): def set_temperature(self, **kwargs): """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return self._thermostat.target_temperature = temperature diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 96de36a1f29..2de121920af 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -377,12 +377,13 @@ class FitbitSensor(SensorEntity): @property def icon(self) -> str | None: """Icon to use in the frontend, if any.""" - if self.entity_description.key == "devices/battery" and self.extra is not None: - extra_battery = self.extra.get("battery") - if extra_battery is not None: - battery_level = BATTERY_LEVELS.get(extra_battery) - if battery_level is not None: - return icon_for_battery_level(battery_level=battery_level) + if ( + self.entity_description.key == "devices/battery" + and self.extra is not None + and (extra_battery := self.extra.get("battery")) is not None + and (battery_level := BATTERY_LEVELS.get(extra_battery)) is not None + ): + return icon_for_battery_level(battery_level=battery_level) return self.entity_description.icon @property diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index c48deba12d8..4d52240535f 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -364,8 +364,7 @@ class GenericThermostat(ClimateEntity, RestoreEntity): async def async_set_temperature(self, **kwargs): """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return self._target_temp = temperature await self._async_control_heating(force=True) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 8088a73506d..7c40a0bb684 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -282,8 +282,7 @@ class HoneywellUSThermostat(ClimateEntity): def _set_temperature(self, **kwargs) -> None: """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return try: # Get current mode @@ -310,11 +309,9 @@ class HoneywellUSThermostat(ClimateEntity): try: if HVAC_MODE_HEAT_COOL in self._hvac_mode_map: - temperature = kwargs.get(ATTR_TARGET_TEMP_HIGH) - if temperature: + if temperature := kwargs.get(ATTR_TARGET_TEMP_HIGH): self._device.setpoint_cool = temperature - temperature = kwargs.get(ATTR_TARGET_TEMP_LOW) - if temperature: + if temperature := kwargs.get(ATTR_TARGET_TEMP_LOW): self._device.setpoint_heat = temperature except somecomfort.SomeComfortError as err: _LOGGER.error("Invalid temperature %s: %s", temperature, err) diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index 594d84a8068..6c3a10757bb 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -186,9 +186,8 @@ def get_service(hass, config, discovery_info=None): hass.http.register_view(HTML5PushCallbackView(registrations)) gcm_api_key = config.get(ATTR_GCM_API_KEY) - gcm_sender_id = config.get(ATTR_GCM_SENDER_ID) - if gcm_sender_id is not None: + if config.get(ATTR_GCM_SENDER_ID) is not None: add_manifest_json_key(ATTR_GCM_SENDER_ID, config.get(ATTR_GCM_SENDER_ID)) return HTML5NotificationService( @@ -332,9 +331,7 @@ class HTML5PushCallbackView(HomeAssistantView): # https://auth0.com/docs/quickstart/backend/python def check_authorization_header(self, request): """Check the authorization header.""" - - auth = request.headers.get(AUTHORIZATION) - if not auth: + if not (auth := request.headers.get(AUTHORIZATION)): return self.json_message( "Authorization header is expected", status_code=HTTPStatus.UNAUTHORIZED ) @@ -463,9 +460,7 @@ class HTML5NotificationService(BaseNotificationService): ATTR_TITLE: kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT), } - data = kwargs.get(ATTR_DATA) - - if data: + if data := kwargs.get(ATTR_DATA): # Pick out fields that should go into the notification directly vs # into the notification data dictionary. @@ -496,9 +491,8 @@ class HTML5NotificationService(BaseNotificationService): if priority not in ["normal", "high"]: priority = DEFAULT_PRIORITY payload["timestamp"] = timestamp * 1000 # Javascript ms since epoch - targets = kwargs.get(ATTR_TARGET) - if not targets: + if not (targets := kwargs.get(ATTR_TARGET)): targets = self.registrations.keys() for target in list(targets): diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 91342cca839..22289738711 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -218,8 +218,7 @@ class KNXClimate(KnxEntity, ClimateEntity): async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return await self._device.set_target_temperature(temperature) self.async_write_ha_state() diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py index 1deebf6b531..f4b5866eacd 100644 --- a/homeassistant/components/mobile_app/device_tracker.py +++ b/homeassistant/components/mobile_app/device_tracker.py @@ -60,8 +60,7 @@ class MobileAppEntity(TrackerEntity, RestoreEntity): """Return device specific attributes.""" attrs = {} for key in ATTR_KEYS: - value = self._data.get(key) - if value is not None: + if (value := self._data.get(key)) is not None: attrs[key] = value return attrs @@ -74,9 +73,7 @@ class MobileAppEntity(TrackerEntity, RestoreEntity): @property def latitude(self): """Return latitude value of the device.""" - gps = self._data.get(ATTR_GPS) - - if gps is None: + if (gps := self._data.get(ATTR_GPS)) is None: return None return gps[0] @@ -84,9 +81,7 @@ class MobileAppEntity(TrackerEntity, RestoreEntity): @property def longitude(self): """Return longitude value of the device.""" - gps = self._data.get(ATTR_GPS) - - if gps is None: + if (gps := self._data.get(ATTR_GPS)) is None: return None return gps[1] diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py index 36afbac71c8..21f92d73020 100644 --- a/homeassistant/components/mobile_app/notify.py +++ b/homeassistant/components/mobile_app/notify.py @@ -114,9 +114,7 @@ class MobileAppNotificationService(BaseNotificationService): ): data[ATTR_TITLE] = kwargs.get(ATTR_TITLE) - targets = kwargs.get(ATTR_TARGET) - - if not targets: + if not (targets := kwargs.get(ATTR_TARGET)): targets = push_registrations(self.hass).values() if kwargs.get(ATTR_DATA) is not None: diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 8a06b693e9a..7d8b6ad4b53 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -264,9 +264,7 @@ async def webhook_fire_event(hass, config_entry, data): @validate_schema({vol.Required(ATTR_CAMERA_ENTITY_ID): cv.string}) async def webhook_stream_camera(hass, config_entry, data): """Handle a request to HLS-stream a camera.""" - camera = hass.states.get(data[ATTR_CAMERA_ENTITY_ID]) - - if camera is None: + if (camera := hass.states.get(data[ATTR_CAMERA_ENTITY_ID])) is None: return webhook_response( {"success": False}, registration=config_entry.data, diff --git a/homeassistant/components/rss_feed_template/__init__.py b/homeassistant/components/rss_feed_template/__init__.py index 222b533235d..fb830bccee7 100644 --- a/homeassistant/components/rss_feed_template/__init__.py +++ b/homeassistant/components/rss_feed_template/__init__.py @@ -43,8 +43,7 @@ def setup(hass, config): requires_auth = feedconfig.get("requires_api_password") - title = feedconfig.get("title") - if title is not None: + if (title := feedconfig.get("title")) is not None: title.hass = hass items = feedconfig.get("items") diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index c4589205e34..b0b211e4a7d 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -112,8 +112,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_assume_state(service): """Set state according to external service call..""" - entity_ids = service.data.get(ATTR_ENTITY_ID) - if entity_ids: + if entity_ids := service.data.get(ATTR_ENTITY_ID): target_climate = [ device for device in devices if device.entity_id in entity_ids ] @@ -299,8 +298,7 @@ class SensiboClimate(ClimateEntity): async def async_set_temperature(self, **kwargs): """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return temperature = int(temperature) if temperature not in self._temperatures_list: diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index 59cef93de49..925a76140f3 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -216,9 +216,7 @@ class UniversalMediaPlayer(MediaPlayerEntity): def _entity_lkp(self, entity_id, state_attr=None): """Look up an entity state.""" - state_obj = self.hass.states.get(entity_id) - - if state_obj is None: + if (state_obj := self.hass.states.get(entity_id)) is None: return if state_attr: From 2e5c9b69d4ca3a023631c9efdf8f8320a1d8efa1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 22 Oct 2021 11:32:15 +0200 Subject: [PATCH 0646/1038] Use DeviceInfo on zha (#58202) Co-authored-by: epenet --- homeassistant/components/zha/entity.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 6fd68056025..0ba75a99306 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -92,14 +92,14 @@ class BaseZhaEntity(LogMixin, entity.Entity): """Return a device description for device registry.""" zha_device_info = self._zha_device.device_info ieee = zha_device_info["ieee"] - return { - "connections": {(CONNECTION_ZIGBEE, ieee)}, - "identifiers": {(DOMAIN, ieee)}, - ATTR_MANUFACTURER: zha_device_info[ATTR_MANUFACTURER], - ATTR_MODEL: zha_device_info[ATTR_MODEL], - ATTR_NAME: zha_device_info[ATTR_NAME], - "via_device": (DOMAIN, self.hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID]), - } + return entity.DeviceInfo( + connections={(CONNECTION_ZIGBEE, ieee)}, + identifiers={(DOMAIN, ieee)}, + manufacturer=zha_device_info[ATTR_MANUFACTURER], + model=zha_device_info[ATTR_MODEL], + name=zha_device_info[ATTR_NAME], + via_device=(DOMAIN, self.hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID]), + ) @callback def async_state_changed(self) -> None: From 99903859264b062895af8a3d50c2618583479c88 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 22 Oct 2021 11:34:45 +0200 Subject: [PATCH 0647/1038] Use assignment expressions 25 (#58182) --- homeassistant/components/ambiclimate/climate.py | 3 +-- .../components/ambiclimate/config_flow.py | 3 +-- homeassistant/components/bond/light.py | 3 +-- homeassistant/components/brunt/cover.py | 3 +-- homeassistant/components/daikin/climate.py | 6 ++---- homeassistant/components/dyson/climate.py | 3 +-- .../components/egardia/alarm_control_panel.py | 3 +-- .../components/forecast_solar/__init__.py | 4 +--- .../components/forecast_solar/energy.py | 4 +--- homeassistant/components/hdmi_cec/__init__.py | 3 +-- homeassistant/components/iaqualink/light.py | 7 ++----- homeassistant/components/msteams/notify.py | 17 +++++++---------- homeassistant/components/neato/camera.py | 4 +--- .../components/openweathermap/__init__.py | 3 +-- homeassistant/components/plant/__init__.py | 3 +-- homeassistant/components/ps4/config_flow.py | 3 +-- homeassistant/components/ps4/media_player.py | 3 +-- homeassistant/components/tasmota/__init__.py | 3 +-- homeassistant/components/tasmota/light.py | 3 +-- homeassistant/components/tfiac/climate.py | 3 +-- homeassistant/components/withings/__init__.py | 2 +- 21 files changed, 29 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/ambiclimate/climate.py b/homeassistant/components/ambiclimate/climate.py index aa4be202865..d30e48dbe59 100644 --- a/homeassistant/components/ambiclimate/climate.py +++ b/homeassistant/components/ambiclimate/climate.py @@ -157,8 +157,7 @@ class AmbiclimateEntity(ClimateEntity): async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return await self._heater.set_target_temperature(temperature) diff --git a/homeassistant/components/ambiclimate/config_flow.py b/homeassistant/components/ambiclimate/config_flow.py index 04d1b749d10..623e96a4a67 100644 --- a/homeassistant/components/ambiclimate/config_flow.py +++ b/homeassistant/components/ambiclimate/config_flow.py @@ -143,8 +143,7 @@ class AmbiclimateAuthCallbackView(HomeAssistantView): async def get(self, request: web.Request) -> str: """Receive authorization token.""" # pylint: disable=no-self-use - code = request.query.get("code") - if code is None: + if (code := request.query.get("code")) is None: return "No code" hass = request.app["hass"] hass.async_create_task( diff --git a/homeassistant/components/bond/light.py b/homeassistant/components/bond/light.py index 82bfa24e44d..dd4699ad006 100644 --- a/homeassistant/components/bond/light.py +++ b/homeassistant/components/bond/light.py @@ -273,8 +273,7 @@ class BondFireplace(BondEntity, LightEntity): """Turn the fireplace on.""" _LOGGER.debug("Fireplace async_turn_on called with: %s", kwargs) - brightness = kwargs.get(ATTR_BRIGHTNESS) - if brightness: + if brightness := kwargs.get(ATTR_BRIGHTNESS): flame = round((brightness * 100) / 255) await self._hub.bond.action(self._device.device_id, Action.set_flame(flame)) else: diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py index 5c9d7b3d4a5..650ce9c05c6 100644 --- a/homeassistant/components/brunt/cover.py +++ b/homeassistant/components/brunt/cover.py @@ -43,8 +43,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): bapi = BruntAPI(username=username, password=password) try: - things = bapi.getThings()["things"] - if not things: + if not (things := bapi.getThings()["things"]): _LOGGER.error("No things present in account") else: add_entities( diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index b3e833bb64f..c8e962b9c76 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -136,12 +136,10 @@ class DaikinClimate(ClimateEntity): values = {} for attr in (ATTR_TEMPERATURE, ATTR_FAN_MODE, ATTR_SWING_MODE, ATTR_HVAC_MODE): - value = settings.get(attr) - if value is None: + if (value := settings.get(attr)) is None: continue - daikin_attr = HA_ATTR_TO_DAIKIN.get(attr) - if daikin_attr is not None: + if (daikin_attr := HA_ATTR_TO_DAIKIN.get(attr)) is not None: if attr == ATTR_HVAC_MODE: values[daikin_attr] = HA_STATE_TO_DAIKIN[value] elif value in self._list[attr]: diff --git a/homeassistant/components/dyson/climate.py b/homeassistant/components/dyson/climate.py index 4f4c4d7cbba..19993a6498c 100644 --- a/homeassistant/components/dyson/climate.py +++ b/homeassistant/components/dyson/climate.py @@ -142,8 +142,7 @@ class DysonClimateEntity(DysonEntity, ClimateEntity): def set_temperature(self, **kwargs): """Set new target temperature.""" - target_temp = kwargs.get(ATTR_TEMPERATURE) - if target_temp is None: + if (target_temp := kwargs.get(ATTR_TEMPERATURE)) is None: _LOGGER.error("Missing target temperature %s", kwargs) return target_temp = int(target_temp) diff --git a/homeassistant/components/egardia/alarm_control_panel.py b/homeassistant/components/egardia/alarm_control_panel.py index b133a96b820..e386ad6beac 100644 --- a/homeassistant/components/egardia/alarm_control_panel.py +++ b/homeassistant/components/egardia/alarm_control_panel.py @@ -97,8 +97,7 @@ class EgardiaAlarm(alarm.AlarmControlPanelEntity): def handle_status_event(self, event): """Handle the Egardia system status event.""" - statuscode = event.get("status") - if statuscode is not None: + if (statuscode := event.get("status")) is not None: status = self.lookupstatusfromcode(statuscode) self.parsestatus(status) self.schedule_update_ha_state() diff --git a/homeassistant/components/forecast_solar/__init__.py b/homeassistant/components/forecast_solar/__init__.py index 9638ea4e4dd..1fd77b9797c 100644 --- a/homeassistant/components/forecast_solar/__init__.py +++ b/homeassistant/components/forecast_solar/__init__.py @@ -25,11 +25,9 @@ PLATFORMS = ["sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Forecast.Solar from a config entry.""" - api_key = entry.options.get(CONF_API_KEY) # Our option flow may cause it to be an empty string, # this if statement is here to catch that. - if not api_key: - api_key = None + api_key = entry.options.get(CONF_API_KEY) or None session = async_get_clientsession(hass) forecast = ForecastSolar( diff --git a/homeassistant/components/forecast_solar/energy.py b/homeassistant/components/forecast_solar/energy.py index 6bf63910e5f..33537396330 100644 --- a/homeassistant/components/forecast_solar/energy.py +++ b/homeassistant/components/forecast_solar/energy.py @@ -10,9 +10,7 @@ async def async_get_solar_forecast( hass: HomeAssistant, config_entry_id: str ) -> dict[str, dict[str, float | int]] | None: """Get solar forecast for a config entry ID.""" - coordinator = hass.data[DOMAIN].get(config_entry_id) - - if coordinator is None: + if (coordinator := hass.data[DOMAIN].get(config_entry_id)) is None: return None return { diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index 87391634251..47bd8d160c6 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -297,8 +297,7 @@ def setup(hass: HomeAssistant, base_config: ConfigType) -> bool: # noqa: C901 def _select_device(call): """Select the active device.""" - addr = call.data[ATTR_DEVICE] - if not addr: + if not (addr := call.data[ATTR_DEVICE]): _LOGGER.error("Device not found: %s", call.data[ATTR_DEVICE]) return if addr in device_aliases: diff --git a/homeassistant/components/iaqualink/light.py b/homeassistant/components/iaqualink/light.py index 79030e1e3ca..17b686947ce 100644 --- a/homeassistant/components/iaqualink/light.py +++ b/homeassistant/components/iaqualink/light.py @@ -48,14 +48,11 @@ class HassAqualinkLight(AqualinkEntity, LightEntity): This handles brightness and light effects for lights that do support them. """ - brightness = kwargs.get(ATTR_BRIGHTNESS) - effect = kwargs.get(ATTR_EFFECT) - # For now I'm assuming lights support either effects or brightness. - if effect: + if effect := kwargs.get(ATTR_EFFECT): effect = AqualinkLightEffect[effect].value await self.dev.set_effect(effect) - elif brightness: + elif brightness := kwargs.get(ATTR_BRIGHTNESS): # Aqualink supports percentages in 25% increments. pct = int(round(brightness * 4.0 / 255)) * 25 await self.dev.set_brightness(pct) diff --git a/homeassistant/components/msteams/notify.py b/homeassistant/components/msteams/notify.py index eafac85c8c2..421b8093a8d 100644 --- a/homeassistant/components/msteams/notify.py +++ b/homeassistant/components/msteams/notify.py @@ -52,17 +52,14 @@ class MSTeamsNotificationService(BaseNotificationService): teams_message.text(message) - if data is not None: - file_url = data.get(ATTR_FILE_URL) + if data is not None and (file_url := data.get(ATTR_FILE_URL)) is not None: + if not file_url.startswith("http"): + _LOGGER.error("URL should start with http or https") + return - if file_url is not None: - if not file_url.startswith("http"): - _LOGGER.error("URL should start with http or https") - return - - message_section = pymsteams.cardsection() - message_section.addImage(file_url) - teams_message.addSection(message_section) + message_section = pymsteams.cardsection() + message_section.addImage(file_url) + teams_message.addSection(message_section) try: teams_message.send() except RuntimeError as err: diff --git a/homeassistant/components/neato/camera.py b/homeassistant/components/neato/camera.py index 392d586068d..ee70116a7d3 100644 --- a/homeassistant/components/neato/camera.py +++ b/homeassistant/components/neato/camera.py @@ -89,11 +89,9 @@ class NeatoCleaningMap(Camera): self._available = False return - image_url = None if self._mapdata: map_data: dict[str, Any] = self._mapdata[self._robot_serial]["maps"][0] - image_url = map_data["url"] - if image_url == self._image_url: + if (image_url := map_data["url"]) == self._image_url: _LOGGER.debug( "The map image_url for '%s' is the same as old", self.entity_id ) diff --git a/homeassistant/components/openweathermap/__init__.py b/homeassistant/components/openweathermap/__init__.py index d9643d6a4d3..58219ee70b3 100644 --- a/homeassistant/components/openweathermap/__init__.py +++ b/homeassistant/components/openweathermap/__init__.py @@ -71,8 +71,7 @@ async def async_migrate_entry(hass, entry): _LOGGER.debug("Migrating OpenWeatherMap entry from version %s", version) if version == 1: - mode = data[CONF_MODE] - if mode == FORECAST_MODE_FREE_DAILY: + if (mode := data[CONF_MODE]) == FORECAST_MODE_FREE_DAILY: mode = FORECAST_MODE_ONECALL_DAILY new_data = {**data, CONF_MODE: mode} diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index 290993959b3..5e734c7ba62 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -291,8 +291,7 @@ class Plant(Entity): ) for entity_id in self._sensormap: - state = self.hass.states.get(entity_id) - if state is not None: + if (state := self.hass.states.get(entity_id)) is not None: self.state_changed(entity_id, state) def _load_history_from_db(self): diff --git a/homeassistant/components/ps4/config_flow.py b/homeassistant/components/ps4/config_flow.py index 7424e0f5e1a..23ee1e061e3 100644 --- a/homeassistant/components/ps4/config_flow.py +++ b/homeassistant/components/ps4/config_flow.py @@ -88,8 +88,7 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: if user_input[CONF_MODE] == CONF_MANUAL: try: - device = user_input[CONF_IP_ADDRESS] - if device: + if device := user_input[CONF_IP_ADDRESS]: self.m_device = device except KeyError: errors[CONF_IP_ADDRESS] = "no_ipaddress" diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index 512894a9889..bdc8ba9714d 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -203,8 +203,7 @@ class PS4Device(MediaPlayerEntity): store = self._games[self._media_content_id] # If locked get attributes from file. - locked = store.get(ATTR_LOCKED) - if locked: + if store.get(ATTR_LOCKED): self._media_title = store.get(ATTR_MEDIA_TITLE) self._source = self._media_title self._media_image = store.get(ATTR_MEDIA_IMAGE_URL) diff --git a/homeassistant/components/tasmota/__init__.py b/homeassistant/components/tasmota/__init__.py index b863cda796d..fd156e20c3c 100644 --- a/homeassistant/components/tasmota/__init__.py +++ b/homeassistant/components/tasmota/__init__.py @@ -202,8 +202,7 @@ async def websocket_remove_device( device_id = msg["device_id"] dev_registry = await hass.helpers.device_registry.async_get_registry() - device = dev_registry.async_get(device_id) - if not device: + if not (device := dev_registry.async_get(device_id)): connection.send_error( msg["id"], websocket_api.const.ERR_NOT_FOUND, "Device not found" ) diff --git a/homeassistant/components/tasmota/light.py b/homeassistant/components/tasmota/light.py index de25a25fd4f..c09b4c71948 100644 --- a/homeassistant/components/tasmota/light.py +++ b/homeassistant/components/tasmota/light.py @@ -162,8 +162,7 @@ class TasmotaLight( def state_updated(self, state: bool, **kwargs: Any) -> None: """Handle state updates.""" self._on_off_state = state - attributes = kwargs.get("attributes") - if attributes: + if attributes := kwargs.get("attributes"): if "brightness" in attributes: brightness = float(attributes["brightness"]) percent_bright = brightness / TASMOTA_BRIGHTNESS_MAX diff --git a/homeassistant/components/tfiac/climate.py b/homeassistant/components/tfiac/climate.py index 1e9bb86d8fd..7c350d48775 100644 --- a/homeassistant/components/tfiac/climate.py +++ b/homeassistant/components/tfiac/climate.py @@ -171,8 +171,7 @@ class TfiacClimate(ClimateEntity): async def async_set_temperature(self, **kwargs): """Set new target temperature.""" - temp = kwargs.get(ATTR_TEMPERATURE) - if temp is not None: + if (temp := kwargs.get(ATTR_TEMPERATURE)) is not None: await self._client.set_state(TARGET_TEMP, temp) async def async_set_hvac_mode(self, hvac_mode): diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 2cf6d297f12..7132b3bff9d 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -65,7 +65,7 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Withings component.""" conf = config.get(DOMAIN, {}) - if not conf: + if not (conf := config.get(DOMAIN, {})): return True # Make the config available to the oauth2 config flow. From 0b302ab14168afed82ee80474ca270d143b2d161 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 22 Oct 2021 11:36:40 +0200 Subject: [PATCH 0648/1038] Use DeviceInfo on zwave (#58183) Co-authored-by: epenet --- homeassistant/components/zwave/__init__.py | 21 ++++++++-------- homeassistant/components/zwave/node_entity.py | 25 +++++++++++-------- homeassistant/const.py | 1 + 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index e14352a92a3..b5a77c82050 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -12,6 +12,7 @@ from homeassistant import config_entries from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, + ATTR_VIA_DEVICE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ) @@ -25,7 +26,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import generate_entity_id +from homeassistant.helpers.entity import DeviceInfo, generate_entity_id from homeassistant.helpers.entity_component import DEFAULT_SCAN_INTERVAL from homeassistant.helpers.entity_platform import EntityPlatform from homeassistant.helpers.entity_registry import ( @@ -1301,21 +1302,21 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): return self._unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device information.""" identifier, name = node_device_id_and_name( self.node, self.values.primary.instance ) - info = { - "name": name, - "identifiers": {identifier}, - "manufacturer": self.node.manufacturer_name, - "model": self.node.product_name, - } + info = DeviceInfo( + name=name, + identifiers={identifier}, + manufacturer=self.node.manufacturer_name, + model=self.node.product_name, + ) if self.values.primary.instance > 1: - info["via_device"] = (DOMAIN, self.node_id) + info[ATTR_VIA_DEVICE] = (DOMAIN, self.node_id) elif self.node_id > 1: - info["via_device"] = (DOMAIN, 1) + info[ATTR_VIA_DEVICE] = (DOMAIN, 1) return info @property diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py index 3fa26439ad5..b17034e0e8a 100644 --- a/homeassistant/components/zwave/node_entity.py +++ b/homeassistant/components/zwave/node_entity.py @@ -2,10 +2,15 @@ # pylint: disable=import-outside-toplevel from itertools import count -from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, ATTR_WAKEUP +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, + ATTR_ENTITY_ID, + ATTR_VIA_DEVICE, + ATTR_WAKEUP, +) from homeassistant.core import callback from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity_registry import async_get_registry from .const import ( @@ -151,17 +156,17 @@ class ZWaveNodeEntity(ZWaveBaseEntity): return self._unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device information.""" identifier, name = node_device_id_and_name(self.node) - info = { - "identifiers": {identifier}, - "manufacturer": self.node.manufacturer_name, - "model": self.node.product_name, - "name": name, - } + info = DeviceInfo( + identifiers={identifier}, + manufacturer=self.node.manufacturer_name, + model=self.node.product_name, + name=name, + ) if self.node_id > 1: - info["via_device"] = (DOMAIN, 1) + info[ATTR_VIA_DEVICE] = (DOMAIN, 1) return info def maybe_update_application_version(self, value): diff --git a/homeassistant/const.py b/homeassistant/const.py index 6e9f8ee97d7..470acf06ec6 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -355,6 +355,7 @@ ATTR_MODE: Final = "mode" ATTR_MANUFACTURER: Final = "manufacturer" ATTR_MODEL: Final = "model" ATTR_SW_VERSION: Final = "sw_version" +ATTR_VIA_DEVICE: Final = "via_device" ATTR_BATTERY_CHARGING: Final = "battery_charging" ATTR_BATTERY_LEVEL: Final = "battery_level" From 42427113018f78fce3a188380e32ffe438c8a30c Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 22 Oct 2021 02:37:30 -0700 Subject: [PATCH 0649/1038] Bump google-nest-sdm to 0.3.8 (#58186) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index c9f99f00eb1..b7ac58a571a 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.3.7"], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.3.8"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index bbfec519206..9b30036eb47 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -735,7 +735,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.3.7 +google-nest-sdm==0.3.8 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2cc9d5526a6..bf95651ea07 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -449,7 +449,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.3.7 +google-nest-sdm==0.3.8 # homeassistant.components.google_travel_time googlemaps==2.5.1 From 766a693514c168a696e6429790d5f52d8ec0c215 Mon Sep 17 00:00:00 2001 From: Tomer <57483589+tomer-w@users.noreply.github.com> Date: Fri, 22 Oct 2021 12:45:16 +0300 Subject: [PATCH 0650/1038] Fix registration UI to work for Israel devices (#58192) --- homeassistant/components/tuya/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index a57ee5e397e..a20206c9369 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -572,7 +572,7 @@ TUYA_COUNTRIES = [ Country("Iraq", "964"), Country("Ireland", "353", TuyaCloudOpenAPIEndpoint.EUROPE), Country("Isle of Man", "44-1624"), - Country("Israel", "972"), + Country("Israel", "972", TuyaCloudOpenAPIEndpoint.EUROPE), Country("Italy", "39", TuyaCloudOpenAPIEndpoint.EUROPE), Country("Ivory Coast", "225"), Country("Jamaica", "1-876"), From c00a5fad8fecb305834bc8e962117f8f245adb1e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 22 Oct 2021 11:45:40 +0200 Subject: [PATCH 0651/1038] Cleanup device registration in Onewire (#58101) * Add checks for device registry * Move registry checks to init.py * Run device registry check on disabled devices * Empty commit for testing * Register devices during initialisation * Adjust tests accordingly * Add via_device to device info * Adjust access to device registry Co-authored-by: epenet --- homeassistant/components/onewire/__init__.py | 36 ++--- .../components/onewire/binary_sensor.py | 30 ++--- homeassistant/components/onewire/model.py | 26 +++- .../components/onewire/onewirehub.py | 112 +++++++++++++--- homeassistant/components/onewire/sensor.py | 41 +++--- homeassistant/components/onewire/switch.py | 31 ++--- tests/components/onewire/__init__.py | 124 +++++++++++++++--- tests/components/onewire/const.py | 27 ++-- .../components/onewire/test_binary_sensor.py | 10 +- tests/components/onewire/test_sensor.py | 123 +++-------------- tests/components/onewire/test_switch.py | 10 +- 11 files changed, 317 insertions(+), 253 deletions(-) diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py index b99f095de7b..5981a654820 100644 --- a/homeassistant/components/onewire/__init__.py +++ b/homeassistant/components/onewire/__init__.py @@ -5,7 +5,7 @@ import logging from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import device_registry as dr from .const import DOMAIN, PLATFORMS from .onewirehub import CannotConnect, OneWireHub @@ -25,31 +25,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = onewirehub - async def cleanup_registry() -> None: + async def cleanup_registry(onewirehub: OneWireHub) -> None: # Get registries - device_registry, entity_registry = await asyncio.gather( - hass.helpers.device_registry.async_get_registry(), - hass.helpers.entity_registry.async_get_registry(), - ) + device_registry = dr.async_get(hass) # Generate list of all device entries - registry_devices = [ - entry.id - for entry in dr.async_entries_for_config_entry( - device_registry, entry.entry_id - ) - ] + registry_devices = list( + dr.async_entries_for_config_entry(device_registry, entry.entry_id) + ) # Remove devices that don't belong to any entity - for device_id in registry_devices: - if not er.async_entries_for_device( - entity_registry, device_id, include_disabled_entities=True - ): + for device in registry_devices: + if not onewirehub.has_device_in_cache(device): _LOGGER.debug( - "Removing device `%s` because it does not have any entities", - device_id, + "Removing device `%s` because it is no longer available", + device.id, ) - device_registry.async_remove_device(device_id) + device_registry.async_remove_device(device.id) - async def start_platforms() -> None: + async def start_platforms(onewirehub: OneWireHub) -> None: """Start platforms and cleanup devices.""" # wait until all required platforms are ready await asyncio.gather( @@ -58,9 +50,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for platform in PLATFORMS ) ) - await cleanup_registry() + await cleanup_registry(onewirehub) - hass.async_create_task(start_platforms()) + hass.async_create_task(start_platforms(onewirehub)) return True diff --git a/homeassistant/components/onewire/binary_sensor.py b/homeassistant/components/onewire/binary_sensor.py index 0a57e0c1b19..7f569b150c2 100644 --- a/homeassistant/components/onewire/binary_sensor.py +++ b/homeassistant/components/onewire/binary_sensor.py @@ -3,21 +3,16 @@ from __future__ import annotations from dataclasses import dataclass import os +from typing import TYPE_CHECKING from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) +from homeassistant.components.onewire.model import OWServerDeviceDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, - CONF_TYPE, -) +from homeassistant.const import CONF_TYPE from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( @@ -89,24 +84,17 @@ def get_entities(onewirehub: OneWireHub) -> list[BinarySensorEntity]: return [] entities: list[BinarySensorEntity] = [] - for device in onewirehub.devices: - family = device["family"] - device_type = device["type"] - device_id = os.path.split(os.path.split(device["path"])[0])[1] + if TYPE_CHECKING: + assert isinstance(device, OWServerDeviceDescription) + family = device.family + device_id = device.id + device_info = device.device_info if family not in DEVICE_BINARY_SENSORS: continue - device_info: DeviceInfo = { - ATTR_IDENTIFIERS: {(DOMAIN, device_id)}, - ATTR_MANUFACTURER: "Maxim Integrated", - ATTR_MODEL: device_type, - ATTR_NAME: device_id, - } for description in DEVICE_BINARY_SENSORS[family]: - device_file = os.path.join( - os.path.split(device["path"])[0], description.key - ) + device_file = os.path.join(os.path.split(device.path)[0], description.key) name = f"{device_id} {description.name}" entities.append( OneWireProxyBinarySensor( diff --git a/homeassistant/components/onewire/model.py b/homeassistant/components/onewire/model.py index 2aaef861a50..370b26c2530 100644 --- a/homeassistant/components/onewire/model.py +++ b/homeassistant/components/onewire/model.py @@ -1,12 +1,32 @@ """Type definitions for 1-Wire integration.""" from __future__ import annotations -from typing import TypedDict +from dataclasses import dataclass + +from pi1wire import OneWireInterface + +from homeassistant.helpers.entity import DeviceInfo -class OWServerDeviceDescription(TypedDict): +@dataclass +class OWDeviceDescription: + """OWDeviceDescription device description class.""" + + device_info: DeviceInfo + + +@dataclass +class OWDirectDeviceDescription(OWDeviceDescription): + """SysBus device description class.""" + + interface: OneWireInterface + + +@dataclass +class OWServerDeviceDescription(OWDeviceDescription): """OWServer device description class.""" - path: str family: str + id: str + path: str type: str diff --git a/homeassistant/components/onewire/onewirehub.py b/homeassistant/components/onewire/onewirehub.py index d3b6773e74e..0d7f85fee68 100644 --- a/homeassistant/components/onewire/onewirehub.py +++ b/homeassistant/components/onewire/onewirehub.py @@ -1,24 +1,43 @@ """Hub for communication with 1-Wire server or mount_dir.""" from __future__ import annotations +import logging import os +from typing import TYPE_CHECKING from pi1wire import Pi1Wire from pyownet import protocol from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE +from homeassistant.const import ( + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + CONF_HOST, + CONF_PORT, + CONF_TYPE, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import DeviceEntry +from homeassistant.helpers.entity import DeviceInfo -from .const import CONF_MOUNT_DIR, CONF_TYPE_OWSERVER, CONF_TYPE_SYSBUS -from .model import OWServerDeviceDescription +from .const import CONF_MOUNT_DIR, CONF_TYPE_OWSERVER, CONF_TYPE_SYSBUS, DOMAIN +from .model import ( + OWDeviceDescription, + OWDirectDeviceDescription, + OWServerDeviceDescription, +) DEVICE_COUPLERS = { # Family : [branches] "1F": ["aux", "main"] } +_LOGGER = logging.getLogger(__name__) + class OneWireHub: """Hub to communicate with SysBus or OWServer.""" @@ -29,7 +48,7 @@ class OneWireHub: self.type: str | None = None self.pi1proxy: Pi1Wire | None = None self.owproxy: protocol._Proxy | None = None - self.devices: list | None = None + self.devices: list[OWDeviceDescription] | None = None async def connect(self, host: str, port: int) -> None: """Connect to the owserver host.""" @@ -56,42 +75,99 @@ class OneWireHub: port = config_entry.data[CONF_PORT] await self.connect(host, port) await self.discover_devices() + if TYPE_CHECKING: + assert self.devices + # Register discovered devices on Hub + device_registry = dr.async_get(self.hass) + for device in self.devices: + device_info: DeviceInfo = device.device_info + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers=device_info[ATTR_IDENTIFIERS], + manufacturer=device_info[ATTR_MANUFACTURER], + model=device_info[ATTR_MODEL], + name=device_info[ATTR_NAME], + via_device=device_info.get("via_device"), + ) async def discover_devices(self) -> None: """Discover all devices.""" if self.devices is None: if self.type == CONF_TYPE_SYSBUS: - assert self.pi1proxy self.devices = await self.hass.async_add_executor_job( - self.pi1proxy.find_all_sensors + self._discover_devices_sysbus ) if self.type == CONF_TYPE_OWSERVER: self.devices = await self.hass.async_add_executor_job( self._discover_devices_owserver ) + def _discover_devices_sysbus(self) -> list[OWDeviceDescription]: + """Discover all sysbus devices.""" + devices: list[OWDeviceDescription] = [] + assert self.pi1proxy + for interface in self.pi1proxy.find_all_sensors(): + family = interface.mac_address[:2] + device_id = f"{family}-{interface.mac_address[2:]}" + device_info: DeviceInfo = { + ATTR_IDENTIFIERS: {(DOMAIN, device_id)}, + ATTR_MANUFACTURER: "Maxim Integrated", + ATTR_MODEL: family, + ATTR_NAME: device_id, + } + device = OWDirectDeviceDescription( + device_info=device_info, + interface=interface, + ) + devices.append(device) + return devices + def _discover_devices_owserver( - self, path: str = "/" - ) -> list[OWServerDeviceDescription]: + self, path: str = "/", parent_id: str | None = None + ) -> list[OWDeviceDescription]: """Discover all owserver devices.""" - devices = [] + devices: list[OWDeviceDescription] = [] assert self.owproxy for device_path in self.owproxy.dir(path): + device_id = os.path.split(os.path.split(device_path)[0])[1] device_family = self.owproxy.read(f"{device_path}family").decode() + _LOGGER.debug("read `%sfamily`: %s", device_path, device_family) device_type = self.owproxy.read(f"{device_path}type").decode() + _LOGGER.debug("read `%stype`: %s", device_path, device_type) + device_info: DeviceInfo = { + ATTR_IDENTIFIERS: {(DOMAIN, device_id)}, + ATTR_MANUFACTURER: "Maxim Integrated", + ATTR_MODEL: device_type, + ATTR_NAME: device_id, + } + if parent_id: + device_info["via_device"] = (DOMAIN, parent_id) + device = OWServerDeviceDescription( + device_info=device_info, + id=device_id, + family=device_family, + path=device_path, + type=device_type, + ) + devices.append(device) if device_branches := DEVICE_COUPLERS.get(device_family): for branch in device_branches: - devices += self._discover_devices_owserver(f"{device_path}{branch}") - else: - devices.append( - { - "path": device_path, - "family": device_family, - "type": device_type, - } - ) + devices += self._discover_devices_owserver( + f"{device_path}{branch}", device_id + ) + return devices + def has_device_in_cache(self, device: DeviceEntry) -> bool: + """Check if device was present in the cache.""" + if TYPE_CHECKING: + assert self.devices + for internal_device in self.devices: + for identifier in internal_device.device_info[ATTR_IDENTIFIERS]: + if identifier in device.identifiers: + return True + return False + class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index b1f08b864b1..c13dcd4ebf3 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -7,10 +7,14 @@ from dataclasses import dataclass import logging import os from types import MappingProxyType -from typing import Any +from typing import TYPE_CHECKING, Any from pi1wire import InvalidCRCException, OneWireInterface, UnsupportResponseException +from homeassistant.components.onewire.model import ( + OWDirectDeviceDescription, + OWServerDeviceDescription, +) from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, @@ -19,10 +23,6 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, CONF_TYPE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_HUMIDITY, @@ -382,11 +382,14 @@ def get_entities( if conf_type == CONF_TYPE_OWSERVER: assert onewirehub.owproxy for device in onewirehub.devices: - family = device["family"] - device_type = device["type"] - device_id = os.path.split(os.path.split(device["path"])[0])[1] + if TYPE_CHECKING: + assert isinstance(device, OWServerDeviceDescription) + family = device.family + device_type = device.type + device_id = device.id + device_info = device.device_info device_sub_type = "std" - device_path = device["path"] + device_path = device.path if "EF" in family: device_sub_type = "HobbyBoard" family = device_type @@ -401,12 +404,6 @@ def get_entities( device_id, ) continue - device_info: DeviceInfo = { - ATTR_IDENTIFIERS: {(DOMAIN, device_id)}, - ATTR_MANUFACTURER: "Maxim Integrated", - ATTR_MODEL: device_type, - ATTR_NAME: device_id, - } for description in get_sensor_types(device_sub_type)[family]: if description.key.startswith("moisture/"): s_id = description.key.split(".")[1] @@ -421,7 +418,7 @@ def get_entities( description.native_unit_of_measurement = PERCENTAGE description.name = f"Wetness {s_id}" device_file = os.path.join( - os.path.split(device["path"])[0], description.key + os.path.split(device.path)[0], description.key ) name = f"{device_names.get(device_id, device_id)} {description.name}" entities.append( @@ -439,9 +436,13 @@ def get_entities( elif conf_type == CONF_TYPE_SYSBUS: base_dir = config[CONF_MOUNT_DIR] _LOGGER.debug("Initializing using SysBus %s", base_dir) - for p1sensor in onewirehub.devices: + for device in onewirehub.devices: + if TYPE_CHECKING: + assert isinstance(device, OWDirectDeviceDescription) + p1sensor: OneWireInterface = device.interface family = p1sensor.mac_address[:2] device_id = f"{family}-{p1sensor.mac_address[2:]}" + device_info = device.device_info if family not in DEVICE_SUPPORT_SYSBUS: _LOGGER.warning( "Ignoring unknown family (%s) of sensor found for device: %s", @@ -450,12 +451,6 @@ def get_entities( ) continue - device_info = { - ATTR_IDENTIFIERS: {(DOMAIN, device_id)}, - ATTR_MANUFACTURER: "Maxim Integrated", - ATTR_MODEL: family, - ATTR_NAME: device_id, - } description = SIMPLE_TEMPERATURE_SENSOR_DESCRIPTION device_file = f"/sys/bus/w1/devices/{device_id}/w1_slave" name = f"{device_names.get(device_id, device_id)} {description.name}" diff --git a/homeassistant/components/onewire/switch.py b/homeassistant/components/onewire/switch.py index aadc1315712..712077c62bd 100644 --- a/homeassistant/components/onewire/switch.py +++ b/homeassistant/components/onewire/switch.py @@ -4,19 +4,13 @@ from __future__ import annotations from dataclasses import dataclass import logging import os -from typing import Any +from typing import TYPE_CHECKING, Any +from homeassistant.components.onewire.model import OWServerDeviceDescription from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, - CONF_TYPE, -) +from homeassistant.const import CONF_TYPE from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( @@ -120,23 +114,16 @@ def get_entities(onewirehub: OneWireHub) -> list[SwitchEntity]: entities: list[SwitchEntity] = [] for device in onewirehub.devices: - family = device["family"] - device_type = device["type"] - device_id = os.path.split(os.path.split(device["path"])[0])[1] + if TYPE_CHECKING: + assert isinstance(device, OWServerDeviceDescription) + family = device.family + device_id = device.id + device_info = device.device_info if family not in DEVICE_SWITCHES: continue - - device_info: DeviceInfo = { - ATTR_IDENTIFIERS: {(DOMAIN, device_id)}, - ATTR_MANUFACTURER: "Maxim Integrated", - ATTR_MODEL: device_type, - ATTR_NAME: device_id, - } for description in DEVICE_SWITCHES[family]: - device_file = os.path.join( - os.path.split(device["path"])[0], description.key - ) + device_file = os.path.join(os.path.split(device.path)[0], description.key) name = f"{device_id} {description.name}" entities.append( OneWireProxySwitch( diff --git a/tests/components/onewire/__init__.py b/tests/components/onewire/__init__.py index 8223b1bc841..25ff4a15cfd 100644 --- a/tests/components/onewire/__init__.py +++ b/tests/components/onewire/__init__.py @@ -8,8 +8,16 @@ from unittest.mock import MagicMock from pyownet.protocol import ProtocolError from homeassistant.components.onewire.const import DEFAULT_SYSBUS_MOUNT_DIR -from homeassistant.const import ATTR_ENTITY_ID, ATTR_STATE +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + ATTR_STATE, +) from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceRegistry from homeassistant.helpers.entity_registry import EntityRegistry from .const import ( @@ -36,6 +44,28 @@ def check_and_enable_disabled_entities( entity_registry.async_update_entity(entity_id, **{"disabled_by": None}) +def check_device_registry( + device_registry: DeviceRegistry, expected_devices: list[MappingProxyType] +) -> None: + """Ensure that the expected_devices are correctly registered.""" + for expected_device in expected_devices: + registry_entry = device_registry.async_get_device( + expected_device[ATTR_IDENTIFIERS] + ) + assert registry_entry is not None + assert registry_entry.identifiers == expected_device[ATTR_IDENTIFIERS] + assert registry_entry.manufacturer == expected_device[ATTR_MANUFACTURER] + assert registry_entry.name == expected_device[ATTR_NAME] + assert registry_entry.model == expected_device[ATTR_MODEL] + if expected_via_device := expected_device.get("via_device"): + assert registry_entry.via_device_id is not None + parent_entry = device_registry.async_get_device({expected_via_device}) + assert parent_entry is not None + assert registry_entry.via_device_id == parent_entry.id + else: + assert registry_entry.via_device_id is None + + def check_entities( hass: HomeAssistant, entity_registry: EntityRegistry, @@ -57,39 +87,97 @@ def check_entities( def setup_owproxy_mock_devices( - owproxy: MagicMock, platform: str, device_ids: list(str) + owproxy: MagicMock, platform: str, device_ids: list[str] ) -> None: """Set up mock for owproxy.""" - dir_return_value = [] + main_dir_return_value = [] + sub_dir_side_effect = [] main_read_side_effect = [] sub_read_side_effect = [] for device_id in device_ids: - mock_device = MOCK_OWPROXY_DEVICES[device_id] - - # Setup directory listing - dir_return_value += [f"/{device_id}/"] - - # Setup device reads - main_read_side_effect += [device_id[0:2].encode()] - if ATTR_INJECT_READS in mock_device: - main_read_side_effect += mock_device[ATTR_INJECT_READS] - - # Setup sub-device reads - device_sensors = mock_device.get(platform, []) - for expected_sensor in device_sensors: - sub_read_side_effect.append(expected_sensor[ATTR_INJECT_READS]) + _setup_owproxy_mock_device( + main_dir_return_value, + sub_dir_side_effect, + main_read_side_effect, + sub_read_side_effect, + device_id, + platform, + ) # Ensure enough read side effect + dir_side_effect = [main_dir_return_value] + sub_dir_side_effect read_side_effect = ( main_read_side_effect + sub_read_side_effect + [ProtocolError("Missing injected value")] * 20 ) - owproxy.return_value.dir.return_value = dir_return_value + owproxy.return_value.dir.side_effect = dir_side_effect owproxy.return_value.read.side_effect = read_side_effect +def _setup_owproxy_mock_device( + main_dir_return_value: list, + sub_dir_side_effect: list, + main_read_side_effect: list, + sub_read_side_effect: list, + device_id: str, + platform: str, +) -> None: + """Set up mock for owproxy.""" + mock_device = MOCK_OWPROXY_DEVICES[device_id] + + # Setup directory listing + main_dir_return_value += [f"/{device_id}/"] + if "branches" in mock_device: + # Setup branch directory listing + for branch, branch_details in mock_device["branches"].items(): + sub_dir_side_effect.append( + [ # dir on branch + f"/{device_id}/{branch}/{sub_device_id}/" + for sub_device_id in branch_details + ] + ) + + _setup_owproxy_mock_device_reads( + main_read_side_effect, + sub_read_side_effect, + mock_device, + device_id, + platform, + ) + + if "branches" in mock_device: + for branch_details in mock_device["branches"].values(): + for sub_device_id, sub_device in branch_details.items(): + _setup_owproxy_mock_device_reads( + main_read_side_effect, + sub_read_side_effect, + sub_device, + sub_device_id, + platform, + ) + + +def _setup_owproxy_mock_device_reads( + main_read_side_effect: list, + sub_read_side_effect: list, + mock_device: Any, + device_id: str, + platform: str, +) -> None: + """Set up mock for owproxy.""" + # Setup device reads + main_read_side_effect += [device_id[0:2].encode()] + if ATTR_INJECT_READS in mock_device: + main_read_side_effect += mock_device[ATTR_INJECT_READS] + + # Setup sub-device reads + device_sensors = mock_device.get(platform, []) + for expected_sensor in device_sensors: + sub_read_side_effect.append(expected_sensor[ATTR_INJECT_READS]) + + def setup_sysbus_mock_devices( platform: str, device_ids: list[str] ) -> tuple[list[str], list[Any]]: diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index 93006ee4f81..91b7b618c37 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -212,12 +212,21 @@ MOCK_OWPROXY_DEVICES = { ATTR_INJECT_READS: [ b"DS2409", # read device type ], - ATTR_DEVICE_INFO: { - ATTR_IDENTIFIERS: {(DOMAIN, "1F.111111111111")}, - ATTR_MANUFACTURER: MANUFACTURER, - ATTR_MODEL: "DS2409", - ATTR_NAME: "1F.111111111111", - }, + ATTR_DEVICE_INFO: [ + { + ATTR_IDENTIFIERS: {(DOMAIN, "1F.111111111111")}, + ATTR_MANUFACTURER: MANUFACTURER, + ATTR_MODEL: "DS2409", + ATTR_NAME: "1F.111111111111", + }, + { + ATTR_IDENTIFIERS: {(DOMAIN, "1D.111111111111")}, + ATTR_MANUFACTURER: MANUFACTURER, + ATTR_MODEL: "DS2423", + ATTR_NAME: "1D.111111111111", + "via_device": (DOMAIN, "1F.111111111111"), + }, + ], "branches": { "aux": {}, "main": { @@ -225,12 +234,6 @@ MOCK_OWPROXY_DEVICES = { ATTR_INJECT_READS: [ b"DS2423", # read device type ], - ATTR_DEVICE_INFO: { - ATTR_IDENTIFIERS: {(DOMAIN, "1D.111111111111")}, - ATTR_MANUFACTURER: MANUFACTURER, - ATTR_MODEL: "DS2423", - ATTR_NAME: "1D.111111111111", - }, SENSOR_DOMAIN: [ { ATTR_DEVICE_FILE: "/1F.111111111111/main/1D.111111111111/counter.A", diff --git a/tests/components/onewire/test_binary_sensor.py b/tests/components/onewire/test_binary_sensor.py index ff9dd29c2c2..90e53924cab 100644 --- a/tests/components/onewire/test_binary_sensor.py +++ b/tests/components/onewire/test_binary_sensor.py @@ -6,15 +6,17 @@ import pytest from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.config_validation import ensure_list from . import ( check_and_enable_disabled_entities, + check_device_registry, check_entities, setup_owproxy_mock_devices, ) -from .const import MOCK_OWPROXY_DEVICES +from .const import ATTR_DEVICE_INFO, MOCK_OWPROXY_DEVICES -from tests.common import mock_registry +from tests.common import mock_device_registry, mock_registry @pytest.fixture(autouse=True) @@ -31,17 +33,19 @@ async def test_owserver_binary_sensor( This test forces all entities to be enabled. """ + device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) mock_device = MOCK_OWPROXY_DEVICES[device_id] expected_entities = mock_device.get(BINARY_SENSOR_DOMAIN, []) + expected_devices = ensure_list(mock_device.get(ATTR_DEVICE_INFO)) setup_owproxy_mock_devices(owproxy, BINARY_SENSOR_DOMAIN, [device_id]) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() + check_device_registry(device_registry, expected_devices) assert len(entity_registry.entities) == len(expected_entities) - check_and_enable_disabled_entities(entity_registry, expected_entities) setup_owproxy_mock_devices(owproxy, BINARY_SENSOR_DOMAIN, [device_id]) diff --git a/tests/components/onewire/test_sensor.py b/tests/components/onewire/test_sensor.py index 23af72bac41..ffa9d0b5319 100644 --- a/tests/components/onewire/test_sensor.py +++ b/tests/components/onewire/test_sensor.py @@ -1,44 +1,24 @@ """Tests for 1-Wire sensor platform.""" from unittest.mock import MagicMock, patch -from pyownet.protocol import Error as ProtocolError import pytest -from homeassistant.components.onewire.const import DOMAIN -from homeassistant.components.sensor import ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_ENTITY_ID, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, - ATTR_STATE, - ATTR_UNIT_OF_MEASUREMENT, -) from homeassistant.core import HomeAssistant +from homeassistant.helpers.config_validation import ensure_list from . import ( check_and_enable_disabled_entities, + check_device_registry, check_entities, setup_owproxy_mock_devices, setup_sysbus_mock_devices, ) -from .const import ( - ATTR_DEVICE_FILE, - ATTR_DEVICE_INFO, - ATTR_INJECT_READS, - ATTR_UNIQUE_ID, - MOCK_OWPROXY_DEVICES, - MOCK_SYSBUS_DEVICES, -) +from .const import ATTR_DEVICE_INFO, MOCK_OWPROXY_DEVICES, MOCK_SYSBUS_DEVICES from tests.common import mock_device_registry, mock_registry -MOCK_COUPLERS = { - key: value for (key, value) in MOCK_OWPROXY_DEVICES.items() if "branches" in value -} - @pytest.fixture(autouse=True) def override_platforms(): @@ -47,99 +27,36 @@ def override_platforms(): yield -@pytest.mark.parametrize("device_id", ["1F.111111111111"], indirect=True) -async def test_sensors_on_owserver_coupler( - hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock, device_id: str -): - """Test for 1-Wire sensors connected to DS2409 coupler.""" - - entity_registry = mock_registry(hass) - - mock_coupler = MOCK_COUPLERS[device_id] - - dir_side_effect = [] # List of lists of string - read_side_effect = [] # List of byte arrays - - dir_side_effect.append([f"/{device_id}/"]) # dir on root - read_side_effect.append(device_id[0:2].encode()) # read family on root - if ATTR_INJECT_READS in mock_coupler: - read_side_effect += mock_coupler[ATTR_INJECT_READS] - - expected_entities = [] - for branch, branch_details in mock_coupler["branches"].items(): - dir_side_effect.append( - [ # dir on branch - f"/{device_id}/{branch}/{sub_device_id}/" - for sub_device_id in branch_details - ] - ) - - for sub_device_id, sub_device in branch_details.items(): - read_side_effect.append(sub_device_id[0:2].encode()) - if ATTR_INJECT_READS in sub_device: - read_side_effect.extend(sub_device[ATTR_INJECT_READS]) - - expected_entities += sub_device[SENSOR_DOMAIN] - for expected_entity in sub_device[SENSOR_DOMAIN]: - read_side_effect.append(expected_entity[ATTR_INJECT_READS]) - - # Ensure enough read side effect - read_side_effect.extend([ProtocolError("Missing injected value")] * 10) - owproxy.return_value.dir.side_effect = dir_side_effect - owproxy.return_value.read.side_effect = read_side_effect - - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert len(entity_registry.entities) == len(expected_entities) - - for expected_entity in expected_entities: - entity_id = expected_entity[ATTR_ENTITY_ID] - registry_entry = entity_registry.entities.get(entity_id) - assert registry_entry is not None - assert registry_entry.unique_id == expected_entity[ATTR_UNIQUE_ID] - state = hass.states.get(entity_id) - assert state.state == expected_entity[ATTR_STATE] - for attr in (ATTR_DEVICE_CLASS, ATTR_STATE_CLASS, ATTR_UNIT_OF_MEASUREMENT): - assert state.attributes.get(attr) == expected_entity.get(attr) - assert state.attributes[ATTR_DEVICE_FILE] == expected_entity[ATTR_DEVICE_FILE] - - -async def test_owserver_setup_valid_device( +async def test_owserver_sensor( hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock, device_id: str ): """Test for 1-Wire device. As they would be on a clean setup: all binary-sensors and switches disabled. """ - entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) + entity_registry = mock_registry(hass) mock_device = MOCK_OWPROXY_DEVICES[device_id] expected_entities = mock_device.get(SENSOR_DOMAIN, []) + if "branches" in mock_device: + for branch_details in mock_device["branches"].values(): + for sub_device in branch_details.values(): + expected_entities += sub_device[SENSOR_DOMAIN] + expected_devices = ensure_list(mock_device.get(ATTR_DEVICE_INFO)) setup_owproxy_mock_devices(owproxy, SENSOR_DOMAIN, [device_id]) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() + check_device_registry(device_registry, expected_devices) assert len(entity_registry.entities) == len(expected_entities) - check_and_enable_disabled_entities(entity_registry, expected_entities) setup_owproxy_mock_devices(owproxy, SENSOR_DOMAIN, [device_id]) await hass.config_entries.async_reload(config_entry.entry_id) await hass.async_block_till_done() - if len(expected_entities) > 0: - device_info = mock_device[ATTR_DEVICE_INFO] - assert len(device_registry.devices) == 1 - registry_entry = device_registry.async_get_device({(DOMAIN, device_id)}) - assert registry_entry is not None - assert registry_entry.identifiers == {(DOMAIN, device_id)} - assert registry_entry.manufacturer == device_info[ATTR_MANUFACTURER] - assert registry_entry.name == device_info[ATTR_NAME] - assert registry_entry.model == device_info[ATTR_MODEL] - check_entities(hass, entity_registry, expected_entities) @@ -149,9 +66,8 @@ async def test_onewiredirect_setup_valid_device( hass: HomeAssistant, sysbus_config_entry: ConfigEntry, device_id: str ): """Test that sysbus config entry works correctly.""" - - entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) + entity_registry = mock_registry(hass) glob_result, read_side_effect = setup_sysbus_mock_devices( SENSOR_DOMAIN, [device_id] @@ -159,6 +75,7 @@ async def test_onewiredirect_setup_valid_device( mock_device = MOCK_SYSBUS_DEVICES[device_id] expected_entities = mock_device.get(SENSOR_DOMAIN, []) + expected_devices = ensure_list(mock_device.get(ATTR_DEVICE_INFO)) with patch("pi1wire._finder.glob.glob", return_value=glob_result,), patch( "pi1wire.OneWire.get_temperature", @@ -167,16 +84,6 @@ async def test_onewiredirect_setup_valid_device( await hass.config_entries.async_setup(sysbus_config_entry.entry_id) await hass.async_block_till_done() + check_device_registry(device_registry, expected_devices) assert len(entity_registry.entities) == len(expected_entities) - - if len(expected_entities) > 0: - device_info = mock_device[ATTR_DEVICE_INFO] - assert len(device_registry.devices) == 1 - registry_entry = device_registry.async_get_device({(DOMAIN, device_id)}) - assert registry_entry is not None - assert registry_entry.identifiers == {(DOMAIN, device_id)} - assert registry_entry.manufacturer == device_info[ATTR_MANUFACTURER] - assert registry_entry.name == device_info[ATTR_NAME] - assert registry_entry.model == device_info[ATTR_MODEL] - check_entities(hass, entity_registry, expected_entities) diff --git a/tests/components/onewire/test_switch.py b/tests/components/onewire/test_switch.py index 766a41a5862..ffe5042b514 100644 --- a/tests/components/onewire/test_switch.py +++ b/tests/components/onewire/test_switch.py @@ -13,15 +13,17 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.config_validation import ensure_list from . import ( check_and_enable_disabled_entities, + check_device_registry, check_entities, setup_owproxy_mock_devices, ) -from .const import MOCK_OWPROXY_DEVICES +from .const import ATTR_DEVICE_INFO, MOCK_OWPROXY_DEVICES -from tests.common import mock_registry +from tests.common import mock_device_registry, mock_registry @pytest.fixture(autouse=True) @@ -38,17 +40,19 @@ async def test_owserver_switch( This test forces all entities to be enabled. """ + device_registry = mock_device_registry(hass) entity_registry = mock_registry(hass) mock_device = MOCK_OWPROXY_DEVICES[device_id] expected_entities = mock_device.get(SWITCH_DOMAIN, []) + expected_devices = ensure_list(mock_device.get(ATTR_DEVICE_INFO)) setup_owproxy_mock_devices(owproxy, SWITCH_DOMAIN, [device_id]) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() + check_device_registry(device_registry, expected_devices) assert len(entity_registry.entities) == len(expected_entities) - check_and_enable_disabled_entities(entity_registry, expected_entities) setup_owproxy_mock_devices(owproxy, SWITCH_DOMAIN, [device_id]) From 58c5b5058cf4036f1ca8bafc0545f20d140451bc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 22 Oct 2021 11:52:48 +0200 Subject: [PATCH 0652/1038] Use DeviceInfo on components with via_device (H) (#58211) Co-authored-by: epenet --- .../components/hive/binary_sensor.py | 19 +++++++------- homeassistant/components/hive/climate.py | 19 +++++++------- homeassistant/components/hive/light.py | 19 +++++++------- homeassistant/components/hive/sensor.py | 19 +++++++------- homeassistant/components/hive/switch.py | 22 +++++++++------- homeassistant/components/hive/water_heater.py | 19 +++++++------- .../homematicip_cloud/alarm_control_panel.py | 14 +++++----- .../components/homematicip_cloud/climate.py | 14 +++++----- .../homematicip_cloud/generic_entity.py | 16 ++++++------ homeassistant/components/hue/sensor_device.py | 18 ++++++------- .../hunterdouglas_powerview/entity.py | 26 ++++++++++--------- 11 files changed, 108 insertions(+), 97 deletions(-) diff --git a/homeassistant/components/hive/binary_sensor.py b/homeassistant/components/hive/binary_sensor.py index d5f1ca53afd..71a2e787aec 100644 --- a/homeassistant/components/hive/binary_sensor.py +++ b/homeassistant/components/hive/binary_sensor.py @@ -9,6 +9,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_SOUND, BinarySensorEntity, ) +from homeassistant.helpers.entity import DeviceInfo from . import HiveEntity from .const import ATTR_MODE, DOMAIN @@ -46,16 +47,16 @@ class HiveBinarySensorEntity(HiveEntity, BinarySensorEntity): return self._unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device information.""" - return { - "identifiers": {(DOMAIN, self.device["device_id"])}, - "name": self.device["device_name"], - "model": self.device["deviceData"]["model"], - "manufacturer": self.device["deviceData"]["manufacturer"], - "sw_version": self.device["deviceData"]["version"], - "via_device": (DOMAIN, self.device["parentDevice"]), - } + return DeviceInfo( + identifiers={(DOMAIN, self.device["device_id"])}, + name=self.device["device_name"], + model=self.device["deviceData"]["model"], + manufacturer=self.device["deviceData"]["manufacturer"], + sw_version=self.device["deviceData"]["version"], + via_device=(DOMAIN, self.device["parentDevice"]), + ) @property def device_class(self): diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index 80a6bb0941e..930a6698518 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -19,6 +19,7 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.entity import DeviceInfo from . import HiveEntity, refresh_system from .const import ( @@ -117,16 +118,16 @@ class HiveClimateEntity(HiveEntity, ClimateEntity): return self._unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device information.""" - return { - "identifiers": {(DOMAIN, self.device["device_id"])}, - "name": self.device["device_name"], - "model": self.device["deviceData"]["model"], - "manufacturer": self.device["deviceData"]["manufacturer"], - "sw_version": self.device["deviceData"]["version"], - "via_device": (DOMAIN, self.device["parentDevice"]), - } + return DeviceInfo( + identifiers={(DOMAIN, self.device["device_id"])}, + name=self.device["device_name"], + model=self.device["deviceData"]["model"], + manufacturer=self.device["deviceData"]["manufacturer"], + sw_version=self.device["deviceData"]["version"], + via_device=(DOMAIN, self.device["parentDevice"]), + ) @property def supported_features(self): diff --git a/homeassistant/components/hive/light.py b/homeassistant/components/hive/light.py index 46e8c5b5790..37158da7b58 100644 --- a/homeassistant/components/hive/light.py +++ b/homeassistant/components/hive/light.py @@ -10,6 +10,7 @@ from homeassistant.components.light import ( SUPPORT_COLOR_TEMP, LightEntity, ) +from homeassistant.helpers.entity import DeviceInfo import homeassistant.util.color as color_util from . import HiveEntity, refresh_system @@ -40,16 +41,16 @@ class HiveDeviceLight(HiveEntity, LightEntity): return self._unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device information.""" - return { - "identifiers": {(DOMAIN, self.device["device_id"])}, - "name": self.device["device_name"], - "model": self.device["deviceData"]["model"], - "manufacturer": self.device["deviceData"]["manufacturer"], - "sw_version": self.device["deviceData"]["version"], - "via_device": (DOMAIN, self.device["parentDevice"]), - } + return DeviceInfo( + identifiers={(DOMAIN, self.device["device_id"])}, + name=self.device["device_name"], + model=self.device["deviceData"]["model"], + manufacturer=self.device["deviceData"]["manufacturer"], + sw_version=self.device["deviceData"]["version"], + via_device=(DOMAIN, self.device["parentDevice"]), + ) @property def name(self): diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index 5ea81bff123..abb45d04dec 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -3,6 +3,7 @@ from datetime import timedelta from homeassistant.components.sensor import DEVICE_CLASS_BATTERY, SensorEntity +from homeassistant.helpers.entity import DeviceInfo from . import HiveEntity from .const import DOMAIN @@ -35,16 +36,16 @@ class HiveSensorEntity(HiveEntity, SensorEntity): return self._unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device information.""" - return { - "identifiers": {(DOMAIN, self.device["device_id"])}, - "name": self.device["device_name"], - "model": self.device["deviceData"]["model"], - "manufacturer": self.device["deviceData"]["manufacturer"], - "sw_version": self.device["deviceData"]["version"], - "via_device": (DOMAIN, self.device["parentDevice"]), - } + return DeviceInfo( + identifiers={(DOMAIN, self.device["device_id"])}, + name=self.device["device_name"], + model=self.device["deviceData"]["model"], + manufacturer=self.device["deviceData"]["manufacturer"], + sw_version=self.device["deviceData"]["version"], + via_device=(DOMAIN, self.device["parentDevice"]), + ) @property def available(self): diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py index 7ad81a25f0e..9c009e1627c 100644 --- a/homeassistant/components/hive/switch.py +++ b/homeassistant/components/hive/switch.py @@ -1,7 +1,10 @@ """Support for the Hive switches.""" +from __future__ import annotations + from datetime import timedelta from homeassistant.components.switch import SwitchEntity +from homeassistant.helpers.entity import DeviceInfo from . import HiveEntity, refresh_system from .const import ATTR_MODE, DOMAIN @@ -31,17 +34,18 @@ class HiveDevicePlug(HiveEntity, SwitchEntity): return self._unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo | None: """Return device information.""" if self.device["hiveType"] == "activeplug": - return { - "identifiers": {(DOMAIN, self.device["device_id"])}, - "name": self.device["device_name"], - "model": self.device["deviceData"]["model"], - "manufacturer": self.device["deviceData"]["manufacturer"], - "sw_version": self.device["deviceData"]["version"], - "via_device": (DOMAIN, self.device["parentDevice"]), - } + return DeviceInfo( + identifiers={(DOMAIN, self.device["device_id"])}, + name=self.device["device_name"], + model=self.device["deviceData"]["model"], + manufacturer=self.device["deviceData"]["manufacturer"], + sw_version=self.device["deviceData"]["version"], + via_device=(DOMAIN, self.device["parentDevice"]), + ) + return None @property def name(self): diff --git a/homeassistant/components/hive/water_heater.py b/homeassistant/components/hive/water_heater.py index b9377a378c3..8cefda074b6 100644 --- a/homeassistant/components/hive/water_heater.py +++ b/homeassistant/components/hive/water_heater.py @@ -13,6 +13,7 @@ from homeassistant.components.water_heater import ( ) from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.entity import DeviceInfo from . import HiveEntity, refresh_system from .const import ( @@ -78,16 +79,16 @@ class HiveWaterHeater(HiveEntity, WaterHeaterEntity): return self._unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device information.""" - return { - "identifiers": {(DOMAIN, self.device["device_id"])}, - "name": self.device["device_name"], - "model": self.device["deviceData"]["model"], - "manufacturer": self.device["deviceData"]["manufacturer"], - "sw_version": self.device["deviceData"]["version"], - "via_device": (DOMAIN, self.device["parentDevice"]), - } + return DeviceInfo( + identifiers={(DOMAIN, self.device["device_id"])}, + name=self.device["device_name"], + model=self.device["deviceData"]["model"], + manufacturer=self.device["deviceData"]["manufacturer"], + sw_version=self.device["deviceData"]["version"], + via_device=(DOMAIN, self.device["parentDevice"]), + ) @property def supported_features(self): diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index 212737b7018..6249b004d23 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -47,13 +47,13 @@ class HomematicipAlarmControlPanelEntity(AlarmControlPanelEntity): @property def device_info(self) -> DeviceInfo: """Return device specific attributes.""" - return { - "identifiers": {(HMIPC_DOMAIN, f"ACP {self._home.id}")}, - "name": self.name, - "manufacturer": "eQ-3", - "model": CONST_ALARM_CONTROL_PANEL_NAME, - "via_device": (HMIPC_DOMAIN, self._home.id), - } + return DeviceInfo( + identifiers={(HMIPC_DOMAIN, f"ACP {self._home.id}")}, + name=self.name, + manufacturer="eQ-3", + model=CONST_ALARM_CONTROL_PANEL_NAME, + via_device=(HMIPC_DOMAIN, self._home.id), + ) @property def state(self) -> str: diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 060d265c62a..df1c3d11f31 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -76,13 +76,13 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): @property def device_info(self) -> DeviceInfo: """Return device specific attributes.""" - return { - "identifiers": {(HMIPC_DOMAIN, self._device.id)}, - "name": self._device.label, - "manufacturer": "eQ-3", - "model": self._device.modelType, - "via_device": (HMIPC_DOMAIN, self._device.homeId), - } + return DeviceInfo( + identifiers={(HMIPC_DOMAIN, self._device.id)}, + name=self._device.label, + manufacturer="eQ-3", + model=self._device.modelType, + via_device=(HMIPC_DOMAIN, self._device.homeId), + ) @property def temperature_unit(self) -> str: diff --git a/homeassistant/components/homematicip_cloud/generic_entity.py b/homeassistant/components/homematicip_cloud/generic_entity.py index 976650aa46b..2de3ded42db 100644 --- a/homeassistant/components/homematicip_cloud/generic_entity.py +++ b/homeassistant/components/homematicip_cloud/generic_entity.py @@ -96,18 +96,18 @@ class HomematicipGenericEntity(Entity): """Return device specific attributes.""" # Only physical devices should be HA devices. if isinstance(self._device, AsyncDevice): - return { - "identifiers": { + return DeviceInfo( + identifiers={ # Serial numbers of Homematic IP device (HMIPC_DOMAIN, self._device.id) }, - "name": self._device.label, - "manufacturer": self._device.oem, - "model": self._device.modelType, - "sw_version": self._device.firmwareVersion, + name=self._device.label, + manufacturer=self._device.oem, + model=self._device.modelType, + sw_version=self._device.firmwareVersion, # Link to the homematic ip access point. - "via_device": (HMIPC_DOMAIN, self._device.homeId), - } + via_device=(HMIPC_DOMAIN, self._device.homeId), + ) return None async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/hue/sensor_device.py b/homeassistant/components/hue/sensor_device.py index 8670f0853a3..4efb89a913f 100644 --- a/homeassistant/components/hue/sensor_device.py +++ b/homeassistant/components/hue/sensor_device.py @@ -40,19 +40,19 @@ class GenericHueDevice(entity.Entity): return self.primary_sensor.raw.get("swupdate", {}).get("state") @property - def device_info(self): + def device_info(self) -> entity.DeviceInfo: """Return the device info. Links individual entities together in the hass device registry. """ - return { - "identifiers": {(HUE_DOMAIN, self.device_id)}, - "name": self.primary_sensor.name, - "manufacturer": self.primary_sensor.manufacturername, - "model": (self.primary_sensor.productname or self.primary_sensor.modelid), - "sw_version": self.primary_sensor.swversion, - "via_device": (HUE_DOMAIN, self.bridge.api.config.bridgeid), - } + return entity.DeviceInfo( + identifiers={(HUE_DOMAIN, self.device_id)}, + name=self.primary_sensor.name, + manufacturer=self.primary_sensor.manufacturername, + model=(self.primary_sensor.productname or self.primary_sensor.modelid), + sw_version=self.primary_sensor.swversion, + via_device=(HUE_DOMAIN, self.bridge.api.config.bridgeid), + ) async def async_added_to_hass(self) -> None: """Handle entity being added to Home Assistant.""" diff --git a/homeassistant/components/hunterdouglas_powerview/entity.py b/homeassistant/components/hunterdouglas_powerview/entity.py index db4b984703c..a5a4fe852fa 100644 --- a/homeassistant/components/hunterdouglas_powerview/entity.py +++ b/homeassistant/components/hunterdouglas_powerview/entity.py @@ -2,7 +2,9 @@ from aiopvapi.resources.shade import ATTR_TYPE +from homeassistant.const import ATTR_MODEL, ATTR_SW_VERSION import homeassistant.helpers.device_registry as dr +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -63,21 +65,21 @@ class ShadeEntity(HDEntity): self._shade = shade @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" - device_info = { - "identifiers": {(DOMAIN, self._shade.id)}, - "name": self._shade_name, - "suggested_area": self._room_name, - "manufacturer": MANUFACTURER, - "model": str(self._shade.raw_data[ATTR_TYPE]), - "via_device": (DOMAIN, self._device_info[DEVICE_SERIAL_NUMBER]), - } + device_info = DeviceInfo( + identifiers={(DOMAIN, self._shade.id)}, + name=self._shade_name, + suggested_area=self._room_name, + manufacturer=MANUFACTURER, + model=str(self._shade.raw_data[ATTR_TYPE]), + via_device=(DOMAIN, self._device_info[DEVICE_SERIAL_NUMBER]), + ) for shade in self._shade.shade_types: - if shade.shade_type == device_info["model"]: - device_info["model"] = shade.description + if shade.shade_type == device_info[ATTR_MODEL]: + device_info[ATTR_MODEL] = shade.description break if FIRMWARE not in self._shade.raw_data: @@ -86,6 +88,6 @@ class ShadeEntity(HDEntity): firmware = self._shade.raw_data[FIRMWARE] sw_version = f"{firmware[FIRMWARE_REVISION]}.{firmware[FIRMWARE_SUB_REVISION]}.{firmware[FIRMWARE_BUILD]}" - device_info["sw_version"] = sw_version + device_info[ATTR_SW_VERSION] = sw_version return device_info From 024c892b2afb3ac67fb025f18b97e066515aa566 Mon Sep 17 00:00:00 2001 From: Tom Brien Date: Fri, 22 Oct 2021 11:04:12 +0100 Subject: [PATCH 0653/1038] Remove black color name for light dropdowns (#58207) --- homeassistant/components/light/services.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 3b7df4e70c5..f3fe306eb90 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -53,7 +53,7 @@ turn_on: - "azure" - "beige" - "bisque" - - "black" + # Black is omitted from this list as nonsensical for lights - "blanchedalmond" - "blue" - "blueviolet" @@ -362,7 +362,7 @@ toggle: - "azure" - "beige" - "bisque" - - "black" + # Black is omitted from this list as nonsensical for lights - "blanchedalmond" - "blue" - "blueviolet" From 5c3d2a507108239b760fc8276bd82bf89c22261d Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 22 Oct 2021 04:11:10 -0600 Subject: [PATCH 0654/1038] Make sure IQVIA data storage conforms to standards (#57811) --- homeassistant/components/iqvia/__init__.py | 12 +++++++----- homeassistant/components/iqvia/const.py | 2 -- homeassistant/components/iqvia/sensor.py | 11 +++++------ 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index 0a782669846..8d556251e4e 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -24,7 +24,6 @@ from homeassistant.helpers.update_coordinator import ( from .const import ( CONF_ZIP_CODE, - DATA_COORDINATOR, DOMAIN, LOGGER, TYPE_ALLERGY_FORECAST, @@ -45,7 +44,7 @@ PLATFORMS = ["sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up IQVIA as config entry.""" hass.data.setdefault(DOMAIN, {}) - coordinators = {} + hass.data[DOMAIN][entry.entry_id] = {} if not entry.unique_id: # If the config entry doesn't already have a unique ID, set one: @@ -67,7 +66,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return cast(Dict[str, Any], data) + coordinators = {} init_data_update_tasks = [] + for sensor_type, api_coro in ( (TYPE_ALLERGY_FORECAST, client.allergens.extended), (TYPE_ALLERGY_INDEX, client.allergens.current), @@ -93,7 +94,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # API calls fail: raise ConfigEntryNotReady() - hass.data[DOMAIN].setdefault(DATA_COORDINATOR, {})[entry.entry_id] = coordinators + hass.data[DOMAIN][entry.entry_id] = coordinators hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -103,7 +104,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload an OpenUV config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok @@ -139,7 +141,7 @@ class IQVIAEntity(CoordinatorEntity): if self.entity_description.key == TYPE_ALLERGY_FORECAST: self.async_on_remove( - self.hass.data[DOMAIN][DATA_COORDINATOR][self._entry.entry_id][ + self.hass.data[DOMAIN][self._entry.entry_id][ TYPE_ALLERGY_OUTLOOK ].async_add_listener(self._handle_coordinator_update) ) diff --git a/homeassistant/components/iqvia/const.py b/homeassistant/components/iqvia/const.py index cbcda26982e..3ed961f2e74 100644 --- a/homeassistant/components/iqvia/const.py +++ b/homeassistant/components/iqvia/const.py @@ -7,8 +7,6 @@ DOMAIN = "iqvia" CONF_ZIP_CODE = "zip_code" -DATA_COORDINATOR = "coordinator" - TYPE_ALLERGY_FORECAST = "allergy_average_forecasted" TYPE_ALLERGY_INDEX = "allergy_index" TYPE_ALLERGY_OUTLOOK = "allergy_outlook" diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index 187816f5f9f..d4c01c6fb67 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -18,7 +18,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import IQVIAEntity from .const import ( - DATA_COORDINATOR, DOMAIN, TYPE_ALLERGY_FORECAST, TYPE_ALLERGY_INDEX, @@ -133,7 +132,7 @@ async def async_setup_entry( """Set up IQVIA sensors based on a config entry.""" sensors: list[ForecastSensor | IndexSensor] = [ ForecastSensor( - hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ + hass.data[DOMAIN][entry.entry_id][ API_CATEGORY_MAPPING.get(description.key, description.key) ], entry, @@ -144,7 +143,7 @@ async def async_setup_entry( sensors.extend( [ IndexSensor( - hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ + hass.data[DOMAIN][entry.entry_id][ API_CATEGORY_MAPPING.get(description.key, description.key) ], entry, @@ -206,9 +205,9 @@ class ForecastSensor(IQVIAEntity, SensorEntity): ) if self.entity_description.key == TYPE_ALLERGY_FORECAST: - outlook_coordinator = self.hass.data[DOMAIN][DATA_COORDINATOR][ - self._entry.entry_id - ][TYPE_ALLERGY_OUTLOOK] + outlook_coordinator = self.hass.data[DOMAIN][self._entry.entry_id][ + TYPE_ALLERGY_OUTLOOK + ] if not outlook_coordinator.last_update_success: return From 8dfa628af0803b643fe666808597913f21249818 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 22 Oct 2021 12:12:19 +0200 Subject: [PATCH 0655/1038] Use DeviceInfo on components with via_device (A-G) (#58210) Co-authored-by: epenet --- homeassistant/components/acmeda/base.py | 14 +++++------ homeassistant/components/bosch_shc/entity.py | 16 ++++++------ .../components/deconz/deconz_device.py | 23 +++++++++-------- homeassistant/components/deconz/light.py | 17 +++++++------ homeassistant/components/directv/const.py | 3 --- homeassistant/components/directv/entity.py | 25 +++++++------------ homeassistant/components/elkm1/__init__.py | 10 ++++---- homeassistant/components/freebox/sensor.py | 14 +++++------ homeassistant/components/fritz/common.py | 16 ++++++------ homeassistant/components/guardian/__init__.py | 12 ++++----- 10 files changed, 71 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/acmeda/base.py b/homeassistant/components/acmeda/base.py index 459f4ab2097..df835950380 100644 --- a/homeassistant/components/acmeda/base.py +++ b/homeassistant/components/acmeda/base.py @@ -77,11 +77,11 @@ class AcmedaBase(entity.Entity): return self.roller.name @property - def device_info(self): + def device_info(self) -> entity.DeviceInfo: """Return the device info.""" - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.roller.name, - "manufacturer": "Rollease Acmeda", - "via_device": (DOMAIN, self.roller.hub.id), - } + return entity.DeviceInfo( + identifiers={(DOMAIN, self.unique_id)}, + name=self.roller.name, + manufacturer="Rollease Acmeda", + via_device=(DOMAIN, self.roller.hub.id), + ) diff --git a/homeassistant/components/bosch_shc/entity.py b/homeassistant/components/bosch_shc/entity.py index a8966ce2f4a..1176c29e351 100644 --- a/homeassistant/components/bosch_shc/entity.py +++ b/homeassistant/components/bosch_shc/entity.py @@ -2,7 +2,7 @@ from boschshcpy.device import SHCDevice from homeassistant.helpers.device_registry import async_get as get_dev_reg -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN @@ -28,18 +28,18 @@ class SHCEntity(Entity): self._entry_id = entry_id self._attr_name = device.name self._attr_unique_id = device.serial - self._attr_device_info = { - "identifiers": {(DOMAIN, device.id)}, - "name": device.name, - "manufacturer": device.manufacturer, - "model": device.device_model, - "via_device": ( + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device.id)}, + name=device.name, + manufacturer=device.manufacturer, + model=device.device_model, + via_device=( DOMAIN, device.parent_device_id if device.parent_device_id is not None else parent_id, ), - } + ) async def async_added_to_hass(self): """Subscribe to SHC events.""" diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index e8f27e35f98..bbd4051c177 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -1,9 +1,10 @@ """Base class for deCONZ devices.""" +from __future__ import annotations from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN as DECONZ_DOMAIN @@ -30,20 +31,20 @@ class DeconzBase: return self._device.unique_id.split("-", 1)[0] @property - def device_info(self): + def device_info(self) -> DeviceInfo | None: """Return a device description for device registry.""" if self.serial is None: return None - return { - "connections": {(CONNECTION_ZIGBEE, self.serial)}, - "identifiers": {(DECONZ_DOMAIN, self.serial)}, - "manufacturer": self._device.manufacturer, - "model": self._device.model_id, - "name": self._device.name, - "sw_version": self._device.software_version, - "via_device": (DECONZ_DOMAIN, self.gateway.api.config.bridge_id), - } + return DeviceInfo( + connections={(CONNECTION_ZIGBEE, self.serial)}, + identifiers={(DECONZ_DOMAIN, self.serial)}, + manufacturer=self._device.manufacturer, + model=self._device.model_id, + name=self._device.name, + sw_version=self._device.software_version, + via_device=(DECONZ_DOMAIN, self.gateway.api.config.bridge_id), + ) class DeconzDevice(DeconzBase, Entity): diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 0069e0fb7d9..6bb4f5c5b00 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -35,6 +35,7 @@ from homeassistant.components.light import ( ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.util.color import color_hs_to_xy from .const import DOMAIN as DECONZ_DOMAIN, POWER_PLUGS @@ -266,15 +267,15 @@ class DeconzGroup(DeconzBaseLight): return self._unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" - return { - "identifiers": {(DECONZ_DOMAIN, self.unique_id)}, - "manufacturer": "Dresden Elektronik", - "model": "deCONZ group", - "name": self._device.name, - "via_device": (DECONZ_DOMAIN, self.gateway.api.config.bridge_id), - } + return DeviceInfo( + identifiers={(DECONZ_DOMAIN, self.unique_id)}, + manufacturer="Dresden Elektronik", + model="deCONZ group", + name=self._device.name, + via_device=(DECONZ_DOMAIN, self.gateway.api.config.bridge_id), + ) @property def extra_state_attributes(self): diff --git a/homeassistant/components/directv/const.py b/homeassistant/components/directv/const.py index b840b7bd2dc..e90fd6879c7 100644 --- a/homeassistant/components/directv/const.py +++ b/homeassistant/components/directv/const.py @@ -1,6 +1,4 @@ """Constants for the DirecTV integration.""" -from typing import Final - DOMAIN = "directv" # Attributes @@ -8,7 +6,6 @@ ATTR_MEDIA_CURRENTLY_RECORDING = "media_currently_recording" ATTR_MEDIA_RATING = "media_rating" ATTR_MEDIA_RECORDED = "media_recorded" ATTR_MEDIA_START_TIME = "media_start_time" -ATTR_VIA_DEVICE: Final = "via_device" CONF_RECEIVER_ID = "receiver_id" diff --git a/homeassistant/components/directv/entity.py b/homeassistant/components/directv/entity.py index 2e6ffb81a52..b3ac8fd7df6 100644 --- a/homeassistant/components/directv/entity.py +++ b/homeassistant/components/directv/entity.py @@ -3,16 +3,9 @@ from __future__ import annotations from directv import DIRECTV -from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, - ATTR_SW_VERSION, -) from homeassistant.helpers.entity import DeviceInfo, Entity -from .const import ATTR_VIA_DEVICE, DOMAIN +from .const import DOMAIN class DIRECTVEntity(Entity): @@ -28,11 +21,11 @@ class DIRECTVEntity(Entity): @property def device_info(self) -> DeviceInfo: """Return device information about this DirecTV receiver.""" - return { - ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)}, - ATTR_NAME: self.name, - ATTR_MANUFACTURER: self.dtv.device.info.brand, - ATTR_MODEL: None, - ATTR_SW_VERSION: self.dtv.device.info.version, - ATTR_VIA_DEVICE: (DOMAIN, self.dtv.device.info.receiver_id), - } + return DeviceInfo( + identifiers={(DOMAIN, self._device_id)}, + name=self.name, + manufacturer=self.dtv.device.info.brand, + model=None, + sw_version=self.dtv.device.info.version, + via_device=(DOMAIN, self.dtv.device.info.receiver_id), + ) diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 0b4fa26e833..775ac466445 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -26,7 +26,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util @@ -452,11 +452,11 @@ class ElkEntity(Entity): self._element_callback(self._element, {}) @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Device info connecting via the ElkM1 system.""" - return { - "via_device": (DOMAIN, f"{self._prefix}_system"), - } + return DeviceInfo( + via_device=(DOMAIN, f"{self._prefix}_system"), + ) class ElkAttachedEntity(ElkEntity): diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 654a73b786c..6b56300d743 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -163,16 +163,16 @@ class FreeboxDiskSensor(FreeboxSensor): @property def device_info(self) -> DeviceInfo: """Return the device information.""" - return { - "identifiers": {(DOMAIN, self._disk["id"])}, - "name": f"Disk {self._disk['id']}", - "model": self._disk["model"], - "sw_version": self._disk["firmware"], - "via_device": ( + return DeviceInfo( + identifiers={(DOMAIN, self._disk["id"])}, + name=f"Disk {self._disk['id']}", + model=self._disk["model"], + sw_version=self._disk["firmware"], + via_device=( DOMAIN, self._router.mac, ), - } + ) @callback def async_update_state(self) -> None: diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 053b0e117d9..89356ac5b42 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -351,17 +351,17 @@ class FritzDeviceBase(Entity): @property def device_info(self) -> DeviceInfo: """Return the device information.""" - return { - "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, - "identifiers": {(DOMAIN, self._mac)}, - "default_name": self.name, - "default_manufacturer": "AVM", - "default_model": "FRITZ!Box Tracked device", - "via_device": ( + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, self._mac)}, + identifiers={(DOMAIN, self._mac)}, + default_name=self.name, + default_manufacturer="AVM", + default_model="FRITZ!Box Tracked device", + via_device=( DOMAIN, self._router.unique_id, ), - } + ) @property def should_poll(self) -> bool: diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 94413c76578..9cb2590a289 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -11,7 +11,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, CONF_IP_ADDRESS, CONF_PORT from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -244,11 +244,11 @@ class PairedSensorEntity(GuardianEntity): super().__init__(entry, description) paired_sensor_uid = coordinator.data["uid"] - self._attr_device_info = { - "identifiers": {(DOMAIN, paired_sensor_uid)}, - "name": f"Guardian Paired Sensor {paired_sensor_uid}", - "via_device": (DOMAIN, entry.data[CONF_UID]), - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, paired_sensor_uid)}, + name=f"Guardian Paired Sensor {paired_sensor_uid}", + via_device=(DOMAIN, entry.data[CONF_UID]), + ) self._attr_name = ( f"Guardian Paired Sensor {paired_sensor_uid}: {description.name}" ) From 8bc10db0bb79f780922b1bddc51b3204fca58bf7 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 22 Oct 2021 04:14:58 -0600 Subject: [PATCH 0656/1038] Make sure RainMachine data storage conforms to standards (#57816) --- .../components/rainmachine/__init__.py | 29 ++++++++++--------- .../components/rainmachine/binary_sensor.py | 4 +-- .../components/rainmachine/sensor.py | 4 +-- .../components/rainmachine/switch.py | 6 ++-- 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index b8ea030cb71..2c2cb0ef3c1 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from datetime import timedelta from functools import partial -from typing import Any +from typing import Any, cast from regenmaschine import Client from regenmaschine.controller import Controller @@ -123,14 +123,13 @@ def async_get_controller_for_service_call( hass: HomeAssistant, call: ServiceCall ) -> Controller: """Get the controller related to a service call (by device ID).""" - controllers: dict[str, Controller] = hass.data[DOMAIN][DATA_CONTROLLER] device_id = call.data[CONF_DEVICE_ID] device_registry = dr.async_get(hass) if device_entry := device_registry.async_get(device_id): for entry_id in device_entry.config_entries: - if entry_id in controllers: - return controllers[entry_id] + if controller := hass.data[DOMAIN][entry_id][DATA_CONTROLLER]: + return cast(Controller, controller) raise ValueError(f"No controller for device ID: {device_id}") @@ -145,10 +144,10 @@ async def async_update_programs_and_zones( """ await asyncio.gather( *[ - hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ + hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR][ DATA_PROGRAMS ].async_refresh(), - hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ + hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR][ DATA_ZONES ].async_refresh(), ] @@ -157,8 +156,9 @@ async def async_update_programs_and_zones( async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up RainMachine as config entry.""" - hass.data.setdefault(DOMAIN, {DATA_CONTROLLER: {}, DATA_COORDINATOR: {}}) - hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = {} + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = {} + websession = aiohttp_client.async_get_clientsession(hass) client = Client(session=websession) @@ -174,8 +174,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # regenmaschine can load multiple controllers at once, but we only grab the one # we loaded above: - controller = hass.data[DOMAIN][DATA_CONTROLLER][ - entry.entry_id + controller = hass.data[DOMAIN][entry.entry_id][ + DATA_CONTROLLER ] = get_client_controller(client) entry_updates: dict[str, Any] = {} @@ -215,6 +215,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return data controller_init_tasks = [] + coordinators = {} + for api_category in ( DATA_PROGRAMS, DATA_PROVISION_SETTINGS, @@ -222,9 +224,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_RESTRICTIONS_UNIVERSAL, DATA_ZONES, ): - coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ - api_category - ] = DataUpdateCoordinator( + coordinator = coordinators[api_category] = DataUpdateCoordinator( hass, LOGGER, name=f'{controller.name} ("{api_category}")', @@ -234,6 +234,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: controller_init_tasks.append(coordinator.async_refresh()) await asyncio.gather(*controller_init_tasks) + hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] = coordinators hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -297,7 +298,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload an RainMachine config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) + hass.data[DOMAIN].pop(entry.entry_id) if len(hass.config_entries.async_entries(DOMAIN)) == 1: # If this is the last instance of RainMachine, deregister any services defined diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 55d009ad6f5..853b9f24b33 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -115,8 +115,8 @@ 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][DATA_CONTROLLER][entry.entry_id] - coordinators = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] + controller = hass.data[DOMAIN][entry.entry_id][DATA_CONTROLLER] + coordinators = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] @callback def async_get_sensor(api_category: str) -> partial: diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 6c95552e26f..dee0f1f6e57 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -101,8 +101,8 @@ 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][DATA_CONTROLLER][entry.entry_id] - coordinators = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] + controller = hass.data[DOMAIN][entry.entry_id][DATA_CONTROLLER] + coordinators = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] @callback def async_get_sensor(api_category: str) -> partial: diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index e693664bab4..345da380316 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -149,11 +149,11 @@ async def async_setup_entry( ): platform.async_register_entity_service(service_name, schema, method) - controller = hass.data[DOMAIN][DATA_CONTROLLER][entry.entry_id] - programs_coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ + controller = hass.data[DOMAIN][entry.entry_id][DATA_CONTROLLER] + programs_coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR][ DATA_PROGRAMS ] - zones_coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][DATA_ZONES] + zones_coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR][DATA_ZONES] entities: list[RainMachineProgram | RainMachineZone] = [ RainMachineProgram( From 61e093cecde1490c397c39943d6a44b839e78386 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 22 Oct 2021 04:17:25 -0600 Subject: [PATCH 0657/1038] Make sure Ambient PWS data storage conforms to standards (#57807) --- .../components/ambient_station/__init__.py | 14 ++++++++------ .../components/ambient_station/binary_sensor.py | 4 ++-- homeassistant/components/ambient_station/const.py | 2 -- homeassistant/components/ambient_station/sensor.py | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index fc1f7d71d74..4e7e555ad94 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -26,7 +26,6 @@ from homeassistant.helpers.event import async_call_later from .const import ( ATTR_LAST_DATA, CONF_APP_KEY, - DATA_CLIENT, DOMAIN, LOGGER, TYPE_SOLARRADIATION, @@ -59,7 +58,8 @@ def async_hydrate_station_data(data: dict[str, Any]) -> dict[str, Any]: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the Ambient PWS as config entry.""" - hass.data.setdefault(DOMAIN, {DATA_CLIENT: {}}) + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = {} if not entry.unique_id: hass.config_entries.async_update_entry( @@ -78,7 +78,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ), ) hass.loop.create_task(ambient.ws_connect()) - hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] = ambient + hass.data[DOMAIN][entry.entry_id] = ambient except WebsocketError as err: LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady from err @@ -97,10 +97,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload an Ambient PWS config entry.""" - ambient = hass.data[DOMAIN][DATA_CLIENT].pop(entry.entry_id) - hass.async_create_task(ambient.ws_disconnect()) + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + ambient = hass.data[DOMAIN].pop(entry.entry_id) + hass.async_create_task(ambient.ws_disconnect()) - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + return unload_ok async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/ambient_station/binary_sensor.py b/homeassistant/components/ambient_station/binary_sensor.py index 79c53246f15..0c819a9a9b7 100644 --- a/homeassistant/components/ambient_station/binary_sensor.py +++ b/homeassistant/components/ambient_station/binary_sensor.py @@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AmbientWeatherEntity -from .const import ATTR_LAST_DATA, DATA_CLIENT, DOMAIN +from .const import ATTR_LAST_DATA, DOMAIN TYPE_BATT1 = "batt1" TYPE_BATT10 = "batt10" @@ -234,7 +234,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Ambient PWS binary sensors based on a config entry.""" - ambient = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] + ambient = hass.data[DOMAIN][entry.entry_id] async_add_entities( [ diff --git a/homeassistant/components/ambient_station/const.py b/homeassistant/components/ambient_station/const.py index cf5c97be045..4e0ec598fb1 100644 --- a/homeassistant/components/ambient_station/const.py +++ b/homeassistant/components/ambient_station/const.py @@ -8,7 +8,5 @@ ATTR_LAST_DATA = "last_data" CONF_APP_KEY = "app_key" -DATA_CLIENT = "data_client" - TYPE_SOLARRADIATION = "solarradiation" TYPE_SOLARRADIATION_LX = "solarradiation_lx" diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index 0247d03b6fd..66bccb30b55 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -39,7 +39,7 @@ from . import ( AmbientStation, AmbientWeatherEntity, ) -from .const import ATTR_LAST_DATA, DATA_CLIENT, DOMAIN +from .const import ATTR_LAST_DATA, DOMAIN TYPE_24HOURRAININ = "24hourrainin" TYPE_BAROMABSIN = "baromabsin" @@ -609,7 +609,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Ambient PWS sensors based on a config entry.""" - ambient = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] + ambient = hass.data[DOMAIN][entry.entry_id] async_add_entities( [ From a0bb2c8b339e2f7dd90ea987ea248bb159088178 Mon Sep 17 00:00:00 2001 From: avee87 <6134677+avee87@users.noreply.github.com> Date: Fri, 22 Oct 2021 11:23:21 +0100 Subject: [PATCH 0658/1038] Add memory/cpu percentage to Supervisor addons entities (#57468) * Add memory/cpu percentage to Supervisor addons entities * fixed lint * Use a single update function. Fixed tests * use constant * review comments * oops --- homeassistant/components/hassio/__init__.py | 68 +++++-- homeassistant/components/hassio/const.py | 2 + homeassistant/components/hassio/handler.py | 8 + homeassistant/components/hassio/sensor.py | 46 ++++- tests/components/hassio/__init__.py | 46 ----- tests/components/hassio/test_handler.py | 12 ++ tests/components/hassio/test_init.py | 48 +++++ tests/components/hassio/test_sensor.py | 172 ++++++++++++++++++ tests/components/hassio/test_websocket_api.py | 48 ++++- 9 files changed, 383 insertions(+), 67 deletions(-) create mode 100644 tests/components/hassio/test_sensor.py diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 6a6a0143bf2..01d080ef6a9 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -1,6 +1,7 @@ """Support for Hass.io.""" from __future__ import annotations +import asyncio from datetime import timedelta import logging import os @@ -45,6 +46,7 @@ from .const import ( ATTR_SLUG, ATTR_URL, ATTR_VERSION, + DATA_KEY_ADDONS, DOMAIN, SupervisorEntityModel, ) @@ -75,7 +77,8 @@ DATA_STORE = "hassio_store" DATA_INFO = "hassio_info" DATA_OS_INFO = "hassio_os_info" DATA_SUPERVISOR_INFO = "hassio_supervisor_info" -HASSIO_UPDATE_INTERVAL = timedelta(minutes=55) +DATA_ADDONS_STATS = "hassio_addons_stats" +HASSIO_UPDATE_INTERVAL = timedelta(minutes=5) ADDONS_COORDINATOR = "hassio_addons_coordinator" @@ -343,6 +346,16 @@ def get_supervisor_info(hass): return hass.data.get(DATA_SUPERVISOR_INFO) +@callback +@bind_hass +def get_addons_stats(hass): + """Return Addons stats. + + Async friendly. + """ + return hass.data.get(DATA_ADDONS_STATS) + + @callback @bind_hass def get_os_info(hass): @@ -499,25 +512,50 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: DOMAIN, service, async_service_handler, schema=settings.schema ) + async def update_addon_stats(slug): + """Update single addon stats.""" + stats = await hassio.get_addon_stats(slug) + return (slug, stats) + async def update_info_data(now): """Update last available supervisor information.""" + try: - hass.data[DATA_INFO] = await hassio.get_info() - hass.data[DATA_HOST_INFO] = await hassio.get_host_info() - hass.data[DATA_STORE] = await hassio.get_store() - hass.data[DATA_CORE_INFO] = await hassio.get_core_info() - hass.data[DATA_SUPERVISOR_INFO] = await hassio.get_supervisor_info() - hass.data[DATA_OS_INFO] = await hassio.get_os_info() + ( + hass.data[DATA_INFO], + hass.data[DATA_HOST_INFO], + hass.data[DATA_STORE], + hass.data[DATA_CORE_INFO], + hass.data[DATA_SUPERVISOR_INFO], + hass.data[DATA_OS_INFO], + ) = await asyncio.gather( + hassio.get_info(), + hassio.get_host_info(), + hassio.get_store(), + hassio.get_core_info(), + hassio.get_supervisor_info(), + hassio.get_os_info(), + ) + + addon_slugs = [ + addon[ATTR_SLUG] + for addon in hass.data[DATA_SUPERVISOR_INFO].get("addons", []) + ] + stats_data = await asyncio.gather( + *[update_addon_stats(slug) for slug in addon_slugs] + ) + hass.data[DATA_ADDONS_STATS] = dict(stats_data) + if ADDONS_COORDINATOR in hass.data: await hass.data[ADDONS_COORDINATOR].async_refresh() except HassioAPIError as err: - _LOGGER.warning("Can't read last version: %s", err) + _LOGGER.warning("Can't read Supervisor data: %s", err) hass.helpers.event.async_track_point_in_utc_time( update_info_data, utcnow() + HASSIO_UPDATE_INTERVAL ) - # Fetch last version + # Fetch data await update_info_data(None) async def async_handle_core_service(call): @@ -675,6 +713,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): """Update data via library.""" new_data = {} supervisor_info = get_supervisor_info(self.hass) + addons_stats = get_addons_stats(self.hass) store_data = get_store(self.hass) repositories = { @@ -682,9 +721,10 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): for repo in store_data.get("repositories", []) } - new_data["addons"] = { + new_data[DATA_KEY_ADDONS] = { addon[ATTR_SLUG]: { **addon, + **((addons_stats or {}).get(addon[ATTR_SLUG], {})), ATTR_REPOSITORY: repositories.get( addon.get(ATTR_REPOSITORY), addon.get(ATTR_REPOSITORY, "") ), @@ -697,7 +737,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): # If this is the initial refresh, register all addons and return the dict if not self.data: async_register_addons_in_dev_reg( - self.entry_id, self.dev_reg, new_data["addons"].values() + self.entry_id, self.dev_reg, new_data[DATA_KEY_ADDONS].values() ) if self.is_hass_os: async_register_os_in_dev_reg( @@ -711,13 +751,15 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): if self.entry_id in device.config_entries and device.model == SupervisorEntityModel.ADDON } - if stale_addons := supervisor_addon_devices - set(new_data["addons"]): + if stale_addons := supervisor_addon_devices - set(new_data[DATA_KEY_ADDONS]): async_remove_addons_from_dev_reg(self.dev_reg, stale_addons) # If there are new add-ons, we should reload the config entry so we can # create new devices and entities. We can return an empty dict because # coordinator will be recreated. - if self.data and set(new_data["addons"]) - set(self.data["addons"]): + if self.data and set(new_data[DATA_KEY_ADDONS]) - set( + self.data[DATA_KEY_ADDONS] + ): self.hass.async_create_task( self.hass.config_entries.async_reload(self.entry_id) ) diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index d78829e0fda..1b24013163f 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -42,6 +42,8 @@ EVENT_SUPERVISOR_EVENT = "supervisor_event" ATTR_VERSION = "version" ATTR_VERSION_LATEST = "version_latest" ATTR_UPDATE_AVAILABLE = "update_available" +ATTR_CPU_PERCENT = "cpu_percent" +ATTR_MEMORY_PERCENT = "memory_percent" ATTR_SLUG = "slug" ATTR_URL = "url" ATTR_REPOSITORY = "repository" diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 37b645eb7d3..7d4b5da8f5f 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -118,6 +118,14 @@ class HassIO: """ return self.send_command(f"/addons/{addon}/info", method="get") + @api_data + def get_addon_stats(self, addon): + """Return stats for an Add-on. + + This method returns a coroutine. + """ + return self.send_command(f"/addons/{addon}/stats", method="get") + @api_data def get_store(self): """Return data from the store. diff --git a/homeassistant/components/hassio/sensor.py b/homeassistant/components/hassio/sensor.py index 55678eb29c4..0608a9f817b 100644 --- a/homeassistant/components/hassio/sensor.py +++ b/homeassistant/components/hassio/sensor.py @@ -1,16 +1,28 @@ """Sensor platform for Hass.io addons.""" from __future__ import annotations -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ADDONS_COORDINATOR -from .const import ATTR_VERSION, ATTR_VERSION_LATEST, DATA_KEY_ADDONS, DATA_KEY_OS +from .const import ( + ATTR_CPU_PERCENT, + ATTR_MEMORY_PERCENT, + ATTR_VERSION, + ATTR_VERSION_LATEST, + DATA_KEY_ADDONS, + DATA_KEY_OS, +) from .entity import HassioAddonEntity, HassioOSEntity -ENTITY_DESCRIPTIONS = ( +COMMON_ENTITY_DESCRIPTIONS = ( SensorEntityDescription( entity_registry_enabled_default=False, key=ATTR_VERSION, @@ -23,6 +35,27 @@ ENTITY_DESCRIPTIONS = ( ), ) +ADDON_ENTITY_DESCRIPTIONS = COMMON_ENTITY_DESCRIPTIONS + ( + SensorEntityDescription( + entity_registry_enabled_default=False, + key=ATTR_CPU_PERCENT, + name="CPU Percent", + icon="mdi:cpu-64-bit", + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + entity_registry_enabled_default=False, + key=ATTR_MEMORY_PERCENT, + name="Memory Percent", + icon="mdi:memory", + native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), +) + +OS_ENTITY_DESCRIPTIONS = COMMON_ENTITY_DESCRIPTIONS + async def async_setup_entry( hass: HomeAssistant, @@ -34,8 +67,8 @@ async def async_setup_entry( entities = [] - for entity_description in ENTITY_DESCRIPTIONS: - for addon in coordinator.data[DATA_KEY_ADDONS].values(): + for addon in coordinator.data[DATA_KEY_ADDONS].values(): + for entity_description in ADDON_ENTITY_DESCRIPTIONS: entities.append( HassioAddonSensor( addon=addon, @@ -44,7 +77,8 @@ async def async_setup_entry( ) ) - if coordinator.is_hass_os: + if coordinator.is_hass_os: + for entity_description in OS_ENTITY_DESCRIPTIONS: entities.append( HassioOSSensor( coordinator=coordinator, diff --git a/tests/components/hassio/__init__.py b/tests/components/hassio/__init__.py index f3f35b62562..79520c6fd12 100644 --- a/tests/components/hassio/__init__.py +++ b/tests/components/hassio/__init__.py @@ -1,48 +1,2 @@ """Tests for Hass.io component.""" -import pytest - HASSIO_TOKEN = "123456" - - -@pytest.fixture(autouse=True) -def mock_all(aioclient_mock): - """Mock all setup requests.""" - aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) - aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"}) - aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) - aioclient_mock.get( - "http://127.0.0.1/info", - json={ - "result": "ok", - "data": {"supervisor": "222", "homeassistant": "0.110.0", "hassos": None}, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/host/info", - json={ - "result": "ok", - "data": { - "result": "ok", - "data": { - "chassis": "vm", - "operating_system": "Debian GNU/Linux 10 (buster)", - "kernel": "4.19.0-6-amd64", - }, - }, - }, - ) - aioclient_mock.get( - "http://127.0.0.1/core/info", - json={"result": "ok", "data": {"version_latest": "1.0.0"}}, - ) - aioclient_mock.get( - "http://127.0.0.1/os/info", - json={"result": "ok", "data": {"version_latest": "1.0.0"}}, - ) - aioclient_mock.get( - "http://127.0.0.1/supervisor/info", - json={"result": "ok", "data": {"version_latest": "1.0.0"}}, - ) - aioclient_mock.get( - "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} - ) diff --git a/tests/components/hassio/test_handler.py b/tests/components/hassio/test_handler.py index 33fb00b4485..26cc35b0bf1 100644 --- a/tests/components/hassio/test_handler.py +++ b/tests/components/hassio/test_handler.py @@ -177,6 +177,18 @@ async def test_api_addon_info(hassio_handler, aioclient_mock): assert aioclient_mock.call_count == 1 +async def test_api_addon_stats(hassio_handler, aioclient_mock): + """Test setup with API Add-on stats.""" + aioclient_mock.get( + "http://127.0.0.1/addons/test/stats", + json={"result": "ok", "data": {"memory_percent": 0.01}}, + ) + + data = await hassio_handler.get_addon_stats("test") + assert data["memory_percent"] == 0.01 + assert aioclient_mock.call_count == 1 + + async def test_api_discovery_message(hassio_handler, aioclient_mock): """Test setup with API discovery message.""" aioclient_mock.get( diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index cfa457695ac..62d2cb67d0d 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -90,6 +90,54 @@ def mock_all(aioclient_mock, request): ], }, ) + aioclient_mock.get( + "http://127.0.0.1/addons/test/stats", + json={ + "result": "ok", + "data": { + "cpu_percent": 0.99, + "memory_usage": 182611968, + "memory_limit": 3977146368, + "memory_percent": 4.59, + "network_rx": 362570232, + "network_tx": 82374138, + "blk_read": 46010945536, + "blk_write": 15051526144, + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/addons/test2/stats", + json={ + "result": "ok", + "data": { + "cpu_percent": 0.8, + "memory_usage": 51941376, + "memory_limit": 3977146368, + "memory_percent": 1.31, + "network_rx": 31338284, + "network_tx": 15692900, + "blk_read": 740077568, + "blk_write": 6004736, + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/addons/test3/stats", + json={ + "result": "ok", + "data": { + "cpu_percent": 0.8, + "memory_usage": 51941376, + "memory_limit": 3977146368, + "memory_percent": 1.31, + "network_rx": 31338284, + "network_tx": 15692900, + "blk_read": 740077568, + "blk_write": 6004736, + }, + }, + ) aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) diff --git a/tests/components/hassio/test_sensor.py b/tests/components/hassio/test_sensor.py new file mode 100644 index 00000000000..9039d302640 --- /dev/null +++ b/tests/components/hassio/test_sensor.py @@ -0,0 +1,172 @@ +"""The tests for the hassio sensors.""" + +import os +from unittest.mock import patch + +import pytest + +from homeassistant.components.hassio import DOMAIN +from homeassistant.helpers import entity_registry +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + +MOCK_ENVIRON = {"HASSIO": "127.0.0.1", "HASSIO_TOKEN": "abcdefgh"} + + +@pytest.fixture(autouse=True) +def mock_all(aioclient_mock, request): + """Mock all setup requests.""" + aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) + aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"}) + aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) + aioclient_mock.get( + "http://127.0.0.1/info", + json={ + "result": "ok", + "data": {"supervisor": "222", "homeassistant": "0.110.0", "hassos": None}, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/store", + json={ + "result": "ok", + "data": {"addons": [], "repositories": []}, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/host/info", + json={ + "result": "ok", + "data": { + "result": "ok", + "data": { + "chassis": "vm", + "operating_system": "Debian GNU/Linux 10 (buster)", + "kernel": "4.19.0-6-amd64", + }, + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/core/info", + json={"result": "ok", "data": {"version_latest": "1.0.0", "version": "1.0.0"}}, + ) + aioclient_mock.get( + "http://127.0.0.1/os/info", + json={"result": "ok", "data": {"version_latest": "1.0.0", "version": "1.0.0"}}, + ) + aioclient_mock.get( + "http://127.0.0.1/supervisor/info", + json={ + "result": "ok", + "data": { + "result": "ok", + "version_latest": "1.0.0", + "addons": [ + { + "name": "test", + "slug": "test", + "installed": True, + "update_available": False, + "version": "2.0.0", + "version_latest": "2.0.1", + "repository": "core", + "url": "https://github.com/home-assistant/addons/test", + }, + { + "name": "test2", + "slug": "test2", + "installed": True, + "update_available": False, + "version": "3.1.0", + "version_latest": "3.2.0", + "repository": "core", + "url": "https://github.com", + }, + ], + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/addons/test/stats", + json={ + "result": "ok", + "data": { + "cpu_percent": 0.99, + "memory_usage": 182611968, + "memory_limit": 3977146368, + "memory_percent": 4.59, + "network_rx": 362570232, + "network_tx": 82374138, + "blk_read": 46010945536, + "blk_write": 15051526144, + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/addons/test2/stats", + json={ + "result": "ok", + "data": { + "cpu_percent": 0.8, + "memory_usage": 51941376, + "memory_limit": 3977146368, + "memory_percent": 1.31, + "network_rx": 31338284, + "network_tx": 15692900, + "blk_read": 740077568, + "blk_write": 6004736, + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} + ) + + +async def test_sensors(hass, aioclient_mock): + """Test hassio OS and addons sensors.""" + + with patch.dict(os.environ, MOCK_ENVIRON): + result = await async_setup_component( + hass, + "hassio", + {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, + ) + assert result + + config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) + config_entry.add_to_hass(hass) + + await hass.async_block_till_done() + + sensors = { + "sensor.home_assistant_operating_system_version": "1.0.0", + "sensor.home_assistant_operating_system_newest_version": "1.0.0", + "sensor.test_version": "2.0.0", + "sensor.test_newest_version": "2.0.1", + "sensor.test2_version": "3.1.0", + "sensor.test2_newest_version": "3.2.0", + "sensor.test_cpu_percent": "0.99", + "sensor.test2_cpu_percent": "0.8", + "sensor.test_memory_percent": "4.59", + "sensor.test2_memory_percent": "1.31", + } + + """Check that entities are disabled by default.""" + for sensor in sensors: + assert hass.states.get(sensor) is None + + """Enable sensors.""" + ent_reg = entity_registry.async_get(hass) + for sensor in sensors: + ent_reg.async_update_entity(sensor, disabled_by=None) + await hass.config_entries.async_reload(config_entry.entry_id) + + await hass.async_block_till_done() + + """Check sensor values.""" + for sensor, value in sensors.items(): + state = hass.states.get(sensor) + assert state.state == value diff --git a/tests/components/hassio/test_websocket_api.py b/tests/components/hassio/test_websocket_api.py index 5578194b87c..931c1527b78 100644 --- a/tests/components/hassio/test_websocket_api.py +++ b/tests/components/hassio/test_websocket_api.py @@ -1,4 +1,6 @@ """Test websocket API.""" +import pytest + from homeassistant.components.hassio.const import ( ATTR_DATA, ATTR_ENDPOINT, @@ -14,11 +16,53 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component -from . import mock_all # noqa: F401 - from tests.common import async_mock_signal +@pytest.fixture(autouse=True) +def mock_all(aioclient_mock): + """Mock all setup requests.""" + aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) + aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"}) + aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) + aioclient_mock.get( + "http://127.0.0.1/info", + json={ + "result": "ok", + "data": {"supervisor": "222", "homeassistant": "0.110.0", "hassos": None}, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/host/info", + json={ + "result": "ok", + "data": { + "result": "ok", + "data": { + "chassis": "vm", + "operating_system": "Debian GNU/Linux 10 (buster)", + "kernel": "4.19.0-6-amd64", + }, + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/core/info", + json={"result": "ok", "data": {"version_latest": "1.0.0"}}, + ) + aioclient_mock.get( + "http://127.0.0.1/os/info", + json={"result": "ok", "data": {"version_latest": "1.0.0"}}, + ) + aioclient_mock.get( + "http://127.0.0.1/supervisor/info", + json={"result": "ok", "data": {"version_latest": "1.0.0"}}, + ) + aioclient_mock.get( + "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} + ) + + async def test_ws_subscription(hassio_env, hass: HomeAssistant, hass_ws_client): """Test websocket subscription.""" assert await async_setup_component(hass, "hassio", {}) From 6dd72869a6688fae4f997ba5603878975135edc5 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 22 Oct 2021 04:25:05 -0600 Subject: [PATCH 0659/1038] Make sure Guardian data storage conforms to standards (#57809) --- homeassistant/components/guardian/__init__.py | 50 +++++++------------ .../components/guardian/binary_sensor.py | 11 ++-- homeassistant/components/guardian/const.py | 1 - homeassistant/components/guardian/sensor.py | 11 ++-- homeassistant/components/guardian/switch.py | 12 ++--- 5 files changed, 34 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 9cb2590a289..4284d793764 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -29,7 +29,6 @@ from .const import ( DATA_COORDINATOR, DATA_COORDINATOR_PAIRED_SENSOR, DATA_PAIRED_SENSOR_MANAGER, - DATA_UNSUB_DISPATCHER_CONNECT, DOMAIN, LOGGER, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED, @@ -41,22 +40,13 @@ PLATFORMS = ["binary_sensor", "sensor", "switch"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Elexa Guardian from a config entry.""" - hass.data.setdefault( - DOMAIN, - { - DATA_CLIENT: {}, - DATA_COORDINATOR: {}, - DATA_COORDINATOR_PAIRED_SENSOR: {}, - DATA_PAIRED_SENSOR_MANAGER: {}, - DATA_UNSUB_DISPATCHER_CONNECT: {}, - }, - ) - client = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] = Client( - entry.data[CONF_IP_ADDRESS], port=entry.data[CONF_PORT] - ) - hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = {} - hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][entry.entry_id] = {} - hass.data[DOMAIN][DATA_UNSUB_DISPATCHER_CONNECT][entry.entry_id] = [] + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = { + DATA_COORDINATOR: {}, + DATA_COORDINATOR_PAIRED_SENSOR: {}, + } + + client = Client(entry.data[CONF_IP_ADDRESS], port=entry.data[CONF_PORT]) # The valve controller's UDP-based API can't handle concurrent requests very well, # so we use a lock to ensure that only one API request is reaching it at a time: @@ -71,7 +61,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: (API_VALVE_STATUS, client.valve.status), (API_WIFI_STATUS, client.wifi.status), ): - coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ + coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR][ api ] = GuardianDataUpdateCoordinator( hass, @@ -84,11 +74,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: init_valve_controller_tasks.append(coordinator.async_refresh()) await asyncio.gather(*init_valve_controller_tasks) + hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] = client # Set up an object to evaluate each batch of paired sensor UIDs and add/remove # devices as appropriate: - paired_sensor_manager = hass.data[DOMAIN][DATA_PAIRED_SENSOR_MANAGER][ - entry.entry_id + paired_sensor_manager = hass.data[DOMAIN][entry.entry_id][ + DATA_PAIRED_SENSOR_MANAGER ] = PairedSensorManager(hass, entry, client, api_lock) await paired_sensor_manager.async_process_latest_paired_sensor_uids() @@ -99,7 +90,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: paired_sensor_manager.async_process_latest_paired_sensor_uids() ) - hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ + hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR][ API_SENSOR_PAIR_DUMP ].async_add_listener(async_process_paired_sensor_uids) @@ -113,12 +104,7 @@ 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][DATA_CLIENT].pop(entry.entry_id) - hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) - hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR].pop(entry.entry_id) - for unsub in hass.data[DOMAIN][DATA_UNSUB_DISPATCHER_CONNECT][entry.entry_id]: - unsub() - hass.data[DOMAIN][DATA_UNSUB_DISPATCHER_CONNECT].pop(entry.entry_id) + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok @@ -146,8 +132,8 @@ class PairedSensorManager: self._paired_uids.add(uid) - coordinator = self._hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][ - self._entry.entry_id + coordinator = self._hass.data[DOMAIN][self._entry.entry_id][ + DATA_COORDINATOR_PAIRED_SENSOR ][uid] = GuardianDataUpdateCoordinator( self._hass, client=self._client, @@ -170,7 +156,7 @@ class PairedSensorManager: """Process a list of new UIDs.""" try: uids = set( - self._hass.data[DOMAIN][DATA_COORDINATOR][self._entry.entry_id][ + self._hass.data[DOMAIN][self._entry.entry_id][DATA_COORDINATOR][ API_SENSOR_PAIR_DUMP ].data["paired_uids"] ) @@ -197,8 +183,8 @@ class PairedSensorManager: # Clear out objects related to this paired sensor: self._paired_uids.remove(uid) - self._hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][ - self._entry.entry_id + self._hass.data[DOMAIN][self._entry.entry_id][ + DATA_COORDINATOR_PAIRED_SENSOR ].pop(uid) # Remove the paired sensor device from the device registry (which will diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py index 7db91d41885..6ac07274501 100644 --- a/homeassistant/components/guardian/binary_sensor.py +++ b/homeassistant/components/guardian/binary_sensor.py @@ -22,7 +22,6 @@ from .const import ( CONF_UID, DATA_COORDINATOR, DATA_COORDINATOR_PAIRED_SENSOR, - DATA_UNSUB_DISPATCHER_CONNECT, DOMAIN, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED, ) @@ -69,7 +68,7 @@ async def async_setup_entry( @callback def add_new_paired_sensor(uid: str) -> None: """Add a new paired sensor.""" - coordinator = hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][entry.entry_id][ + coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR_PAIRED_SENSOR][ uid ] @@ -81,7 +80,7 @@ async def async_setup_entry( ) # Handle adding paired sensors after HASS startup: - hass.data[DOMAIN][DATA_UNSUB_DISPATCHER_CONNECT][entry.entry_id].append( + entry.async_on_unload( async_dispatcher_connect( hass, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED.format(entry.data[CONF_UID]), @@ -92,7 +91,7 @@ async def async_setup_entry( # Add all valve controller-specific binary sensors: sensors: list[PairedSensorBinarySensor | ValveControllerBinarySensor] = [ ValveControllerBinarySensor( - entry, hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id], description + entry, hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR], description ) for description in VALVE_CONTROLLER_DESCRIPTIONS ] @@ -101,8 +100,8 @@ async def async_setup_entry( sensors.extend( [ PairedSensorBinarySensor(entry, coordinator, description) - for coordinator in hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][ - entry.entry_id + for coordinator in hass.data[DOMAIN][entry.entry_id][ + DATA_COORDINATOR_PAIRED_SENSOR ].values() for description in PAIRED_SENSOR_DESCRIPTIONS ] diff --git a/homeassistant/components/guardian/const.py b/homeassistant/components/guardian/const.py index e27e8a37047..5e3779cc447 100644 --- a/homeassistant/components/guardian/const.py +++ b/homeassistant/components/guardian/const.py @@ -18,6 +18,5 @@ DATA_CLIENT = "client" DATA_COORDINATOR = "coordinator" DATA_COORDINATOR_PAIRED_SENSOR = "coordinator_paired_sensor" DATA_PAIRED_SENSOR_MANAGER = "paired_sensor_manager" -DATA_UNSUB_DISPATCHER_CONNECT = "unsub_dispatcher_connect" SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED = "guardian_paired_sensor_coordinator_added_{0}" diff --git a/homeassistant/components/guardian/sensor.py b/homeassistant/components/guardian/sensor.py index 3a014d5cbf4..9a88805baaf 100644 --- a/homeassistant/components/guardian/sensor.py +++ b/homeassistant/components/guardian/sensor.py @@ -26,7 +26,6 @@ from .const import ( CONF_UID, DATA_COORDINATOR, DATA_COORDINATOR_PAIRED_SENSOR, - DATA_UNSUB_DISPATCHER_CONNECT, DOMAIN, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED, ) @@ -75,7 +74,7 @@ async def async_setup_entry( @callback def add_new_paired_sensor(uid: str) -> None: """Add a new paired sensor.""" - coordinator = hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][entry.entry_id][ + coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR_PAIRED_SENSOR][ uid ] @@ -87,7 +86,7 @@ async def async_setup_entry( ) # Handle adding paired sensors after HASS startup: - hass.data[DOMAIN][DATA_UNSUB_DISPATCHER_CONNECT][entry.entry_id].append( + entry.async_on_unload( async_dispatcher_connect( hass, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED.format(entry.data[CONF_UID]), @@ -98,7 +97,7 @@ async def async_setup_entry( # Add all valve controller-specific binary sensors: sensors: list[PairedSensorSensor | ValveControllerSensor] = [ ValveControllerSensor( - entry, hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id], description + entry, hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR], description ) for description in VALVE_CONTROLLER_DESCRIPTIONS ] @@ -107,8 +106,8 @@ async def async_setup_entry( sensors.extend( [ PairedSensorSensor(entry, coordinator, description) - for coordinator in hass.data[DOMAIN][DATA_COORDINATOR_PAIRED_SENSOR][ - entry.entry_id + for coordinator in hass.data[DOMAIN][entry.entry_id][ + DATA_COORDINATOR_PAIRED_SENSOR ].values() for description in PAIRED_SENSOR_DESCRIPTIONS ] diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py index 9b7db16dd15..7417499e53a 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -81,8 +81,8 @@ async def async_setup_entry( [ ValveControllerSwitch( entry, - hass.data[DOMAIN][DATA_CLIENT][entry.entry_id], - hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id], + hass.data[DOMAIN][entry.entry_id][DATA_CLIENT], + hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR], ) ] ) @@ -160,8 +160,8 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): LOGGER.error("Error while adding paired sensor: %s", err) return - await self.hass.data[DOMAIN][DATA_PAIRED_SENSOR_MANAGER][ - self._entry.entry_id + await self.hass.data[DOMAIN][self._entry.entry_id][ + DATA_PAIRED_SENSOR_MANAGER ].async_pair_sensor(uid) async def async_reboot(self) -> None: @@ -189,8 +189,8 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): LOGGER.error("Error while removing paired sensor: %s", err) return - await self.hass.data[DOMAIN][DATA_PAIRED_SENSOR_MANAGER][ - self._entry.entry_id + await self.hass.data[DOMAIN][self._entry.entry_id][ + DATA_PAIRED_SENSOR_MANAGER ].async_unpair_sensor(uid) async def async_upgrade_firmware( From 176ed4f7ba562ab25734b847d35039afda48a6a9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 22 Oct 2021 12:31:12 +0200 Subject: [PATCH 0660/1038] Use DeviceInfo on components with via_device (I-T) (#58212) Co-authored-by: epenet --- .../components/iaqualink/__init__.py | 14 ++++---- .../components/insteon/insteon_entity.py | 20 +++++------ homeassistant/components/izone/climate.py | 15 +++++---- .../components/lutron_caseta/__init__.py | 20 +++++------ .../components/motion_blinds/cover.py | 15 +++++---- homeassistant/components/netgear/router.py | 16 ++++----- homeassistant/components/notion/__init__.py | 18 +++++----- homeassistant/components/plex/media_player.py | 33 ++++++++++--------- homeassistant/components/point/__init__.py | 22 ++++++------- .../components/tradfri/base_class.py | 16 ++++----- 10 files changed, 96 insertions(+), 93 deletions(-) diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 2bd0cade3b9..7eb7256aa99 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -238,10 +238,10 @@ class AqualinkEntity(Entity): @property def device_info(self) -> DeviceInfo: """Return the device info.""" - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "model": self.dev.__class__.__name__.replace("Aqualink", ""), - "manufacturer": "Jandy", - "via_device": (DOMAIN, self.dev.system.serial), - } + return DeviceInfo( + identifiers={(DOMAIN, self.unique_id)}, + name=self.name, + model=self.dev.__class__.__name__.replace("Aqualink", ""), + manufacturer="Jandy", + via_device=(DOMAIN, self.dev.system.serial), + ) diff --git a/homeassistant/components/insteon/insteon_entity.py b/homeassistant/components/insteon/insteon_entity.py index e555725a188..f3f71e37ecf 100644 --- a/homeassistant/components/insteon/insteon_entity.py +++ b/homeassistant/components/insteon/insteon_entity.py @@ -9,7 +9,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ( DOMAIN, @@ -79,16 +79,16 @@ class InsteonEntity(Entity): return {"insteon_address": self.address, "insteon_group": self.group} @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device information.""" - return { - "identifiers": {(DOMAIN, str(self._insteon_device.address))}, - "name": f"{self._insteon_device.description} {self._insteon_device.address}", - "model": f"{self._insteon_device.model} ({self._insteon_device.cat!r}, 0x{self._insteon_device.subcat:02x})", - "sw_version": f"{self._insteon_device.firmware:02x} Engine Version: {self._insteon_device.engine_version}", - "manufacturer": "Smart Home", - "via_device": (DOMAIN, str(devices.modem.address)), - } + return DeviceInfo( + identifiers={(DOMAIN, str(self._insteon_device.address))}, + name=f"{self._insteon_device.description} {self._insteon_device.address}", + model=f"{self._insteon_device.model} ({self._insteon_device.cat!r}, 0x{self._insteon_device.subcat:02x})", + sw_version=f"{self._insteon_device.firmware:02x} Engine Version: {self._insteon_device.engine_version}", + manufacturer="Smart Home", + via_device=(DOMAIN, str(devices.modem.address)), + ) @callback def async_entity_update(self, name, address, value, group): diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index bd1de0f935e..ddbb59aedad 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -35,6 +35,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.temperature import display_temp as show_temp from homeassistant.helpers.typing import ConfigType @@ -483,13 +484,13 @@ class ZoneDevice(ClimateEntity): } self._supported_features |= SUPPORT_TARGET_TEMPERATURE - self._device_info = { - "identifiers": {(IZONE, controller.unique_id, zone.index)}, - "name": self.name, - "manufacturer": "IZone", - "via_device": (IZONE, controller.unique_id), - "model": zone.type.name.title(), - } + self._device_info = DeviceInfo( + identifiers={(IZONE, controller.unique_id, zone.index)}, + name=self.name, + manufacturer="IZone", + via_device=(IZONE, controller.unique_id), + model=zone.type.name.title(), + ) async def async_added_to_hass(self): """Call on adding to hass.""" diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 4aefb2fb50e..7e3cde2ccac 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -17,7 +17,7 @@ from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ( ACTION_PRESS, @@ -329,16 +329,16 @@ class LutronCasetaDevice(Entity): return str(self.serial) @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" - return { - "identifiers": {(DOMAIN, self.serial)}, - "name": self.name, - "suggested_area": self._device["name"].split("_")[0], - "manufacturer": MANUFACTURER, - "model": f"{self._device['model']} ({self._device['type']})", - "via_device": (DOMAIN, self._bridge_device["serial"]), - } + return DeviceInfo( + identifiers={(DOMAIN, self.serial)}, + name=self.name, + suggested_area=self._device["name"].split("_")[0], + manufacturer=MANUFACTURER, + model=f"{self._device['model']} ({self._device['type']})", + via_device=(DOMAIN, self._bridge_device["serial"]), + ) @property def extra_state_attributes(self): diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index 16e5255240c..1e4251c22f2 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -17,6 +17,7 @@ from homeassistant.components.cover import ( CoverEntity, ) from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -138,13 +139,13 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): self._attr_device_class = device_class self._attr_name = f"{blind.blind_type}-{blind.mac[12:]}" self._attr_unique_id = blind.mac - self._attr_device_info = { - "identifiers": {(DOMAIN, blind.mac)}, - "manufacturer": MANUFACTURER, - "name": f"{blind.blind_type}-{blind.mac[12:]}", - "model": blind.blind_type, - "via_device": (DOMAIN, config_entry.unique_id), - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, blind.mac)}, + manufacturer=MANUFACTURER, + name=f"{blind.blind_type}-{blind.mac[12:]}", + model=blind.blind_type, + via_device=(DOMAIN, config_entry.unique_id), + ) @property def available(self): diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index 12abac53c38..3c2497f2131 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -23,7 +23,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import HomeAssistantType @@ -273,14 +273,14 @@ class NetgearDeviceEntity(Entity): return self._name @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device information.""" - return { - "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, - "default_name": self._device_name, - "default_model": self._device["device_model"], - "via_device": (DOMAIN, self._router.unique_id), - } + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, self._mac)}, + default_name=self._device_name, + default_model=self._device["device_model"], + via_device=(DOMAIN, self._router.unique_id), + ) @property def should_poll(self) -> bool: diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index a65e8ab338c..74fd2b90117 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -17,7 +17,7 @@ from homeassistant.helpers import ( config_validation as cv, device_registry as dr, ) -from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -147,14 +147,14 @@ class NotionEntity(CoordinatorEntity): bridge = self.coordinator.data["bridges"].get(bridge_id, {}) sensor = self.coordinator.data["sensors"][sensor_id] - self._attr_device_info = { - "identifiers": {(DOMAIN, sensor["hardware_id"])}, - "manufacturer": "Silicon Labs", - "model": sensor["hardware_revision"], - "name": str(sensor["name"]), - "sw_version": sensor["firmware_version"], - "via_device": (DOMAIN, bridge.get("hardware_id")), - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, sensor["hardware_id"])}, + manufacturer="Silicon Labs", + model=sensor["hardware_revision"], + name=str(sensor["name"]), + sw_version=sensor["firmware_version"], + via_device=(DOMAIN, bridge.get("hardware_id")), + ) self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} self._attr_name = f'{sensor["name"]}: {description.name}' diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 2e89ede121d..5a60c1e8b32 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -27,6 +27,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.helpers.network import is_internal_request @@ -523,28 +524,28 @@ class PlexMediaPlayer(MediaPlayerEntity): return attributes @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" if self.machine_identifier is None: return None if self.device_product in TRANSIENT_DEVICE_MODELS: - return { - "identifiers": {(PLEX_DOMAIN, "plex.tv-clients")}, - "name": "Plex Client Service", - "manufacturer": "Plex", - "model": "Plex Clients", - "entry_type": "service", - } + return DeviceInfo( + identifiers={(PLEX_DOMAIN, "plex.tv-clients")}, + name="Plex Client Service", + manufacturer="Plex", + model="Plex Clients", + entry_type="service", + ) - return { - "identifiers": {(PLEX_DOMAIN, self.machine_identifier)}, - "manufacturer": self.device_platform or "Plex", - "model": self.device_product or self.device_make, - "name": self.name, - "sw_version": self.device_version, - "via_device": (PLEX_DOMAIN, self.plex_server.machine_identifier), - } + return DeviceInfo( + identifiers={(PLEX_DOMAIN, self.machine_identifier)}, + manufacturer=self.device_platform or "Plex", + model=self.device_product or self.device_make, + name=self.name, + sw_version=self.device_version, + via_device=(PLEX_DOMAIN, self.plex_server.machine_identifier), + ) async def async_browse_media(self, media_content_type=None, media_content_id=None): """Implement the websocket media browsing helper.""" diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index 66f640cb7be..45b7dbb9e3e 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -21,7 +21,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.util.dt import as_local, parse_datetime, utc_from_timestamp @@ -308,20 +308,20 @@ class MinutPointEntity(Entity): return attrs @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" device = self.device.device - return { - "connections": { + return DeviceInfo( + connections={ (device_registry.CONNECTION_NETWORK_MAC, device["device_mac"]) }, - "identifieres": device["device_id"], - "manufacturer": "Minut", - "model": f"Point v{device['hardware_version']}", - "name": device["description"], - "sw_version": device["firmware"]["installed"], - "via_device": (DOMAIN, device["home"]), - } + identifieres=device["device_id"], + manufacturer="Minut", + model=f"Point v{device['hardware_version']}", + name=device["description"], + sw_version=device["firmware"]["installed"], + via_device=(DOMAIN, device["home"]), + ) @property def name(self): diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py index b0679a2a8ce..1222e9b7792 100644 --- a/homeassistant/components/tradfri/base_class.py +++ b/homeassistant/components/tradfri/base_class.py @@ -109,14 +109,14 @@ class TradfriBaseDevice(TradfriBaseClass): def device_info(self) -> DeviceInfo: """Return the device info.""" info = self._device.device_info - return { - "identifiers": {(DOMAIN, self._device.id)}, - "manufacturer": info.manufacturer, - "model": info.model_number, - "name": self._attr_name, - "sw_version": info.firmware_version, - "via_device": (DOMAIN, self._gateway_id), - } + return DeviceInfo( + identifiers={(DOMAIN, self._device.id)}, + manufacturer=info.manufacturer, + model=info.model_number, + name=self._attr_name, + sw_version=info.firmware_version, + via_device=(DOMAIN, self._gateway_id), + ) def _refresh(self, device: Device) -> None: """Refresh the device data.""" From a3d1159a130a31753bd06c6512f97ab4471175ae Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 22 Oct 2021 12:52:33 +0200 Subject: [PATCH 0661/1038] Use DeviceInfo on components with via_device (R-X) (#58213) Co-authored-by: epenet --- homeassistant/components/roon/media_player.py | 17 ++--- .../ruckus_unleashed/device_tracker.py | 10 +-- .../components/sia/sia_entity_base.py | 10 +-- .../components/simplisafe/__init__.py | 16 ++--- homeassistant/components/somfy/entity.py | 18 +++--- .../components/synology_dsm/camera.py | 12 ++-- .../components/synology_dsm/switch.py | 16 ++--- homeassistant/components/tado/entity.py | 20 +++--- homeassistant/components/toon/models.py | 62 +++++++++---------- homeassistant/components/wilight/__init__.py | 20 +++--- .../components/xiaomi_aqara/__init__.py | 30 ++++----- .../components/xiaomi_miio/gateway.py | 20 +++--- 12 files changed, 126 insertions(+), 125 deletions(-) diff --git a/homeassistant/components/roon/media_player.py b/homeassistant/components/roon/media_player.py index 0d7f736961f..08dbe12849b 100644 --- a/homeassistant/components/roon/media_player.py +++ b/homeassistant/components/roon/media_player.py @@ -35,6 +35,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.helpers.entity import DeviceInfo from homeassistant.util import convert from homeassistant.util.dt import utcnow @@ -160,18 +161,18 @@ class RoonDevice(MediaPlayerEntity): return [self._server.entity_id(roon_name) for roon_name in roon_names] @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" dev_model = "player" if self.player_data.get("source_controls"): dev_model = self.player_data["source_controls"][0].get("display_name") - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "manufacturer": "RoonLabs", - "model": dev_model, - "via_device": (DOMAIN, self._server.roon_id), - } + return DeviceInfo( + identifiers={(DOMAIN, self.unique_id)}, + name=self.name, + manufacturer="RoonLabs", + model=dev_model, + via_device=(DOMAIN, self._server.roon_id), + ) def update_data(self, player_data=None): """Update session object.""" diff --git a/homeassistant/components/ruckus_unleashed/device_tracker.py b/homeassistant/components/ruckus_unleashed/device_tracker.py index a776930b5ac..6a923e05641 100644 --- a/homeassistant/components/ruckus_unleashed/device_tracker.py +++ b/homeassistant/components/ruckus_unleashed/device_tracker.py @@ -121,12 +121,12 @@ class RuckusUnleashedDevice(CoordinatorEntity, ScannerEntity): def device_info(self) -> DeviceInfo | None: """Return the device information.""" if self.is_connected: - return { - "name": self.name, - "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, - "via_device": ( + return DeviceInfo( + name=self.name, + connections={(CONNECTION_NETWORK_MAC, self._mac)}, + via_device=( CONNECTION_NETWORK_MAC, self.coordinator.data[API_CLIENTS][self._mac][API_ACCESS_POINT], ), - } + ) return None diff --git a/homeassistant/components/sia/sia_entity_base.py b/homeassistant/components/sia/sia_entity_base.py index 0a84615d6eb..14334d1f8cb 100644 --- a/homeassistant/components/sia/sia_entity_base.py +++ b/homeassistant/components/sia/sia_entity_base.py @@ -125,8 +125,8 @@ class SIABaseEntity(RestoreEntity): """Return the device_info.""" assert self._attr_name is not None assert self.unique_id is not None - return { - "name": self._attr_name, - "identifiers": {(DOMAIN, self.unique_id)}, - "via_device": (DOMAIN, f"{self._port}_{self._account}"), - } + return DeviceInfo( + name=self._attr_name, + identifiers={(DOMAIN, self.unique_id)}, + via_device=(DOMAIN, f"{self._port}_{self._account}"), + ) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index d41d7b03eda..fb6e09a202b 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -33,6 +33,7 @@ from homeassistant.helpers import ( config_validation as cv, device_registry as dr, ) +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.service import ( async_register_admin_service, verify_domain_control, @@ -442,14 +443,13 @@ class SimpliSafeEntity(CoordinatorEntity): serial = system.serial self._attr_extra_state_attributes = {ATTR_SYSTEM_ID: system.system_id} - self._attr_device_info = { - "identifiers": {(DOMAIN, serial)}, - "manufacturer": "SimpliSafe", - "model": model, - "name": device_name, - "via_device": (DOMAIN, system.system_id), - } - + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, serial)}, + manufacturer="SimpliSafe", + model=model, + name=device_name, + via_device=(DOMAIN, system.system_id), + ) self._attr_name = f"{system.address} {device_name} {' '.join([w.title() for w in model.split('_')])}" self._attr_unique_id = serial self._device = device diff --git a/homeassistant/components/somfy/entity.py b/homeassistant/components/somfy/entity.py index 88ff86e8849..2d92c8a77c0 100644 --- a/homeassistant/components/somfy/entity.py +++ b/homeassistant/components/somfy/entity.py @@ -3,7 +3,7 @@ from abc import abstractmethod from homeassistant.core import callback -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -33,19 +33,19 @@ class SomfyEntity(CoordinatorEntity, Entity): return self.device.name @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device specific attributes. Implemented by platform classes. """ - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "model": self.device.type, - "via_device": (DOMAIN, self.device.parent_id), + return DeviceInfo( + identifiers={(DOMAIN, self.unique_id)}, + name=self.name, + model=self.device.type, + via_device=(DOMAIN, self.device.parent_id), # For the moment, Somfy only returns their own device. - "manufacturer": "Somfy", - } + manufacturer="Somfy", + ) def has_capability(self, capability: str) -> bool: """Test if device has a capability.""" diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index c305d11f4e0..8d4f74bd132 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -89,20 +89,20 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera): @property def device_info(self) -> DeviceInfo: """Return the device information.""" - return { - "identifiers": { + return DeviceInfo( + identifiers={ ( DOMAIN, f"{self._api.information.serial}_{self.camera_data.id}", ) }, - "name": self.camera_data.name, - "model": self.camera_data.model, - "via_device": ( + name=self.camera_data.name, + model=self.camera_data.model, + via_device=( DOMAIN, f"{self._api.information.serial}_{SynoSurveillanceStation.INFO_API_KEY}", ), - } + ) @property def available(self) -> bool: diff --git a/homeassistant/components/synology_dsm/switch.py b/homeassistant/components/synology_dsm/switch.py index c5144d64a48..fb3f50479dd 100644 --- a/homeassistant/components/synology_dsm/switch.py +++ b/homeassistant/components/synology_dsm/switch.py @@ -106,16 +106,16 @@ class SynoDSMSurveillanceHomeModeToggle(SynologyDSMBaseEntity, ToggleEntity): @property def device_info(self) -> DeviceInfo: """Return the device information.""" - return { - "identifiers": { + return DeviceInfo( + identifiers={ ( DOMAIN, f"{self._api.information.serial}_{SynoSurveillanceStation.INFO_API_KEY}", ) }, - "name": "Surveillance Station", - "manufacturer": "Synology", - "model": self._api.information.model, - "sw_version": self._version, - "via_device": (DOMAIN, self._api.information.serial), - } + name="Surveillance Station", + manufacturer="Synology", + model=self._api.information.model, + sw_version=self._version, + via_device=(DOMAIN, self._api.information.serial), + ) diff --git a/homeassistant/components/tado/entity.py b/homeassistant/components/tado/entity.py index 270d6f1e911..c10d0b50ab9 100644 --- a/homeassistant/components/tado/entity.py +++ b/homeassistant/components/tado/entity.py @@ -1,5 +1,5 @@ """Base class for Tado entity.""" -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DEFAULT_NAME, DOMAIN, TADO_HOME, TADO_ZONE @@ -15,16 +15,16 @@ class TadoDeviceEntity(Entity): self.device_id = device_info["shortSerialNo"] @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN, self.device_id)}, - "name": self.device_name, - "manufacturer": DEFAULT_NAME, - "sw_version": self._device_info["currentFwVersion"], - "model": self._device_info["deviceType"], - "via_device": (DOMAIN, self._device_info["serialNo"]), - } + return DeviceInfo( + identifiers={(DOMAIN, self.device_id)}, + name=self.device_name, + manufacturer=DEFAULT_NAME, + sw_version=self._device_info["currentFwVersion"], + model=self._device_info["deviceType"], + via_device=(DOMAIN, self._device_info["serialNo"]), + ) @property def should_poll(self): diff --git a/homeassistant/components/toon/models.py b/homeassistant/components/toon/models.py index a95a8f622a8..116613640ad 100644 --- a/homeassistant/components/toon/models.py +++ b/homeassistant/components/toon/models.py @@ -41,11 +41,11 @@ class ToonElectricityMeterDeviceEntity(ToonEntity): def device_info(self) -> DeviceInfo: """Return device information about this entity.""" agreement_id = self.coordinator.data.agreement.agreement_id - return { - "name": "Electricity Meter", - "identifiers": {(DOMAIN, agreement_id, "electricity")}, - "via_device": (DOMAIN, agreement_id, "meter_adapter"), - } + return DeviceInfo( + name="Electricity Meter", + identifiers={(DOMAIN, agreement_id, "electricity")}, + via_device=(DOMAIN, agreement_id, "meter_adapter"), + ) class ToonGasMeterDeviceEntity(ToonEntity): @@ -55,11 +55,11 @@ class ToonGasMeterDeviceEntity(ToonEntity): def device_info(self) -> DeviceInfo: """Return device information about this entity.""" agreement_id = self.coordinator.data.agreement.agreement_id - return { - "name": "Gas Meter", - "identifiers": {(DOMAIN, agreement_id, "gas")}, - "via_device": (DOMAIN, agreement_id, "electricity"), - } + return DeviceInfo( + name="Gas Meter", + identifiers={(DOMAIN, agreement_id, "gas")}, + via_device=(DOMAIN, agreement_id, "electricity"), + ) class ToonWaterMeterDeviceEntity(ToonEntity): @@ -69,11 +69,11 @@ class ToonWaterMeterDeviceEntity(ToonEntity): def device_info(self) -> DeviceInfo: """Return device information about this entity.""" agreement_id = self.coordinator.data.agreement.agreement_id - return { - "name": "Water Meter", - "identifiers": {(DOMAIN, agreement_id, "water")}, - "via_device": (DOMAIN, agreement_id, "electricity"), - } + return DeviceInfo( + name="Water Meter", + identifiers={(DOMAIN, agreement_id, "water")}, + via_device=(DOMAIN, agreement_id, "electricity"), + ) class ToonSolarDeviceEntity(ToonEntity): @@ -83,11 +83,11 @@ class ToonSolarDeviceEntity(ToonEntity): def device_info(self) -> DeviceInfo: """Return device information about this entity.""" agreement_id = self.coordinator.data.agreement.agreement_id - return { - "name": "Solar Panels", - "identifiers": {(DOMAIN, agreement_id, "solar")}, - "via_device": (DOMAIN, agreement_id, "meter_adapter"), - } + return DeviceInfo( + name="Solar Panels", + identifiers={(DOMAIN, agreement_id, "solar")}, + via_device=(DOMAIN, agreement_id, "meter_adapter"), + ) class ToonBoilerModuleDeviceEntity(ToonEntity): @@ -97,12 +97,12 @@ class ToonBoilerModuleDeviceEntity(ToonEntity): def device_info(self) -> DeviceInfo: """Return device information about this entity.""" agreement_id = self.coordinator.data.agreement.agreement_id - return { - "name": "Boiler Module", - "manufacturer": "Eneco", - "identifiers": {(DOMAIN, agreement_id, "boiler_module")}, - "via_device": (DOMAIN, agreement_id), - } + return DeviceInfo( + name="Boiler Module", + manufacturer="Eneco", + identifiers={(DOMAIN, agreement_id, "boiler_module")}, + via_device=(DOMAIN, agreement_id), + ) class ToonBoilerDeviceEntity(ToonEntity): @@ -112,11 +112,11 @@ class ToonBoilerDeviceEntity(ToonEntity): def device_info(self) -> DeviceInfo: """Return device information about this entity.""" agreement_id = self.coordinator.data.agreement.agreement_id - return { - "name": "Boiler", - "identifiers": {(DOMAIN, agreement_id, "boiler")}, - "via_device": (DOMAIN, agreement_id, "boiler_module"), - } + return DeviceInfo( + name="Boiler", + identifiers={(DOMAIN, agreement_id, "boiler")}, + via_device=(DOMAIN, agreement_id, "boiler_module"), + ) @dataclass diff --git a/homeassistant/components/wilight/__init__.py b/homeassistant/components/wilight/__init__.py index fd71c7342e8..c77814519a1 100644 --- a/homeassistant/components/wilight/__init__.py +++ b/homeassistant/components/wilight/__init__.py @@ -3,7 +3,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .parent_device import WiLightParent @@ -78,16 +78,16 @@ class WiLightDevice(Entity): return self._unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" - return { - "name": self._name, - "identifiers": {(DOMAIN, self._unique_id)}, - "model": self._model, - "manufacturer": "WiLight", - "sw_version": self._sw_version, - "via_device": (DOMAIN, self._device_id), - } + return DeviceInfo( + name=self._name, + identifiers={(DOMAIN, self._unique_id)}, + model=self._model, + manufacturer="WiLight", + sw_version=self._sw_version, + via_device=(DOMAIN, self._device_id), + ) @property def available(self): diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index 44637c530ef..87020b870a1 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -20,7 +20,7 @@ from homeassistant.core import callback from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import format_mac -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow @@ -276,23 +276,23 @@ class XiaomiDevice(Entity): return self._device_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info of the Xiaomi Aqara device.""" if self._is_gateway: - device_info = { - "identifiers": {(DOMAIN, self._device_id)}, - "model": self._model, - } + device_info = DeviceInfo( + identifiers={(DOMAIN, self._device_id)}, + model=self._model, + ) else: - device_info = { - "connections": {(dr.CONNECTION_ZIGBEE, self._device_id)}, - "identifiers": {(DOMAIN, self._device_id)}, - "manufacturer": "Xiaomi Aqara", - "model": self._model, - "name": self._device_name, - "sw_version": self._protocol, - "via_device": (DOMAIN, self._gateway_id), - } + DeviceInfo( + connections={(dr.CONNECTION_ZIGBEE, self._device_id)}, + identifiers={(DOMAIN, self._device_id)}, + manufacturer="Xiaomi Aqara", + model=self._model, + name=self._device_name, + sw_version=self._protocol, + via_device=(DOMAIN, self._gateway_id), + ) return device_info diff --git a/homeassistant/components/xiaomi_miio/gateway.py b/homeassistant/components/xiaomi_miio/gateway.py index c873a56fb44..af6ef2e29a5 100644 --- a/homeassistant/components/xiaomi_miio/gateway.py +++ b/homeassistant/components/xiaomi_miio/gateway.py @@ -7,7 +7,7 @@ from micloud.micloudexception import MiCloudAccessDenied from miio import DeviceException, gateway from miio.gateway.gateway import GATEWAY_MODEL_EU -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -151,16 +151,16 @@ class XiaomiGatewayDevice(CoordinatorEntity, Entity): return self._name @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info of the gateway.""" - return { - "identifiers": {(DOMAIN, self._sub_device.sid)}, - "via_device": (DOMAIN, self._entry.unique_id), - "manufacturer": "Xiaomi", - "name": self._sub_device.name, - "model": self._sub_device.model, - "sw_version": self._sub_device.firmware_version, - } + return DeviceInfo( + identifiers={(DOMAIN, self._sub_device.sid)}, + via_device=(DOMAIN, self._entry.unique_id), + manufacturer="Xiaomi", + name=self._sub_device.name, + model=self._sub_device.model, + sw_version=self._sub_device.firmware_version, + ) @property def available(self): From eab235173be849c5ff32a430dc7e4769b4942066 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 22 Oct 2021 14:06:04 +0200 Subject: [PATCH 0662/1038] Use assignment expressions 28 (#58189) --- homeassistant/auth/mfa_modules/notify.py | 3 +-- homeassistant/components/device_tracker/legacy.py | 6 ++---- homeassistant/components/dialogflow/__init__.py | 3 +-- homeassistant/components/file/sensor.py | 3 +-- .../components/homekit_controller/binary_sensor.py | 3 +-- homeassistant/components/homekit_controller/climate.py | 3 +-- homeassistant/components/homekit_controller/cover.py | 3 +-- homeassistant/components/homekit_controller/fan.py | 3 +-- homeassistant/components/homekit_controller/sensor.py | 3 +-- homeassistant/components/homekit_controller/switch.py | 3 +-- homeassistant/components/logbook/__init__.py | 3 +-- homeassistant/components/maxcube/climate.py | 3 +-- homeassistant/components/meteo_france/__init__.py | 3 +-- homeassistant/components/notify/legacy.py | 3 +-- homeassistant/components/ozw/climate.py | 7 ++----- homeassistant/components/ozw/light.py | 3 +-- homeassistant/components/sensor/significant_change.py | 4 +--- homeassistant/components/tankerkoenig/__init__.py | 5 ++--- homeassistant/components/vallox/__init__.py | 3 +-- homeassistant/components/zwave_js/__init__.py | 3 +-- homeassistant/components/zwave_js/climate.py | 9 +++------ homeassistant/components/zwave_js/device_trigger.py | 3 +-- homeassistant/components/zwave_js/helpers.py | 6 ++---- homeassistant/components/zwave_js/migrate.py | 3 +-- 24 files changed, 30 insertions(+), 61 deletions(-) diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index ec5d5b7cd03..12e80ae47b0 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -245,8 +245,7 @@ class NotifyAuthModule(MultiFactorAuthModule): await self._async_load() assert self._user_settings is not None - notify_setting = self._user_settings.get(user_id) - if notify_setting is None: + if (notify_setting := self._user_settings.get(user_id)) is None: _LOGGER.error("Cannot find user %s", user_id) return diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 31d060200f0..94638c031a3 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -396,8 +396,7 @@ async def get_tracker(hass: HomeAssistant, config: ConfigType) -> DeviceTracker: consider_home = conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME) defaults = conf.get(CONF_NEW_DEVICE_DEFAULTS, {}) - track_new = conf.get(CONF_TRACK_NEW) - if track_new is None: + if (track_new := conf.get(CONF_TRACK_NEW)) is None: track_new = defaults.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) devices = await async_load_config(yaml_path, hass, consider_home) @@ -492,8 +491,7 @@ class DeviceTracker: raise HomeAssistantError("Neither mac or device id passed in") if mac is not None: mac = str(mac).upper() - device = self.mac_to_dev.get(mac) - if device is None: + if (device := self.mac_to_dev.get(mac)) is None: dev_id = util.slugify(host_name or "") or util.slugify(mac) else: dev_id = cv.slug(str(dev_id).lower()) diff --git a/homeassistant/components/dialogflow/__init__.py b/homeassistant/components/dialogflow/__init__.py index 6003f17c9e9..9473fd537ad 100644 --- a/homeassistant/components/dialogflow/__init__.py +++ b/homeassistant/components/dialogflow/__init__.py @@ -106,8 +106,7 @@ async def async_handle_message(hass, message): "Dialogflow V1 API will be removed on October 23, 2019. Please change your DialogFlow settings to use the V2 api" ) req = message.get("result") - action_incomplete = req.get("actionIncomplete", True) - if action_incomplete: + if req.get("actionIncomplete", True): return elif _api_version is V2: diff --git a/homeassistant/components/file/sensor.py b/homeassistant/components/file/sensor.py index 73b262c9090..6a18fa4cc8b 100644 --- a/homeassistant/components/file/sensor.py +++ b/homeassistant/components/file/sensor.py @@ -34,9 +34,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= file_path = config.get(CONF_FILE_PATH) name = config.get(CONF_NAME) unit = config.get(CONF_UNIT_OF_MEASUREMENT) - value_template = config.get(CONF_VALUE_TEMPLATE) - if value_template is not None: + if (value_template := config.get(CONF_VALUE_TEMPLATE)) is not None: value_template.hass = hass if hass.config.is_allowed_path(file_path): diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py index 64257c47f47..ad079f8322d 100644 --- a/homeassistant/components/homekit_controller/binary_sensor.py +++ b/homeassistant/components/homekit_controller/binary_sensor.py @@ -123,8 +123,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def async_add_service(service): - entity_class = ENTITY_TYPES.get(service.short_type) - if not entity_class: + if not (entity_class := ENTITY_TYPES.get(service.short_type)): return False info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([entity_class(conn, info)], True) diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index ec0f383356e..880369bb6cb 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -93,8 +93,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def async_add_service(service): - entity_class = ENTITY_TYPES.get(service.short_type) - if not entity_class: + if not (entity_class := ENTITY_TYPES.get(service.short_type)): return False info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([entity_class(conn, info)], True) diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index 7e71edd6a75..ee736bd2c48 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -41,8 +41,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def async_add_service(service): - entity_class = ENTITY_TYPES.get(service.short_type) - if not entity_class: + if not (entity_class := ENTITY_TYPES.get(service.short_type)): return False info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([entity_class(conn, info)], True) diff --git a/homeassistant/components/homekit_controller/fan.py b/homeassistant/components/homekit_controller/fan.py index 89f24a66a94..0c0c9ccda9c 100644 --- a/homeassistant/components/homekit_controller/fan.py +++ b/homeassistant/components/homekit_controller/fan.py @@ -154,8 +154,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def async_add_service(service): - entity_class = ENTITY_TYPES.get(service.short_type) - if not entity_class: + if not (entity_class := ENTITY_TYPES.get(service.short_type)): return False info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([entity_class(conn, info)], True) diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index 765acfe74b6..15324a2436e 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -362,8 +362,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def async_add_service(service): - entity_class = ENTITY_TYPES.get(service.short_type) - if not entity_class: + if not (entity_class := ENTITY_TYPES.get(service.short_type)): return False info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([entity_class(conn, info)], True) diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py index 36ed379bc80..4ae9ed5a5f0 100644 --- a/homeassistant/components/homekit_controller/switch.py +++ b/homeassistant/components/homekit_controller/switch.py @@ -110,8 +110,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def async_add_service(service): - entity_class = ENTITY_TYPES.get(service.short_type) - if not entity_class: + if not (entity_class := ENTITY_TYPES.get(service.short_type)): return False info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([entity_class(conn, info)], True) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 1dc13c9fb14..a758f850b93 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -205,8 +205,7 @@ class LogbookView(HomeAssistantView): else: period = int(period) - entity_ids = request.query.get("entity") - if entity_ids: + if entity_ids := request.query.get("entity"): try: entity_ids = cv.entity_ids(entity_ids) except vol.Invalid: diff --git a/homeassistant/components/maxcube/climate.py b/homeassistant/components/maxcube/climate.py index d3dd780134a..2b2395acfc6 100644 --- a/homeassistant/components/maxcube/climate.py +++ b/homeassistant/components/maxcube/climate.py @@ -200,8 +200,7 @@ class MaxCubeClimate(ClimateEntity): def set_temperature(self, **kwargs): """Set new target temperatures.""" - temp = kwargs.get(ATTR_TEMPERATURE) - if temp is None: + if (temp := kwargs.get(ATTR_TEMPERATURE)) is None: raise ValueError( f"No {ATTR_TEMPERATURE} parameter passed to set_temperature method." ) diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index 142d90e6284..27203aab298 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -43,8 +43,7 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Meteo-France from legacy config file.""" - conf = config.get(DOMAIN) - if not conf: + if not (conf := config.get(DOMAIN)): return True for city_conf in conf: diff --git a/homeassistant/components/notify/legacy.py b/homeassistant/components/notify/legacy.py index a2bf9c0c173..8eb007b6398 100644 --- a/homeassistant/components/notify/legacy.py +++ b/homeassistant/components/notify/legacy.py @@ -202,9 +202,8 @@ class BaseNotificationService: """Handle sending notification message service calls.""" kwargs = {} message = service.data[ATTR_MESSAGE] - title = service.data.get(ATTR_TITLE) - if title: + if title := service.data.get(ATTR_TITLE): check_templates_warn(self.hass, title) title.hass = self.hass kwargs[ATTR_TITLE] = title.async_render(parse_result=False) diff --git a/homeassistant/components/ozw/climate.py b/homeassistant/components/ozw/climate.py index e403c4f5517..30feb5cafcb 100644 --- a/homeassistant/components/ozw/climate.py +++ b/homeassistant/components/ozw/climate.py @@ -254,9 +254,7 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): Must know if single or double setpoint. """ - hvac_mode = kwargs.get(ATTR_HVAC_MODE) - - if hvac_mode is not None: + if (hvac_mode := kwargs.get(ATTR_HVAC_MODE)) is not None: await self.async_set_hvac_mode(hvac_mode) if len(self._current_mode_setpoint_values) == 1: @@ -290,8 +288,7 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): "Thermostat %s does not support setting a mode", self.entity_id ) return - hvac_mode_value = self._hvac_modes.get(hvac_mode) - if hvac_mode_value is None: + if (hvac_mode_value := self._hvac_modes.get(hvac_mode)) is None: _LOGGER.warning("Received an invalid hvac mode: %s", hvac_mode) return self.values.mode.send_value(hvac_mode_value) diff --git a/homeassistant/components/ozw/light.py b/homeassistant/components/ozw/light.py index 7c52da23fb4..1292b558b6d 100644 --- a/homeassistant/components/ozw/light.py +++ b/homeassistant/components/ozw/light.py @@ -177,8 +177,7 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity): # transition specified by user new_value = int(max(0, min(7620, kwargs[ATTR_TRANSITION]))) if ozw_version < (1, 6, 1205): - transition = kwargs[ATTR_TRANSITION] - if transition <= 127: + if (transition := kwargs[ATTR_TRANSITION]) <= 127: new_value = int(transition) else: minutes = int(transition / 60) diff --git a/homeassistant/components/sensor/significant_change.py b/homeassistant/components/sensor/significant_change.py index 5c180be62f3..50013ff907d 100644 --- a/homeassistant/components/sensor/significant_change.py +++ b/homeassistant/components/sensor/significant_change.py @@ -48,9 +48,7 @@ def async_check_significant_change( **kwargs: Any, ) -> bool | None: """Test if state significantly changed.""" - device_class = new_attrs.get(ATTR_DEVICE_CLASS) - - if device_class is None: + if (device_class := new_attrs.get(ATTR_DEVICE_CLASS)) is None: return None absolute_change: float | None = None diff --git a/homeassistant/components/tankerkoenig/__init__.py b/homeassistant/components/tankerkoenig/__init__.py index bf2468b704c..b85f3482a5d 100644 --- a/homeassistant/components/tankerkoenig/__init__.py +++ b/homeassistant/components/tankerkoenig/__init__.py @@ -132,8 +132,7 @@ class TankerkoenigData: return False # Add stations found via location + radius - nearby_stations = data["stations"] - if not nearby_stations: + if not (nearby_stations := data["stations"]): if not additional_stations: _LOGGER.error( "Could not find any station in range." @@ -144,7 +143,7 @@ class TankerkoenigData: "Could not find any station in range. Will only use manually specified stations" ) else: - for station in data["stations"]: + for station in nearby_stations: self.add_station(station) # Add manually specified additional stations diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py index bdd7242a76a..620114863a8 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -143,8 +143,7 @@ class ValloxStateProxy: if metric_key not in vlxDevConstants.__dict__: raise KeyError(f"Unknown metric key: {metric_key}") - value = self._metric_cache[metric_key] - if value is None: + if (value := self._metric_cache[metric_key]) is None: return None if not isinstance(value, (str, int, float)): diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index c50292ea427..37ffdf5f216 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -140,8 +140,7 @@ async def async_setup_entry( # noqa: C901 hass: HomeAssistant, entry: ConfigEntry ) -> bool: """Set up Z-Wave JS from a config entry.""" - use_addon = entry.data.get(CONF_USE_ADDON) - if use_addon: + if use_addon := entry.data.get(CONF_USE_ADDON): await async_ensure_addon_running(hass, entry) client = ZwaveClient(entry.data[CONF_URL], async_get_clientsession(hass)) diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index f4fd8d10886..f9c94cc938d 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -209,8 +209,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): def _setpoint_value(self, setpoint_type: ThermostatSetpointType) -> ZwaveValue: """Optionally return a ZwaveValue for a setpoint.""" - val = self._setpoint_values[setpoint_type] - if val is None: + if (val := self._setpoint_values[setpoint_type]) is None: raise ValueError("Value requested is not available") return val @@ -231,8 +230,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): mode_id = int(mode_id) if mode_id in THERMOSTAT_MODES: # treat value as hvac mode - hass_mode = ZW_HVAC_MODE_MAP.get(mode_id) - if hass_mode: + if hass_mode := ZW_HVAC_MODE_MAP.get(mode_id): all_modes[hass_mode] = mode_id else: # treat value as hvac preset @@ -470,8 +468,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" - hvac_mode_id = self._hvac_modes.get(hvac_mode) - if hvac_mode_id is None: + if (hvac_mode_id := self._hvac_modes.get(hvac_mode)) is None: raise ValueError(f"Received an invalid hvac mode: {hvac_mode}") if not self._current_mode: diff --git a/homeassistant/components/zwave_js/device_trigger.py b/homeassistant/components/zwave_js/device_trigger.py index 11236697198..368226d36a5 100644 --- a/homeassistant/components/zwave_js/device_trigger.py +++ b/homeassistant/components/zwave_js/device_trigger.py @@ -236,8 +236,7 @@ def get_trigger_platform_from_type(trigger_type: str) -> str: trigger_split = trigger_type.split(".") # Our convention for trigger types is to have the trigger type at the beginning # delimited by a `.`. For zwave_js triggers, there is a `.` in the name - trigger_platform = trigger_split[0] - if trigger_platform == DOMAIN: + if (trigger_platform := trigger_split[0]) == DOMAIN: return ".".join(trigger_split[:2]) return trigger_platform diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index 4894c40b8ae..aa6db532616 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -87,9 +87,8 @@ def async_get_node_from_device_id( """ if not dev_reg: dev_reg = dr.async_get(hass) - device_entry = dev_reg.async_get(device_id) - if not device_entry: + if not (device_entry := dev_reg.async_get(device_id)): raise ValueError(f"Device ID {device_id} is not valid") # Use device config entry ID's to validate that this is a valid zwave_js device @@ -230,8 +229,7 @@ def async_get_node_status_sensor_entity_id( ent_reg = er.async_get(hass) if not dev_reg: dev_reg = dr.async_get(hass) - device = dev_reg.async_get(device_id) - if not device: + if not (device := dev_reg.async_get(device_id)): raise HomeAssistantError("Invalid Device ID provided") entry_id = next(entry_id for entry_id in device.config_entries) diff --git a/homeassistant/components/zwave_js/migrate.py b/homeassistant/components/zwave_js/migrate.py index 6598f26d45c..d9b46ac725c 100644 --- a/homeassistant/components/zwave_js/migrate.py +++ b/homeassistant/components/zwave_js/migrate.py @@ -331,8 +331,7 @@ async def async_migrate_legacy_zwave( ent_reg = async_get_entity_registry(hass) for zwave_js_entity_id, zwave_entry in migration_map.entity_entries.items(): zwave_entity_id = zwave_entry["entity_id"] - entity_entry = ent_reg.async_get(zwave_entity_id) - if not entity_entry: + if not (entity_entry := ent_reg.async_get(zwave_entity_id)): continue ent_reg.async_remove(zwave_entity_id) ent_reg.async_update_entity( From ea2e94a4e5c8e3a30808e66588a656025a303419 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 22 Oct 2021 14:07:19 +0200 Subject: [PATCH 0663/1038] Use assignment expressions 24 (#58181) --- .../components/androidtv/media_player.py | 3 +-- homeassistant/components/arwn/sensor.py | 3 +-- homeassistant/components/doorbird/__init__.py | 4 +--- homeassistant/components/energy/validate.py | 12 +++--------- .../components/huawei_lte/__init__.py | 3 +-- .../components/hyperion/config_flow.py | 3 +-- homeassistant/components/konnected/__init__.py | 18 ++++++------------ homeassistant/components/konnected/handlers.py | 3 +-- homeassistant/components/konnected/panel.py | 4 +--- homeassistant/components/mazda/__init__.py | 3 +-- homeassistant/components/mill/climate.py | 3 +-- homeassistant/components/netatmo/climate.py | 3 +-- .../components/oasa_telematics/sensor.py | 3 +-- homeassistant/components/scsgate/switch.py | 4 +--- homeassistant/components/smtp/notify.py | 6 ++---- homeassistant/components/spider/climate.py | 3 +-- .../components/system_log/__init__.py | 3 +-- .../components/threshold/binary_sensor.py | 3 +-- homeassistant/components/twilio_call/notify.py | 5 +---- homeassistant/components/unifi/config_flow.py | 3 +-- .../components/yamaha_musiccast/config_flow.py | 3 +-- 21 files changed, 29 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 533470181c1..89deeec25b8 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -533,8 +533,7 @@ class ADBDevice(MediaPlayerEntity): @adb_decorator() async def adb_command(self, cmd): """Send an ADB command to an Android TV / Fire TV device.""" - key = KEYS.get(cmd) - if key: + if key := KEYS.get(cmd): await self.aftv.adb_shell(f"input keyevent {key}") return diff --git a/homeassistant/components/arwn/sensor.py b/homeassistant/components/arwn/sensor.py index 321be5035cd..2571d35f98e 100644 --- a/homeassistant/components/arwn/sensor.py +++ b/homeassistant/components/arwn/sensor.py @@ -95,8 +95,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if not sensors: return - store = hass.data.get(DATA_ARWN) - if store is None: + if (store := hass.data.get(DATA_ARWN)) is None: store = hass.data[DATA_ARWN] = {} if isinstance(sensors, ArwnSensor): diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 4c720d5b9de..63270ee1f1b 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -68,9 +68,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: def _reset_device_favorites_handler(event): """Handle clearing favorites on device.""" - token = event.data.get("token") - - if token is None: + if (token := event.data.get("token")) is None: return doorstation = get_doorstation_by_token(hass, token) diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py index c7f6c46aa1c..b2a939bffce 100644 --- a/homeassistant/components/energy/validate.py +++ b/homeassistant/components/energy/validate.py @@ -103,9 +103,7 @@ async def _async_validate_usage_stat( ) return - state = hass.states.get(entity_id) - - if state is None: + if (state := hass.states.get(entity_id)) is None: result.append( ValidationIssue( "entity_not_defined", @@ -180,9 +178,7 @@ def _async_validate_price_entity( unit_error: str, ) -> None: """Validate that the price entity is correct.""" - state = hass.states.get(entity_id) - - if state is None: + if (state := hass.states.get(entity_id)) is None: result.append( ValidationIssue( "entity_not_defined", @@ -228,9 +224,7 @@ async def _async_validate_cost_stat( if not recorder.is_entity_recorded(hass, stat_id): result.append(ValidationIssue("recorder_untracked", stat_id)) - state = hass.states.get(stat_id) - - if state is None: + if (state := hass.states.get(stat_id)) is None: result.append(ValidationIssue("entity_not_defined", stat_id)) return diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 92122f1b2be..9ee5927695d 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -314,8 +314,7 @@ async def async_setup_entry( # noqa: C901 # Override settings from YAML config, but only if they're changed in it # Old values are stored as *_from_yaml in the config entry - yaml_config = hass.data[DOMAIN].config.get(url) - if yaml_config: + if yaml_config := hass.data[DOMAIN].config.get(url): # Config values new_data = {} for key in CONF_USERNAME, CONF_PASSWORD: diff --git a/homeassistant/components/hyperion/config_flow.py b/homeassistant/components/hyperion/config_flow.py index 81fef6429f6..6c76f03e3de 100644 --- a/homeassistant/components/hyperion/config_flow.py +++ b/homeassistant/components/hyperion/config_flow.py @@ -203,8 +203,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): except ValueError: self._data[CONF_PORT] = const.DEFAULT_PORT_JSON - hyperion_id = discovery_info.get(ATTR_UPNP_SERIAL) - if not hyperion_id: + if not (hyperion_id := discovery_info.get(ATTR_UPNP_SERIAL)): return self.async_abort(reason="no_id") # For discovery mechanisms, we set the unique_id as early as possible to diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index 8fd439a0355..43537154e41 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -221,8 +221,7 @@ PLATFORMS = ["binary_sensor", "sensor", "switch"] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Konnected platform.""" - cfg = config.get(DOMAIN) - if cfg is None: + if (cfg := config.get(DOMAIN)) is None: cfg = {} if DOMAIN not in hass.data: @@ -336,14 +335,12 @@ class KonnectedView(HomeAssistantView): "updating instructions" ) - device = data[CONF_DEVICES].get(device_id) - if device is None: + if (device := data[CONF_DEVICES].get(device_id)) is None: return self.json_message( "unregistered device", status_code=HTTPStatus.BAD_REQUEST ) - panel = device.get("panel") - if panel is not None: + if (panel := device.get("panel")) is not None: # connect if we haven't already hass.async_create_task(panel.async_connect()) @@ -382,14 +379,12 @@ class KonnectedView(HomeAssistantView): hass = request.app["hass"] data = hass.data[DOMAIN] - device = data[CONF_DEVICES].get(device_id) - if not device: + if not (device := data[CONF_DEVICES].get(device_id)): return self.json_message( f"Device {device_id} not configured", status_code=HTTPStatus.NOT_FOUND ) - panel = device.get("panel") - if panel is not None: + if (panel := device.get("panel")) is not None: # connect if we haven't already hass.async_create_task(panel.async_connect()) @@ -427,8 +422,7 @@ class KonnectedView(HomeAssistantView): resp[CONF_PIN] = ZONE_TO_PIN[zone_num] # Make sure entity is setup - zone_entity_id = zone.get(ATTR_ENTITY_ID) - if zone_entity_id: + if zone_entity_id := zone.get(ATTR_ENTITY_ID): resp["state"] = self.binary_value( hass.states.get(zone_entity_id).state, zone[CONF_ACTIVATION] ) diff --git a/homeassistant/components/konnected/handlers.py b/homeassistant/components/konnected/handlers.py index 879d0d4cf8f..b3c3f440e6f 100644 --- a/homeassistant/components/konnected/handlers.py +++ b/homeassistant/components/konnected/handlers.py @@ -51,8 +51,7 @@ async def async_handle_addr_update(hass, context, msg): """Handle an addressable sensor update.""" _LOGGER.debug("[addr handler] context: %s msg: %s", context, msg) addr, temp = msg.get("addr"), msg.get("temp") - entity_id = context.get(addr) - if entity_id: + if entity_id := context.get(addr): async_dispatcher_send(hass, f"konnected.{entity_id}.update", temp) else: msg["device_id"] = context.get("device_id") diff --git a/homeassistant/components/konnected/panel.py b/homeassistant/components/konnected/panel.py index cf2f33de332..137fdada8c5 100644 --- a/homeassistant/components/konnected/panel.py +++ b/homeassistant/components/konnected/panel.py @@ -347,9 +347,7 @@ class AlarmPanel: @callback def async_current_settings_payload(self): """Return a dict of configuration currently stored on the device.""" - settings = self.status["settings"] - if not settings: - settings = {} + settings = self.status["settings"] or {} return { "sensors": [ diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index 74a92b3e371..a9775b6d6df 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -110,9 +110,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def validate_mazda_device_id(device_id): """Check that a device ID exists in the registry and has at least one 'mazda' identifier.""" dev_reg = device_registry.async_get(hass) - device_entry = dev_reg.async_get(device_id) - if device_entry is None: + if (device_entry := dev_reg.async_get(device_id)) is None: raise vol.Invalid("Invalid device ID") mazda_identifiers = [ diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index 3ab9c64942c..3890a08de2b 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -98,8 +98,7 @@ class MillHeater(CoordinatorEntity, ClimateEntity): async def async_set_temperature(self, **kwargs): """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return await self.coordinator.mill_data_connection.set_heater_temp( self._id, int(temperature) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 6b0f9cf5124..1e2dc3f0195 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -444,8 +444,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature for 2 hours.""" - temp = kwargs.get(ATTR_TEMPERATURE) - if temp is None: + if (temp := kwargs.get(ATTR_TEMPERATURE)) is None: return await self._home_status.async_set_room_thermpoint( self._id, STATE_NETATMO_MANUAL, temp diff --git a/homeassistant/components/oasa_telematics/sensor.py b/homeassistant/components/oasa_telematics/sensor.py index 4c9b583a36b..1a51738cb77 100644 --- a/homeassistant/components/oasa_telematics/sensor.py +++ b/homeassistant/components/oasa_telematics/sensor.py @@ -176,8 +176,7 @@ class OASATelematicsData: current_time = dt_util.utcnow() for result in results: - btime2 = result.get("btime2") - if btime2 is not None: + if (btime2 := result.get("btime2")) is not None: arrival_min = int(btime2) timestamp = current_time + timedelta(minutes=arrival_min) arrival_data = { diff --git a/homeassistant/components/scsgate/switch.py b/homeassistant/components/scsgate/switch.py index f5719aa24dc..175f40c3eba 100644 --- a/homeassistant/components/scsgate/switch.py +++ b/homeassistant/components/scsgate/switch.py @@ -62,9 +62,7 @@ def _setup_traditional_switches(logger, config, scsgate, add_entities_callback): def _setup_scenario_switches(logger, config, scsgate, hass): """Add only SCSGate scenario switches.""" - scenario = config.get(CONF_SCENARIO) - - if scenario: + if scenario := config.get(CONF_SCENARIO): for entity_info in scenario.values(): if entity_info[CONF_SCS_ID] in scsgate.devices: continue diff --git a/homeassistant/components/smtp/notify.py b/homeassistant/components/smtp/notify.py index 0586a5838fc..ef743912bc9 100644 --- a/homeassistant/components/smtp/notify.py +++ b/homeassistant/components/smtp/notify.py @@ -170,9 +170,8 @@ class MailNotificationService(BaseNotificationService): build a multipart HTML if html config is defined. """ subject = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) - data = kwargs.get(ATTR_DATA) - if data: + if data := kwargs.get(ATTR_DATA): if ATTR_HTML in data: msg = _build_html_msg( message, data[ATTR_HTML], images=data.get(ATTR_IMAGES, []) @@ -184,8 +183,7 @@ class MailNotificationService(BaseNotificationService): msg["Subject"] = subject - recipients = kwargs.get(ATTR_TARGET) - if not recipients: + if not (recipients := kwargs.get(ATTR_TARGET)): recipients = self.recipients msg["To"] = recipients if isinstance(recipients, str) else ",".join(recipients) if self._sender_name: diff --git a/homeassistant/components/spider/climate.py b/homeassistant/components/spider/climate.py index 78764ccf4e7..52146518ee3 100644 --- a/homeassistant/components/spider/climate.py +++ b/homeassistant/components/spider/climate.py @@ -113,8 +113,7 @@ class SpiderThermostat(ClimateEntity): def set_temperature(self, **kwargs): """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is None: + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return self.thermostat.set_temperature(temperature) diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 9c868724d9b..8a88eef7bfc 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -197,8 +197,7 @@ class LogErrorHandler(logging.Handler): async def async_setup(hass, config): """Set up the logger component.""" - conf = config.get(DOMAIN) - if conf is None: + if (conf := config.get(DOMAIN)) is None: conf = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN] simple_queue = queue.SimpleQueue() diff --git a/homeassistant/components/threshold/binary_sensor.py b/homeassistant/components/threshold/binary_sensor.py index 1a53a599394..2db9f8aba06 100644 --- a/homeassistant/components/threshold/binary_sensor.py +++ b/homeassistant/components/threshold/binary_sensor.py @@ -95,8 +95,7 @@ class ThresholdSensor(BinarySensorEntity): @callback def async_threshold_sensor_state_listener(event): """Handle sensor state changes.""" - new_state = event.data.get("new_state") - if new_state is None: + if (new_state := event.data.get("new_state")) is None: return try: diff --git a/homeassistant/components/twilio_call/notify.py b/homeassistant/components/twilio_call/notify.py index 83ca081b26e..ca007a8231e 100644 --- a/homeassistant/components/twilio_call/notify.py +++ b/homeassistant/components/twilio_call/notify.py @@ -43,10 +43,7 @@ class TwilioCallNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Call to specified target users.""" - - targets = kwargs.get(ATTR_TARGET) - - if not targets: + if not (targets := kwargs.get(ATTR_TARGET)): _LOGGER.info("At least 1 target is required") return diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 5edc71f9f5d..ea737599b20 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -235,8 +235,7 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): CONF_SITE_ID: DEFAULT_SITE_ID, } - port = MODEL_PORTS.get(model_description) - if port is not None: + if (port := MODEL_PORTS.get(model_description)) is not None: self.config[CONF_PORT] = port return await self.async_step_user() diff --git a/homeassistant/components/yamaha_musiccast/config_flow.py b/homeassistant/components/yamaha_musiccast/config_flow.py index f4ad455fb04..6645af20d83 100644 --- a/homeassistant/components/yamaha_musiccast/config_flow.py +++ b/homeassistant/components/yamaha_musiccast/config_flow.py @@ -54,8 +54,7 @@ class MusicCastFlowHandler(ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - serial_number = info.get("system_id") - if serial_number is None: + if (serial_number := info.get("system_id")) is None: errors["base"] = "no_musiccast_device" if not errors: From 3c52bc214d827b70fddfe8f88cace19aa2f726d5 Mon Sep 17 00:00:00 2001 From: Regev Brody Date: Fri, 22 Oct 2021 15:18:01 +0300 Subject: [PATCH 0664/1038] Add Smoke Detector (ywbj) device support to Tuya (#58170) --- .../components/tuya/binary_sensor.py | 17 +++++++++++++++++ homeassistant/components/tuya/const.py | 4 ++++ homeassistant/components/tuya/sensor.py | 19 +++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index 8adf930641c..ad026d2cef0 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -10,6 +10,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, DEVICE_CLASS_SAFETY, + DEVICE_CLASS_SMOKE, DEVICE_CLASS_TAMPER, DEVICE_CLASS_VIBRATION, BinarySensorEntity, @@ -98,6 +99,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { device_class=DEVICE_CLASS_TAMPER, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), + TAMPER_BINARY_SENSOR, ), # PIR Detector # https://developer.tuya.com/en/docs/iot/categorypir?id=Kaiuz3ss11b80 @@ -128,6 +130,21 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { ), TAMPER_BINARY_SENSOR, ), + # Smoke Detector + # https://developer.tuya.com/en/docs/iot/categoryywbj?id=Kaiuz3f6sf952 + "ywbj": ( + TuyaBinarySensorEntityDescription( + key=DPCode.SMOKE_SENSOR_STATUS, + device_class=DEVICE_CLASS_SMOKE, + on_value="alarm", + ), + TuyaBinarySensorEntityDescription( + key=DPCode.SMOKE_SENSOR_STATE, + device_class=DEVICE_CLASS_SMOKE, + on_value="1", + ), + TAMPER_BINARY_SENSOR, + ), # Vibration Sensor # https://developer.tuya.com/en/docs/iot/categoryzd?id=Kaiuz3a5vrzno "zd": ( diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index a20206c9369..2c6bb67fa16 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -132,6 +132,7 @@ class DPCode(str, Enum): ANION = "anion" # Ionizer unit BATTERY_PERCENTAGE = "battery_percentage" # Battery percentage BATTERY_STATE = "battery_state" # Battery state + BATTERY_VALUE = "battery_value" # Battery value BRIGHT_CONTROLLER = "bright_controller" BRIGHT_STATE = "bright_state" # Brightness status BRIGHT_VALUE = "bright_value" # Brightness @@ -144,6 +145,9 @@ class DPCode(str, Enum): CH2O_VALUE = "ch2o_value" CHILD_LOCK = "child_lock" # Child lock CO2_STATE = "co2_state" + SMOKE_SENSOR_STATUS = "smoke_sensor_status" + SMOKE_SENSOR_STATE = "smoke_sensor_state" + SMOKE_SENSOR_VALUE = "smoke_sensor_value" CO2_VALUE = "co2_value" # CO2 concentration COLOR_DATA_V2 = "color_data_v2" COLOUR_DATA = "colour_data" # Colored light mode diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index c1407652d92..6624635fe18 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -57,6 +57,13 @@ BATTERY_SENSORS: tuple[SensorEntityDescription, ...] = ( icon="mdi:battery", entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), + SensorEntityDescription( + key=DPCode.BATTERY_VALUE, + name="Battery", + device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=STATE_CLASS_MEASUREMENT, + ), ) # All descriptions can be found here. Mostly the Integer data types in the @@ -198,6 +205,18 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { # Emergency Button # https://developer.tuya.com/en/docs/iot/categorysos?id=Kaiuz3oi6agjy "sos": BATTERY_SENSORS, + # Smoke Detector + # https://developer.tuya.com/en/docs/iot/categoryywbj?id=Kaiuz3f6sf952 + "ywbj": ( + SensorEntityDescription( + key=DPCode.SMOKE_SENSOR_VALUE, + name="Smoke Amount", + icon="mdi:smoke-detector", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + device_class=STATE_CLASS_MEASUREMENT, + ), + *BATTERY_SENSORS, + ), # Vibration Sensor # https://developer.tuya.com/en/docs/iot/categoryzd?id=Kaiuz3a5vrzno "zd": BATTERY_SENSORS, From 55ffc85a0a5b36e60ba156a70f59efb37ea914bc Mon Sep 17 00:00:00 2001 From: David K Turner Date: Fri, 22 Oct 2021 07:19:19 -0500 Subject: [PATCH 0665/1038] Enable long-term statistics for OpenWeatherMap sensors (#57781) --- homeassistant/components/openweathermap/const.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 7647d64d7a2..607f223167f 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -1,7 +1,10 @@ """Consts for the OpenWeatherMap.""" from __future__ import annotations -from homeassistant.components.sensor import SensorEntityDescription +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntityDescription, +) from homeassistant.components.weather import ( ATTR_CONDITION_CLOUDY, ATTR_CONDITION_EXCEPTIONAL, @@ -173,55 +176,65 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Dew Point", native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_TEMPERATURE, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_FEELS_LIKE_TEMPERATURE, name="Feels like temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_WIND_SPEED, name="Wind speed", native_unit_of_measurement=SPEED_METERS_PER_SECOND, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_WIND_BEARING, name="Wind bearing", native_unit_of_measurement=DEGREE, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_HUMIDITY, name="Humidity", native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_PRESSURE, name="Pressure", native_unit_of_measurement=PRESSURE_HPA, device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_CLOUDS, name="Cloud coverage", native_unit_of_measurement=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_RAIN, name="Rain", native_unit_of_measurement=LENGTH_MILLIMETERS, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_SNOW, name="Snow", native_unit_of_measurement=LENGTH_MILLIMETERS, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_PRECIPITATION_KIND, @@ -231,6 +244,7 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key=ATTR_API_UV_INDEX, name="UV Index", native_unit_of_measurement=UV_INDEX, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key=ATTR_API_CONDITION, From a598d9f35363939562cdfd1f0cc376b41a169120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 22 Oct 2021 15:21:34 +0300 Subject: [PATCH 0666/1038] Use HTTPStatus instead of HTTP_ consts and magic values in components/a* (#57988) --- homeassistant/components/abode/config_flow.py | 6 ++- homeassistant/components/aftership/sensor.py | 7 +-- homeassistant/components/airly/config_flow.py | 14 ++--- homeassistant/components/alexa/auth.py | 5 +- .../components/alexa/flash_briefings.py | 9 ++-- .../components/alexa/state_report.py | 7 +-- .../components/arest/binary_sensor.py | 11 ++-- homeassistant/components/arest/sensor.py | 4 +- homeassistant/components/arest/switch.py | 15 +++--- homeassistant/components/august/gateway.py | 10 ++-- tests/components/abode/test_config_flow.py | 18 +++---- tests/components/abode/test_init.py | 7 ++- tests/components/airly/test_config_flow.py | 15 ++---- .../components/alexa/test_flash_briefings.py | 14 ++--- tests/components/alexa/test_intent.py | 23 +++++---- .../components/alexa/test_smart_home_http.py | 5 +- tests/components/almond/test_config_flow.py | 3 +- tests/components/api/test_init.py | 51 ++++++++++--------- tests/components/august/test_camera.py | 3 +- tests/components/auth/test_init.py | 31 +++++------ tests/components/auth/test_init_link_user.py | 17 ++++--- tests/components/auth/test_login_flow.py | 17 ++++--- 22 files changed, 145 insertions(+), 147 deletions(-) diff --git a/homeassistant/components/abode/config_flow.py b/homeassistant/components/abode/config_flow.py index 8b2f622d6e7..0c22766e373 100644 --- a/homeassistant/components/abode/config_flow.py +++ b/homeassistant/components/abode/config_flow.py @@ -1,4 +1,6 @@ """Config flow for the Abode Security System component.""" +from http import HTTPStatus + from abodepy import Abode from abodepy.exceptions import AbodeAuthenticationException, AbodeException from abodepy.helpers.errors import MFA_CODE_REQUIRED @@ -6,7 +8,7 @@ from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, HTTP_BAD_REQUEST +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from .const import DEFAULT_CACHEDB, DOMAIN, LOGGER @@ -51,7 +53,7 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): LOGGER.error("Unable to connect to Abode: %s", ex) - if ex.errcode == HTTP_BAD_REQUEST: + if ex.errcode == HTTPStatus.BAD_REQUEST: errors = {"base": "invalid_auth"} else: diff --git a/homeassistant/components/aftership/sensor.py b/homeassistant/components/aftership/sensor.py index f7d89767d54..be3fd74d6bd 100644 --- a/homeassistant/components/aftership/sensor.py +++ b/homeassistant/components/aftership/sensor.py @@ -1,6 +1,7 @@ """Support for non-delivered packages recorded in AfterShip.""" from __future__ import annotations +from http import HTTPStatus import logging from typing import Any, Final @@ -11,7 +12,7 @@ from homeassistant.components.sensor import ( PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, SensorEntity, ) -from homeassistant.const import CONF_API_KEY, CONF_NAME, HTTP_OK +from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -64,7 +65,7 @@ async def async_setup_platform( await aftership.get_trackings() - if not aftership.meta or aftership.meta["code"] != HTTP_OK: + if not aftership.meta or aftership.meta["code"] != HTTPStatus.OK: _LOGGER.error( "No tracking data found. Check API key is correct: %s", aftership.meta ) @@ -151,7 +152,7 @@ class AfterShipSensor(SensorEntity): if not self.aftership.meta: _LOGGER.error("Unknown errors when querying") return - if self.aftership.meta["code"] != HTTP_OK: + if self.aftership.meta["code"] != HTTPStatus.OK: _LOGGER.error( "Errors when querying AfterShip. %s", str(self.aftership.meta) ) diff --git a/homeassistant/components/airly/config_flow.py b/homeassistant/components/airly/config_flow.py index 598aa15b9b6..a6fa9f2d1d6 100644 --- a/homeassistant/components/airly/config_flow.py +++ b/homeassistant/components/airly/config_flow.py @@ -1,6 +1,7 @@ """Adds config flow for Airly.""" from __future__ import annotations +from http import HTTPStatus from typing import Any from aiohttp import ClientSession @@ -10,14 +11,7 @@ import async_timeout import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import ( - CONF_API_KEY, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, - HTTP_NOT_FOUND, - HTTP_UNAUTHORIZED, -) +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -60,9 +54,9 @@ class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): use_nearest=True, ) except AirlyError as err: - if err.status_code == HTTP_UNAUTHORIZED: + if err.status_code == HTTPStatus.UNAUTHORIZED: errors["base"] = "invalid_api_key" - if err.status_code == HTTP_NOT_FOUND: + if err.status_code == HTTPStatus.NOT_FOUND: errors["base"] = "wrong_location" else: if not location_point_valid: diff --git a/homeassistant/components/alexa/auth.py b/homeassistant/components/alexa/auth.py index 433b2929602..91729763804 100644 --- a/homeassistant/components/alexa/auth.py +++ b/homeassistant/components/alexa/auth.py @@ -1,13 +1,14 @@ """Support for Alexa skill auth.""" import asyncio from datetime import timedelta +from http import HTTPStatus import json import logging import aiohttp import async_timeout -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, HTTP_OK +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import callback from homeassistant.helpers import aiohttp_client from homeassistant.util import dt @@ -119,7 +120,7 @@ class Auth: _LOGGER.debug("LWA response header: %s", response.headers) _LOGGER.debug("LWA response status: %s", response.status) - if response.status != HTTP_OK: + if response.status != HTTPStatus.OK: _LOGGER.error("Error calling LWA to get auth token") return None diff --git a/homeassistant/components/alexa/flash_briefings.py b/homeassistant/components/alexa/flash_briefings.py index 68d6368a5e2..1521afcae5a 100644 --- a/homeassistant/components/alexa/flash_briefings.py +++ b/homeassistant/components/alexa/flash_briefings.py @@ -1,11 +1,12 @@ """Support for Alexa skill service end point.""" import copy import hmac +from http import HTTPStatus import logging import uuid from homeassistant.components import http -from homeassistant.const import CONF_PASSWORD, HTTP_NOT_FOUND, HTTP_UNAUTHORIZED +from homeassistant.const import CONF_PASSWORD from homeassistant.core import callback from homeassistant.helpers import template import homeassistant.util.dt as dt_util @@ -58,7 +59,7 @@ class AlexaFlashBriefingView(http.HomeAssistantView): if request.query.get(API_PASSWORD) is None: err = "No password provided for Alexa flash briefing: %s" _LOGGER.error(err, briefing_id) - return b"", HTTP_UNAUTHORIZED + return b"", HTTPStatus.UNAUTHORIZED if not hmac.compare_digest( request.query[API_PASSWORD].encode("utf-8"), @@ -66,12 +67,12 @@ class AlexaFlashBriefingView(http.HomeAssistantView): ): err = "Wrong password for Alexa flash briefing: %s" _LOGGER.error(err, briefing_id) - return b"", HTTP_UNAUTHORIZED + return b"", HTTPStatus.UNAUTHORIZED if not isinstance(self.flash_briefings.get(briefing_id), list): err = "No configured Alexa flash briefing was found for: %s" _LOGGER.error(err, briefing_id) - return b"", HTTP_NOT_FOUND + return b"", HTTPStatus.NOT_FOUND briefing = [] diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index 7a23706b4ba..e611960b9d9 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -2,13 +2,14 @@ from __future__ import annotations import asyncio +from http import HTTPStatus import json import logging import aiohttp import async_timeout -from homeassistant.const import HTTP_ACCEPTED, MATCH_ALL, STATE_ON +from homeassistant.const import MATCH_ALL, STATE_ON from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers.significant_change import create_checker import homeassistant.util.dt as dt_util @@ -148,7 +149,7 @@ async def async_send_changereport_message( _LOGGER.debug("Sent: %s", json.dumps(message_serialized)) _LOGGER.debug("Received (%s): %s", response.status, response_text) - if response.status == HTTP_ACCEPTED: + if response.status == HTTPStatus.ACCEPTED: return response_json = json.loads(response_text) @@ -279,7 +280,7 @@ async def async_send_doorbell_event_message(hass, config, alexa_entity): _LOGGER.debug("Sent: %s", json.dumps(message_serialized)) _LOGGER.debug("Received (%s): %s", response.status, response_text) - if response.status == HTTP_ACCEPTED: + if response.status == HTTPStatus.ACCEPTED: return response_json = json.loads(response_text) diff --git a/homeassistant/components/arest/binary_sensor.py b/homeassistant/components/arest/binary_sensor.py index d59e6d0cccb..1280e013f8d 100644 --- a/homeassistant/components/arest/binary_sensor.py +++ b/homeassistant/components/arest/binary_sensor.py @@ -1,5 +1,6 @@ """Support for an exposed aREST RESTful API of a device.""" from datetime import timedelta +from http import HTTPStatus import logging import requests @@ -10,13 +11,7 @@ from homeassistant.components.binary_sensor import ( PLATFORM_SCHEMA, BinarySensorEntity, ) -from homeassistant.const import ( - CONF_DEVICE_CLASS, - CONF_NAME, - CONF_PIN, - CONF_RESOURCE, - HTTP_OK, -) +from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_PIN, CONF_RESOURCE import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle @@ -78,7 +73,7 @@ class ArestBinarySensor(BinarySensorEntity): if pin is not None: request = requests.get(f"{resource}/mode/{pin}/i", timeout=10) - if request.status_code != HTTP_OK: + if request.status_code != HTTPStatus.OK: _LOGGER.error("Can't set mode of %s", resource) def update(self): diff --git a/homeassistant/components/arest/sensor.py b/homeassistant/components/arest/sensor.py index addd666e30e..7ca6d230a08 100644 --- a/homeassistant/components/arest/sensor.py +++ b/homeassistant/components/arest/sensor.py @@ -1,5 +1,6 @@ """Support for an exposed aREST RESTful API of a device.""" from datetime import timedelta +from http import HTTPStatus import logging import requests @@ -12,7 +13,6 @@ from homeassistant.const import ( CONF_RESOURCE, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, - HTTP_OK, ) from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv @@ -146,7 +146,7 @@ class ArestSensor(SensorEntity): if pin is not None: request = requests.get(f"{resource}/mode/{pin}/i", timeout=10) - if request.status_code != HTTP_OK: + if request.status_code != HTTPStatus.OK: _LOGGER.error("Can't set mode of %s", resource) def update(self): diff --git a/homeassistant/components/arest/switch.py b/homeassistant/components/arest/switch.py index ecbf24c23ca..97a763cb652 100644 --- a/homeassistant/components/arest/switch.py +++ b/homeassistant/components/arest/switch.py @@ -1,12 +1,13 @@ """Support for an exposed aREST RESTful API of a device.""" +from http import HTTPStatus import logging import requests import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity -from homeassistant.const import CONF_NAME, CONF_RESOURCE, HTTP_OK +from homeassistant.const import CONF_NAME, CONF_RESOURCE import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -101,7 +102,7 @@ class ArestSwitchFunction(ArestSwitchBase): request = requests.get(f"{self._resource}/{self._func}", timeout=10) - if request.status_code != HTTP_OK: + if request.status_code != HTTPStatus.OK: _LOGGER.error("Can't find function") return @@ -118,7 +119,7 @@ class ArestSwitchFunction(ArestSwitchBase): f"{self._resource}/{self._func}", timeout=10, params={"params": "1"} ) - if request.status_code == HTTP_OK: + if request.status_code == HTTPStatus.OK: self._attr_is_on = True else: _LOGGER.error("Can't turn on function %s at %s", self._func, self._resource) @@ -129,7 +130,7 @@ class ArestSwitchFunction(ArestSwitchBase): f"{self._resource}/{self._func}", timeout=10, params={"params": "0"} ) - if request.status_code == HTTP_OK: + if request.status_code == HTTPStatus.OK: self._attr_is_on = False else: _LOGGER.error( @@ -157,7 +158,7 @@ class ArestSwitchPin(ArestSwitchBase): self.invert = invert request = requests.get(f"{resource}/mode/{pin}/o", timeout=10) - if request.status_code != HTTP_OK: + if request.status_code != HTTPStatus.OK: _LOGGER.error("Can't set mode") self._attr_available = False @@ -167,7 +168,7 @@ class ArestSwitchPin(ArestSwitchBase): request = requests.get( f"{self._resource}/digital/{self._pin}/{turn_on_payload}", timeout=10 ) - if request.status_code == HTTP_OK: + if request.status_code == HTTPStatus.OK: self._attr_is_on = True else: _LOGGER.error("Can't turn on pin %s at %s", self._pin, self._resource) @@ -178,7 +179,7 @@ class ArestSwitchPin(ArestSwitchBase): request = requests.get( f"{self._resource}/digital/{self._pin}/{turn_off_payload}", timeout=10 ) - if request.status_code == HTTP_OK: + if request.status_code == HTTPStatus.OK: self._attr_is_on = False else: _LOGGER.error("Can't turn off pin %s at %s", self._pin, self._resource) diff --git a/homeassistant/components/august/gateway.py b/homeassistant/components/august/gateway.py index 5499246a187..6c9f9113d98 100644 --- a/homeassistant/components/august/gateway.py +++ b/homeassistant/components/august/gateway.py @@ -1,6 +1,7 @@ """Handle August connection setup and authentication.""" import asyncio +from http import HTTPStatus import logging import os @@ -8,12 +9,7 @@ from aiohttp import ClientError, ClientResponseError from yalexs.api_async import ApiAsync from yalexs.authenticator_async import AuthenticationState, AuthenticatorAsync -from homeassistant.const import ( - CONF_PASSWORD, - CONF_TIMEOUT, - CONF_USERNAME, - HTTP_UNAUTHORIZED, -) +from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.helpers import aiohttp_client from .const import ( @@ -97,7 +93,7 @@ class AugustGateway: # by have no access await self.api.async_get_operable_locks(self.access_token) except ClientResponseError as ex: - if ex.status == HTTP_UNAUTHORIZED: + if ex.status == HTTPStatus.UNAUTHORIZED: raise InvalidAuth from ex raise CannotConnect from ex diff --git a/tests/components/abode/test_config_flow.py b/tests/components/abode/test_config_flow.py index b56762bff40..44582692c73 100644 --- a/tests/components/abode/test_config_flow.py +++ b/tests/components/abode/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Abode config flow.""" +from http import HTTPStatus from unittest.mock import patch from abodepy.exceptions import AbodeAuthenticationException @@ -8,12 +9,7 @@ from homeassistant import data_entry_flow from homeassistant.components.abode import config_flow from homeassistant.components.abode.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER -from homeassistant.const import ( - CONF_PASSWORD, - CONF_USERNAME, - HTTP_BAD_REQUEST, - HTTP_INTERNAL_SERVER_ERROR, -) +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from tests.common import MockConfigEntry @@ -56,7 +52,9 @@ async def test_invalid_credentials(hass): with patch( "homeassistant.components.abode.config_flow.Abode", - side_effect=AbodeAuthenticationException((HTTP_BAD_REQUEST, "auth error")), + side_effect=AbodeAuthenticationException( + (HTTPStatus.BAD_REQUEST, "auth error") + ), ): result = await flow.async_step_user(user_input=conf) assert result["errors"] == {"base": "invalid_auth"} @@ -72,7 +70,7 @@ async def test_connection_error(hass): with patch( "homeassistant.components.abode.config_flow.Abode", side_effect=AbodeAuthenticationException( - (HTTP_INTERNAL_SERVER_ERROR, "connection error") + (HTTPStatus.INTERNAL_SERVER_ERROR, "connection error") ), ): result = await flow.async_step_user(user_input=conf) @@ -117,7 +115,9 @@ async def test_step_mfa(hass): with patch( "homeassistant.components.abode.config_flow.Abode", - side_effect=AbodeAuthenticationException((HTTP_BAD_REQUEST, "invalid mfa")), + side_effect=AbodeAuthenticationException( + (HTTPStatus.BAD_REQUEST, "invalid mfa") + ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"mfa_code": "123456"} diff --git a/tests/components/abode/test_init.py b/tests/components/abode/test_init.py index 5e58695ace6..130f0c6791e 100644 --- a/tests/components/abode/test_init.py +++ b/tests/components/abode/test_init.py @@ -1,4 +1,5 @@ """Tests for the Abode module.""" +from http import HTTPStatus from unittest.mock import patch from abodepy.exceptions import AbodeAuthenticationException, AbodeException @@ -12,7 +13,7 @@ from homeassistant.components.abode import ( ) from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_USERNAME, HTTP_BAD_REQUEST +from homeassistant.const import CONF_USERNAME from .common import setup_platform @@ -68,7 +69,9 @@ async def test_invalid_credentials(hass): """Test Abode credentials changing.""" with patch( "homeassistant.components.abode.Abode", - side_effect=AbodeAuthenticationException((HTTP_BAD_REQUEST, "auth error")), + side_effect=AbodeAuthenticationException( + (HTTPStatus.BAD_REQUEST, "auth error") + ), ), patch( "homeassistant.components.abode.config_flow.AbodeFlowHandler.async_step_reauth", return_value={"type": data_entry_flow.RESULT_TYPE_FORM}, diff --git a/tests/components/airly/test_config_flow.py b/tests/components/airly/test_config_flow.py index 5683a06bb28..c19618da0a7 100644 --- a/tests/components/airly/test_config_flow.py +++ b/tests/components/airly/test_config_flow.py @@ -1,17 +1,12 @@ """Define tests for the Airly config flow.""" +from http import HTTPStatus + from airly.exceptions import AirlyError from homeassistant import data_entry_flow from homeassistant.components.airly.const import CONF_USE_NEAREST, DOMAIN from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import ( - CONF_API_KEY, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, - HTTP_NOT_FOUND, - HTTP_UNAUTHORIZED, -) +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from . import API_NEAREST_URL, API_POINT_URL @@ -40,7 +35,7 @@ async def test_invalid_api_key(hass, aioclient_mock): aioclient_mock.get( API_POINT_URL, exc=AirlyError( - HTTP_UNAUTHORIZED, {"message": "Invalid authentication credentials"} + HTTPStatus.UNAUTHORIZED, {"message": "Invalid authentication credentials"} ), ) @@ -57,7 +52,7 @@ async def test_invalid_location(hass, aioclient_mock): aioclient_mock.get( API_NEAREST_URL, - exc=AirlyError(HTTP_NOT_FOUND, {"message": "Installation was not found"}), + exc=AirlyError(HTTPStatus.NOT_FOUND, {"message": "Installation was not found"}), ) result = await hass.config_entries.flow.async_init( diff --git a/tests/components/alexa/test_flash_briefings.py b/tests/components/alexa/test_flash_briefings.py index 7ab75d8c037..8b9c91e28b5 100644 --- a/tests/components/alexa/test_flash_briefings.py +++ b/tests/components/alexa/test_flash_briefings.py @@ -1,12 +1,12 @@ """The tests for the Alexa component.""" # pylint: disable=protected-access import datetime +from http import HTTPStatus import pytest from homeassistant.components import alexa from homeassistant.components.alexa import const -from homeassistant.const import HTTP_NOT_FOUND, HTTP_UNAUTHORIZED from homeassistant.core import callback from homeassistant.setup import async_setup_component @@ -74,7 +74,7 @@ def _flash_briefing_req(client, briefing_id, password="pass%2Fabc"): async def test_flash_briefing_invalid_id(alexa_client): """Test an invalid Flash Briefing ID.""" req = await _flash_briefing_req(alexa_client, 10000) - assert req.status == HTTP_NOT_FOUND + assert req.status == HTTPStatus.NOT_FOUND text = await req.text() assert text == "" @@ -82,7 +82,7 @@ async def test_flash_briefing_invalid_id(alexa_client): async def test_flash_briefing_no_password(alexa_client): """Test for no Flash Briefing password.""" req = await _flash_briefing_req(alexa_client, "weather", password=None) - assert req.status == HTTP_UNAUTHORIZED + assert req.status == HTTPStatus.UNAUTHORIZED text = await req.text() assert text == "" @@ -90,7 +90,7 @@ async def test_flash_briefing_no_password(alexa_client): async def test_flash_briefing_invalid_password(alexa_client): """Test an invalid Flash Briefing password.""" req = await _flash_briefing_req(alexa_client, "weather", password="wrongpass") - assert req.status == HTTP_UNAUTHORIZED + assert req.status == HTTPStatus.UNAUTHORIZED text = await req.text() assert text == "" @@ -98,7 +98,7 @@ async def test_flash_briefing_invalid_password(alexa_client): async def test_flash_briefing_request_for_password(alexa_client): """Test for "password" Flash Briefing.""" req = await _flash_briefing_req(alexa_client, "password") - assert req.status == HTTP_NOT_FOUND + assert req.status == HTTPStatus.NOT_FOUND text = await req.text() assert text == "" @@ -106,7 +106,7 @@ async def test_flash_briefing_request_for_password(alexa_client): async def test_flash_briefing_date_from_str(alexa_client): """Test the response has a valid date parsed from string.""" req = await _flash_briefing_req(alexa_client, "weather") - assert req.status == 200 + assert req.status == HTTPStatus.OK data = await req.json() assert isinstance( datetime.datetime.strptime( @@ -130,7 +130,7 @@ async def test_flash_briefing_valid(alexa_client): ] req = await _flash_briefing_req(alexa_client, "news_audio") - assert req.status == 200 + assert req.status == HTTPStatus.OK json = await req.json() assert isinstance( datetime.datetime.strptime( diff --git a/tests/components/alexa/test_intent.py b/tests/components/alexa/test_intent.py index c838bf5b3a3..d6c32996330 100644 --- a/tests/components/alexa/test_intent.py +++ b/tests/components/alexa/test_intent.py @@ -1,5 +1,6 @@ """The tests for the Alexa component.""" # pylint: disable=protected-access +from http import HTTPStatus import json import pytest @@ -134,7 +135,7 @@ async def test_intent_launch_request(alexa_client): }, } req = await _intent_req(alexa_client, data) - assert req.status == 200 + assert req.status == HTTPStatus.OK data = await req.json() text = data.get("response", {}).get("outputSpeech", {}).get("text") assert text == "LaunchRequest has been received." @@ -160,7 +161,7 @@ async def test_intent_launch_request_not_configured(alexa_client): }, } req = await _intent_req(alexa_client, data) - assert req.status == 200 + assert req.status == HTTPStatus.OK data = await req.json() text = data.get("response", {}).get("outputSpeech", {}).get("text") assert text == "This intent is not yet configured within Home Assistant." @@ -194,7 +195,7 @@ async def test_intent_request_with_slots(alexa_client): }, } req = await _intent_req(alexa_client, data) - assert req.status == 200 + assert req.status == HTTPStatus.OK data = await req.json() text = data.get("response", {}).get("outputSpeech", {}).get("text") assert text == "You told us your sign is virgo." @@ -247,7 +248,7 @@ async def test_intent_request_with_slots_and_synonym_resolution(alexa_client): }, } req = await _intent_req(alexa_client, data) - assert req.status == 200 + assert req.status == HTTPStatus.OK data = await req.json() text = data.get("response", {}).get("outputSpeech", {}).get("text") assert text == "You told us your sign is Virgo." @@ -300,7 +301,7 @@ async def test_intent_request_with_slots_and_multi_synonym_resolution(alexa_clie }, } req = await _intent_req(alexa_client, data) - assert req.status == 200 + assert req.status == HTTPStatus.OK data = await req.json() text = data.get("response", {}).get("outputSpeech", {}).get("text") assert text == "You told us your sign is V zodiac." @@ -334,7 +335,7 @@ async def test_intent_request_with_slots_but_no_value(alexa_client): }, } req = await _intent_req(alexa_client, data) - assert req.status == 200 + assert req.status == HTTPStatus.OK data = await req.json() text = data.get("response", {}).get("outputSpeech", {}).get("text") assert text == "You told us your sign is ." @@ -365,7 +366,7 @@ async def test_intent_request_without_slots(hass, alexa_client): }, } req = await _intent_req(alexa_client, data) - assert req.status == 200 + assert req.status == HTTPStatus.OK json = await req.json() text = json.get("response", {}).get("outputSpeech", {}).get("text") @@ -375,7 +376,7 @@ async def test_intent_request_without_slots(hass, alexa_client): hass.states.async_set("device_tracker.anne_therese", "home") req = await _intent_req(alexa_client, data) - assert req.status == 200 + assert req.status == HTTPStatus.OK json = await req.json() text = json.get("response", {}).get("outputSpeech", {}).get("text") assert text == "You are both home, you silly" @@ -404,7 +405,7 @@ async def test_intent_request_calling_service(alexa_client): } call_count = len(calls) req = await _intent_req(alexa_client, data) - assert req.status == 200 + assert req.status == HTTPStatus.OK assert call_count + 1 == len(calls) call = calls[-1] assert call.domain == "test" @@ -445,7 +446,7 @@ async def test_intent_session_ended_request(alexa_client): } req = await _intent_req(alexa_client, data) - assert req.status == 200 + assert req.status == HTTPStatus.OK text = await req.text() assert text == "" @@ -482,7 +483,7 @@ async def test_intent_from_built_in_intent_library(alexa_client): }, } req = await _intent_req(alexa_client, data) - assert req.status == 200 + assert req.status == HTTPStatus.OK data = await req.json() text = data.get("response", {}).get("outputSpeech", {}).get("text") assert text == "Playing the shins." diff --git a/tests/components/alexa/test_smart_home_http.py b/tests/components/alexa/test_smart_home_http.py index c46a61aef41..650a8523f89 100644 --- a/tests/components/alexa/test_smart_home_http.py +++ b/tests/components/alexa/test_smart_home_http.py @@ -1,8 +1,9 @@ """Test Smart Home HTTP endpoints.""" +from http import HTTPStatus import json from homeassistant.components.alexa import DOMAIN, smart_home_http -from homeassistant.const import CONTENT_TYPE_JSON, HTTP_NOT_FOUND +from homeassistant.const import CONTENT_TYPE_JSON from homeassistant.setup import async_setup_component from . import get_new_request @@ -39,4 +40,4 @@ async def test_http_api_disabled(hass, hass_client): config = {"alexa": {}} response = await do_http_discovery(config, hass, hass_client) - assert response.status == HTTP_NOT_FOUND + assert response.status == HTTPStatus.NOT_FOUND diff --git a/tests/components/almond/test_config_flow.py b/tests/components/almond/test_config_flow.py index bd1f23d956c..e0e88d0f43b 100644 --- a/tests/components/almond/test_config_flow.py +++ b/tests/components/almond/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Almond config flow.""" import asyncio +from http import HTTPStatus from unittest.mock import patch from homeassistant import config_entries, data_entry_flow, setup @@ -129,7 +130,7 @@ async def test_full_flow( client = await hass_client_no_auth() resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert resp.headers["content-type"] == "text/html; charset=utf-8" aioclient_mock.post( diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py index 400755c39cd..b9a7b80fcf6 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -1,5 +1,6 @@ """The tests for the Home Assistant API component.""" # pylint: disable=protected-access +from http import HTTPStatus import json from unittest.mock import patch @@ -26,7 +27,7 @@ async def test_api_list_state_entities(hass, mock_api_client): """Test if the debug interface allows us to list state entities.""" hass.states.async_set("test.entity", "hello") resp = await mock_api_client.get(const.URL_API_STATES) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK json = await resp.json() remote_data = [ha.State.from_dict(item) for item in json] @@ -37,7 +38,7 @@ async def test_api_get_state(hass, mock_api_client): """Test if the debug interface allows us to get a state.""" hass.states.async_set("hello.world", "nice", {"attr": 1}) resp = await mock_api_client.get("/api/states/hello.world") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK json = await resp.json() data = ha.State.from_dict(json) @@ -52,7 +53,7 @@ async def test_api_get_state(hass, mock_api_client): async def test_api_get_non_existing_state(hass, mock_api_client): """Test if the debug interface allows us to get a state.""" resp = await mock_api_client.get("/api/states/does_not_exist") - assert resp.status == const.HTTP_NOT_FOUND + assert resp.status == HTTPStatus.NOT_FOUND async def test_api_state_change(hass, mock_api_client): @@ -75,7 +76,7 @@ async def test_api_state_change_of_non_existing_entity(hass, mock_api_client): "/api/states/test_entity.that_does_not_exist", json={"state": new_state} ) - assert resp.status == 201 + assert resp.status == HTTPStatus.CREATED assert hass.states.get("test_entity.that_does_not_exist").state == new_state @@ -87,7 +88,7 @@ async def test_api_state_change_with_bad_data(hass, mock_api_client): "/api/states/test_entity.that_does_not_exist", json={} ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST # pylint: disable=invalid-name @@ -97,13 +98,13 @@ async def test_api_state_change_to_zero_value(hass, mock_api_client): "/api/states/test_entity.with_zero_state", json={"state": 0} ) - assert resp.status == 201 + assert resp.status == HTTPStatus.CREATED resp = await mock_api_client.post( "/api/states/test_entity.with_zero_state", json={"state": 0.0} ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK # pylint: disable=invalid-name @@ -190,7 +191,7 @@ async def test_api_fire_event_with_invalid_json(hass, mock_api_client): await hass.async_block_till_done() - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST assert len(test_value) == 0 # Try now with valid but unusable JSON @@ -200,7 +201,7 @@ async def test_api_fire_event_with_invalid_json(hass, mock_api_client): await hass.async_block_till_done() - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST assert len(test_value) == 0 @@ -319,7 +320,7 @@ async def test_api_template_error(hass, mock_api_client): const.URL_API_TEMPLATE, json={"template": "{{ states.sensor.temperature.state"} ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST async def test_stream(hass, mock_api_client): @@ -327,7 +328,7 @@ async def test_stream(hass, mock_api_client): listen_count = _listen_count(hass) resp = await mock_api_client.get(const.URL_API_STREAM) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert listen_count + 1 == _listen_count(hass) hass.bus.async_fire("test_event") @@ -344,7 +345,7 @@ async def test_stream_with_restricted(hass, mock_api_client): resp = await mock_api_client.get( f"{const.URL_API_STREAM}?restrict=test_event1,test_event3" ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert listen_count + 1 == _listen_count(hass) hass.bus.async_fire("test_event1") @@ -392,7 +393,7 @@ async def test_api_error_log( resp = await client.get(const.URL_API_ERROR_LOG) # Verify auth required - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED with patch( "aiohttp.web.FileResponse", return_value=web.Response(text="Hello") @@ -404,7 +405,7 @@ async def test_api_error_log( assert len(mock_file.mock_calls) == 1 assert mock_file.mock_calls[0][1][0] == hass.data[DATA_LOGGING] - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert await resp.text() == "Hello" # Verify we require admin user @@ -413,7 +414,7 @@ async def test_api_error_log( const.URL_API_ERROR_LOG, headers={"Authorization": f"Bearer {hass_access_token}"}, ) - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED async def test_api_fire_event_context(hass, mock_api_client, hass_access_token): @@ -473,7 +474,7 @@ async def test_event_stream_requires_admin(hass, mock_api_client, hass_admin_use """Test user needs to be admin to access event stream.""" hass_admin_user.groups = [] resp = await mock_api_client.get("/api/stream") - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED async def test_states_view_filters(hass, mock_api_client, hass_admin_user): @@ -482,7 +483,7 @@ async def test_states_view_filters(hass, mock_api_client, hass_admin_user): hass.states.async_set("test.entity", "hello") hass.states.async_set("test.not_visible_entity", "invisible") resp = await mock_api_client.get(const.URL_API_STATES) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK json = await resp.json() assert len(json) == 1 assert json[0]["entity_id"] == "test.entity" @@ -492,35 +493,35 @@ async def test_get_entity_state_read_perm(hass, mock_api_client, hass_admin_user """Test getting a state requires read permission.""" hass_admin_user.mock_policy({}) resp = await mock_api_client.get("/api/states/light.test") - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED async def test_post_entity_state_admin(hass, mock_api_client, hass_admin_user): """Test updating state requires admin.""" hass_admin_user.groups = [] resp = await mock_api_client.post("/api/states/light.test") - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED async def test_delete_entity_state_admin(hass, mock_api_client, hass_admin_user): """Test deleting entity requires admin.""" hass_admin_user.groups = [] resp = await mock_api_client.delete("/api/states/light.test") - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED async def test_post_event_admin(hass, mock_api_client, hass_admin_user): """Test sending event requires admin.""" hass_admin_user.groups = [] resp = await mock_api_client.post("/api/events/state_changed") - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED async def test_rendering_template_admin(hass, mock_api_client, hass_admin_user): """Test rendering a template requires admin.""" hass_admin_user.groups = [] resp = await mock_api_client.post(const.URL_API_TEMPLATE) - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED async def test_rendering_template_legacy_user( @@ -533,13 +534,13 @@ async def test_rendering_template_legacy_user( const.URL_API_TEMPLATE, json={"template": "{{ states.sensor.temperature.state }}"}, ) - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED async def test_api_call_service_not_found(hass, mock_api_client): """Test if the API fails 400 if unknown service.""" resp = await mock_api_client.post("/api/services/test_domain/test_service") - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST async def test_api_call_service_bad_data(hass, mock_api_client): @@ -558,7 +559,7 @@ async def test_api_call_service_bad_data(hass, mock_api_client): resp = await mock_api_client.post( "/api/services/test_domain/test_service", json={"hello": 5} ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST async def test_api_get_discovery_info(hass, mock_api_client): diff --git a/tests/components/august/test_camera.py b/tests/components/august/test_camera.py index bc9cd5d2bd7..3c5379aa59e 100644 --- a/tests/components/august/test_camera.py +++ b/tests/components/august/test_camera.py @@ -1,5 +1,6 @@ """The camera tests for the august platform.""" +from http import HTTPStatus from unittest.mock import patch from homeassistant.const import STATE_IDLE @@ -30,6 +31,6 @@ async def test_create_doorbell(hass, hass_client_no_auth): client = await hass_client_no_auth() resp = await client.get(url) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK body = await resp.text() assert body == "image" diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index b615ba4156c..2c96f545b41 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -1,5 +1,6 @@ """Integration tests for the auth component.""" from datetime import timedelta +from http import HTTPStatus from unittest.mock import patch from homeassistant.auth import InvalidAuthError @@ -43,7 +44,7 @@ async def test_login_new_user_and_trying_refresh_token(hass, aiohttp_client): "redirect_uri": CLIENT_REDIRECT_URI, }, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK step = await resp.json() resp = await client.post( @@ -51,7 +52,7 @@ async def test_login_new_user_and_trying_refresh_token(hass, aiohttp_client): json={"client_id": CLIENT_ID, "username": "test-user", "password": "test-pass"}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK step = await resp.json() code = step["result"] @@ -61,7 +62,7 @@ async def test_login_new_user_and_trying_refresh_token(hass, aiohttp_client): data={"client_id": CLIENT_ID, "grant_type": "authorization_code", "code": code}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK tokens = await resp.json() assert ( @@ -78,7 +79,7 @@ async def test_login_new_user_and_trying_refresh_token(hass, aiohttp_client): }, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK tokens = await resp.json() assert "refresh_token" not in tokens assert ( @@ -87,12 +88,12 @@ async def test_login_new_user_and_trying_refresh_token(hass, aiohttp_client): # Test using access token to hit API. resp = await client.get("/api/") - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED resp = await client.get( "/api/", headers={"authorization": f"Bearer {tokens['access_token']}"} ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK def test_auth_code_store_expiration(): @@ -179,7 +180,7 @@ async def test_refresh_token_system_generated(hass, aiohttp_client): }, ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST result = await resp.json() assert result["error"] == "invalid_request" @@ -188,7 +189,7 @@ async def test_refresh_token_system_generated(hass, aiohttp_client): data={"grant_type": "refresh_token", "refresh_token": refresh_token.token}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK tokens = await resp.json() assert ( await hass.auth.async_validate_access_token(tokens["access_token"]) is not None @@ -206,7 +207,7 @@ async def test_refresh_token_different_client_id(hass, aiohttp_client): data={"grant_type": "refresh_token", "refresh_token": refresh_token.token}, ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST result = await resp.json() assert result["error"] == "invalid_request" @@ -220,7 +221,7 @@ async def test_refresh_token_different_client_id(hass, aiohttp_client): }, ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST result = await resp.json() assert result["error"] == "invalid_request" @@ -234,7 +235,7 @@ async def test_refresh_token_different_client_id(hass, aiohttp_client): }, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK tokens = await resp.json() assert ( await hass.auth.async_validate_access_token(tokens["access_token"]) is not None @@ -262,7 +263,7 @@ async def test_refresh_token_provider_rejected( }, ) - assert resp.status == 403 + assert resp.status == HTTPStatus.FORBIDDEN result = await resp.json() assert result["error"] == "access_denied" assert result["error_description"] == "Invalid access" @@ -283,7 +284,7 @@ async def test_revoking_refresh_token(hass, aiohttp_client): }, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK tokens = await resp.json() assert ( await hass.auth.async_validate_access_token(tokens["access_token"]) is not None @@ -293,7 +294,7 @@ async def test_revoking_refresh_token(hass, aiohttp_client): resp = await client.post( "/auth/token", data={"token": refresh_token.token, "action": "revoke"} ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK # Old access token should be no longer valid assert await hass.auth.async_validate_access_token(tokens["access_token"]) is None @@ -308,7 +309,7 @@ async def test_revoking_refresh_token(hass, aiohttp_client): }, ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST async def test_ws_long_lived_access_token(hass, hass_ws_client, hass_access_token): diff --git a/tests/components/auth/test_init_link_user.py b/tests/components/auth/test_init_link_user.py index 711f8ad9c26..036dad4265f 100644 --- a/tests/components/auth/test_init_link_user.py +++ b/tests/components/auth/test_init_link_user.py @@ -1,4 +1,5 @@ """Tests for the link user flow.""" +from http import HTTPStatus from unittest.mock import patch from . import async_setup_auth @@ -40,7 +41,7 @@ async def async_get_code(hass, aiohttp_client): "type": "link_user", }, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK step = await resp.json() resp = await client.post( @@ -48,7 +49,7 @@ async def async_get_code(hass, aiohttp_client): json={"client_id": CLIENT_ID, "username": "2nd-user", "password": "2nd-pass"}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK step = await resp.json() return { @@ -72,7 +73,7 @@ async def test_link_user(hass, aiohttp_client): headers={"authorization": f"Bearer {info['access_token']}"}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert len(info["user"].credentials) == 1 @@ -89,7 +90,7 @@ async def test_link_user_invalid_client_id(hass, aiohttp_client): headers={"authorization": f"Bearer {info['access_token']}"}, ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST assert len(info["user"].credentials) == 0 @@ -105,7 +106,7 @@ async def test_link_user_invalid_code(hass, aiohttp_client): headers={"authorization": f"Bearer {info['access_token']}"}, ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST assert len(info["user"].credentials) == 0 @@ -122,7 +123,7 @@ async def test_link_user_invalid_auth(hass, aiohttp_client): headers={"authorization": "Bearer invalid"}, ) - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED assert len(info["user"].credentials) == 0 @@ -142,7 +143,7 @@ async def test_link_user_already_linked_same_user(hass, aiohttp_client): headers={"authorization": f"Bearer {info['access_token']}"}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK # The credential was not added because it saw that it was already linked assert len(info["user"].credentials) == 0 @@ -165,7 +166,7 @@ async def test_link_user_already_linked_other_user(hass, aiohttp_client): headers={"authorization": f"Bearer {info['access_token']}"}, ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST # The credential was not added because it saw that it was already linked assert len(info["user"].credentials) == 0 assert len(another_user.credentials) == 0 diff --git a/tests/components/auth/test_login_flow.py b/tests/components/auth/test_login_flow.py index e6e5281d601..ce3d37598d7 100644 --- a/tests/components/auth/test_login_flow.py +++ b/tests/components/auth/test_login_flow.py @@ -1,4 +1,5 @@ """Tests for the login flow.""" +from http import HTTPStatus from unittest.mock import patch from . import async_setup_auth @@ -10,7 +11,7 @@ async def test_fetch_auth_providers(hass, aiohttp_client): """Test fetching auth providers.""" client = await async_setup_auth(hass, aiohttp_client) resp = await client.get("/auth/providers") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert await resp.json() == [ {"name": "Example", "type": "insecure_example", "id": None} ] @@ -24,7 +25,7 @@ async def test_fetch_auth_providers_onboarding(hass, aiohttp_client): return_value=False, ): resp = await client.get("/auth/providers") - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST assert await resp.json() == { "message": "Onboarding not finished", "code": "onboarding_required", @@ -35,7 +36,7 @@ async def test_cannot_get_flows_in_progress(hass, aiohttp_client): """Test we cannot get flows in progress.""" client = await async_setup_auth(hass, aiohttp_client, []) resp = await client.get("/auth/login_flow") - assert resp.status == 405 + assert resp.status == HTTPStatus.METHOD_NOT_ALLOWED async def test_invalid_username_password(hass, aiohttp_client): @@ -49,7 +50,7 @@ async def test_invalid_username_password(hass, aiohttp_client): "redirect_uri": CLIENT_REDIRECT_URI, }, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK step = await resp.json() # Incorrect username @@ -62,7 +63,7 @@ async def test_invalid_username_password(hass, aiohttp_client): }, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK step = await resp.json() assert step["step_id"] == "init" @@ -78,7 +79,7 @@ async def test_invalid_username_password(hass, aiohttp_client): }, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK step = await resp.json() assert step["step_id"] == "init" @@ -101,7 +102,7 @@ async def test_login_exist_user(hass, aiohttp_client): "redirect_uri": CLIENT_REDIRECT_URI, }, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK step = await resp.json() resp = await client.post( @@ -109,7 +110,7 @@ async def test_login_exist_user(hass, aiohttp_client): json={"client_id": CLIENT_ID, "username": "test-user", "password": "test-pass"}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK step = await resp.json() assert step["type"] == "create_entry" assert len(step["result"]) > 1 From 164f09c1f0f6c9442277b6fc6753039e37ea6ac2 Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Fri, 22 Oct 2021 23:22:10 +1100 Subject: [PATCH 0667/1038] Sleep between device requests to detect socket closes (#58087) --- homeassistant/components/dlna_dmr/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/dlna_dmr/data.py b/homeassistant/components/dlna_dmr/data.py index d7b330f0fe8..07046ba4acc 100644 --- a/homeassistant/components/dlna_dmr/data.py +++ b/homeassistant/components/dlna_dmr/data.py @@ -37,7 +37,7 @@ class DlnaDmrData: """Initialize global data.""" self.lock = asyncio.Lock() session = aiohttp_client.async_get_clientsession(hass, verify_ssl=False) - self.requester = AiohttpSessionRequester(session, with_sleep=False) + self.requester = AiohttpSessionRequester(session, with_sleep=True) self.upnp_factory = UpnpFactory(self.requester, non_strict=True) self.event_notifiers = {} self.event_notifier_refs = defaultdict(int) From f25d3cf9b342db5309c9138b503edf8c42e775c5 Mon Sep 17 00:00:00 2001 From: thomas-svrts <81048302+thomas-svrts@users.noreply.github.com> Date: Fri, 22 Oct 2021 14:24:43 +0200 Subject: [PATCH 0668/1038] Gogogate2 add statistics (#58178) Co-authored-by: J. Nick Koston --- homeassistant/components/gogogate2/sensor.py | 12 +++++++++++- tests/components/gogogate2/test_sensor.py | 4 ++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/gogogate2/sensor.py b/homeassistant/components/gogogate2/sensor.py index 7ad248b88d6..8788a69908e 100644 --- a/homeassistant/components/gogogate2/sensor.py +++ b/homeassistant/components/gogogate2/sensor.py @@ -5,7 +5,7 @@ from itertools import chain from ismartgate.common import AbstractDoor, get_configured_doors -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEVICE_CLASS_BATTERY, @@ -77,6 +77,11 @@ class DoorSensorBattery(GoGoGate2Entity, SensorEntity): door = self._get_door() return door.voltage # This is a percentage, not an absolute voltage + @property + def state_class(self) -> str: + """Return the Measurement State Class.""" + return STATE_CLASS_MEASUREMENT + @property def extra_state_attributes(self): """Return the state attributes.""" @@ -104,6 +109,11 @@ class DoorSensorTemperature(GoGoGate2Entity, SensorEntity): """Return the name of the door.""" return f"{self._get_door().name} temperature" + @property + def state_class(self) -> str: + """Return the Measurement State Class.""" + return STATE_CLASS_MEASUREMENT + @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" diff --git a/tests/components/gogogate2/test_sensor.py b/tests/components/gogogate2/test_sensor.py index 5adc4532750..1a59c5fd3c5 100644 --- a/tests/components/gogogate2/test_sensor.py +++ b/tests/components/gogogate2/test_sensor.py @@ -171,6 +171,7 @@ async def test_sensor_update(gogogate2api_mock, hass: HomeAssistant) -> None: "door_id": 1, "friendly_name": "Door1 battery", "sensor_id": "ABCD", + "state_class": "measurement", } temp_attributes = { "device_class": "temperature", @@ -178,6 +179,7 @@ async def test_sensor_update(gogogate2api_mock, hass: HomeAssistant) -> None: "friendly_name": "Door1 temperature", "sensor_id": "ABCD", "unit_of_measurement": "°C", + "state_class": "measurement", } api = MagicMock(GogoGate2Api) @@ -245,6 +247,7 @@ async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None: "door_id": 1, "friendly_name": "Door1 battery", "sensor_id": "ABCD", + "state_class": "measurement", } temp_attributes = { "device_class": "temperature", @@ -252,6 +255,7 @@ async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None: "friendly_name": "Door1 temperature", "sensor_id": "ABCD", "unit_of_measurement": "°C", + "state_class": "measurement", } sensor_response = _mocked_ismartgate_sensor_response(35, -4.0) From ab2ff457264b628705a988d15a26f13f572e4d50 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 22 Oct 2021 14:25:36 +0200 Subject: [PATCH 0669/1038] Warn if state_changed events are excluded from recorder (#58021) --- homeassistant/components/recorder/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 471e86609ae..83652b7864c 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -249,6 +249,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) exclude = conf[CONF_EXCLUDE] exclude_t = exclude.get(CONF_EVENT_TYPES, []) + if EVENT_STATE_CHANGED in exclude_t: + _LOGGER.warning( + "State change events are excluded, recorder will not record state changes." + "This will become an error in Home Assistant Core 2022.2" + ) instance = hass.data[DATA_INSTANCE] = Recorder( hass=hass, auto_purge=auto_purge, From 2148c84386a043aad4d005c5dece80580120babb Mon Sep 17 00:00:00 2001 From: Lars Date: Fri, 22 Oct 2021 14:30:03 +0200 Subject: [PATCH 0670/1038] Support for Fritz!DECT 500 lightbulbs (#52830) --- CODEOWNERS | 2 +- homeassistant/components/fritzbox/const.py | 5 +- homeassistant/components/fritzbox/light.py | 155 +++++++++++++++ .../components/fritzbox/manifest.json | 2 +- tests/components/fritzbox/__init__.py | 19 ++ tests/components/fritzbox/test_light.py | 185 ++++++++++++++++++ 6 files changed, 365 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/fritzbox/light.py create mode 100644 tests/components/fritzbox/test_light.py diff --git a/CODEOWNERS b/CODEOWNERS index 936fa50cb9f..bd99e2eb737 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -181,7 +181,7 @@ homeassistant/components/foscam/* @skgsergio homeassistant/components/freebox/* @hacf-fr @Quentame homeassistant/components/freedompro/* @stefano055415 homeassistant/components/fritz/* @mammuth @AaronDavidSchneider @chemelli74 -homeassistant/components/fritzbox/* @mib1185 +homeassistant/components/fritzbox/* @mib1185 @flabbamann homeassistant/components/fronius/* @nielstron homeassistant/components/frontend/* @home-assistant/frontend homeassistant/components/garages_amsterdam/* @klaasnicolaas diff --git a/homeassistant/components/fritzbox/const.py b/homeassistant/components/fritzbox/const.py index 67e7c9dc564..9d537bec617 100644 --- a/homeassistant/components/fritzbox/const.py +++ b/homeassistant/components/fritzbox/const.py @@ -11,6 +11,9 @@ ATTR_STATE_LOCKED: Final = "locked" ATTR_STATE_SUMMER_MODE: Final = "summer_mode" ATTR_STATE_WINDOW_OPEN: Final = "window_open" +COLOR_MODE: Final = "1" +COLOR_TEMP_MODE: Final = "4" + CONF_CONNECTIONS: Final = "connections" CONF_COORDINATOR: Final = "coordinator" @@ -21,4 +24,4 @@ DOMAIN: Final = "fritzbox" LOGGER: Final[logging.Logger] = logging.getLogger(__package__) -PLATFORMS: Final[list[str]] = ["binary_sensor", "climate", "switch", "sensor"] +PLATFORMS: Final[list[str]] = ["binary_sensor", "climate", "light", "switch", "sensor"] diff --git a/homeassistant/components/fritzbox/light.py b/homeassistant/components/fritzbox/light.py new file mode 100644 index 00000000000..3f9e3cabfa2 --- /dev/null +++ b/homeassistant/components/fritzbox/light.py @@ -0,0 +1,155 @@ +"""Support for AVM FRITZ!SmartHome lightbulbs.""" +from __future__ import annotations + +from typing import Any + +from pyfritzhome.fritzhomedevice import FritzhomeDevice + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + LightEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.util import color + +from . import FritzBoxEntity +from .const import ( + COLOR_MODE, + COLOR_TEMP_MODE, + CONF_COORDINATOR, + DOMAIN as FRITZBOX_DOMAIN, +) + +SUPPORTED_COLOR_MODES = {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the FRITZ!SmartHome light from ConfigEntry.""" + entities: list[FritzboxLight] = [] + coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR] + + for ain, device in coordinator.data.items(): + if not device.has_lightbulb: + continue + + supported_color_temps = await hass.async_add_executor_job( + device.get_color_temps + ) + + supported_colors = await hass.async_add_executor_job(device.get_colors) + + entities.append( + FritzboxLight( + coordinator, + ain, + supported_colors, + supported_color_temps, + ) + ) + + async_add_entities(entities) + + +class FritzboxLight(FritzBoxEntity, LightEntity): + """The light class for FRITZ!SmartHome lightbulbs.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator[dict[str, FritzhomeDevice]], + ain: str, + supported_colors: dict, + supported_color_temps: list[str], + ) -> None: + """Initialize the FritzboxLight entity.""" + super().__init__(coordinator, ain, None) + + max_kelvin = int(max(supported_color_temps)) + min_kelvin = int(min(supported_color_temps)) + + # max kelvin is min mireds and min kelvin is max mireds + self._attr_min_mireds = color.color_temperature_kelvin_to_mired(max_kelvin) + self._attr_max_mireds = color.color_temperature_kelvin_to_mired(min_kelvin) + + # Fritz!DECT 500 only supports 12 values for hue, with 3 saturations each. + # Map supported colors to dict {hue: [sat1, sat2, sat3]} for easier lookup + self._supported_hs = {} + for values in supported_colors.values(): + hue = int(values[0][0]) + self._supported_hs[hue] = [ + int(values[0][1]), + int(values[1][1]), + int(values[2][1]), + ] + + @property + def is_on(self) -> bool: + """If the light is currently on or off.""" + return self.device.state # type: ignore [no-any-return] + + @property + def brightness(self) -> int: + """Return the current Brightness.""" + return self.device.level # type: ignore [no-any-return] + + @property + def hs_color(self) -> tuple[float, float] | None: + """Return the hs color value.""" + if self.device.color_mode != COLOR_MODE: + return None + + hue = self.device.hue + saturation = self.device.saturation + + return (hue, float(saturation) * 100.0 / 255.0) + + @property + def color_temp(self) -> int | None: + """Return the CT color value.""" + if self.device.color_mode != COLOR_TEMP_MODE: + return None + + kelvin = self.device.color_temp + return color.color_temperature_kelvin_to_mired(kelvin) + + @property + def supported_color_modes(self) -> set: + """Flag supported color modes.""" + return SUPPORTED_COLOR_MODES + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the light on.""" + if kwargs.get(ATTR_BRIGHTNESS) is not None: + level = kwargs[ATTR_BRIGHTNESS] + await self.hass.async_add_executor_job(self.device.set_level, level) + if kwargs.get(ATTR_HS_COLOR) is not None: + hass_hue = int(kwargs[ATTR_HS_COLOR][0]) + hass_saturation = round(kwargs[ATTR_HS_COLOR][1] * 255.0 / 100.0) + # find supported hs values closest to what user selected + hue = min(self._supported_hs.keys(), key=lambda x: abs(x - hass_hue)) + saturation = min( + self._supported_hs[hue], key=lambda x: abs(x - hass_saturation) + ) + await self.hass.async_add_executor_job( + self.device.set_color, (hue, saturation) + ) + + if kwargs.get(ATTR_COLOR_TEMP) is not None: + kelvin = color.color_temperature_kelvin_to_mired(kwargs[ATTR_COLOR_TEMP]) + await self.hass.async_add_executor_job(self.device.set_color_temp, kelvin) + + await self.hass.async_add_executor_job(self.device.set_state_on) + await self.coordinator.async_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the light off.""" + await self.hass.async_add_executor_job(self.device.set_state_off) + await self.coordinator.async_refresh() diff --git a/homeassistant/components/fritzbox/manifest.json b/homeassistant/components/fritzbox/manifest.json index c1db226d348..98c02d0166e 100644 --- a/homeassistant/components/fritzbox/manifest.json +++ b/homeassistant/components/fritzbox/manifest.json @@ -8,7 +8,7 @@ "st": "urn:schemas-upnp-org:device:fritzbox:1" } ], - "codeowners": ["@mib1185"], + "codeowners": ["@mib1185", "@flabbamann"], "config_flow": true, "iot_class": "local_polling" } diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index 2b9a1a783f9..27abd38f8cb 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -57,6 +57,7 @@ class FritzDeviceBinarySensorMock(FritzDeviceBaseMock): has_alarm = True has_powermeter = False has_switch = False + has_lightbulb = False has_temperature_sensor = False has_thermostat = False present = True @@ -75,6 +76,7 @@ class FritzDeviceClimateMock(FritzDeviceBaseMock): fw_version = "1.2.3" has_alarm = False has_powermeter = False + has_lightbulb = False has_switch = False has_temperature_sensor = False has_thermostat = True @@ -94,6 +96,7 @@ class FritzDeviceSensorMock(FritzDeviceBaseMock): fw_version = "1.2.3" has_alarm = False has_powermeter = False + has_lightbulb = False has_switch = False has_temperature_sensor = True has_thermostat = False @@ -113,6 +116,7 @@ class FritzDeviceSwitchMock(FritzDeviceBaseMock): fw_version = "1.2.3" has_alarm = False has_powermeter = True + has_lightbulb = False has_switch = True has_temperature_sensor = True has_thermostat = False @@ -121,3 +125,18 @@ class FritzDeviceSwitchMock(FritzDeviceBaseMock): power = 5678 present = True temperature = 1.23 + + +class FritzDeviceLightMock(FritzDeviceBaseMock): + """Mock of a AVM Fritz!Box light device.""" + + fw_version = "1.2.3" + has_alarm = False + has_powermeter = False + has_lightbulb = True + has_switch = False + has_temperature_sensor = False + has_thermostat = False + level = 100 + present = True + state = True diff --git a/tests/components/fritzbox/test_light.py b/tests/components/fritzbox/test_light.py new file mode 100644 index 00000000000..5b17e36abb2 --- /dev/null +++ b/tests/components/fritzbox/test_light.py @@ -0,0 +1,185 @@ +"""Tests for AVM Fritz!Box light component.""" +from datetime import timedelta +from unittest.mock import Mock + +from requests.exceptions import HTTPError + +from homeassistant.components.fritzbox.const import ( + COLOR_MODE, + COLOR_TEMP_MODE, + DOMAIN as FB_DOMAIN, +) +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + DOMAIN, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + CONF_DEVICES, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, +) +from homeassistant.core import HomeAssistant +from homeassistant.util import color +import homeassistant.util.dt as dt_util + +from . import FritzDeviceLightMock, setup_config_entry +from .const import CONF_FAKE_NAME, MOCK_CONFIG + +from tests.common import async_fire_time_changed + +ENTITY_ID = f"{DOMAIN}.{CONF_FAKE_NAME}" + + +async def test_setup(hass: HomeAssistant, fritz: Mock): + """Test setup of platform.""" + device = FritzDeviceLightMock() + device.get_color_temps.return_value = [2700, 6500] + device.get_colors.return_value = { + "Red": [("100", "70", "10"), ("100", "50", "10"), ("100", "30", "10")] + } + device.color_mode = COLOR_TEMP_MODE + device.color_temp = 2700 + + 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.state == STATE_ON + assert state.attributes[ATTR_FRIENDLY_NAME] == "fake_name" + assert state.attributes[ATTR_COLOR_TEMP] == color.color_temperature_kelvin_to_mired( + 2700 + ) + + +async def test_setup_color(hass: HomeAssistant, fritz: Mock): + """Test setup of platform in color mode.""" + device = FritzDeviceLightMock() + device.get_color_temps.return_value = [2700, 6500] + device.get_colors.return_value = { + "Red": [("100", "70", "10"), ("100", "50", "10"), ("100", "30", "10")] + } + device.color_mode = COLOR_MODE + device.hue = 100 + device.saturation = 70 * 255.0 / 100.0 + + 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.state == STATE_ON + assert state.attributes[ATTR_FRIENDLY_NAME] == "fake_name" + assert state.attributes[ATTR_BRIGHTNESS] == 100 + assert state.attributes[ATTR_HS_COLOR] == (100, 70) + + +async def test_turn_on(hass: HomeAssistant, fritz: Mock): + """Test turn device on.""" + device = FritzDeviceLightMock() + device.get_color_temps.return_value = [2700, 6500] + device.get_colors.return_value = { + "Red": [("100", "70", "10"), ("100", "50", "10"), ("100", "30", "10")] + } + 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_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_BRIGHTNESS: 100, ATTR_COLOR_TEMP: 300}, + True, + ) + assert device.set_state_on.call_count == 1 + assert device.set_level.call_count == 1 + assert device.set_color_temp.call_count == 1 + + +async def test_turn_on_color(hass: HomeAssistant, fritz: Mock): + """Test turn device on in color mode.""" + device = FritzDeviceLightMock() + device.get_color_temps.return_value = [2700, 6500] + device.get_colors.return_value = { + "Red": [("100", "70", "10"), ("100", "50", "10"), ("100", "30", "10")] + } + 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_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_BRIGHTNESS: 100, ATTR_HS_COLOR: (100, 70)}, + True, + ) + assert device.set_state_on.call_count == 1 + assert device.set_level.call_count == 1 + assert device.set_color.call_count == 1 + + +async def test_turn_off(hass: HomeAssistant, fritz: Mock): + """Test turn device off.""" + device = FritzDeviceLightMock() + device.get_color_temps.return_value = [2700, 6500] + device.get_colors.return_value = { + "Red": [("100", "70", "10"), ("100", "50", "10"), ("100", "30", "10")] + } + 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_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + assert device.set_state_off.call_count == 1 + + +async def test_update(hass: HomeAssistant, fritz: Mock): + """Test update without error.""" + device = FritzDeviceLightMock() + device.get_color_temps.return_value = [2700, 6500] + device.get_colors.return_value = { + "Red": [("100", "70", "10"), ("100", "50", "10"), ("100", "30", "10")] + } + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) + assert device.update.call_count == 1 + assert fritz().login.call_count == 1 + + next_update = dt_util.utcnow() + timedelta(seconds=200) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + assert device.update.call_count == 2 + assert fritz().login.call_count == 1 + + +async def test_update_error(hass: HomeAssistant, fritz: Mock): + """Test update with error.""" + device = FritzDeviceLightMock() + device.get_color_temps.return_value = [2700, 6500] + device.get_colors.return_value = { + "Red": [("100", "70", "10"), ("100", "50", "10"), ("100", "30", "10")] + } + device.update.side_effect = HTTPError("Boom") + assert not await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) + assert device.update.call_count == 1 + assert fritz().login.call_count == 1 + + next_update = dt_util.utcnow() + timedelta(seconds=200) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + assert device.update.call_count == 2 + assert fritz().login.call_count == 2 From 1a9ac6b6577d99c8bcccfbd76064ee92a1a94437 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 22 Oct 2021 14:34:02 +0200 Subject: [PATCH 0671/1038] Switch Fritz to device selector for services (#58005) --- homeassistant/components/fritz/services.yaml | 29 ++++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/fritz/services.yaml b/homeassistant/components/fritz/services.yaml index 87b0e6fca71..16e54176248 100644 --- a/homeassistant/components/fritz/services.yaml +++ b/homeassistant/components/fritz/services.yaml @@ -1,13 +1,24 @@ reconnect: description: Reconnects your FRITZ!Box internet connection - target: - entity: - integration: fritz - domain: binary_sensor - + fields: + device_id: + name: Fritz!Box Device + description: Select the Fritz!Box to reconnect + required: true + selector: + device: + integration: fritz + entity: + device_class: connectivity reboot: description: Reboots your FRITZ!Box - target: - entity: - integration: fritz - domain: binary_sensor + fields: + device_id: + name: Fritz!Box Device + description: Select the Fritz!Box to reboot + required: true + selector: + device: + integration: fritz + entity: + device_class: connectivity From c84fee7c6ed0e333ef3d01d2eb2cafb3fa08781d Mon Sep 17 00:00:00 2001 From: Ryan Fleming Date: Fri, 22 Oct 2021 09:25:12 -0400 Subject: [PATCH 0672/1038] Rework octoprint (#58040) Co-authored-by: Martin Hjelmare --- .coveragerc | 2 +- CODEOWNERS | 1 + .../components/octoprint/__init__.py | 380 +++++-------- .../components/octoprint/binary_sensor.py | 135 +++-- .../components/octoprint/config_flow.py | 202 +++++++ homeassistant/components/octoprint/const.py | 5 + .../components/octoprint/manifest.json | 12 +- homeassistant/components/octoprint/sensor.py | 333 +++++++---- .../components/octoprint/strings.json | 29 + .../components/octoprint/translations/en.json | 29 + homeassistant/generated/config_flows.py | 1 + homeassistant/generated/ssdp.py | 6 + homeassistant/generated/zeroconf.py | 5 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/octoprint/__init__.py | 82 +++ .../octoprint/test_binary_sensor.py | 54 ++ .../components/octoprint/test_config_flow.py | 519 ++++++++++++++++++ tests/components/octoprint/test_sensor.py | 76 +++ 19 files changed, 1485 insertions(+), 392 deletions(-) create mode 100644 homeassistant/components/octoprint/config_flow.py create mode 100644 homeassistant/components/octoprint/const.py create mode 100644 homeassistant/components/octoprint/strings.json create mode 100644 homeassistant/components/octoprint/translations/en.json create mode 100644 tests/components/octoprint/__init__.py create mode 100644 tests/components/octoprint/test_binary_sensor.py create mode 100644 tests/components/octoprint/test_config_flow.py create mode 100644 tests/components/octoprint/test_sensor.py diff --git a/.coveragerc b/.coveragerc index eac2dbf54a8..3bf6aa04d35 100644 --- a/.coveragerc +++ b/.coveragerc @@ -735,7 +735,7 @@ omit = homeassistant/components/nx584/alarm_control_panel.py homeassistant/components/nzbget/coordinator.py homeassistant/components/obihai/* - homeassistant/components/octoprint/* + homeassistant/components/octoprint/__init__.py homeassistant/components/oem/climate.py homeassistant/components/oasa_telematics/sensor.py homeassistant/components/ohmconnect/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index bd99e2eb737..3f67d50ccd0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -366,6 +366,7 @@ homeassistant/components/nut/* @bdraco @ollo69 homeassistant/components/nws/* @MatthewFlamm homeassistant/components/nzbget/* @chriscla homeassistant/components/obihai/* @dshokouhi +homeassistant/components/octoprint/* @rfleming71 homeassistant/components/ohmconnect/* @robbiet480 homeassistant/components/ombi/* @larssont homeassistant/components/omnilogic/* @oliver84 @djtimca @gentoosu diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index 31474611783..7ee6a3169da 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -1,11 +1,11 @@ """Support for monitoring OctoPrint 3D printers.""" +from datetime import timedelta import logging -import time -from aiohttp.hdrs import CONTENT_TYPE -import requests +from pyoctoprintapi import ApiError, OctoprintClient, PrinterOffline import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_API_KEY, CONF_BINARY_SENSORS, @@ -16,23 +16,18 @@ from homeassistant.const import ( CONF_PORT, CONF_SENSORS, CONF_SSL, - CONTENT_TYPE_JSON, - PERCENTAGE, - TEMP_CELSIUS, - TIME_SECONDS, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import slugify as util_slugify +import homeassistant.util.dt as dt_util + +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -CONF_BED = "bed" -CONF_NUMBER_OF_TOOLS = "number_of_tools" - -DEFAULT_NAME = "OctoPrint" -DOMAIN = "octoprint" - def has_all_unique_names(value): """Validate that printers have an unique name.""" @@ -51,11 +46,15 @@ def ensure_valid_path(value): return value -BINARY_SENSOR_TYPES = { - # API Endpoint, Group, Key, unit - "Printing": ["printer", "state", "printing", None], - "Printing Error": ["printer", "state", "error", None], -} +PLATFORMS = ["binary_sensor", "sensor"] +DEFAULT_NAME = "Octoprint" +CONF_NUMBER_OF_TOOLS = "number_of_tools" +CONF_BED = "bed" + +BINARY_SENSOR_TYPES = [ + "Printing", + "Printing Error", +] BINARY_SENSOR_SCHEMA = vol.Schema( { @@ -66,26 +65,13 @@ BINARY_SENSOR_SCHEMA = vol.Schema( } ) -SENSOR_TYPES = { - # API Endpoint, Group, Key, unit, icon - "Temperatures": ["printer", "temperature", "*", TEMP_CELSIUS], - "Current State": ["printer", "state", "text", None, "mdi:printer-3d"], - "Job Percentage": [ - "job", - "progress", - "completion", - PERCENTAGE, - "mdi:file-percent", - ], - "Time Remaining": [ - "job", - "progress", - "printTimeLeft", - TIME_SECONDS, - "mdi:clock-end", - ], - "Time Elapsed": ["job", "progress", "printTime", TIME_SECONDS, "mdi:clock-start"], -} +SENSOR_TYPES = [ + "Temperatures", + "Current State", + "Job Percentage", + "Time Remaining", + "Time Elapsed", +] SENSOR_SCHEMA = vol.Schema( { @@ -97,207 +83,145 @@ SENSOR_SCHEMA = vol.Schema( ) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.All( - cv.ensure_list, - [ - vol.Schema( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_SSL, default=False): cv.boolean, - vol.Optional(CONF_PORT, default=80): cv.port, - vol.Optional(CONF_PATH, default="/"): ensure_valid_path, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_NUMBER_OF_TOOLS, default=0): cv.positive_int, - vol.Optional(CONF_BED, default=False): cv.boolean, - vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, - vol.Optional( - CONF_BINARY_SENSORS, default={} - ): BINARY_SENSOR_SCHEMA, - } - ) - ], - has_all_unique_names, - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_SSL, default=False): cv.boolean, + vol.Optional(CONF_PORT, default=80): cv.port, + vol.Optional(CONF_PATH, default="/"): ensure_valid_path, + # Following values are not longer used in the configuration of the integration + # and are here for historical purposes + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional( + CONF_NUMBER_OF_TOOLS, default=0 + ): cv.positive_int, + vol.Optional(CONF_BED, default=False): cv.boolean, + vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, + vol.Optional( + CONF_BINARY_SENSORS, default={} + ): BINARY_SENSOR_SCHEMA, + } + ) + ], + has_all_unique_names, + ) + }, + ), extra=vol.ALLOW_EXTRA, ) -def setup(hass, config): +async def async_setup(hass: HomeAssistant, config: dict): """Set up the OctoPrint component.""" - printers = hass.data[DOMAIN] = {} - success = False - if DOMAIN not in config: - # Skip the setup if there is no configuration present return True - for printer in config[DOMAIN]: - name = printer[CONF_NAME] - protocol = "https" if printer[CONF_SSL] else "http" - base_url = ( - f"{protocol}://{printer[CONF_HOST]}:{printer[CONF_PORT]}" - f"{printer[CONF_PATH]}api/" - ) - api_key = printer[CONF_API_KEY] - number_of_tools = printer[CONF_NUMBER_OF_TOOLS] - bed = printer[CONF_BED] - try: - octoprint_api = OctoPrintAPI(base_url, api_key, bed, number_of_tools) - printers[base_url] = octoprint_api - octoprint_api.get("printer") - octoprint_api.get("job") - except requests.exceptions.RequestException as conn_err: - _LOGGER.error("Error setting up OctoPrint API: %r", conn_err) - continue + domain_config = config[DOMAIN] - sensors = printer[CONF_SENSORS][CONF_MONITORED_CONDITIONS] - load_platform( + for conf in domain_config: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_API_KEY: conf[CONF_API_KEY], + CONF_HOST: conf[CONF_HOST], + CONF_PATH: conf[CONF_PATH], + CONF_PORT: conf[CONF_PORT], + CONF_SSL: conf[CONF_SSL], + }, + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up OctoPrint from a config entry.""" + + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + + websession = async_get_clientsession(hass) + client = OctoprintClient( + entry.data[CONF_HOST], + websession, + entry.data[CONF_PORT], + entry.data[CONF_SSL], + entry.data[CONF_PATH], + ) + + client.set_api_key(entry.data[CONF_API_KEY]) + + coordinator = OctoprintDataUpdateCoordinator(hass, client, entry.entry_id, 30) + + await coordinator.async_config_entry_first_refresh() + + hass.data[DOMAIN][entry.entry_id] = {"coordinator": coordinator, "client": client} + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """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 + + +class OctoprintDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching Octoprint data.""" + + def __init__( + self, + hass: HomeAssistant, + octoprint: OctoprintClient, + config_entry_id: str, + interval: int, + ) -> None: + """Initialize.""" + super().__init__( hass, - "sensor", - DOMAIN, - {"name": name, "base_url": base_url, "sensors": sensors}, - config, + _LOGGER, + name=f"octoprint-{config_entry_id}", + update_interval=timedelta(seconds=interval), ) - b_sensors = printer[CONF_BINARY_SENSORS][CONF_MONITORED_CONDITIONS] - load_platform( - hass, - "binary_sensor", - DOMAIN, - {"name": name, "base_url": base_url, "sensors": b_sensors}, - config, - ) - success = True + self._octoprint = octoprint + self._printer_offline = False + self.data = {"printer": None, "job": None, "last_read_time": None} - return success - - -class OctoPrintAPI: - """Simple JSON wrapper for OctoPrint's API.""" - - def __init__(self, api_url, key, bed, number_of_tools): - """Initialize OctoPrint API and set headers needed later.""" - self.api_url = api_url - self.headers = {CONTENT_TYPE: CONTENT_TYPE_JSON, "X-Api-Key": key} - self.printer_last_reading = [{}, None] - self.job_last_reading = [{}, None] - self.job_available = False - self.printer_available = False - self.printer_error_logged = False - self.available = False - self.available_error_logged = False - self.job_error_logged = False - self.bed = bed - self.number_of_tools = number_of_tools - - def get_tools(self): - """Get the list of tools that temperature is monitored on.""" - tools = [] - if self.number_of_tools > 0: - for tool_number in range(0, self.number_of_tools): - tools.append(f"tool{tool_number!s}") - if self.bed: - tools.append("bed") - if not self.bed and self.number_of_tools == 0: - temps = self.printer_last_reading[0].get("temperature") - if temps is not None: - tools = temps.keys() - return tools - - def get(self, endpoint): - """Send a get request, and return the response as a dict.""" - # Only query the API at most every 30 seconds - now = time.time() - if endpoint == "job": - last_time = self.job_last_reading[1] - if last_time is not None and now - last_time < 30.0: - return self.job_last_reading[0] - elif endpoint == "printer": - last_time = self.printer_last_reading[1] - if last_time is not None and now - last_time < 30.0: - return self.printer_last_reading[0] - - url = self.api_url + endpoint + async def _async_update_data(self): + """Update data via API.""" + printer = None try: - response = requests.get(url, headers=self.headers, timeout=9) - response.raise_for_status() - if endpoint == "job": - self.job_last_reading[0] = response.json() - self.job_last_reading[1] = time.time() - self.job_available = True - elif endpoint == "printer": - self.printer_last_reading[0] = response.json() - self.printer_last_reading[1] = time.time() - self.printer_available = True + job = await self._octoprint.get_job_info() + except ApiError as err: + raise UpdateFailed(err) from err - self.available = self.printer_available and self.job_available - if self.available: - self.job_error_logged = False - self.printer_error_logged = False - self.available_error_logged = False + # If octoprint is on, but the printer is disconnected + # printer will return a 409, so continue using the last + # reading if there is one + try: + printer = await self._octoprint.get_printer_info() + except PrinterOffline: + if not self._printer_offline: + _LOGGER.error("Unable to retrieve printer information: Printer offline") + self._printer_offline = True + except ApiError as err: + raise UpdateFailed(err) from err + else: + self._printer_offline = False - return response.json() - - except requests.ConnectionError as exc_con: - log_string = f"Failed to connect to Octoprint server. Error: {exc_con}" - - if not self.available_error_logged: - _LOGGER.error(log_string) - self.job_available = False - self.printer_available = False - self.available_error_logged = True - - return None - - except requests.HTTPError as ex_http: - status_code = ex_http.response.status_code - - log_string = f"Failed to update OctoPrint status. Error: {ex_http}" - # Only log the first failure - if endpoint == "job": - log_string = f"Endpoint: job {log_string}" - if not self.job_error_logged: - _LOGGER.error(log_string) - self.job_error_logged = True - self.job_available = False - elif endpoint == "printer": - if ( - status_code == 409 - ): # octoprint returns HTTP 409 when printer is not connected (and many other states) - self.printer_available = False - else: - log_string = f"Endpoint: printer {log_string}" - if not self.printer_error_logged: - _LOGGER.error(log_string) - self.printer_error_logged = True - self.printer_available = False - - self.available = False - - return None - - def update(self, sensor_type, end_point, group, tool=None): - """Return the value for sensor_type from the provided endpoint.""" - if (response := self.get(end_point)) is not None: - return get_value_from_json(response, sensor_type, group, tool) - - return response - - -def get_value_from_json(json_dict, sensor_type, group, tool): - """Return the value for sensor_type from the JSON.""" - if group not in json_dict: - return None - - if sensor_type in json_dict[group]: - if sensor_type == "target" and json_dict[sensor_type] is None: - return 0 - - return json_dict[group][sensor_type] - - if tool is not None and sensor_type in json_dict[group][tool]: - return json_dict[group][tool][sensor_type] - - return None + return {"job": job, "printer": printer, "last_read_time": dt_util.utcnow()} diff --git a/homeassistant/components/octoprint/binary_sensor.py b/homeassistant/components/octoprint/binary_sensor.py index 0f740525f84..e7806999698 100644 --- a/homeassistant/components/octoprint/binary_sensor.py +++ b/homeassistant/components/octoprint/binary_sensor.py @@ -1,61 +1,74 @@ """Support for monitoring OctoPrint binary sensors.""" +from __future__ import annotations + +from abc import abstractmethod import logging -import requests +from pyoctoprintapi import OctoprintPrinterInfo from homeassistant.components.binary_sensor import BinarySensorEntity +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 . import BINARY_SENSOR_TYPES, DOMAIN as COMPONENT_DOMAIN +from .const import DOMAIN as COMPONENT_DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the available OctoPrint binary sensors.""" - if discovery_info is None: - return + coordinator: DataUpdateCoordinator = hass.data[COMPONENT_DOMAIN][ + config_entry.entry_id + ]["coordinator"] + device_id = config_entry.unique_id - name = discovery_info["name"] - base_url = discovery_info["base_url"] - monitored_conditions = discovery_info["sensors"] - octoprint_api = hass.data[COMPONENT_DOMAIN][base_url] + assert device_id is not None - devices = [] - for octo_type in monitored_conditions: - new_sensor = OctoPrintBinarySensor( - octoprint_api, - octo_type, - BINARY_SENSOR_TYPES[octo_type][2], - name, - BINARY_SENSOR_TYPES[octo_type][3], - BINARY_SENSOR_TYPES[octo_type][0], - BINARY_SENSOR_TYPES[octo_type][1], - "flags", - ) - devices.append(new_sensor) - add_entities(devices, True) + entities: list[BinarySensorEntity] = [ + OctoPrintPrintingBinarySensor(coordinator, device_id), + OctoPrintPrintingErrorBinarySensor(coordinator, device_id), + ] + + async_add_entities(entities) -class OctoPrintBinarySensor(BinarySensorEntity): +class OctoPrintBinarySensorBase(CoordinatorEntity, BinarySensorEntity): """Representation an OctoPrint binary sensor.""" def __init__( - self, api, condition, sensor_type, sensor_name, unit, endpoint, group, tool=None - ): + self, + coordinator: DataUpdateCoordinator, + sensor_type: str, + device_id: str, + ) -> None: """Initialize a new OctoPrint sensor.""" - self.sensor_name = sensor_name - if tool is None: - self._name = f"{sensor_name} {condition}" - else: - self._name = f"{sensor_name} {condition}" + super().__init__(coordinator) + self._name = f"Octoprint {sensor_type}" self.sensor_type = sensor_type - self.api = api - self._state = False - self._unit_of_measurement = unit - self.api_endpoint = endpoint - self.api_group = group - self.api_tool = tool - _LOGGER.debug("Created OctoPrint binary sensor %r", self) + self._device_id = device_id + + @property + def device_info(self): + """Device info.""" + return { + "identifiers": {(COMPONENT_DOMAIN, self._device_id)}, + "manufacturer": "Octoprint", + "name": "Octoprint", + } + + @property + def unique_id(self): + """Return a unique id.""" + return f"{self.sensor_type}-{self._device_id}" @property def name(self): @@ -65,19 +78,39 @@ class OctoPrintBinarySensor(BinarySensorEntity): @property def is_on(self): """Return true if binary sensor is on.""" - return bool(self._state) + printer = self.coordinator.data["printer"] + if not printer: + return None + + return bool(self._get_flag_state(printer)) @property - def device_class(self): - """Return the class of this sensor, from DEVICE_CLASSES.""" - return None + def available(self) -> bool: + """Return if entity is available.""" + return self.coordinator.last_update_success and self.coordinator.data["printer"] - def update(self): - """Update state of sensor.""" - try: - self._state = self.api.update( - self.sensor_type, self.api_endpoint, self.api_group, self.api_tool - ) - except requests.exceptions.ConnectionError: - # Error calling the api, already logged in api.update() - return + @abstractmethod + def _get_flag_state(self, printer_info: OctoprintPrinterInfo) -> bool | None: + """Return the value of the sensor flag.""" + + +class OctoPrintPrintingBinarySensor(OctoPrintBinarySensorBase): + """Representation an OctoPrint binary sensor.""" + + def __init__(self, coordinator: DataUpdateCoordinator, device_id: str) -> None: + """Initialize a new OctoPrint sensor.""" + super().__init__(coordinator, "Printing", device_id) + + def _get_flag_state(self, printer_info: OctoprintPrinterInfo) -> bool | None: + return bool(printer_info.state.flags.printing) + + +class OctoPrintPrintingErrorBinarySensor(OctoPrintBinarySensorBase): + """Representation an OctoPrint binary sensor.""" + + def __init__(self, coordinator: DataUpdateCoordinator, device_id: str) -> None: + """Initialize a new OctoPrint sensor.""" + super().__init__(coordinator, "Printing Error", device_id) + + def _get_flag_state(self, printer_info: OctoprintPrinterInfo) -> bool | None: + return bool(printer_info.state.flags.error) diff --git a/homeassistant/components/octoprint/config_flow.py b/homeassistant/components/octoprint/config_flow.py new file mode 100644 index 00000000000..5962aedc89f --- /dev/null +++ b/homeassistant/components/octoprint/config_flow.py @@ -0,0 +1,202 @@ +"""Config flow for OctoPrint integration.""" +import logging +from urllib.parse import urlsplit + +from pyoctoprintapi import ApiError, OctoprintClient, OctoprintException +import voluptuous as vol + +from homeassistant import config_entries, data_entry_flow, exceptions +from homeassistant.const import ( + CONF_API_KEY, + CONF_HOST, + CONF_PATH, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, +) +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +def _schema_with_defaults(username="", host=None, port=80, path="/", ssl=False): + return vol.Schema( + { + vol.Required(CONF_USERNAME, default=username): cv.string, + vol.Required(CONF_HOST, default=host): cv.string, + vol.Optional(CONF_PORT, default=port): cv.port, + vol.Optional(CONF_PATH, default=path): cv.string, + vol.Optional(CONF_SSL, default=ssl): cv.boolean, + }, + extra=vol.ALLOW_EXTRA, + ) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for OctoPrint.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + api_key_task = None + + def __init__(self) -> None: + """Handle a config flow for OctoPrint.""" + self.discovery_schema = None + self._user_input = None + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + # When coming back from the progress steps, the user_input is stored in the + # instance variable instead of being passed in + if user_input is None and self._user_input: + user_input = self._user_input + + if user_input is None: + data = self.discovery_schema or _schema_with_defaults() + return self.async_show_form(step_id="user", data_schema=data) + + if CONF_API_KEY in user_input: + errors = {} + try: + return await self._finish_config(user_input) + except data_entry_flow.AbortFlow as err: + raise err from None + except CannotConnect: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + errors["base"] = "unknown" + + if errors: + return self.async_show_form( + step_id="user", + errors=errors, + data_schema=_schema_with_defaults( + user_input.get(CONF_USERNAME), + user_input[CONF_HOST], + user_input[CONF_PORT], + user_input[CONF_PATH], + user_input[CONF_SSL], + ), + ) + + self.api_key_task = None + return await self.async_step_get_api_key(user_input) + + async def async_step_get_api_key(self, user_input): + """Get an Application Api Key.""" + if not self.api_key_task: + self.api_key_task = self.hass.async_create_task( + self._async_get_auth_key(user_input) + ) + return self.async_show_progress( + step_id="get_api_key", progress_action="get_api_key" + ) + + try: + await self.api_key_task + except OctoprintException as err: + _LOGGER.exception("Failed to get an application key: %s", err) + return self.async_show_progress_done(next_step_id="auth_failed") + except Exception as err: # pylint: disable=broad-except + _LOGGER.exception("Failed to get an application key : %s", err) + return self.async_show_progress_done(next_step_id="auth_failed") + + # store this off here to pick back up in the user step + self._user_input = user_input + return self.async_show_progress_done(next_step_id="user") + + async def _finish_config(self, user_input): + """Finish the configuration setup.""" + session = async_get_clientsession(self.hass) + octoprint = OctoprintClient( + user_input[CONF_HOST], + session, + user_input[CONF_PORT], + user_input[CONF_SSL], + user_input[CONF_PATH], + ) + octoprint.set_api_key(user_input[CONF_API_KEY]) + + try: + discovery = await octoprint.get_discovery_info() + except ApiError as err: + _LOGGER.error("Failed to connect to printer") + raise CannotConnect from err + + await self.async_set_unique_id(discovery.upnp_uuid, raise_on_progress=False) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=user_input[CONF_HOST], data=user_input) + + async def async_step_auth_failed(self, user_input): + """Handle api fetch failure.""" + return self.async_abort(reason="auth_failed") + + async def async_step_import(self, user_input): + """Handle import.""" + return await self.async_step_user(user_input) + + async def async_step_zeroconf(self, discovery_info): + """Handle discovery flow.""" + uuid = discovery_info["properties"]["uuid"] + await self.async_set_unique_id(uuid) + self._abort_if_unique_id_configured() + + self.context["title_placeholders"] = { + CONF_HOST: discovery_info[CONF_HOST], + } + + self.discovery_schema = _schema_with_defaults( + host=discovery_info[CONF_HOST], + port=discovery_info[CONF_PORT], + path=discovery_info["properties"][CONF_PATH], + ) + + return await self.async_step_user() + + async def async_step_ssdp(self, discovery_info): + """Handle ssdp discovery flow.""" + uuid = discovery_info["UDN"][5:] + await self.async_set_unique_id(uuid) + self._abort_if_unique_id_configured() + + url = urlsplit(discovery_info["presentationURL"]) + self.context["title_placeholders"] = { + CONF_HOST: url.hostname, + } + + self.discovery_schema = _schema_with_defaults( + host=url.hostname, + port=url.port, + ) + + return await self.async_step_user() + + async def _async_get_auth_key(self, user_input: dict): + """Get application api key.""" + session = async_get_clientsession(self.hass) + octoprint = OctoprintClient( + user_input[CONF_HOST], + session, + user_input[CONF_PORT], + user_input[CONF_SSL], + user_input[CONF_PATH], + ) + + try: + user_input[CONF_API_KEY] = await octoprint.request_app_key( + "Home Assistant", user_input[CONF_USERNAME], 30 + ) + finally: + # Continue the flow after show progress when the task is done. + self.hass.async_create_task( + self.hass.config_entries.flow.async_configure( + flow_id=self.flow_id, user_input=user_input + ) + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/octoprint/const.py b/homeassistant/components/octoprint/const.py new file mode 100644 index 00000000000..df22cb8d8f8 --- /dev/null +++ b/homeassistant/components/octoprint/const.py @@ -0,0 +1,5 @@ +"""Constants for the OctoPrint integration.""" + +DOMAIN = "octoprint" + +DEFAULT_NAME = "OctoPrint" diff --git a/homeassistant/components/octoprint/manifest.json b/homeassistant/components/octoprint/manifest.json index 85436f96176..3984eb86621 100644 --- a/homeassistant/components/octoprint/manifest.json +++ b/homeassistant/components/octoprint/manifest.json @@ -1,8 +1,16 @@ { "domain": "octoprint", "name": "OctoPrint", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/octoprint", - "after_dependencies": ["discovery"], - "codeowners": [], + "requirements": ["pyoctoprintapi==0.1.6"], + "codeowners": ["@rfleming71"], + "zeroconf": ["_octoprint._tcp.local."], + "ssdp": [ + { + "manufacturer": "The OctoPrint Project", + "deviceType": "urn:schemas-upnp-org:device:Basic:1" + } + ], "iot_class": "local_polling" } diff --git a/homeassistant/components/octoprint/sensor.py b/homeassistant/components/octoprint/sensor.py index d456813a4ff..3eef0654870 100644 --- a/homeassistant/components/octoprint/sensor.py +++ b/homeassistant/components/octoprint/sensor.py @@ -1,143 +1,256 @@ """Support for monitoring OctoPrint sensors.""" +from __future__ import annotations + +from datetime import timedelta import logging -import requests +from pyoctoprintapi import OctoprintJobInfo, OctoprintPrinterInfo from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity -from homeassistant.const import DEVICE_CLASS_TEMPERATURE, PERCENTAGE, TEMP_CELSIUS +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, + PERCENTAGE, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) -from . import DOMAIN as COMPONENT_DOMAIN, SENSOR_TYPES +from . import DOMAIN as COMPONENT_DOMAIN _LOGGER = logging.getLogger(__name__) -NOTIFICATION_ID = "octoprint_notification" -NOTIFICATION_TITLE = "OctoPrint sensor setup error" +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the available OctoPrint binary sensors.""" + coordinator: DataUpdateCoordinator = hass.data[COMPONENT_DOMAIN][ + config_entry.entry_id + ]["coordinator"] + device_id = config_entry.unique_id -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the available OctoPrint sensors.""" - if discovery_info is None: - return + assert device_id is not None - name = discovery_info["name"] - base_url = discovery_info["base_url"] - monitored_conditions = discovery_info["sensors"] - octoprint_api = hass.data[COMPONENT_DOMAIN][base_url] - tools = octoprint_api.get_tools() - - if "Temperatures" in monitored_conditions and not tools: - hass.components.persistent_notification.create( - "Your printer appears to be offline.
" - "If you do not want to have your printer on
" - " at all times, and you would like to monitor
" - "temperatures, please add
" - "bed and/or number_of_tools to your configuration
" - "and restart.", - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) - - devices = [] - types = ["actual", "target"] - for octo_type in monitored_conditions: - if octo_type == "Temperatures": - for tool in tools: - for temp_type in types: - new_sensor = OctoPrintSensor( - api=octoprint_api, - condition=temp_type, - sensor_type=temp_type, - sensor_name=name, - unit=SENSOR_TYPES[octo_type][3], - endpoint=SENSOR_TYPES[octo_type][0], - group=SENSOR_TYPES[octo_type][1], - tool=tool, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, + entities: list[SensorEntity] = [] + if coordinator.data["printer"]: + printer_info = coordinator.data["printer"] + types = ["actual", "target"] + for tool in printer_info.temperatures: + for temp_type in types: + entities.append( + OctoPrintTemperatureSensor( + coordinator, + tool.name, + temp_type, + device_id, ) - devices.append(new_sensor) - else: - new_sensor = OctoPrintSensor( - api=octoprint_api, - condition=octo_type, - sensor_type=SENSOR_TYPES[octo_type][2], - sensor_name=name, - unit=SENSOR_TYPES[octo_type][3], - endpoint=SENSOR_TYPES[octo_type][0], - group=SENSOR_TYPES[octo_type][1], - icon=SENSOR_TYPES[octo_type][4], - ) - devices.append(new_sensor) - add_entities(devices, True) + ) + else: + _LOGGER.error("Printer appears to be offline, skipping temperature sensors") + + entities.append(OctoPrintStatusSensor(coordinator, device_id)) + entities.append(OctoPrintJobPercentageSensor(coordinator, device_id)) + entities.append(OctoPrintEstimatedFinishTimeSensor(coordinator, device_id)) + entities.append(OctoPrintStartTimeSensor(coordinator, device_id)) + + async_add_entities(entities) -class OctoPrintSensor(SensorEntity): +class OctoPrintSensorBase(CoordinatorEntity, SensorEntity): """Representation of an OctoPrint sensor.""" def __init__( self, - api, - condition, - sensor_type, - sensor_name, - unit, - endpoint, - group, - tool=None, - icon=None, - device_class=None, - state_class=None, - ): + coordinator: DataUpdateCoordinator, + sensor_type: str, + device_id: str, + ) -> None: """Initialize a new OctoPrint sensor.""" - self.sensor_name = sensor_name - if tool is None: - self._name = f"{sensor_name} {condition}" - else: - self._name = f"{sensor_name} {condition} {tool} temp" - self.sensor_type = sensor_type - self.api = api - self._state = None - self._unit_of_measurement = unit - self.api_endpoint = endpoint - self.api_group = group - self.api_tool = tool - self._icon = icon - self._attr_device_class = device_class - self._attr_state_class = state_class - _LOGGER.debug("Created OctoPrint sensor %r", self) + super().__init__(coordinator) + self._sensor_type = sensor_type + self._name = f"Octoprint {sensor_type}" + self._device_id = device_id + + @property + def device_info(self): + """Device info.""" + return { + "identifiers": {(COMPONENT_DOMAIN, self._device_id)}, + "manufacturer": "Octoprint", + "name": "Octoprint", + } + + @property + def unique_id(self): + """Return a unique id.""" + return f"{self._sensor_type}-{self._device_id}" @property def name(self): """Return the name of the sensor.""" return self._name + +class OctoPrintStatusSensor(OctoPrintSensorBase): + """Representation of an OctoPrint sensor.""" + + def __init__(self, coordinator: DataUpdateCoordinator, device_id: str) -> None: + """Initialize a new OctoPrint sensor.""" + super().__init__(coordinator, "Current State", device_id) + @property def native_value(self): - """Return the state of the sensor.""" - sensor_unit = self.unit_of_measurement - if sensor_unit in (TEMP_CELSIUS, PERCENTAGE): - # API sometimes returns null and not 0 - if self._state is None: - self._state = 0 - return round(self._state, 2) - return self._state + """Return sensor state.""" + printer: OctoprintPrinterInfo = self.coordinator.data["printer"] + if not printer: + return None - @property - def native_unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - - def update(self): - """Update state of sensor.""" - try: - self._state = self.api.update( - self.sensor_type, self.api_endpoint, self.api_group, self.api_tool - ) - except requests.exceptions.ConnectionError: - # Error calling the api, already logged in api.update() - return + return printer.state.text @property def icon(self): """Icon to use in the frontend.""" - return self._icon + return "mdi:printer-3d" + + @property + def available(self) -> bool: + """Return if entity is available.""" + return self.coordinator.last_update_success and self.coordinator.data["printer"] + + +class OctoPrintJobPercentageSensor(OctoPrintSensorBase): + """Representation of an OctoPrint sensor.""" + + def __init__(self, coordinator: DataUpdateCoordinator, device_id: str) -> None: + """Initialize a new OctoPrint sensor.""" + super().__init__(coordinator, "Job Percentage", device_id) + + @property + def native_value(self): + """Return sensor state.""" + job: OctoprintJobInfo = self.coordinator.data["job"] + if not job: + return None + + state = job.progress.completion + if not state: + return 0 + + return round(state, 2) + + @property + def native_unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return PERCENTAGE + + @property + def icon(self): + """Icon to use in the frontend.""" + return "mdi:file-percent" + + +class OctoPrintEstimatedFinishTimeSensor(OctoPrintSensorBase): + """Representation of an OctoPrint sensor.""" + + def __init__(self, coordinator: DataUpdateCoordinator, device_id: str) -> None: + """Initialize a new OctoPrint sensor.""" + super().__init__(coordinator, "Estimated Finish Time", device_id) + + @property + def native_value(self): + """Return sensor state.""" + job: OctoprintJobInfo = self.coordinator.data["job"] + if not job or not job.progress.print_time_left or job.state != "Printing": + return None + + read_time = self.coordinator.data["last_read_time"] + + return (read_time + timedelta(seconds=job.progress.print_time_left)).isoformat() + + @property + def device_class(self): + """Return the device class of the sensor.""" + return DEVICE_CLASS_TIMESTAMP + + +class OctoPrintStartTimeSensor(OctoPrintSensorBase): + """Representation of an OctoPrint sensor.""" + + def __init__(self, coordinator: DataUpdateCoordinator, device_id: str) -> None: + """Initialize a new OctoPrint sensor.""" + super().__init__(coordinator, "Start Time", device_id) + + @property + def native_value(self): + """Return sensor state.""" + job: OctoprintJobInfo = self.coordinator.data["job"] + + if not job or not job.progress.print_time or job.state != "Printing": + return None + + read_time = self.coordinator.data["last_read_time"] + + return (read_time - timedelta(seconds=job.progress.print_time)).isoformat() + + @property + def device_class(self): + """Return the device class of the sensor.""" + return DEVICE_CLASS_TIMESTAMP + + +class OctoPrintTemperatureSensor(OctoPrintSensorBase): + """Representation of an OctoPrint sensor.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator, + tool: str, + temp_type: str, + device_id: str, + ) -> None: + """Initialize a new OctoPrint sensor.""" + super().__init__(coordinator, f"{temp_type} {tool} temp", device_id) + self._temp_type = temp_type + self._api_tool = tool + self._attr_state_class = STATE_CLASS_MEASUREMENT + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return TEMP_CELSIUS + + @property + def device_class(self): + """Return the device class of this entity.""" + return DEVICE_CLASS_TEMPERATURE + + @property + def native_value(self): + """Return sensor state.""" + printer: OctoprintPrinterInfo = self.coordinator.data["printer"] + if not printer: + return None + + for temp in printer.temperatures: + if temp.name == self._api_tool: + return round( + temp.actual_temp + if self._temp_type == "actual" + else temp.target_temp, + 2, + ) + + return None + + @property + def available(self) -> bool: + """Return if entity is available.""" + return self.coordinator.last_update_success and self.coordinator.data["printer"] diff --git a/homeassistant/components/octoprint/strings.json b/homeassistant/components/octoprint/strings.json new file mode 100644 index 00000000000..c52486d8406 --- /dev/null +++ b/homeassistant/components/octoprint/strings.json @@ -0,0 +1,29 @@ +{ + "config": { + "flow_title": "OctoPrint Printer: {host}", + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "path": "Application Path", + "port": "Port Number", + "ssl": "Use SSL", + "username": "[%key:common::config_flow::data::username%]" + } + } + }, + "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%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "auth_failed": "Failed to retrieve application api key" + }, + "progress": { + "get_api_key": "Open the OctoPrint UI and click 'Allow' on the Access Request for 'Home Assistant'." + } + } +} diff --git a/homeassistant/components/octoprint/translations/en.json b/homeassistant/components/octoprint/translations/en.json new file mode 100644 index 00000000000..75d8355cbf3 --- /dev/null +++ b/homeassistant/components/octoprint/translations/en.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "auth_failed": "Failed to retrieve application api key", + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "flow_title": "OctoPrint Printer: {host}", + "progress": { + "get_api_key": "Open the OctoPrint UI and click 'Allow' on the Access Request for 'Home Assistant'." + }, + "step": { + "user": { + "data": { + "host": "Host", + "path": "Application Path", + "port": "Port Number", + "ssl": "Use SSL", + "username": "Username" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 042babb6313..39beb7bcb10 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -201,6 +201,7 @@ FLOWS = [ "nut", "nws", "nzbget", + "octoprint", "omnilogic", "ondilo_ico", "onewire", diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index cb255e5acfe..1676037bbf2 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -171,6 +171,12 @@ SSDP = { "manufacturer": "NETGEAR, Inc." } ], + "octoprint": [ + { + "deviceType": "urn:schemas-upnp-org:device:Basic:1", + "manufacturer": "The OctoPrint Project" + } + ], "roku": [ { "deviceType": "urn:roku-com:device:player:1-0", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 041406af50a..8ab7a7f8e31 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -194,6 +194,11 @@ ZEROCONF = { "domain": "nut" } ], + "_octoprint._tcp.local.": [ + { + "domain": "octoprint" + } + ], "_plexmediasvr._tcp.local.": [ { "domain": "plex" diff --git a/requirements_all.txt b/requirements_all.txt index 9b30036eb47..18783fa64ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1678,6 +1678,9 @@ pynzbgetapi==0.2.0 # homeassistant.components.obihai pyobihai==1.3.1 +# homeassistant.components.octoprint +pyoctoprintapi==0.1.6 + # homeassistant.components.ombi pyombi==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf95651ea07..81e969b6d74 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1006,6 +1006,9 @@ pynx584==0.5 # homeassistant.components.nzbget pynzbgetapi==0.2.0 +# homeassistant.components.octoprint +pyoctoprintapi==0.1.6 + # homeassistant.components.openuv pyopenuv==2.2.1 diff --git a/tests/components/octoprint/__init__.py b/tests/components/octoprint/__init__.py new file mode 100644 index 00000000000..4af4a2ea131 --- /dev/null +++ b/tests/components/octoprint/__init__.py @@ -0,0 +1,82 @@ +"""Tests for the OctoPrint integration.""" +from __future__ import annotations + +from typing import Any +from unittest.mock import patch + +from pyoctoprintapi import ( + DiscoverySettings, + OctoprintJobInfo, + OctoprintPrinterInfo, + TrackingSetting, +) + +from homeassistant.components.octoprint import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.helpers.typing import UNDEFINED, UndefinedType + +from tests.common import MockConfigEntry + +DEFAULT_JOB = { + "job": {}, + "progress": {"completion": 50}, +} + +DEFAULT_PRINTER = { + "state": { + "flags": {"printing": True, "error": False}, + "text": "Operational", + }, + "temperature": [], +} + + +async def init_integration( + hass, + platform, + printer: dict[str, Any] | UndefinedType | None = UNDEFINED, + job: dict[str, Any] | None = None, +): + """Set up the octoprint integration in Home Assistant.""" + printer_info: OctoprintPrinterInfo | None = None + if printer is UNDEFINED: + printer = DEFAULT_PRINTER + if printer is not None: + printer_info = OctoprintPrinterInfo(printer) + if job is None: + job = DEFAULT_JOB + with patch("homeassistant.components.octoprint.PLATFORMS", [platform]), patch( + "pyoctoprintapi.OctoprintClient.get_server_info", return_value={} + ), patch( + "pyoctoprintapi.OctoprintClient.get_printer_info", + return_value=printer_info, + ), patch( + "pyoctoprintapi.OctoprintClient.get_job_info", + return_value=OctoprintJobInfo(job), + ), patch( + "pyoctoprintapi.OctoprintClient.get_tracking_info", + return_value=TrackingSetting({"unique_id": "uuid"}), + ), patch( + "pyoctoprintapi.OctoprintClient.get_discovery_info", + return_value=DiscoverySettings({"upnpUuid": "uuid"}), + ): + config_entry = MockConfigEntry( + domain=DOMAIN, + entry_id="uuid", + unique_id="uuid", + data={ + "host": "1.1.1.1", + "api_key": "test-key", + "name": "Octoprint", + "port": 81, + "ssl": True, + "path": "/", + }, + title="Octoprint", + ) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.LOADED diff --git a/tests/components/octoprint/test_binary_sensor.py b/tests/components/octoprint/test_binary_sensor.py new file mode 100644 index 00000000000..139ed0dc139 --- /dev/null +++ b/tests/components/octoprint/test_binary_sensor.py @@ -0,0 +1,54 @@ +"""The tests for Octoptint binary sensor module.""" + +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE + +from . import init_integration + + +async def test_sensors(hass): + """Test the underlying sensors.""" + printer = { + "state": { + "flags": {"printing": True, "error": False}, + "text": "Operational", + }, + "temperature": [], + } + await init_integration(hass, "binary_sensor", printer=printer) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + state = hass.states.get("binary_sensor.octoprint_printing") + assert state is not None + assert state.state == STATE_ON + assert state.name == "Octoprint Printing" + entry = entity_registry.async_get("binary_sensor.octoprint_printing") + assert entry.unique_id == "Printing-uuid" + + state = hass.states.get("binary_sensor.octoprint_printing_error") + assert state is not None + assert state.state == STATE_OFF + assert state.name == "Octoprint Printing Error" + entry = entity_registry.async_get("binary_sensor.octoprint_printing_error") + assert entry.unique_id == "Printing Error-uuid" + + +async def test_sensors_printer_offline(hass): + """Test the underlying sensors when the printer is offline.""" + await init_integration(hass, "binary_sensor", printer=None) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + state = hass.states.get("binary_sensor.octoprint_printing") + assert state is not None + assert state.state == STATE_UNAVAILABLE + assert state.name == "Octoprint Printing" + entry = entity_registry.async_get("binary_sensor.octoprint_printing") + assert entry.unique_id == "Printing-uuid" + + state = hass.states.get("binary_sensor.octoprint_printing_error") + assert state is not None + assert state.state == STATE_UNAVAILABLE + assert state.name == "Octoprint Printing Error" + entry = entity_registry.async_get("binary_sensor.octoprint_printing_error") + assert entry.unique_id == "Printing Error-uuid" diff --git a/tests/components/octoprint/test_config_flow.py b/tests/components/octoprint/test_config_flow.py new file mode 100644 index 00000000000..f176e5ab288 --- /dev/null +++ b/tests/components/octoprint/test_config_flow.py @@ -0,0 +1,519 @@ +"""Test the OctoPrint config flow.""" +from unittest.mock import patch + +from pyoctoprintapi import ApiError, DiscoverySettings + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.octoprint.const import DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_form(hass): + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert not result["errors"] + + with patch( + "pyoctoprintapi.OctoprintClient.request_app_key", return_value="test-key" + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "testuser", + "host": "1.1.1.1", + "name": "Printer", + "port": 81, + "ssl": True, + "path": "/", + }, + ) + await hass.async_block_till_done() + assert result["type"] == "progress" + + with patch( + "pyoctoprintapi.OctoprintClient.get_server_info", + return_value=True, + ), patch( + "pyoctoprintapi.OctoprintClient.get_discovery_info", + return_value=DiscoverySettings({"upnpUuid": "uuid"}), + ), patch( + "homeassistant.components.octoprint.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.octoprint.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "1.1.1.1" + assert result2["data"] == { + "username": "testuser", + "host": "1.1.1.1", + "api_key": "test-key", + "name": "Printer", + "port": 81, + "ssl": True, + "path": "/", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "testuser", + "host": "1.1.1.1", + "name": "Printer", + "port": 81, + "ssl": True, + "path": "/", + }, + ) + assert result["type"] == "progress" + + with patch( + "pyoctoprintapi.OctoprintClient.request_app_key", return_value="test-key" + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + ) + + assert result["type"] == "progress_done" + with patch( + "pyoctoprintapi.OctoprintClient.get_discovery_info", + side_effect=ApiError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "testuser", + "host": "1.1.1.1", + "name": "Printer", + "port": 81, + "ssl": True, + "path": "/", + "api_key": "test-key", + }, + ) + + assert result["type"] == "form" + assert result["errors"]["base"] == "cannot_connect" + + +async def test_form_unknown_exception(hass): + """Test we handle a random error.""" + 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"], + { + "username": "testuser", + "host": "1.1.1.1", + "name": "Printer", + "port": 81, + "ssl": True, + "path": "/", + }, + ) + assert result["type"] == "progress" + + with patch( + "pyoctoprintapi.OctoprintClient.request_app_key", return_value="test-key" + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + ) + + assert result["type"] == "progress_done" + with patch( + "pyoctoprintapi.OctoprintClient.get_discovery_info", + side_effect=Exception, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "testuser", + "host": "1.1.1.1", + "name": "Printer", + "port": 81, + "ssl": True, + "path": "/", + "api_key": "test-key", + }, + ) + + assert result["type"] == "form" + assert result["errors"]["base"] == "unknown" + + +async def test_show_zerconf_form(hass: HomeAssistant) -> None: + """Test that the zeroconf confirmation form is served.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data={ + "host": "192.168.1.123", + "port": 80, + "hostname": "example.local.", + "uuid": "83747482", + "properties": {"uuid": "83747482", "path": "/foo/"}, + }, + ) + assert result["type"] == "form" + assert not result["errors"] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "testuser", + }, + ) + assert result["type"] == "progress" + + with patch( + "pyoctoprintapi.OctoprintClient.request_app_key", return_value="test-key" + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + ) + await hass.async_block_till_done() + + assert result["type"] == "progress_done" + + with patch( + "pyoctoprintapi.OctoprintClient.get_server_info", + return_value=True, + ), patch( + "pyoctoprintapi.OctoprintClient.get_discovery_info", + return_value=DiscoverySettings({"upnpUuid": "uuid"}), + ), patch( + "homeassistant.components.octoprint.async_setup", return_value=True + ), patch( + "homeassistant.components.octoprint.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "testuser", + "host": "1.1.1.1", + "name": "Printer", + "port": 81, + "ssl": True, + "path": "/", + "api_key": "test-key", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_show_ssdp_form(hass: HomeAssistant) -> None: + """Test that the zeroconf confirmation form is served.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data={ + "presentationURL": "http://192.168.1.123:80/discovery/device.xml", + "port": 80, + "UDN": "uuid:83747482", + }, + ) + assert result["type"] == "form" + assert not result["errors"] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "testuser", + }, + ) + assert result["type"] == "progress" + + with patch( + "pyoctoprintapi.OctoprintClient.request_app_key", return_value="test-key" + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + ) + await hass.async_block_till_done() + + assert result["type"] == "progress_done" + + with patch( + "pyoctoprintapi.OctoprintClient.get_server_info", + return_value=True, + ), patch( + "pyoctoprintapi.OctoprintClient.get_discovery_info", + return_value=DiscoverySettings({"upnpUuid": "uuid"}), + ), patch( + "homeassistant.components.octoprint.async_setup", return_value=True + ), patch( + "homeassistant.components.octoprint.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "testuser", + "host": "1.1.1.1", + "name": "Printer", + "port": 81, + "ssl": True, + "path": "/", + "api_key": "test-key", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_import_yaml(hass: HomeAssistant) -> None: + """Test that the yaml import works.""" + with patch( + "pyoctoprintapi.OctoprintClient.get_server_info", + return_value=True, + ), patch( + "pyoctoprintapi.OctoprintClient.get_discovery_info", + return_value=DiscoverySettings({"upnpUuid": "uuid"}), + ), patch( + "pyoctoprintapi.OctoprintClient.request_app_key", return_value="test-key" + ), patch( + "homeassistant.components.octoprint.async_setup", return_value=True + ), patch( + "homeassistant.components.octoprint.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + "host": "1.1.1.1", + "api_key": "test-key", + "name": "Printer", + "port": 81, + "ssl": True, + "path": "/", + }, + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert "errors" not in result + + +async def test_import_duplicate_yaml(hass: HomeAssistant) -> None: + """Test that the yaml import works.""" + MockConfigEntry( + domain=DOMAIN, + data={"host": "192.168.1.123"}, + source=config_entries.SOURCE_IMPORT, + unique_id="uuid", + ).add_to_hass(hass) + + with patch( + "pyoctoprintapi.OctoprintClient.get_discovery_info", + return_value=DiscoverySettings({"upnpUuid": "uuid"}), + ), patch( + "pyoctoprintapi.OctoprintClient.request_app_key", return_value="test-key" + ) as request_app_key: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + "host": "1.1.1.1", + "api_key": "test-key", + "name": "Printer", + "port": 81, + "ssl": True, + "path": "/", + }, + ) + await hass.async_block_till_done() + assert len(request_app_key.mock_calls) == 0 + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_failed_auth(hass: HomeAssistant) -> None: + """Test we handle a random error.""" + 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"], + { + "username": "testuser", + "host": "1.1.1.1", + "name": "Printer", + "port": 81, + "ssl": True, + "path": "/", + }, + ) + assert result["type"] == "progress" + + with patch("pyoctoprintapi.OctoprintClient.request_app_key", side_effect=ApiError): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + ) + + assert result["type"] == "progress_done" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == "abort" + assert result["reason"] == "auth_failed" + + +async def test_failed_auth_unexpected_error(hass: HomeAssistant) -> None: + """Test we handle a random error.""" + 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"], + { + "username": "testuser", + "host": "1.1.1.1", + "name": "Printer", + "port": 81, + "ssl": True, + "path": "/", + }, + ) + assert result["type"] == "progress" + + with patch("pyoctoprintapi.OctoprintClient.request_app_key", side_effect=Exception): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + ) + + assert result["type"] == "progress_done" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == "abort" + assert result["reason"] == "auth_failed" + + +async def test_user_duplicate_entry(hass): + """Test that duplicate entries abort.""" + MockConfigEntry( + domain=DOMAIN, + data={"host": "192.168.1.123"}, + source=config_entries.SOURCE_IMPORT, + unique_id="uuid", + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert not result["errors"] + + with patch( + "pyoctoprintapi.OctoprintClient.request_app_key", return_value="test-key" + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "testuser", + "host": "1.1.1.1", + "name": "Printer", + "port": 81, + "ssl": True, + "path": "/", + }, + ) + await hass.async_block_till_done() + assert result["type"] == "progress" + + with patch( + "pyoctoprintapi.OctoprintClient.get_server_info", + return_value=True, + ), patch( + "pyoctoprintapi.OctoprintClient.get_discovery_info", + return_value=DiscoverySettings({"upnpUuid": "uuid"}), + ), patch( + "homeassistant.components.octoprint.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.octoprint.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"] == "abort" + assert result2["reason"] == "already_configured" + assert len(mock_setup.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_duplicate_zerconf_ignored(hass: HomeAssistant) -> None: + """Test that the duplicate zeroconf isn't shown.""" + MockConfigEntry( + domain=DOMAIN, + data={"host": "192.168.1.123"}, + source=config_entries.SOURCE_IMPORT, + unique_id="83747482", + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data={ + "host": "192.168.1.123", + "port": 80, + "hostname": "example.local.", + "uuid": "83747482", + "properties": {"uuid": "83747482", "path": "/foo/"}, + }, + ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_duplicate_ssdp_ignored(hass: HomeAssistant) -> None: + """Test that duplicate ssdp form is note shown.""" + MockConfigEntry( + domain=DOMAIN, + data={"host": "192.168.1.123"}, + source=config_entries.SOURCE_IMPORT, + unique_id="83747482", + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data={ + "presentationURL": "http://192.168.1.123:80/discovery/device.xml", + "port": 80, + "UDN": "uuid:83747482", + }, + ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" diff --git a/tests/components/octoprint/test_sensor.py b/tests/components/octoprint/test_sensor.py new file mode 100644 index 00000000000..3954e9ffbca --- /dev/null +++ b/tests/components/octoprint/test_sensor.py @@ -0,0 +1,76 @@ +"""The tests for Octoptint binary sensor module.""" +from datetime import datetime +from unittest.mock import patch + +from . import init_integration + + +async def test_sensors(hass): + """Test the underlying sensors.""" + printer = { + "state": { + "flags": {}, + "text": "Operational", + }, + "temperature": {"tool1": {"actual": 18.83136, "target": 37.83136}}, + } + job = { + "job": {}, + "progress": {"completion": 50, "printTime": 600, "printTimeLeft": 6000}, + "state": "Printing", + } + with patch( + "homeassistant.util.dt.utcnow", return_value=datetime(2020, 2, 20, 9, 10, 0) + ): + await init_integration(hass, "sensor", printer=printer, job=job) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + state = hass.states.get("sensor.octoprint_job_percentage") + assert state is not None + assert state.state == "50" + assert state.name == "Octoprint Job Percentage" + entry = entity_registry.async_get("sensor.octoprint_job_percentage") + assert entry.unique_id == "Job Percentage-uuid" + + state = hass.states.get("sensor.octoprint_current_state") + assert state is not None + assert state.state == "Operational" + assert state.name == "Octoprint Current State" + entry = entity_registry.async_get("sensor.octoprint_current_state") + assert entry.unique_id == "Current State-uuid" + + state = hass.states.get("sensor.octoprint_actual_tool1_temp") + assert state is not None + assert state.state == "18.83" + assert state.name == "Octoprint actual tool1 temp" + entry = entity_registry.async_get("sensor.octoprint_actual_tool1_temp") + assert entry.unique_id == "actual tool1 temp-uuid" + + state = hass.states.get("sensor.octoprint_target_tool1_temp") + assert state is not None + assert state.state == "37.83" + assert state.name == "Octoprint target tool1 temp" + entry = entity_registry.async_get("sensor.octoprint_target_tool1_temp") + assert entry.unique_id == "target tool1 temp-uuid" + + state = hass.states.get("sensor.octoprint_target_tool1_temp") + assert state is not None + assert state.state == "37.83" + assert state.name == "Octoprint target tool1 temp" + entry = entity_registry.async_get("sensor.octoprint_target_tool1_temp") + assert entry.unique_id == "target tool1 temp-uuid" + + state = hass.states.get("sensor.octoprint_start_time") + assert state is not None + assert state.state == "2020-02-20T09:00:00" + assert state.name == "Octoprint Start Time" + entry = entity_registry.async_get("sensor.octoprint_start_time") + assert entry.unique_id == "Start Time-uuid" + + state = hass.states.get("sensor.octoprint_estimated_finish_time") + assert state is not None + assert state.state == "2020-02-20T10:50:00" + assert state.name == "Octoprint Estimated Finish Time" + entry = entity_registry.async_get("sensor.octoprint_estimated_finish_time") + assert entry.unique_id == "Estimated Finish Time-uuid" From 8bc1509afab09aada67860f2e656becf1b3a585c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 22 Oct 2021 17:28:56 +0300 Subject: [PATCH 0673/1038] Use HTTPStatus instead of HTTP_ consts and magic values in comp.../[de]* (#57990) --- .../components/ddwrt/device_tracker.py | 7 +- homeassistant/components/doorbird/__init__.py | 3 +- .../components/doorbird/config_flow.py | 13 +- .../components/downloader/__init__.py | 4 +- .../components/dte_energy_bridge/sensor.py | 5 +- .../components/dublin_bus_transport/sensor.py | 5 +- homeassistant/components/emoncms/sensor.py | 4 +- .../components/emoncms_history/__init__.py | 4 +- homeassistant/components/evohome/__init__.py | 7 +- tests/components/demo/test_media_player.py | 3 +- tests/components/demo/test_stt.py | 10 +- tests/components/dialogflow/test_init.py | 35 ++--- tests/components/ecobee/test_climate.py | 3 +- tests/components/emulated_hue/test_hue_api.py | 122 +++++++++--------- tests/components/emulated_hue/test_upnp.py | 11 +- 15 files changed, 119 insertions(+), 117 deletions(-) diff --git a/homeassistant/components/ddwrt/device_tracker.py b/homeassistant/components/ddwrt/device_tracker.py index bc4ef7f8f82..303cfe72b97 100644 --- a/homeassistant/components/ddwrt/device_tracker.py +++ b/homeassistant/components/ddwrt/device_tracker.py @@ -1,4 +1,5 @@ """Support for DD-WRT routers.""" +from http import HTTPStatus import logging import re @@ -16,8 +17,6 @@ from homeassistant.const import ( CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, - HTTP_OK, - HTTP_UNAUTHORIZED, ) import homeassistant.helpers.config_validation as cv @@ -152,9 +151,9 @@ class DdWrtDeviceScanner(DeviceScanner): except requests.exceptions.Timeout: _LOGGER.exception("Connection to the router timed out") return - if response.status_code == HTTP_OK: + if response.status_code == HTTPStatus.OK: return _parse_ddwrt_response(response.text) - if response.status_code == HTTP_UNAUTHORIZED: + if response.status_code == HTTPStatus.UNAUTHORIZED: # Authentication error _LOGGER.exception( "Failed to authenticate, check your username and password" diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 63270ee1f1b..3bd82a7ff8e 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -16,7 +16,6 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME, - HTTP_UNAUTHORIZED, ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady @@ -106,7 +105,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: status, info = await hass.async_add_executor_job(_init_doorbird_device, device) except requests.exceptions.HTTPError as err: - if err.response.status_code == HTTP_UNAUTHORIZED: + if err.response.status_code == HTTPStatus.UNAUTHORIZED: _LOGGER.error( "Authorization rejected by DoorBird for %s@%s", username, device_ip ) diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py index 16b9725cf83..4c62fab17ef 100644 --- a/homeassistant/components/doorbird/config_flow.py +++ b/homeassistant/components/doorbird/config_flow.py @@ -1,4 +1,5 @@ """Config flow for DoorBird integration.""" +from http import HTTPStatus from ipaddress import ip_address import logging @@ -7,13 +8,7 @@ import requests import voluptuous as vol from homeassistant import config_entries, core, exceptions -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PASSWORD, - CONF_USERNAME, - HTTP_UNAUTHORIZED, -) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from homeassistant.util.network import is_link_local @@ -45,7 +40,7 @@ async def validate_input(hass: core.HomeAssistant, data): try: status, info = await hass.async_add_executor_job(_check_device, device) except requests.exceptions.HTTPError as err: - if err.response.status_code == HTTP_UNAUTHORIZED: + if err.response.status_code == HTTPStatus.UNAUTHORIZED: raise InvalidAuth from err raise CannotConnect from err except OSError as err: @@ -66,7 +61,7 @@ async def async_verify_supported_device(hass, host): try: await hass.async_add_executor_job(device.doorbell_state) except requests.exceptions.HTTPError as err: - if err.response.status_code == HTTP_UNAUTHORIZED: + if err.response.status_code == HTTPStatus.UNAUTHORIZED: return True except OSError: return False diff --git a/homeassistant/components/downloader/__init__.py b/homeassistant/components/downloader/__init__.py index 89aa4a465cf..7cc64a1507b 100644 --- a/homeassistant/components/downloader/__init__.py +++ b/homeassistant/components/downloader/__init__.py @@ -1,4 +1,5 @@ """Support for functionality to download files.""" +from http import HTTPStatus import logging import os import re @@ -7,7 +8,6 @@ import threading import requests import voluptuous as vol -from homeassistant.const import HTTP_OK import homeassistant.helpers.config_validation as cv from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path @@ -78,7 +78,7 @@ def setup(hass, config): req = requests.get(url, stream=True, timeout=10) - if req.status_code != HTTP_OK: + if req.status_code != HTTPStatus.OK: _LOGGER.warning( "Downloading '%s' failed, status_code=%d", url, req.status_code ) diff --git a/homeassistant/components/dte_energy_bridge/sensor.py b/homeassistant/components/dte_energy_bridge/sensor.py index 5b08e8e142c..d443047a171 100644 --- a/homeassistant/components/dte_energy_bridge/sensor.py +++ b/homeassistant/components/dte_energy_bridge/sensor.py @@ -1,4 +1,5 @@ """Support for monitoring energy usage using the DTE energy bridge.""" +from http import HTTPStatus import logging import requests @@ -9,7 +10,7 @@ from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, SensorEntity, ) -from homeassistant.const import CONF_NAME, HTTP_OK +from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -90,7 +91,7 @@ class DteEnergyBridgeSensor(SensorEntity): ) return - if response.status_code != HTTP_OK: + if response.status_code != HTTPStatus.OK: _LOGGER.warning( "Invalid status_code from DTE Energy Bridge: %s (%s)", response.status_code, diff --git a/homeassistant/components/dublin_bus_transport/sensor.py b/homeassistant/components/dublin_bus_transport/sensor.py index b7daf661e63..8fcbb4dcfed 100644 --- a/homeassistant/components/dublin_bus_transport/sensor.py +++ b/homeassistant/components/dublin_bus_transport/sensor.py @@ -6,12 +6,13 @@ https://data.gov.ie/dataset/real-time-passenger-information-rtpi-for-dublin-bus- """ from contextlib import suppress from datetime import datetime, timedelta +from http import HTTPStatus import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, HTTP_OK, TIME_MINUTES +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, TIME_MINUTES import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util @@ -144,7 +145,7 @@ class PublicTransportData: response = requests.get(_RESOURCE, params, timeout=10) - if response.status_code != HTTP_OK: + if response.status_code != HTTPStatus.OK: self.info = [ {ATTR_DUE_AT: "n/a", ATTR_ROUTE: self.route, ATTR_DUE_IN: "n/a"} ] diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index 125e5e6c333..68a28cf2846 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -1,5 +1,6 @@ """Support for monitoring emoncms feeds.""" from datetime import timedelta +from http import HTTPStatus import logging import requests @@ -20,7 +21,6 @@ from homeassistant.const import ( CONF_VALUE_TEMPLATE, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, - HTTP_OK, POWER_WATT, STATE_UNKNOWN, ) @@ -256,7 +256,7 @@ class EmonCmsData: _LOGGER.error(exception) return else: - if req.status_code == HTTP_OK: + if req.status_code == HTTPStatus.OK: self.data = req.json() else: _LOGGER.error( diff --git a/homeassistant/components/emoncms_history/__init__.py b/homeassistant/components/emoncms_history/__init__.py index 85b48c55755..5cb639de67c 100644 --- a/homeassistant/components/emoncms_history/__init__.py +++ b/homeassistant/components/emoncms_history/__init__.py @@ -1,5 +1,6 @@ """Support for sending data to Emoncms.""" from datetime import timedelta +from http import HTTPStatus import logging import requests @@ -10,7 +11,6 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, CONF_URL, CONF_WHITELIST, - HTTP_OK, STATE_UNAVAILABLE, STATE_UNKNOWN, ) @@ -59,7 +59,7 @@ def setup(hass, config): _LOGGER.error("Error saving data '%s' to '%s'", payload, fullurl) else: - if req.status_code != HTTP_OK: + if req.status_code != HTTPStatus.OK: _LOGGER.error( "Error saving data %s to %s (http status code = %d)", payload, diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index cec59742992..d7b1407642d 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -5,6 +5,7 @@ Such systems include evohome, Round Thermostat, and others. from __future__ import annotations from datetime import datetime as dt, timedelta +from http import HTTPStatus import logging import re from typing import Any @@ -19,8 +20,6 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME, - HTTP_SERVICE_UNAVAILABLE, - HTTP_TOO_MANY_REQUESTS, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, callback @@ -158,13 +157,13 @@ def _handle_exception(err) -> bool: ) except aiohttp.ClientResponseError: - if err.status == HTTP_SERVICE_UNAVAILABLE: + if err.status == HTTPStatus.SERVICE_UNAVAILABLE: _LOGGER.warning( "The vendor says their server is currently unavailable. " "Check the vendor's service status page" ) - elif err.status == HTTP_TOO_MANY_REQUESTS: + elif err.status == HTTPStatus.TOO_MANY_REQUESTS: _LOGGER.warning( "The vendor's API rate limit has been exceeded. " "If this message persists, consider increasing the %s", diff --git a/tests/components/demo/test_media_player.py b/tests/components/demo/test_media_player.py index 9cc01d18b03..4a40795613f 100644 --- a/tests/components/demo/test_media_player.py +++ b/tests/components/demo/test_media_player.py @@ -1,4 +1,5 @@ """The tests for the Demo Media player platform.""" +from http import HTTPStatus from unittest.mock import patch import pytest @@ -460,7 +461,7 @@ async def test_media_image_proxy(hass, hass_client): assert state.state == STATE_PLAYING client = await hass_client() req = await client.get(state.attributes.get(ATTR_ENTITY_PICTURE)) - assert req.status == 200 + assert req.status == HTTPStatus.OK assert await req.text() == fake_picture_data diff --git a/tests/components/demo/test_stt.py b/tests/components/demo/test_stt.py index f749d6288a7..eced954a837 100644 --- a/tests/components/demo/test_stt.py +++ b/tests/components/demo/test_stt.py @@ -1,4 +1,6 @@ """The tests for the demo stt component.""" +from http import HTTPStatus + import pytest from homeassistant.components import stt @@ -19,7 +21,7 @@ async def test_demo_settings(hass_client): response = await client.get("/api/stt/demo") response_data = await response.json() - assert response.status == 200 + assert response.status == HTTPStatus.OK assert response_data == { "languages": ["en", "de"], "bit_rates": [16], @@ -35,7 +37,7 @@ async def test_demo_speech_no_metadata(hass_client): client = await hass_client() response = await client.post("/api/stt/demo", data=b"Test") - assert response.status == 400 + assert response.status == HTTPStatus.BAD_REQUEST async def test_demo_speech_wrong_metadata(hass_client): @@ -49,7 +51,7 @@ async def test_demo_speech_wrong_metadata(hass_client): }, data=b"Test", ) - assert response.status == 415 + assert response.status == HTTPStatus.UNSUPPORTED_MEDIA_TYPE async def test_demo_speech(hass_client): @@ -65,5 +67,5 @@ async def test_demo_speech(hass_client): ) response_data = await response.json() - assert response.status == 200 + assert response.status == HTTPStatus.OK assert response_data == {"text": "Turn the Kitchen Lights on", "result": "success"} diff --git a/tests/components/dialogflow/test_init.py b/tests/components/dialogflow/test_init.py index 1f5b5bccfa9..5c4ef3e99f9 100644 --- a/tests/components/dialogflow/test_init.py +++ b/tests/components/dialogflow/test_init.py @@ -1,5 +1,6 @@ """The tests for the Dialogflow component.""" import copy +from http import HTTPStatus import json import pytest @@ -171,7 +172,7 @@ async def test_intent_action_incomplete_v1(fixture): response = await mock_client.post( f"/api/webhook/{webhook_id}", data=json.dumps(data) ) - assert response.status == 200 + assert response.status == HTTPStatus.OK assert await response.text() == "" @@ -184,7 +185,7 @@ async def test_intent_action_incomplete_v2(fixture): response = await mock_client.post( f"/api/webhook/{webhook_id}", data=json.dumps(data) ) - assert response.status == 200 + assert response.status == HTTPStatus.OK assert await response.text() == "" @@ -226,7 +227,7 @@ async def test_intent_slot_filling_v1(fixture): response = await mock_client.post( f"/api/webhook/{webhook_id}", data=json.dumps(data) ) - assert response.status == 200 + assert response.status == HTTPStatus.OK assert await response.text() == "" @@ -237,7 +238,7 @@ async def test_intent_request_with_parameters_v1(fixture): response = await mock_client.post( f"/api/webhook/{webhook_id}", data=json.dumps(data) ) - assert response.status == 200 + assert response.status == HTTPStatus.OK text = (await response.json()).get("speech") assert text == "You told us your sign is virgo." @@ -249,7 +250,7 @@ async def test_intent_request_with_parameters_v2(fixture): response = await mock_client.post( f"/api/webhook/{webhook_id}", data=json.dumps(data) ) - assert response.status == 200 + assert response.status == HTTPStatus.OK text = (await response.json()).get("fulfillmentText") assert text == "You told us your sign is virgo." @@ -262,7 +263,7 @@ async def test_intent_request_with_parameters_but_empty_v1(fixture): response = await mock_client.post( f"/api/webhook/{webhook_id}", data=json.dumps(data) ) - assert response.status == 200 + assert response.status == HTTPStatus.OK text = (await response.json()).get("speech") assert text == "You told us your sign is ." @@ -275,7 +276,7 @@ async def test_intent_request_with_parameters_but_empty_v2(fixture): response = await mock_client.post( f"/api/webhook/{webhook_id}", data=json.dumps(data) ) - assert response.status == 200 + assert response.status == HTTPStatus.OK text = (await response.json()).get("fulfillmentText") assert text == "You told us your sign is ." @@ -294,7 +295,7 @@ async def test_intent_request_without_slots_v1(hass, fixture): response = await mock_client.post( f"/api/webhook/{webhook_id}", data=json.dumps(data) ) - assert response.status == 200 + assert response.status == HTTPStatus.OK text = (await response.json()).get("speech") assert text == "Anne Therese is at unknown and Paulus is at unknown" @@ -305,7 +306,7 @@ async def test_intent_request_without_slots_v1(hass, fixture): response = await mock_client.post( f"/api/webhook/{webhook_id}", data=json.dumps(data) ) - assert response.status == 200 + assert response.status == HTTPStatus.OK text = (await response.json()).get("speech") assert text == "You are both home, you silly" @@ -324,7 +325,7 @@ async def test_intent_request_without_slots_v2(hass, fixture): response = await mock_client.post( f"/api/webhook/{webhook_id}", data=json.dumps(data) ) - assert response.status == 200 + assert response.status == HTTPStatus.OK text = (await response.json()).get("fulfillmentText") assert text == "Anne Therese is at unknown and Paulus is at unknown" @@ -335,7 +336,7 @@ async def test_intent_request_without_slots_v2(hass, fixture): response = await mock_client.post( f"/api/webhook/{webhook_id}", data=json.dumps(data) ) - assert response.status == 200 + assert response.status == HTTPStatus.OK text = (await response.json()).get("fulfillmentText") assert text == "You are both home, you silly" @@ -353,7 +354,7 @@ async def test_intent_request_calling_service_v1(fixture, calls): response = await mock_client.post( f"/api/webhook/{webhook_id}", data=json.dumps(data) ) - assert response.status == 200 + assert response.status == HTTPStatus.OK assert len(calls) == call_count + 1 call = calls[-1] assert call.domain == "test" @@ -375,7 +376,7 @@ async def test_intent_request_calling_service_v2(fixture, calls): response = await mock_client.post( f"/api/webhook/{webhook_id}", data=json.dumps(data) ) - assert response.status == 200 + assert response.status == HTTPStatus.OK assert len(calls) == call_count + 1 call = calls[-1] assert call.domain == "test" @@ -393,7 +394,7 @@ async def test_intent_with_no_action_v1(fixture): response = await mock_client.post( f"/api/webhook/{webhook_id}", data=json.dumps(data) ) - assert response.status == 200 + assert response.status == HTTPStatus.OK text = (await response.json()).get("speech") assert text == "You have not defined an action in your Dialogflow intent." @@ -407,7 +408,7 @@ async def test_intent_with_no_action_v2(fixture): response = await mock_client.post( f"/api/webhook/{webhook_id}", data=json.dumps(data) ) - assert response.status == 200 + assert response.status == HTTPStatus.OK text = (await response.json()).get("fulfillmentText") assert text == "You have not defined an action in your Dialogflow intent." @@ -420,7 +421,7 @@ async def test_intent_with_unknown_action_v1(fixture): response = await mock_client.post( f"/api/webhook/{webhook_id}", data=json.dumps(data) ) - assert response.status == 200 + assert response.status == HTTPStatus.OK text = (await response.json()).get("speech") assert text == "This intent is not yet configured within Home Assistant." @@ -433,6 +434,6 @@ async def test_intent_with_unknown_action_v2(fixture): response = await mock_client.post( f"/api/webhook/{webhook_id}", data=json.dumps(data) ) - assert response.status == 200 + assert response.status == HTTPStatus.OK text = (await response.json()).get("fulfillmentText") assert text == "This intent is not yet configured within Home Assistant." diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index ec466197995..195c086adda 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -1,4 +1,5 @@ """The test for the Ecobee thermostat module.""" +from http import HTTPStatus from unittest import mock import pytest @@ -78,7 +79,7 @@ async def test_name(thermostat): async def test_current_temperature(ecobee_fixture, thermostat): """Test current temperature.""" assert thermostat.current_temperature == 30 - ecobee_fixture["runtime"]["actualTemperature"] = const.HTTP_NOT_FOUND + ecobee_fixture["runtime"]["actualTemperature"] = HTTPStatus.NOT_FOUND assert thermostat.current_temperature == 40.4 diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index e4d422f9802..19a31b4566b 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -1,6 +1,7 @@ """The tests for the emulated Hue component.""" import asyncio from datetime import timedelta +from http import HTTPStatus from ipaddress import ip_address import json from unittest.mock import patch @@ -43,9 +44,6 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONTENT_TYPE_JSON, - HTTP_NOT_FOUND, - HTTP_OK, - HTTP_UNAUTHORIZED, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, @@ -263,7 +261,7 @@ async def test_discover_lights(hue_client): """Test the discovery of lights.""" result = await hue_client.get("/api/username/lights") - assert result.status == HTTP_OK + assert result.status == HTTPStatus.OK assert CONTENT_TYPE_JSON in result.headers["content-type"] result_json = await result.json() @@ -296,7 +294,7 @@ async def test_discover_lights(hue_client): async def test_light_without_brightness_supported(hass_hue, hue_client): """Test that light without brightness is supported.""" light_without_brightness_json = await perform_get_light_state( - hue_client, "light.no_brightness", HTTP_OK + hue_client, "light.no_brightness", HTTPStatus.OK ) assert light_without_brightness_json["state"][HUE_API_STATE_ON] is True @@ -329,7 +327,7 @@ async def test_lights_all_dimmable(hass, hass_client_no_auth): HueOneLightStateView(config).register(web_app, web_app.router) client = await hass_client_no_auth() light_without_brightness_json = await perform_get_light_state( - client, "light.no_brightness", HTTP_OK + client, "light.no_brightness", HTTPStatus.OK ) assert light_without_brightness_json["state"][HUE_API_STATE_ON] is True assert light_without_brightness_json["type"] == "Dimmable light" @@ -360,7 +358,7 @@ async def test_light_without_brightness_can_be_turned_off(hass_hue, hue_client): ) no_brightness_result_json = await no_brightness_result.json() - assert no_brightness_result.status == HTTP_OK + assert no_brightness_result.status == HTTPStatus.OK assert CONTENT_TYPE_JSON in no_brightness_result.headers["content-type"] assert len(no_brightness_result_json) == 1 @@ -402,7 +400,7 @@ async def test_light_without_brightness_can_be_turned_on(hass_hue, hue_client): no_brightness_result_json = await no_brightness_result.json() - assert no_brightness_result.status == HTTP_OK + assert no_brightness_result.status == HTTPStatus.OK assert CONTENT_TYPE_JSON in no_brightness_result.headers["content-type"] assert len(no_brightness_result_json) == 1 @@ -430,7 +428,7 @@ async def test_reachable_for_state(hass_hue, hue_client, state, is_reachable): hass_hue.states.async_set(entity_id, state) - state_json = await perform_get_light_state(hue_client, entity_id, HTTP_OK) + state_json = await perform_get_light_state(hue_client, entity_id, HTTPStatus.OK) assert state_json["state"]["reachable"] == is_reachable, state_json @@ -439,7 +437,7 @@ async def test_discover_full_state(hue_client): """Test the discovery of full state.""" result = await hue_client.get(f"/api/{HUE_API_USERNAME}") - assert result.status == HTTP_OK + assert result.status == HTTPStatus.OK assert CONTENT_TYPE_JSON in result.headers["content-type"] result_json = await result.json() @@ -489,7 +487,7 @@ async def test_discover_config(hue_client): """Test the discovery of configuration.""" result = await hue_client.get(f"/api/{HUE_API_USERNAME}/config") - assert result.status == 200 + assert result.status == HTTPStatus.OK assert CONTENT_TYPE_JSON in result.headers["content-type"] config_json = await result.json() @@ -526,7 +524,7 @@ async def test_discover_config(hue_client): # Test without username result = await hue_client.get("/api/config") - assert result.status == 200 + assert result.status == HTTPStatus.OK assert CONTENT_TYPE_JSON in result.headers["content-type"] config_json = await result.json() @@ -535,7 +533,7 @@ async def test_discover_config(hue_client): # Test with wrong username username result = await hue_client.get("/api/wronguser/config") - assert result.status == 200 + assert result.status == HTTPStatus.OK assert CONTENT_TYPE_JSON in result.headers["content-type"] config_json = await result.json() @@ -557,7 +555,7 @@ async def test_get_light_state(hass_hue, hue_client): ) office_json = await perform_get_light_state( - hue_client, "light.ceiling_lights", HTTP_OK + hue_client, "light.ceiling_lights", HTTPStatus.OK ) assert office_json["state"][HUE_API_STATE_ON] is True @@ -568,7 +566,7 @@ async def test_get_light_state(hass_hue, hue_client): # Check all lights view result = await hue_client.get("/api/username/lights") - assert result.status == HTTP_OK + assert result.status == HTTPStatus.OK assert CONTENT_TYPE_JSON in result.headers["content-type"] result_json = await result.json() @@ -590,7 +588,7 @@ async def test_get_light_state(hass_hue, hue_client): ) office_json = await perform_get_light_state( - hue_client, "light.ceiling_lights", HTTP_OK + hue_client, "light.ceiling_lights", HTTPStatus.OK ) assert office_json["state"][HUE_API_STATE_ON] is False @@ -599,10 +597,14 @@ async def test_get_light_state(hass_hue, hue_client): assert office_json["state"][HUE_API_STATE_SAT] == 0 # Make sure bedroom light isn't accessible - await perform_get_light_state(hue_client, "light.bed_light", HTTP_UNAUTHORIZED) + await perform_get_light_state( + hue_client, "light.bed_light", HTTPStatus.UNAUTHORIZED + ) # Make sure kitchen light isn't accessible - await perform_get_light_state(hue_client, "light.kitchen_lights", HTTP_UNAUTHORIZED) + await perform_get_light_state( + hue_client, "light.kitchen_lights", HTTPStatus.UNAUTHORIZED + ) async def test_put_light_state(hass, hass_hue, hue_client): @@ -653,7 +655,7 @@ async def test_put_light_state(hass, hass_hue, hue_client): # go through api to get the state back ceiling_json = await perform_get_light_state( - hue_client, "light.ceiling_lights", HTTP_OK + hue_client, "light.ceiling_lights", HTTPStatus.OK ) assert ceiling_json["state"][HUE_API_STATE_BRI] == 123 assert ceiling_json["state"][HUE_API_STATE_HUE] == 4369 @@ -672,7 +674,7 @@ async def test_put_light_state(hass, hass_hue, hue_client): # go through api to get the state back ceiling_json = await perform_get_light_state( - hue_client, "light.ceiling_lights", HTTP_OK + hue_client, "light.ceiling_lights", HTTPStatus.OK ) assert ceiling_json["state"][HUE_API_STATE_BRI] == 254 assert ceiling_json["state"][HUE_API_STATE_HUE] == 4369 @@ -690,7 +692,7 @@ async def test_put_light_state(hass, hass_hue, hue_client): # go through api to get the state back ceiling_json = await perform_get_light_state( - hue_client, "light.ceiling_lights", HTTP_OK + hue_client, "light.ceiling_lights", HTTPStatus.OK ) assert ceiling_json["state"][HUE_API_STATE_BRI] == 100 assert hass.states.get("light.ceiling_lights").attributes[light.ATTR_XY_COLOR] == ( @@ -704,7 +706,7 @@ async def test_put_light_state(hass, hass_hue, hue_client): ceiling_result_json = await ceiling_result.json() - assert ceiling_result.status == HTTP_OK + assert ceiling_result.status == HTTPStatus.OK assert CONTENT_TYPE_JSON in ceiling_result.headers["content-type"] assert len(ceiling_result_json) == 1 @@ -713,7 +715,7 @@ async def test_put_light_state(hass, hass_hue, hue_client): ceiling_lights = hass_hue.states.get("light.ceiling_lights") assert ceiling_lights.state == STATE_OFF ceiling_json = await perform_get_light_state( - hue_client, "light.ceiling_lights", HTTP_OK + hue_client, "light.ceiling_lights", HTTPStatus.OK ) # Removed assert HUE_API_STATE_BRI == 0 as Hue API states bri must be 1..254 assert ceiling_json["state"][HUE_API_STATE_HUE] == 0 @@ -723,13 +725,13 @@ async def test_put_light_state(hass, hass_hue, hue_client): bedroom_result = await perform_put_light_state( hass_hue, hue_client, "light.bed_light", True ) - assert bedroom_result.status == HTTP_UNAUTHORIZED + assert bedroom_result.status == HTTPStatus.UNAUTHORIZED # Make sure we can't change the kitchen light state kitchen_result = await perform_put_light_state( hass_hue, hue_client, "light.kitchen_lights", True ) - assert kitchen_result.status == HTTP_UNAUTHORIZED + assert kitchen_result.status == HTTPStatus.UNAUTHORIZED # Turn the ceiling lights on first and color temp. await hass_hue.services.async_call( @@ -794,7 +796,7 @@ async def test_put_light_state_script(hass, hass_hue, hue_client): script_result_json = await script_result.json() - assert script_result.status == HTTP_OK + assert script_result.status == HTTPStatus.OK assert len(script_result_json) == 2 kitchen_light = hass_hue.states.get("light.kitchen_lights") @@ -817,7 +819,7 @@ async def test_put_light_state_climate_set_temperature(hass_hue, hue_client): hvac_result_json = await hvac_result.json() - assert hvac_result.status == HTTP_OK + assert hvac_result.status == HTTPStatus.OK assert len(hvac_result_json) == 2 hvac = hass_hue.states.get("climate.hvac") @@ -828,7 +830,7 @@ async def test_put_light_state_climate_set_temperature(hass_hue, hue_client): ecobee_result = await perform_put_light_state( hass_hue, hue_client, "climate.ecobee", True ) - assert ecobee_result.status == HTTP_UNAUTHORIZED + assert ecobee_result.status == HTTPStatus.UNAUTHORIZED async def test_put_light_state_humidifier_set_humidity(hass_hue, hue_client): @@ -850,7 +852,7 @@ async def test_put_light_state_humidifier_set_humidity(hass_hue, hue_client): humidifier_result_json = await humidifier_result.json() - assert humidifier_result.status == HTTP_OK + assert humidifier_result.status == HTTPStatus.OK assert len(humidifier_result_json) == 2 hvac = hass_hue.states.get("humidifier.humidifier") @@ -861,7 +863,7 @@ async def test_put_light_state_humidifier_set_humidity(hass_hue, hue_client): hygrostat_result = await perform_put_light_state( hass_hue, hue_client, "humidifier.hygrostat", True ) - assert hygrostat_result.status == HTTP_UNAUTHORIZED + assert hygrostat_result.status == HTTPStatus.UNAUTHORIZED async def test_put_light_state_media_player(hass_hue, hue_client): @@ -884,7 +886,7 @@ async def test_put_light_state_media_player(hass_hue, hue_client): mp_result_json = await mp_result.json() - assert mp_result.status == HTTP_OK + assert mp_result.status == HTTPStatus.OK assert len(mp_result_json) == 2 walkman = hass_hue.states.get("media_player.walkman") @@ -917,7 +919,7 @@ async def test_open_cover_without_position(hass_hue, hue_client): # Go through the API to turn it on cover_result = await perform_put_light_state(hass_hue, hue_client, cover_id, True) - assert cover_result.status == HTTP_OK + assert cover_result.status == HTTPStatus.OK assert CONTENT_TYPE_JSON in cover_result.headers["content-type"] for _ in range(11): @@ -937,7 +939,7 @@ async def test_open_cover_without_position(hass_hue, hue_client): # Go through the API to turn it off cover_result = await perform_put_light_state(hass_hue, hue_client, cover_id, False) - assert cover_result.status == HTTP_OK + assert cover_result.status == HTTPStatus.OK assert CONTENT_TYPE_JSON in cover_result.headers["content-type"] for _ in range(11): @@ -986,7 +988,7 @@ async def test_set_position_cover(hass_hue, hue_client): hass_hue, hue_client, cover_id, False, brightness ) - assert cover_result.status == HTTP_OK + assert cover_result.status == HTTPStatus.OK assert CONTENT_TYPE_JSON in cover_result.headers["content-type"] cover_result_json = await cover_result.json() @@ -1026,7 +1028,7 @@ async def test_put_light_state_fan(hass_hue, hue_client): fan_result_json = await fan_result.json() - assert fan_result.status == HTTP_OK + assert fan_result.status == HTTPStatus.OK assert len(fan_result_json) == 2 living_room_fan = hass_hue.states.get("fan.living_room_fan") @@ -1056,7 +1058,7 @@ async def test_put_light_state_fan(hass_hue, hue_client): with patch.object(hue_api, "STATE_CACHED_TIMEOUT", 0.000001): await asyncio.sleep(0.000001) fan_json = await perform_get_light_state( - hue_client, "fan.living_room_fan", HTTP_OK + hue_client, "fan.living_room_fan", HTTPStatus.OK ) assert round(fan_json["state"][HUE_API_STATE_BRI] * 100 / 254) == 33 @@ -1074,7 +1076,7 @@ async def test_put_light_state_fan(hass_hue, hue_client): with patch.object(hue_api, "STATE_CACHED_TIMEOUT", 0.000001): await asyncio.sleep(0.000001) fan_json = await perform_get_light_state( - hue_client, "fan.living_room_fan", HTTP_OK + hue_client, "fan.living_room_fan", HTTPStatus.OK ) assert ( round(fan_json["state"][HUE_API_STATE_BRI] * 100 / 254) == 67 @@ -1094,7 +1096,7 @@ async def test_put_light_state_fan(hass_hue, hue_client): with patch.object(hue_api, "STATE_CACHED_TIMEOUT", 0.000001): await asyncio.sleep(0.000001) fan_json = await perform_get_light_state( - hue_client, "fan.living_room_fan", HTTP_OK + hue_client, "fan.living_room_fan", HTTPStatus.OK ) assert round(fan_json["state"][HUE_API_STATE_BRI] * 100 / 254) == 100 @@ -1116,18 +1118,18 @@ async def test_put_with_form_urlencoded_content_type(hass_hue, hue_client): data=data, ) - assert result.status == 400 + assert result.status == HTTPStatus.BAD_REQUEST async def test_entity_not_found(hue_client): """Test for entity which are not found.""" result = await hue_client.get("/api/username/lights/98") - assert result.status == HTTP_NOT_FOUND + assert result.status == HTTPStatus.NOT_FOUND result = await hue_client.put("/api/username/lights/98/state") - assert result.status == HTTP_NOT_FOUND + assert result.status == HTTPStatus.NOT_FOUND async def test_allowed_methods(hue_client): @@ -1136,17 +1138,17 @@ async def test_allowed_methods(hue_client): "/api/username/lights/ENTITY_NUMBERS_BY_ID[light.ceiling_lights]/state" ) - assert result.status == 405 + assert result.status == HTTPStatus.METHOD_NOT_ALLOWED result = await hue_client.put( "/api/username/lights/ENTITY_NUMBERS_BY_ID[light.ceiling_lights]" ) - assert result.status == 405 + assert result.status == HTTPStatus.METHOD_NOT_ALLOWED result = await hue_client.put("/api/username/lights") - assert result.status == 405 + assert result.status == HTTPStatus.METHOD_NOT_ALLOWED async def test_proper_put_state_request(hue_client): @@ -1159,7 +1161,7 @@ async def test_proper_put_state_request(hue_client): data=json.dumps({HUE_API_STATE_ON: 1234}), ) - assert result.status == 400 + assert result.status == HTTPStatus.BAD_REQUEST # Test proper brightness value parsing result = await hue_client.put( @@ -1169,7 +1171,7 @@ async def test_proper_put_state_request(hue_client): data=json.dumps({HUE_API_STATE_ON: True, HUE_API_STATE_BRI: "Hello world!"}), ) - assert result.status == 400 + assert result.status == HTTPStatus.BAD_REQUEST async def test_get_empty_groups_state(hue_client): @@ -1177,7 +1179,7 @@ async def test_get_empty_groups_state(hue_client): # Test proper on value parsing result = await hue_client.get("/api/username/groups") - assert result.status == HTTP_OK + assert result.status == HTTPStatus.OK result_json = await result.json() @@ -1205,7 +1207,7 @@ async def perform_put_test_on_ceiling_lights( hass_hue, hue_client, "light.ceiling_lights", True, 56, content_type ) - assert office_result.status == HTTP_OK + assert office_result.status == HTTPStatus.OK assert CONTENT_TYPE_JSON in office_result.headers["content-type"] office_result_json = await office_result.json() @@ -1224,7 +1226,7 @@ async def perform_get_light_state_by_number(client, entity_number, expected_stat assert result.status == expected_status - if expected_status == HTTP_OK: + if expected_status == HTTPStatus.OK: assert CONTENT_TYPE_JSON in result.headers["content-type"] return await result.json() @@ -1305,15 +1307,15 @@ async def test_external_ip_blocked(hue_client): ): for getUrl in getUrls: result = await hue_client.get(getUrl) - assert result.status == HTTP_UNAUTHORIZED + assert result.status == HTTPStatus.UNAUTHORIZED for postUrl in postUrls: result = await hue_client.post(postUrl) - assert result.status == HTTP_UNAUTHORIZED + assert result.status == HTTPStatus.UNAUTHORIZED for putUrl in putUrls: result = await hue_client.put(putUrl) - assert result.status == HTTP_UNAUTHORIZED + assert result.status == HTTPStatus.UNAUTHORIZED async def test_unauthorized_user_blocked(hue_client): @@ -1323,7 +1325,7 @@ async def test_unauthorized_user_blocked(hue_client): ] for getUrl in getUrls: result = await hue_client.get(getUrl) - assert result.status == HTTP_OK + assert result.status == HTTPStatus.OK result_json = await result.json() assert result_json[0]["error"]["description"] == "unauthorized user" @@ -1371,7 +1373,7 @@ async def test_put_then_get_cached_properly(hass, hass_hue, hue_client): # go through api to get the state back, the value returned should match those set in the last PUT request. ceiling_json = await perform_get_light_state( - hue_client, "light.ceiling_lights", HTTP_OK + hue_client, "light.ceiling_lights", HTTPStatus.OK ) assert ceiling_json["state"][HUE_API_STATE_HUE] == 4369 @@ -1388,7 +1390,7 @@ async def test_put_then_get_cached_properly(hass, hass_hue, hue_client): # go through api to get the state back ceiling_json = await perform_get_light_state( - hue_client, "light.ceiling_lights", HTTP_OK + hue_client, "light.ceiling_lights", HTTPStatus.OK ) # Now it should be the real value as the state of the entity has changed to OFF. @@ -1430,7 +1432,7 @@ async def test_put_then_get_cached_properly(hass, hass_hue, hue_client): # go through api to get the state back, the value returned should match those set in the last PUT request. ceiling_json = await perform_get_light_state( - hue_client, "light.ceiling_lights", HTTP_OK + hue_client, "light.ceiling_lights", HTTPStatus.OK ) # With no wait, we must be reading what we set via the PUT call. @@ -1443,7 +1445,7 @@ async def test_put_then_get_cached_properly(hass, hass_hue, hue_client): # go through api to get the state back, the value returned should now match the actual values. ceiling_json = await perform_get_light_state( - hue_client, "light.ceiling_lights", HTTP_OK + hue_client, "light.ceiling_lights", HTTPStatus.OK ) # Once we're after the cached duration, we should see the real value. @@ -1496,7 +1498,7 @@ async def test_put_than_get_when_service_call_fails(hass, hass_hue, hue_client): # go through api to get the state back, the value returned should NOT match those set in the last PUT request # as the waiting to check the state change timed out ceiling_json = await perform_get_light_state( - hue_client, "light.ceiling_lights", HTTP_OK + hue_client, "light.ceiling_lights", HTTPStatus.OK ) assert ceiling_json["state"][HUE_API_STATE_ON] is False @@ -1506,7 +1508,7 @@ async def test_get_invalid_entity(hass, hass_hue, hue_client): """Test the setting of light states and an immediate readback reads the same values.""" # Check that we get an error with an invalid entity number. - await perform_get_light_state_by_number(hue_client, 999, HTTP_NOT_FOUND) + await perform_get_light_state_by_number(hue_client, 999, HTTPStatus.NOT_FOUND) async def test_put_light_state_scene(hass, hass_hue, hue_client): @@ -1524,7 +1526,7 @@ async def test_put_light_state_scene(hass, hass_hue, hue_client): ) scene_result_json = await scene_result.json() - assert scene_result.status == HTTP_OK + assert scene_result.status == HTTPStatus.OK assert len(scene_result_json) == 1 assert hass_hue.states.get("light.kitchen_lights").state == STATE_ON diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index d918b378614..ec04ee7e19c 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -1,4 +1,5 @@ """The tests for the emulated Hue component.""" +from http import HTTPStatus import json import unittest @@ -9,7 +10,7 @@ import pytest from homeassistant import setup from homeassistant.components import emulated_hue from homeassistant.components.emulated_hue import upnp -from homeassistant.const import CONTENT_TYPE_JSON, HTTP_OK +from homeassistant.const import CONTENT_TYPE_JSON from tests.common import get_test_instance_port @@ -149,7 +150,7 @@ async def test_description_xml(hass, hue_client): client = await hue_client() result = await client.get("/description.xml", timeout=5) - assert result.status == HTTP_OK + assert result.status == HTTPStatus.OK assert "text/xml" in result.headers["content-type"] try: @@ -168,7 +169,7 @@ async def test_create_username(hass, hue_client): result = await client.post("/api", data=json.dumps(request_json), timeout=5) - assert result.status == HTTP_OK + assert result.status == HTTPStatus.OK assert CONTENT_TYPE_JSON in result.headers["content-type"] resp_json = await result.json() @@ -188,7 +189,7 @@ async def test_unauthorized_view(hass, hue_client): "/api/unauthorized", data=json.dumps(request_json), timeout=5 ) - assert result.status == HTTP_OK + assert result.status == HTTPStatus.OK assert CONTENT_TYPE_JSON in result.headers["content-type"] resp_json = await result.json() @@ -212,4 +213,4 @@ async def test_valid_username_request(hass, hue_client): result = await client.post("/api", data=json.dumps(request_json), timeout=5) - assert result.status == 400 + assert result.status == HTTPStatus.BAD_REQUEST From 59fe30e589982a4f101e140eaf3e0d12fb668bbd Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 22 Oct 2021 10:35:39 -0400 Subject: [PATCH 0674/1038] Use DeviceInfo class - A (#57859) --- homeassistant/components/abode/__init__.py | 16 +++++----- .../components/accuweather/sensor.py | 13 +++++---- homeassistant/components/acmeda/base.py | 2 +- homeassistant/components/adguard/__init__.py | 14 ++++----- .../components/advantage_air/entity.py | 15 +++++----- .../agent_dvr/alarm_control_panel.py | 13 +++++---- homeassistant/components/agent_dvr/camera.py | 15 +++++----- homeassistant/components/airly/sensor.py | 15 +++++----- homeassistant/components/airtouch4/climate.py | 29 ++++++++++--------- homeassistant/components/airvisual/sensor.py | 14 ++++----- homeassistant/components/ambee/const.py | 1 - homeassistant/components/ambee/sensor.py | 16 +++++----- .../components/ambiclimate/climate.py | 11 +++---- .../components/ambient_station/__init__.py | 12 ++++---- homeassistant/components/apple_tv/__init__.py | 6 ++-- .../components/arcam_fmj/media_player.py | 13 +++++---- homeassistant/components/atag/__init__.py | 14 ++++----- homeassistant/components/august/entity.py | 18 ++++++------ homeassistant/components/aurora/__init__.py | 28 +++++++----------- homeassistant/components/aurora/const.py | 1 - homeassistant/components/axis/axis_base.py | 7 +++-- .../components/azure_devops/__init__.py | 18 ++++-------- 22 files changed, 140 insertions(+), 151 deletions(-) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index be474661eac..3407d387169 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -20,7 +20,7 @@ from homeassistant.const import ( from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ATTRIBUTION, DEFAULT_CACHEDB, DOMAIN, LOGGER @@ -322,14 +322,14 @@ class AbodeDevice(AbodeEntity): } @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device registry information for this entity.""" - return { - "identifiers": {(DOMAIN, self._device.device_id)}, - "manufacturer": "Abode", - "name": self._device.name, - "device_type": self._device.type, - } + return DeviceInfo( + identifiers={(DOMAIN, self._device.device_id)}, + manufacturer="Abode", + model=self._device.type, + name=self._device.name, + ) def _update_callback(self, device): """Update the device state.""" diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 77d604a8f6c..3159293a4cc 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -7,6 +7,7 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, DEVICE_CLASS_TEMPERATURE from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -93,12 +94,12 @@ class AccuWeatherSensor(CoordinatorEntity, SensorEntity): else: self._unit_system = API_IMPERIAL self._attr_native_unit_of_measurement = description.unit_imperial - self._attr_device_info = { - "identifiers": {(DOMAIN, coordinator.location_key)}, - "name": NAME, - "manufacturer": MANUFACTURER, - "entry_type": "service", - } + self._attr_device_info = DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, coordinator.location_key)}, + manufacturer=MANUFACTURER, + name=NAME, + ) self.forecast_day = forecast_day @property diff --git a/homeassistant/components/acmeda/base.py b/homeassistant/components/acmeda/base.py index df835950380..3338bf9667d 100644 --- a/homeassistant/components/acmeda/base.py +++ b/homeassistant/components/acmeda/base.py @@ -81,7 +81,7 @@ class AcmedaBase(entity.Entity): """Return the device info.""" return entity.DeviceInfo( identifiers={(DOMAIN, self.unique_id)}, - name=self.roller.name, manufacturer="Rollease Acmeda", + name=self.roller.name, via_device=(DOMAIN, self.roller.hub.id), ) diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index eedc1fe4b03..d76d32ce066 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -196,14 +196,14 @@ class AdGuardHomeDeviceEntity(AdGuardHomeEntity): @property def device_info(self) -> DeviceInfo: """Return device information about this AdGuard Home instance.""" - return { - "identifiers": { + return DeviceInfo( + entry_type="service", + identifiers={ (DOMAIN, self.adguard.host, self.adguard.port, self.adguard.base_path) # type: ignore }, - "name": "AdGuard Home", - "manufacturer": "AdGuard Team", - "sw_version": self.hass.data[DOMAIN][self._entry.entry_id].get( + manufacturer="AdGuard Team", + name="AdGuard Home", + sw_version=self.hass.data[DOMAIN][self._entry.entry_id].get( DATA_ADGUARD_VERSION ), - "entry_type": "service", - } + ) diff --git a/homeassistant/components/advantage_air/entity.py b/homeassistant/components/advantage_air/entity.py index ffb75e78c01..9514cc7915b 100644 --- a/homeassistant/components/advantage_air/entity.py +++ b/homeassistant/components/advantage_air/entity.py @@ -1,5 +1,6 @@ """Advantage Air parent entity class.""" +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -14,13 +15,13 @@ class AdvantageAirEntity(CoordinatorEntity): self.async_change = instance["async_change"] self.ac_key = ac_key self.zone_key = zone_key - self._attr_device_info = { - "identifiers": {(DOMAIN, self.coordinator.data["system"]["rid"])}, - "name": self.coordinator.data["system"]["name"], - "manufacturer": "Advantage Air", - "model": self.coordinator.data["system"]["sysType"], - "sw_version": self.coordinator.data["system"]["myAppRev"], - } + self._attr_device_info = DeviceInfo( + identifiers={(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 _ac(self): diff --git a/homeassistant/components/agent_dvr/alarm_control_panel.py b/homeassistant/components/agent_dvr/alarm_control_panel.py index 8f139af8963..572f80a138f 100644 --- a/homeassistant/components/agent_dvr/alarm_control_panel.py +++ b/homeassistant/components/agent_dvr/alarm_control_panel.py @@ -11,6 +11,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, ) +from homeassistant.helpers.entity import DeviceInfo from .const import CONNECTION, DOMAIN as AGENT_DOMAIN @@ -45,12 +46,12 @@ class AgentBaseStation(AlarmControlPanelEntity): self._client = client self._attr_name = f"{client.name} {CONST_ALARM_CONTROL_PANEL_NAME}" self._attr_unique_id = f"{client.unique}_CP" - self._attr_device_info = { - "identifiers": {(AGENT_DOMAIN, client.unique)}, - "manufacturer": "Agent", - "model": CONST_ALARM_CONTROL_PANEL_NAME, - "sw_version": client.version, - } + self._attr_device_info = DeviceInfo( + identifiers={(AGENT_DOMAIN, client.unique)}, + manufacturer="Agent", + model=CONST_ALARM_CONTROL_PANEL_NAME, + sw_version=client.version, + ) async def async_update(self): """Update the state of the device.""" diff --git a/homeassistant/components/agent_dvr/camera.py b/homeassistant/components/agent_dvr/camera.py index 8a29428a833..474d1f08b80 100644 --- a/homeassistant/components/agent_dvr/camera.py +++ b/homeassistant/components/agent_dvr/camera.py @@ -13,6 +13,7 @@ from homeassistant.components.mjpeg.camera import ( ) from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME from homeassistant.helpers import entity_platform +from homeassistant.helpers.entity import DeviceInfo from .const import ( ATTRIBUTION, @@ -79,13 +80,13 @@ class AgentCamera(MjpegCamera): self._attr_name = f"{device.client.name} {device.name}" self._attr_unique_id = f"{device._client.unique}_{device.typeID}_{device.id}" super().__init__(device_info) - self._attr_device_info = { - "identifiers": {(AGENT_DOMAIN, self.unique_id)}, - "name": self.name, - "manufacturer": "Agent", - "model": "Camera", - "sw_version": device.client.version, - } + self._attr_device_info = DeviceInfo( + identifiers={(AGENT_DOMAIN, self.unique_id)}, + manufacturer="Agent", + model="Camera", + name=self.name, + sw_version=device.client.version, + ) async def async_update(self): """Update our state from the Agent API.""" diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index fc587f15140..a331e99497e 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -27,6 +27,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 StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -151,14 +152,12 @@ class AirlySensor(CoordinatorEntity, SensorEntity): ) -> None: """Initialize.""" super().__init__(coordinator) - self._attr_device_info = { - "identifiers": { - (DOMAIN, f"{coordinator.latitude}-{coordinator.longitude}") - }, - "name": DEFAULT_NAME, - "manufacturer": MANUFACTURER, - "entry_type": "service", - } + self._attr_device_info = DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, f"{coordinator.latitude}-{coordinator.longitude}")}, + manufacturer=MANUFACTURER, + name=DEFAULT_NAME, + ) self._attr_name = f"{name} {description.name}" self._attr_unique_id = ( f"{coordinator.latitude}-{coordinator.longitude}-{description.key}".lower() diff --git a/homeassistant/components/airtouch4/climate.py b/homeassistant/components/airtouch4/climate.py index 7202feb0527..5bac0c7c9a3 100644 --- a/homeassistant/components/airtouch4/climate.py +++ b/homeassistant/components/airtouch4/climate.py @@ -21,6 +21,7 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import callback +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -96,14 +97,14 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity): return super()._handle_coordinator_update() @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info for this device.""" - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "manufacturer": "Airtouch", - "model": "Airtouch 4", - } + return DeviceInfo( + identifiers={(DOMAIN, self.unique_id)}, + name=self.name, + manufacturer="Airtouch", + model="Airtouch 4", + ) @property def unique_id(self): @@ -211,14 +212,14 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity): return super()._handle_coordinator_update() @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info for this device.""" - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "manufacturer": "Airtouch", - "model": "Airtouch 4", - } + return DeviceInfo( + identifiers={(DOMAIN, self.unique_id)}, + manufacturer="Airtouch", + model="Airtouch 4", + name=self.name, + ) @property def unique_id(self): diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index 1dea2c2e769..08896e13557 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -319,16 +319,16 @@ class AirVisualNodeProSensor(AirVisualEntity, SensorEntity): @property def device_info(self) -> DeviceInfo: """Return device registry information for this entity.""" - return { - "identifiers": {(DOMAIN, self.coordinator.data["serial_number"])}, - "name": self.coordinator.data["settings"]["node_name"], - "manufacturer": "AirVisual", - "model": f'{self.coordinator.data["status"]["model"]}', - "sw_version": ( + return DeviceInfo( + identifiers={(DOMAIN, self.coordinator.data["serial_number"])}, + manufacturer="AirVisual", + model=f'{self.coordinator.data["status"]["model"]}', + name=self.coordinator.data["settings"]["node_name"], + sw_version=( f'Version {self.coordinator.data["status"]["system_version"]}' f'{self.coordinator.data["status"]["app_version"]}' ), - } + ) @callback def update_from_latest_data(self) -> None: diff --git a/homeassistant/components/ambee/const.py b/homeassistant/components/ambee/const.py index 3fd57c17c63..42b19a52995 100644 --- a/homeassistant/components/ambee/const.py +++ b/homeassistant/components/ambee/const.py @@ -21,7 +21,6 @@ DOMAIN: Final = "ambee" LOGGER = logging.getLogger(__package__) SCAN_INTERVAL = timedelta(hours=1) -ATTR_ENTRY_TYPE: Final = "entry_type" ENTRY_TYPE_SERVICE: Final = "service" DEVICE_CLASS_AMBEE_RISK: Final = "ambee__risk" diff --git a/homeassistant/components/ambee/sensor.py b/homeassistant/components/ambee/sensor.py index bd125ac973e..2ddd60a9168 100644 --- a/homeassistant/components/ambee/sensor.py +++ b/homeassistant/components/ambee/sensor.py @@ -7,8 +7,8 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_IDENTIFIERS, ATTR_MANUFACTURER, ATTR_NAME from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import ( @@ -16,7 +16,7 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .const import ATTR_ENTRY_TYPE, DOMAIN, ENTRY_TYPE_SERVICE, SENSORS, SERVICES +from .const import DOMAIN, ENTRY_TYPE_SERVICE, SENSORS, SERVICES async def async_setup_entry( @@ -58,12 +58,12 @@ class AmbeeSensorEntity(CoordinatorEntity, SensorEntity): self.entity_description = description self._attr_unique_id = f"{entry_id}_{service_key}_{description.key}" - self._attr_device_info = { - ATTR_IDENTIFIERS: {(DOMAIN, f"{entry_id}_{service_key}")}, - ATTR_NAME: service, - ATTR_MANUFACTURER: "Ambee", - ATTR_ENTRY_TYPE: ENTRY_TYPE_SERVICE, - } + self._attr_device_info = DeviceInfo( + entry_type=ENTRY_TYPE_SERVICE, + identifiers={(DOMAIN, f"{entry_id}_{service_key}")}, + manufacturer="Ambee", + name=service, + ) @property def native_value(self) -> StateType: diff --git a/homeassistant/components/ambiclimate/climate.py b/homeassistant/components/ambiclimate/climate.py index d30e48dbe59..67fd3adeec7 100644 --- a/homeassistant/components/ambiclimate/climate.py +++ b/homeassistant/components/ambiclimate/climate.py @@ -21,6 +21,7 @@ from homeassistant.const import ( ) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo from .const import ( ATTR_VALUE, @@ -149,11 +150,11 @@ class AmbiclimateEntity(ClimateEntity): self._store = store self._attr_unique_id = heater.device_id self._attr_name = heater.name - self._attr_device_info = { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "manufacturer": "Ambiclimate", - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.unique_id)}, + manufacturer="Ambiclimate", + name=self.name, + ) async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 4e7e555ad94..bd509722f93 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -20,7 +20,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import Entity, EntityDescription +from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription from homeassistant.helpers.event import async_call_later from .const import ( @@ -223,11 +223,11 @@ class AmbientWeatherEntity(Entity): ) -> None: """Initialize the entity.""" self._ambient = ambient - self._attr_device_info = { - "identifiers": {(DOMAIN, mac_address)}, - "name": station_name, - "manufacturer": "Ambient Weather", - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, mac_address)}, + manufacturer="Ambient Weather", + name=station_name, + ) self._attr_name = f"{station_name}_{description.name}" self._attr_unique_id = f"{mac_address}_{description.key}" self._mac_address = mac_address diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index c4efa4ca09a..dd13375080e 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -22,7 +22,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import CONF_CREDENTIALS, CONF_IDENTIFIER, CONF_START_OFF, DOMAIN @@ -91,9 +91,7 @@ class AppleTVEntity(Entity): self.manager = manager self._attr_name = name self._attr_unique_id = identifier - self._attr_device_info = { - "identifiers": {(DOMAIN, identifier)}, - } + self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, identifier)}) async def async_added_to_hass(self): """Handle when an entity is about to be added to Home Assistant.""" diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 115675dfb89..acccb91c98e 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -23,6 +23,7 @@ from homeassistant.components.media_player.const import ( from homeassistant.components.media_player.errors import BrowseError from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo from .config_flow import get_entry_client from .const import ( @@ -114,15 +115,15 @@ class ArcamFmj(MediaPlayerEntity): @property def device_info(self): """Return a device description for device registry.""" - return { - "name": self._device_name, - "identifiers": { + return DeviceInfo( + identifiers={ (DOMAIN, self._uuid), (DOMAIN, self._state.client.host, self._state.client.port), }, - "model": "Arcam FMJ AVR", - "manufacturer": "Arcam", - } + manufacturer="Arcam", + model="Arcam FMJ AVR", + name=self._device_name, + ) async def async_added_to_hass(self): """Once registered, add listener for events.""" diff --git a/homeassistant/components/atag/__init__.py b/homeassistant/components/atag/__init__.py index de785a3a317..69880da5a39 100644 --- a/homeassistant/components/atag/__init__.py +++ b/homeassistant/components/atag/__init__.py @@ -81,10 +81,10 @@ class AtagEntity(CoordinatorEntity): @property def device_info(self) -> DeviceInfo: """Return info for device registry.""" - return { - "identifiers": {(DOMAIN, self.coordinator.data.id)}, - "name": "Atag Thermostat", - "model": "Atag One", - "sw_version": self.coordinator.data.apiversion, - "manufacturer": "Atag", - } + return DeviceInfo( + identifiers={(DOMAIN, self.coordinator.data.id)}, + manufacturer="Atag", + model="Atag One", + name="Atag Thermostat", + sw_version=self.coordinator.data.apiversion, + ) diff --git a/homeassistant/components/august/entity.py b/homeassistant/components/august/entity.py index af8c858a1d4..8da7fe3d418 100644 --- a/homeassistant/components/august/entity.py +++ b/homeassistant/components/august/entity.py @@ -1,6 +1,6 @@ """Base class for August entity.""" from homeassistant.core import callback -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from . import DOMAIN from .const import MANUFACTURER @@ -18,14 +18,14 @@ class AugustEntityMixin(Entity): super().__init__() self._data = data self._device = device - self._attr_device_info = { - "identifiers": {(DOMAIN, self._device_id)}, - "name": device.device_name, - "manufacturer": MANUFACTURER, - "sw_version": self._detail.firmware_version, - "model": self._detail.model, - "suggested_area": _remove_device_types(device.device_name, DEVICE_TYPES), - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._device_id)}, + manufacturer=MANUFACTURER, + model=self._detail.model, + name=device.device_name, + sw_version=self._detail.firmware_version, + suggested_area=_remove_device_types(device.device_name, DEVICE_TYPES), + ) @property def _device_id(self): diff --git a/homeassistant/components/aurora/__init__.py b/homeassistant/components/aurora/__init__.py index 3fa5d5b39a7..1cc378983ca 100644 --- a/homeassistant/components/aurora/__init__.py +++ b/homeassistant/components/aurora/__init__.py @@ -7,17 +7,10 @@ from aiohttp import ClientError from auroranoaa import AuroraForecast from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, -) +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -25,7 +18,6 @@ from homeassistant.helpers.update_coordinator import ( ) from .const import ( - ATTR_ENTRY_TYPE, ATTRIBUTION, AURORA_API, CONF_THRESHOLD, @@ -145,12 +137,12 @@ class AuroraEntity(CoordinatorEntity): self._attr_icon = icon @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Define the device based on name.""" - return { - ATTR_IDENTIFIERS: {(DOMAIN, self.unique_id)}, - ATTR_NAME: self.coordinator.name, - ATTR_MANUFACTURER: "NOAA", - ATTR_MODEL: "Aurora Visibility Sensor", - ATTR_ENTRY_TYPE: "service", - } + return DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, str(self.unique_id))}, + manufacturer="NOAA", + model="Aurora Visibility Sensor", + name=self.coordinator.name, + ) diff --git a/homeassistant/components/aurora/const.py b/homeassistant/components/aurora/const.py index 8ce6bbad3f9..d2f91fb1222 100644 --- a/homeassistant/components/aurora/const.py +++ b/homeassistant/components/aurora/const.py @@ -3,7 +3,6 @@ DOMAIN = "aurora" COORDINATOR = "coordinator" AURORA_API = "aurora_api" -ATTR_ENTRY_TYPE = "entry_type" DEFAULT_POLLING_INTERVAL = 5 CONF_THRESHOLD = "forecast_threshold" DEFAULT_THRESHOLD = 75 diff --git a/homeassistant/components/axis/axis_base.py b/homeassistant/components/axis/axis_base.py index a652aeb6df8..791764dc605 100644 --- a/homeassistant/components/axis/axis_base.py +++ b/homeassistant/components/axis/axis_base.py @@ -1,9 +1,8 @@ """Base classes for Axis entities.""" -from homeassistant.const import ATTR_IDENTIFIERS from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN as AXIS_DOMAIN @@ -15,7 +14,9 @@ class AxisEntityBase(Entity): """Initialize the Axis event.""" self.device = device - self._attr_device_info = {ATTR_IDENTIFIERS: {(AXIS_DOMAIN, device.unique_id)}} + self._attr_device_info = DeviceInfo( + identifiers={(AXIS_DOMAIN, device.unique_id)} + ) async def async_added_to_hass(self): """Subscribe device events.""" diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index 39307ac41df..fe27ec8bcec 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -83,15 +83,9 @@ class AzureDevOpsDeviceEntity(AzureDevOpsEntity): @property def device_info(self) -> DeviceInfo: """Return device information about this Azure DevOps instance.""" - return { - "identifiers": { - ( # type: ignore - DOMAIN, - self.organization, - self.project, - ) - }, - "manufacturer": self.organization, - "name": self.project, - "entry_type": "service", - } + return DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, self.organization, self.project)}, # type: ignore + manufacturer=self.organization, + name=self.project, + ) From fc3e7f5b7e3ef2591db603594c09dee153824af9 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 22 Oct 2021 11:00:00 -0400 Subject: [PATCH 0675/1038] Use DeviceInfo Class D (#58218) --- homeassistant/components/daikin/__init__.py | 17 +++++++++-------- homeassistant/components/demo/binary_sensor.py | 11 ++++++----- homeassistant/components/demo/climate.py | 11 ++++++----- homeassistant/components/demo/cover.py | 11 ++++++----- homeassistant/components/demo/light.py | 11 ++++++----- homeassistant/components/demo/number.py | 11 ++++++----- homeassistant/components/demo/select.py | 9 +++++---- homeassistant/components/demo/sensor.py | 9 +++++---- homeassistant/components/demo/switch.py | 11 ++++++----- .../components/denonavr/media_player.py | 4 ++-- .../devolo_home_control/devolo_device.py | 16 ++++++++-------- homeassistant/components/directv/entity.py | 3 +-- homeassistant/components/doorbird/entity.py | 18 +++++++++--------- homeassistant/components/dsmr/sensor.py | 9 +++++---- .../components/dunehd/media_player.py | 10 +++++----- .../components/dynalite/dynalitebase.py | 10 +++++----- 16 files changed, 90 insertions(+), 81 deletions(-) diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 504f8cd9f86..185537cc7d0 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -13,6 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.entity import DeviceInfo from homeassistant.util import Throttle from .const import CONF_UUID, DOMAIN, KEY_MAC, TIMEOUT @@ -109,13 +110,13 @@ class DaikinApi: return self._available @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" info = self.device.values - return { - "connections": {(CONNECTION_NETWORK_MAC, self.device.mac)}, - "manufacturer": "Daikin", - "model": info.get("model"), - "name": info.get("name"), - "sw_version": info.get("ver", "").replace("_", "."), - } + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, self.device.mac)}, + manufacturer="Daikin", + model=info.get("model"), + name=info.get("name"), + sw_version=info.get("ver", "").replace("_", "."), + ) diff --git a/homeassistant/components/demo/binary_sensor.py b/homeassistant/components/demo/binary_sensor.py index c4186eae505..7cf96d33f5c 100644 --- a/homeassistant/components/demo/binary_sensor.py +++ b/homeassistant/components/demo/binary_sensor.py @@ -4,6 +4,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_MOTION, BinarySensorEntity, ) +from homeassistant.helpers.entity import DeviceInfo from . import DOMAIN @@ -38,15 +39,15 @@ class DemoBinarySensor(BinarySensorEntity): self._sensor_type = device_class @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info.""" - return { - "identifiers": { + return DeviceInfo( + identifiers={ # Serial numbers are unique identifiers within a specific domain (DOMAIN, self.unique_id) }, - "name": self.name, - } + name=self.name, + ) @property def unique_id(self): diff --git a/homeassistant/components/demo/climate.py b/homeassistant/components/demo/climate.py index a6d166662ae..ff8e0c256c6 100644 --- a/homeassistant/components/demo/climate.py +++ b/homeassistant/components/demo/climate.py @@ -20,6 +20,7 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE_RANGE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.helpers.entity import DeviceInfo from . import DOMAIN @@ -154,15 +155,15 @@ class DemoClimate(ClimateEntity): self._target_temperature_low = target_temp_low @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info.""" - return { - "identifiers": { + return DeviceInfo( + identifiers={ # Serial numbers are unique identifiers within a specific domain (DOMAIN, self.unique_id) }, - "name": self.name, - } + name=self.name, + ) @property def unique_id(self): diff --git a/homeassistant/components/demo/cover.py b/homeassistant/components/demo/cover.py index 35f25df5a96..444b6a2a90c 100644 --- a/homeassistant/components/demo/cover.py +++ b/homeassistant/components/demo/cover.py @@ -11,6 +11,7 @@ from homeassistant.components.cover import ( CoverEntity, ) from homeassistant.core import callback +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.event import async_track_utc_time_change from . import DOMAIN @@ -86,15 +87,15 @@ class DemoCover(CoverEntity): self._closed = self.current_cover_position <= 0 @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info.""" - return { - "identifiers": { + return DeviceInfo( + identifiers={ # Serial numbers are unique identifiers within a specific domain (DOMAIN, self.unique_id) }, - "name": self.name, - } + name=self.name, + ) @property def unique_id(self): diff --git a/homeassistant/components/demo/light.py b/homeassistant/components/demo/light.py index 4d9496ca10f..29e7e1395ee 100644 --- a/homeassistant/components/demo/light.py +++ b/homeassistant/components/demo/light.py @@ -19,6 +19,7 @@ from homeassistant.components.light import ( SUPPORT_EFFECT, LightEntity, ) +from homeassistant.helpers.entity import DeviceInfo from . import DOMAIN @@ -138,15 +139,15 @@ class DemoLight(LightEntity): self._features |= SUPPORT_EFFECT @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info.""" - return { - "identifiers": { + return DeviceInfo( + identifiers={ # Serial numbers are unique identifiers within a specific domain (DOMAIN, self.unique_id) }, - "name": self.name, - } + name=self.name, + ) @property def should_poll(self) -> bool: diff --git a/homeassistant/components/demo/number.py b/homeassistant/components/demo/number.py index 8f3ccf47230..d471bdac85f 100644 --- a/homeassistant/components/demo/number.py +++ b/homeassistant/components/demo/number.py @@ -6,6 +6,7 @@ from typing import Literal from homeassistant.components.number import NumberEntity from homeassistant.components.number.const import MODE_AUTO, MODE_BOX, MODE_SLIDER from homeassistant.const import DEVICE_DEFAULT_NAME +from homeassistant.helpers.entity import DeviceInfo from . import DOMAIN @@ -95,15 +96,15 @@ class DemoNumber(NumberEntity): self._attr_step = step @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info.""" - return { - "identifiers": { + return DeviceInfo( + identifiers={ # Serial numbers are unique identifiers within a specific domain (DOMAIN, self.unique_id) }, - "name": self.name, - } + name=self.name, + ) async def async_set_value(self, value): """Update the current value.""" diff --git a/homeassistant/components/demo/select.py b/homeassistant/components/demo/select.py index 8d499c7a258..6a768f80ba7 100644 --- a/homeassistant/components/demo/select.py +++ b/homeassistant/components/demo/select.py @@ -5,6 +5,7 @@ from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_DEFAULT_NAME 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 @@ -66,10 +67,10 @@ class DemoSelect(SelectEntity): self._attr_icon = icon self._attr_device_class = device_class self._attr_options = options - self._attr_device_info = { - "identifiers": {(DOMAIN, unique_id)}, - "name": name, - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + name=name, + ) async def async_select_option(self, option: str) -> None: """Update the current selected option.""" diff --git a/homeassistant/components/demo/sensor.py b/homeassistant/components/demo/sensor.py index 21ec8e1d391..413017ad2f1 100644 --- a/homeassistant/components/demo/sensor.py +++ b/homeassistant/components/demo/sensor.py @@ -20,6 +20,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, StateType @@ -125,10 +126,10 @@ class DemoSensor(SensorEntity): self._attr_state_class = state_class self._attr_unique_id = unique_id - self._attr_device_info = { - "identifiers": {(DOMAIN, unique_id)}, - "name": name, - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + name=name, + ) if battery: self._attr_extra_state_attributes = {ATTR_BATTERY_LEVEL: battery} diff --git a/homeassistant/components/demo/switch.py b/homeassistant/components/demo/switch.py index 84554bf0db1..dd969010bd7 100644 --- a/homeassistant/components/demo/switch.py +++ b/homeassistant/components/demo/switch.py @@ -3,6 +3,7 @@ from __future__ import annotations from homeassistant.components.switch import SwitchEntity from homeassistant.const import DEVICE_DEFAULT_NAME +from homeassistant.helpers.entity import DeviceInfo from . import DOMAIN @@ -52,12 +53,12 @@ class DemoSwitch(SwitchEntity): self._attr_unique_id = unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info.""" - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - } + return DeviceInfo( + identifiers={(DOMAIN, self.unique_id)}, + name=self.name, + ) def turn_on(self, **kwargs): """Turn the switch on.""" diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index f8cfe75f304..39186680e09 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -148,11 +148,11 @@ class DenonDevice(MediaPlayerEntity): self._attr_name = receiver.name self._attr_unique_id = unique_id self._attr_device_info = DeviceInfo( + configuration_url=f"http://{config_entry.data[CONF_HOST]}/", identifiers={(DOMAIN, config_entry.unique_id)}, manufacturer=config_entry.data[CONF_MANUFACTURER], - name=config_entry.title, model=f"{config_entry.data[CONF_MODEL]}-{config_entry.data[CONF_TYPE]}", - configuration_url=f"http://{config_entry.data[CONF_HOST]}/", + name=config_entry.title, ) self._attr_sound_mode_list = receiver.sound_mode_list self._attr_source_list = receiver.input_func_list diff --git a/homeassistant/components/devolo_home_control/devolo_device.py b/homeassistant/components/devolo_home_control/devolo_device.py index 03f850579be..7fdd53d0d87 100644 --- a/homeassistant/components/devolo_home_control/devolo_device.py +++ b/homeassistant/components/devolo_home_control/devolo_device.py @@ -6,7 +6,7 @@ import logging from devolo_home_control_api.devices.zwave import Zwave from devolo_home_control_api.homecontrol import HomeControl -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN from .subscriber import Subscriber @@ -32,15 +32,15 @@ class DevoloDeviceEntity(Entity): ].name self._attr_should_poll = False self._attr_unique_id = element_uid - self._attr_device_info = { - "identifiers": {(DOMAIN, self._device_instance.uid)}, - "name": self._attr_name, - "manufacturer": device_instance.brand, - "model": device_instance.name, - "suggested_area": device_instance.settings_property[ + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._device_instance.uid)}, + manufacturer=device_instance.brand, + model=device_instance.name, + name=self._attr_name, + suggested_area=device_instance.settings_property[ "general_device_settings" ].zone, - } + ) self.subscriber: Subscriber | None = None self.sync_callback = self._sync diff --git a/homeassistant/components/directv/entity.py b/homeassistant/components/directv/entity.py index b3ac8fd7df6..08b24a50a75 100644 --- a/homeassistant/components/directv/entity.py +++ b/homeassistant/components/directv/entity.py @@ -23,9 +23,8 @@ class DIRECTVEntity(Entity): """Return device information about this DirecTV receiver.""" return DeviceInfo( identifiers={(DOMAIN, self._device_id)}, - name=self.name, manufacturer=self.dtv.device.info.brand, - model=None, + name=self.name, sw_version=self.dtv.device.info.version, via_device=(DOMAIN, self.dtv.device.info.receiver_id), ) diff --git a/homeassistant/components/doorbird/entity.py b/homeassistant/components/doorbird/entity.py index 44cbb1f42de..73404e7c199 100644 --- a/homeassistant/components/doorbird/entity.py +++ b/homeassistant/components/doorbird/entity.py @@ -1,7 +1,7 @@ """The DoorBird integration base entity.""" from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ( DOORBIRD_INFO_KEY_BUILD_NUMBER, @@ -23,14 +23,14 @@ class DoorBirdEntity(Entity): self._mac_addr = get_mac_address_from_doorstation_info(doorstation_info) @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Doorbird device info.""" firmware = self._doorstation_info[DOORBIRD_INFO_KEY_FIRMWARE] firmware_build = self._doorstation_info[DOORBIRD_INFO_KEY_BUILD_NUMBER] - return { - "connections": {(dr.CONNECTION_NETWORK_MAC, self._mac_addr)}, - "name": self._doorstation.name, - "manufacturer": MANUFACTURER, - "sw_version": f"{firmware} {firmware_build}", - "model": self._doorstation_info[DOORBIRD_INFO_KEY_DEVICE_TYPE], - } + return DeviceInfo( + connections={(dr.CONNECTION_NETWORK_MAC, self._mac_addr)}, + manufacturer=MANUFACTURER, + model=self._doorstation_info[DOORBIRD_INFO_KEY_DEVICE_TYPE], + name=self._doorstation.name, + sw_version=f"{firmware} {firmware_build}", + ) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index bd02be7d63e..12b2a17016a 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -24,6 +24,7 @@ from homeassistant.const import ( ) from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, EventType, StateType from homeassistant.util import Throttle @@ -230,10 +231,10 @@ class DSMREntity(SensorEntity): if device_serial is None: device_serial = entry.entry_id - self._attr_device_info = { - "identifiers": {(DOMAIN, device_serial)}, - "name": device_name, - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device_serial)}, + name=device_name, + ) self._attr_unique_id = f"{device_serial}_{entity_description.name}".replace( " ", "_" ) diff --git a/homeassistant/components/dunehd/media_player.py b/homeassistant/components/dunehd/media_player.py index 482b92f768e..437b5b66a99 100644 --- a/homeassistant/components/dunehd/media_player.py +++ b/homeassistant/components/dunehd/media_player.py @@ -131,11 +131,11 @@ class DuneHDPlayerEntity(MediaPlayerEntity): @property def device_info(self) -> DeviceInfo: """Return the device info.""" - return { - "identifiers": {(DOMAIN, self._unique_id)}, - "name": DEFAULT_NAME, - "manufacturer": ATTR_MANUFACTURER, - } + return DeviceInfo( + identifiers={(DOMAIN, self._unique_id)}, + manufacturer=ATTR_MANUFACTURER, + name=DEFAULT_NAME, + ) @property def volume_level(self) -> float: diff --git a/homeassistant/components/dynalite/dynalitebase.py b/homeassistant/components/dynalite/dynalitebase.py index 72803f86f02..9bb4f3aeb27 100644 --- a/homeassistant/components/dynalite/dynalitebase.py +++ b/homeassistant/components/dynalite/dynalitebase.py @@ -64,11 +64,11 @@ class DynaliteBase(Entity): @property def device_info(self) -> DeviceInfo: """Device info for this entity.""" - return { - "identifiers": {(DOMAIN, self._device.unique_id)}, - "name": self.name, - "manufacturer": "Dynalite", - } + return DeviceInfo( + identifiers={(DOMAIN, self._device.unique_id)}, + manufacturer="Dynalite", + name=self.name, + ) async def async_added_to_hass(self) -> None: """Added to hass so need to register to dispatch.""" From 51a10f88de216dc06c18ca7bf1852e33a7d79f7c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 22 Oct 2021 17:04:25 +0200 Subject: [PATCH 0676/1038] Use DeviceInfo on components with via_device (#58222) Co-authored-by: epenet --- homeassistant/components/bond/entity.py | 19 +++++++------- .../components/homekit_controller/__init__.py | 22 ++++++++-------- .../homekit_controller/connection.py | 22 +++++++++------- homeassistant/components/myq/__init__.py | 26 ++++++++++++------- .../components/onewire/onewirehub.py | 5 ++-- homeassistant/components/ozw/entity.py | 23 ++++++++-------- homeassistant/components/tellduslive/entry.py | 26 ++++++++++++------- 7 files changed, 81 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/bond/entity.py b/homeassistant/components/bond/entity.py index 3063a3e4efa..e2957984dc7 100644 --- a/homeassistant/components/bond/entity.py +++ b/homeassistant/components/bond/entity.py @@ -10,6 +10,7 @@ from typing import Any from aiohttp import ClientError from bond_api import BPUPSubscriptions +from homeassistant.const import ATTR_MODEL, ATTR_NAME, ATTR_SW_VERSION, ATTR_VIA_DEVICE from homeassistant.core import callback from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.event import async_track_time_interval @@ -54,22 +55,22 @@ class BondEntity(Entity): @property def device_info(self) -> DeviceInfo: """Get a an HA device representing this Bond controlled device.""" - device_info: DeviceInfo = { - "manufacturer": self._hub.make, + device_info = DeviceInfo( + manufacturer=self._hub.make, # type ignore: tuple items should not be Optional - "identifiers": {(DOMAIN, self._hub.bond_id, self._device.device_id)}, # type: ignore[arg-type] - } + identifiers={(DOMAIN, self._hub.bond_id, self._device.device_id)}, # type: ignore[arg-type] + ) if self.name is not None: - device_info["name"] = self.name + device_info[ATTR_NAME] = self.name if self._hub.bond_id is not None: - device_info["via_device"] = (DOMAIN, self._hub.bond_id) + device_info[ATTR_VIA_DEVICE] = (DOMAIN, self._hub.bond_id) if self._device.location is not None: device_info["suggested_area"] = self._device.location if not self._hub.is_bridge: if self._hub.model is not None: - device_info["model"] = self._hub.model + device_info[ATTR_MODEL] = self._hub.model if self._hub.fw_ver is not None: - device_info["sw_version"] = self._hub.fw_ver + device_info[ATTR_SW_VERSION] = self._hub.fw_ver else: model_data = [] if self._device.branding_profile: @@ -77,7 +78,7 @@ class BondEntity(Entity): if self._device.template: model_data.append(self._device.template) if model_data: - device_info["model"] = " ".join(model_data) + device_info[ATTR_MODEL] = " ".join(model_data) return device_info diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index f7c98c66708..92853faedad 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -14,9 +14,9 @@ from aiohomekit.model.characteristics import ( from aiohomekit.model.services import Service, ServicesTypes from homeassistant.components import zeroconf -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ATTR_VIA_DEVICE, EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .config_flow import normalize_hkid from .connection import HKDevice @@ -145,24 +145,24 @@ class HomeKitEntity(Entity): return self._accessory.available and self.service.available @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" info = self.accessory_info accessory_serial = info.value(CharacteristicsTypes.SERIAL_NUMBER) - device_info = { - "identifiers": {(DOMAIN, "serial-number", accessory_serial)}, - "name": info.value(CharacteristicsTypes.NAME), - "manufacturer": info.value(CharacteristicsTypes.MANUFACTURER, ""), - "model": info.value(CharacteristicsTypes.MODEL, ""), - "sw_version": info.value(CharacteristicsTypes.FIRMWARE_REVISION, ""), - } + device_info = DeviceInfo( + identifiers={(DOMAIN, "serial-number", accessory_serial)}, + name=info.value(CharacteristicsTypes.NAME), + manufacturer=info.value(CharacteristicsTypes.MANUFACTURER, ""), + model=info.value(CharacteristicsTypes.MODEL, ""), + sw_version=info.value(CharacteristicsTypes.FIRMWARE_REVISION, ""), + ) # Some devices only have a single accessory - we don't add a # via_device otherwise it would be self referential. bridge_serial = self._accessory.connection_info["serial-number"] if accessory_serial != bridge_serial: - device_info["via_device"] = (DOMAIN, "serial-number", bridge_serial) + device_info[ATTR_VIA_DEVICE] = (DOMAIN, "serial-number", bridge_serial) return device_info diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 16866bedcfe..12ea3b8a397 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -12,8 +12,10 @@ from aiohomekit.model import Accessories from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes +from homeassistant.const import ATTR_IDENTIFIERS, ATTR_VIA_DEVICE from homeassistant.core import callback from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.event import async_track_time_interval from .const import ( @@ -205,29 +207,31 @@ class HKDevice: service_type=ServicesTypes.ACCESSORY_INFORMATION, ) - device_info = { - "identifiers": { + device_info = DeviceInfo( + identifiers={ ( DOMAIN, "serial-number", info.value(CharacteristicsTypes.SERIAL_NUMBER), ) }, - "name": info.value(CharacteristicsTypes.NAME), - "manufacturer": info.value(CharacteristicsTypes.MANUFACTURER, ""), - "model": info.value(CharacteristicsTypes.MODEL, ""), - "sw_version": info.value(CharacteristicsTypes.FIRMWARE_REVISION, ""), - } + name=info.value(CharacteristicsTypes.NAME), + manufacturer=info.value(CharacteristicsTypes.MANUFACTURER, ""), + model=info.value(CharacteristicsTypes.MODEL, ""), + sw_version=info.value(CharacteristicsTypes.FIRMWARE_REVISION, ""), + ) if accessory.aid == 1: # Accessory 1 is the root device (sometimes the only device, sometimes a bridge) # Link the root device to the pairing id for the connection. - device_info["identifiers"].add((DOMAIN, "accessory-id", self.unique_id)) + device_info[ATTR_IDENTIFIERS].add( + (DOMAIN, "accessory-id", self.unique_id) + ) else: # Every pairing has an accessory 1 # It *doesn't* have a via_device, as it is the device we are connecting to # Every other accessory should use it as its via device. - device_info["via_device"] = ( + device_info[ATTR_VIA_DEVICE] = ( DOMAIN, "serial-number", self.connection_info["serial-number"], diff --git a/homeassistant/components/myq/__init__.py b/homeassistant/components/myq/__init__.py index b6ecd8a8e23..8e7300bf2bd 100644 --- a/homeassistant/components/myq/__init__.py +++ b/homeassistant/components/myq/__init__.py @@ -13,10 +13,16 @@ from pymyq.device import MyQDevice from pymyq.errors import InvalidCredentialsError, MyQError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + ATTR_MODEL, + ATTR_VIA_DEVICE, + CONF_PASSWORD, + CONF_USERNAME, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -91,23 +97,23 @@ class MyQEntity(CoordinatorEntity): return self._device.name @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" - device_info = { - "identifiers": {(DOMAIN, self._device.device_id)}, - "name": self._device.name, - "manufacturer": MANUFACTURER, - "sw_version": self._device.firmware_version, - } + device_info = DeviceInfo( + identifiers={(DOMAIN, self._device.device_id)}, + name=self._device.name, + manufacturer=MANUFACTURER, + sw_version=self._device.firmware_version, + ) model = ( KNOWN_MODELS.get(self._device.device_id[2:4]) if self._device.device_id is not None else None ) if model: - device_info["model"] = model + device_info[ATTR_MODEL] = model if self._device.parent_device_id: - device_info["via_device"] = (DOMAIN, self._device.parent_device_id) + device_info[ATTR_VIA_DEVICE] = (DOMAIN, self._device.parent_device_id) return device_info @property diff --git a/homeassistant/components/onewire/onewirehub.py b/homeassistant/components/onewire/onewirehub.py index 0d7f85fee68..c2a88dc6df6 100644 --- a/homeassistant/components/onewire/onewirehub.py +++ b/homeassistant/components/onewire/onewirehub.py @@ -14,6 +14,7 @@ from homeassistant.const import ( ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME, + ATTR_VIA_DEVICE, CONF_HOST, CONF_PORT, CONF_TYPE, @@ -87,7 +88,7 @@ class OneWireHub: manufacturer=device_info[ATTR_MANUFACTURER], model=device_info[ATTR_MODEL], name=device_info[ATTR_NAME], - via_device=device_info.get("via_device"), + via_device=device_info.get(ATTR_VIA_DEVICE), ) async def discover_devices(self) -> None: @@ -141,7 +142,7 @@ class OneWireHub: ATTR_NAME: device_id, } if parent_id: - device_info["via_device"] = (DOMAIN, parent_id) + device_info[ATTR_VIA_DEVICE] = (DOMAIN, parent_id) device = OWServerDeviceDescription( device_info=device_info, id=device_id, diff --git a/homeassistant/components/ozw/entity.py b/homeassistant/components/ozw/entity.py index d5cafa615df..790b60aed69 100644 --- a/homeassistant/components/ozw/entity.py +++ b/homeassistant/components/ozw/entity.py @@ -13,12 +13,13 @@ from openzwavemqtt.const import ( from openzwavemqtt.models.node import OZWNode from openzwavemqtt.models.value import OZWValue +from homeassistant.const import ATTR_NAME, ATTR_SW_VERSION, ATTR_VIA_DEVICE from homeassistant.core import callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from . import const from .const import DOMAIN, PLATFORMS @@ -184,7 +185,7 @@ class ZWaveDeviceEntity(Entity): ) @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device information for the device registry.""" node = self.values.primary.node node_instance = self.values.primary.instance @@ -192,20 +193,20 @@ class ZWaveDeviceEntity(Entity): node_firmware = node.get_value( CommandClass.VERSION, ValueIndex.VERSION_APPLICATION ) - device_info = { - "identifiers": {(DOMAIN, dev_id)}, - "name": create_device_name(node), - "manufacturer": node.node_manufacturer_name, - "model": node.node_product_name, - } + device_info = DeviceInfo( + identifiers={(DOMAIN, dev_id)}, + name=create_device_name(node), + manufacturer=node.node_manufacturer_name, + model=node.node_product_name, + ) if node_firmware is not None: - device_info["sw_version"] = node_firmware.value + device_info[ATTR_SW_VERSION] = node_firmware.value # device with multiple instances is split up into virtual devices for each instance if node_instance > 1: parent_dev_id = create_device_id(node) - device_info["name"] += f" - Instance {node_instance}" - device_info["via_device"] = (DOMAIN, parent_dev_id) + device_info[ATTR_NAME] += f" - Instance {node_instance}" + device_info[ATTR_VIA_DEVICE] = (DOMAIN, parent_dev_id) return device_info @property diff --git a/homeassistant/components/tellduslive/entry.py b/homeassistant/components/tellduslive/entry.py index edb4537aac3..9e0bf7e9693 100644 --- a/homeassistant/components/tellduslive/entry.py +++ b/homeassistant/components/tellduslive/entry.py @@ -4,10 +4,16 @@ import logging from tellduslive import BATTERY_LOW, BATTERY_OK, BATTERY_UNKNOWN -from homeassistant.const import ATTR_BATTERY_LEVEL, DEVICE_DEFAULT_NAME +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_VIA_DEVICE, + DEVICE_DEFAULT_NAME, +) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import SIGNAL_UPDATE_ENTITY @@ -116,17 +122,17 @@ class TelldusLiveEntity(Entity): return self._id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info.""" device = self._client.device_info(self.device.device_id) - device_info = { - "identifiers": {("tellduslive", self.device.device_id)}, - "name": self.device.name, - } + device_info = DeviceInfo( + identifiers={("tellduslive", self.device.device_id)}, + name=self.device.name, + ) if (model := device.get("model")) is not None: - device_info["model"] = model.title() + device_info[ATTR_MODEL] = model.title() if (protocol := device.get("protocol")) is not None: - device_info["manufacturer"] = protocol.title() + device_info[ATTR_MANUFACTURER] = protocol.title() if (client := device.get("client")) is not None: - device_info["via_device"] = ("tellduslive", client) + device_info[ATTR_VIA_DEVICE] = ("tellduslive", client) return device_info From 416d87c01caabfdb7167b2ef9636e8ee69b0b1e4 Mon Sep 17 00:00:00 2001 From: Lars Date: Fri, 22 Oct 2021 17:29:36 +0200 Subject: [PATCH 0677/1038] Fix fritzbox tests (#58227) --- tests/components/fritzbox/test_light.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/components/fritzbox/test_light.py b/tests/components/fritzbox/test_light.py index 5b17e36abb2..24077306647 100644 --- a/tests/components/fritzbox/test_light.py +++ b/tests/components/fritzbox/test_light.py @@ -152,14 +152,14 @@ async def test_update(hass: HomeAssistant, fritz: Mock): assert await setup_config_entry( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz ) - assert device.update.call_count == 1 + assert fritz().update_devices.call_count == 1 assert fritz().login.call_count == 1 next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) await hass.async_block_till_done() - assert device.update.call_count == 2 + assert fritz().update_devices.call_count == 2 assert fritz().login.call_count == 1 @@ -170,16 +170,16 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock): device.get_colors.return_value = { "Red": [("100", "70", "10"), ("100", "50", "10"), ("100", "30", "10")] } - device.update.side_effect = HTTPError("Boom") + fritz().update_devices.side_effect = HTTPError("Boom") assert not await setup_config_entry( hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz ) - assert device.update.call_count == 1 + assert fritz().update_devices.call_count == 1 assert fritz().login.call_count == 1 next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) await hass.async_block_till_done() - assert device.update.call_count == 2 + assert fritz().update_devices.call_count == 2 assert fritz().login.call_count == 2 From f6ffae9e1072d73955d6d7b291afc2b8772b4a7f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 22 Oct 2021 17:40:13 +0200 Subject: [PATCH 0678/1038] Use DeviceInfo on components with configuration_url (#58223) Co-authored-by: epenet --- homeassistant/components/brother/sensor.py | 16 ++++---- homeassistant/components/co2signal/sensor.py | 18 ++++----- homeassistant/components/fritz/common.py | 18 ++++----- homeassistant/components/juicenet/entity.py | 15 +++---- homeassistant/components/nam/__init__.py | 14 +++---- homeassistant/components/plex/sensor.py | 39 ++++++++++--------- homeassistant/components/rachio/entity.py | 20 +++++----- .../components/rainmachine/__init__.py | 20 +++++----- homeassistant/components/sense/sensor.py | 15 +++---- homeassistant/components/sonarr/entity.py | 22 ++++------- homeassistant/components/sonos/entity.py | 20 +++++----- .../components/uptimerobot/entity.py | 18 ++++----- 12 files changed, 116 insertions(+), 119 deletions(-) diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index 94b777147c3..00cb91b2860 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -33,14 +33,14 @@ async def async_setup_entry( sensors = [] - device_info: DeviceInfo = { - "identifiers": {(DOMAIN, coordinator.data.serial)}, - "name": coordinator.data.model, - "manufacturer": ATTR_MANUFACTURER, - "model": coordinator.data.model, - "sw_version": getattr(coordinator.data, "firmware", None), - "configuration_url": f"http://{entry.data[CONF_HOST]}/", - } + device_info = DeviceInfo( + configuration_url=f"http://{entry.data[CONF_HOST]}/", + identifiers={(DOMAIN, coordinator.data.serial)}, + manufacturer=ATTR_MANUFACTURER, + model=coordinator.data.model, + name=coordinator.data.model, + sw_version=getattr(coordinator.data, "firmware", None), + ) for description in SENSOR_TYPES: if description.key in coordinator.data: diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index 6e35a6d751b..b7a36623a3c 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -15,15 +15,13 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import ( ATTR_ATTRIBUTION, - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_TOKEN, PERCENTAGE, ) from homeassistant.helpers import config_validation as cv, update_coordinator +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import StateType from . import CO2SignalCoordinator, CO2SignalResponse @@ -104,13 +102,13 @@ class CO2Sensor(update_coordinator.CoordinatorEntity[CO2SignalResponse], SensorE "country_code": coordinator.data["countryCode"], ATTR_ATTRIBUTION: ATTRIBUTION, } - self._attr_device_info = { - ATTR_IDENTIFIERS: {(DOMAIN, coordinator.entry_id)}, - ATTR_NAME: "CO2 signal", - ATTR_MANUFACTURER: "Tmrow.com", - "entry_type": "service", - "configuration_url": "https://www.electricitymap.org/", - } + self._attr_device_info = DeviceInfo( + configuration_url="https://www.electricitymap.org/", + entry_type="service", + identifiers={(DOMAIN, coordinator.entry_id)}, + manufacturer="Tmrow.com", + name="CO2 signal", + ) self._attr_unique_id = ( f"{coordinator.entry_id}_{description.unique_id or description.key}" ) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 89356ac5b42..6503a5e84ba 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -480,12 +480,12 @@ class FritzBoxBaseEntity: def device_info(self) -> DeviceInfo: """Return the device information.""" - return { - "connections": {(CONNECTION_NETWORK_MAC, self.mac_address)}, - "identifiers": {(DOMAIN, self._fritzbox_tools.unique_id)}, - "name": self._device_name, - "manufacturer": "AVM", - "model": self._fritzbox_tools.model, - "sw_version": self._fritzbox_tools.current_firmware, - "configuration_url": f"http://{self._fritzbox_tools.host}", - } + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, self.mac_address)}, + identifiers={(DOMAIN, self._fritzbox_tools.unique_id)}, + name=self._device_name, + manufacturer="AVM", + model=self._fritzbox_tools.model, + sw_version=self._fritzbox_tools.current_firmware, + configuration_url=f"http://{self._fritzbox_tools.host}", + ) diff --git a/homeassistant/components/juicenet/entity.py b/homeassistant/components/juicenet/entity.py index 2ad6bd9b774..d3b2aa41d19 100644 --- a/homeassistant/components/juicenet/entity.py +++ b/homeassistant/components/juicenet/entity.py @@ -1,5 +1,6 @@ """Adapter to wrap the pyjuicenet api for home assistant.""" +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -20,11 +21,11 @@ class JuiceNetDevice(CoordinatorEntity): return f"{self.device.id}-{self.type}" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device information about this JuiceNet Device.""" - return { - "identifiers": {(DOMAIN, self.device.id)}, - "name": self.device.name, - "manufacturer": "JuiceNet", - "configuration_url": f"https://home.juice.net/Portal/Details?unitID={self.device.id}", - } + return DeviceInfo( + identifiers={(DOMAIN, self.device.id)}, + name=self.device.name, + manufacturer="JuiceNet", + configuration_url=f"https://home.juice.net/Portal/Details?unitID={self.device.id}", + ) diff --git a/homeassistant/components/nam/__init__.py b/homeassistant/components/nam/__init__.py index 8df6a43ba30..4843e96b5a8 100644 --- a/homeassistant/components/nam/__init__.py +++ b/homeassistant/components/nam/__init__.py @@ -115,10 +115,10 @@ class NAMDataUpdateCoordinator(DataUpdateCoordinator): @property def device_info(self) -> DeviceInfo: """Return the device info.""" - return { - "connections": {(CONNECTION_NETWORK_MAC, cast(str, self._unique_id))}, - "name": DEFAULT_NAME, - "sw_version": self.nam.software_version, - "manufacturer": MANUFACTURER, - "configuration_url": f"http://{self.host}/", - } + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, cast(str, self._unique_id))}, + name=DEFAULT_NAME, + sw_version=self.nam.software_version, + manufacturer=MANUFACTURER, + configuration_url=f"http://{self.host}/", + ) diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index 6ca5f50c4ad..b8de361072b 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -1,4 +1,6 @@ """Support for Plex media server monitoring.""" +from __future__ import annotations + import logging from plexapi.exceptions import NotFound @@ -7,6 +9,7 @@ import requests.exceptions from homeassistant.components.sensor import SensorEntity from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from .const import ( CONF_SERVER_IDENTIFIER, @@ -102,19 +105,19 @@ class PlexSensor(SensorEntity): return self._server.sensor_attributes @property - def device_info(self): + def device_info(self) -> DeviceInfo | None: """Return a device description for device registry.""" if self.unique_id is None: return None - return { - "identifiers": {(PLEX_DOMAIN, self._server.machine_identifier)}, - "manufacturer": "Plex", - "model": "Plex Media Server", - "name": self._server.friendly_name, - "sw_version": self._server.version, - "configuration_url": f"{self._server.url_in_use}/web", - } + return DeviceInfo( + identifiers={(PLEX_DOMAIN, self._server.machine_identifier)}, + manufacturer="Plex", + model="Plex Media Server", + name=self._server.friendly_name, + sw_version=self._server.version, + configuration_url=f"{self._server.url_in_use}/web", + ) class PlexLibrarySectionSensor(SensorEntity): @@ -193,16 +196,16 @@ class PlexLibrarySectionSensor(SensorEntity): self._attr_extra_state_attributes["last_added_timestamp"] = media.addedAt @property - def device_info(self): + def device_info(self) -> DeviceInfo | None: """Return a device description for device registry.""" if self.unique_id is None: return None - return { - "identifiers": {(PLEX_DOMAIN, self.server_id)}, - "manufacturer": "Plex", - "model": "Plex Media Server", - "name": self.server_name, - "sw_version": self._server.version, - "configuration_url": f"{self._server.url_in_use}/web", - } + return DeviceInfo( + identifiers={(PLEX_DOMAIN, self.server_id)}, + manufacturer="Plex", + model="Plex Media Server", + name=self.server_name, + sw_version=self._server.version, + configuration_url=f"{self._server.url_in_use}/web", + ) diff --git a/homeassistant/components/rachio/entity.py b/homeassistant/components/rachio/entity.py index 879427ba2c0..a95b6ffb557 100644 --- a/homeassistant/components/rachio/entity.py +++ b/homeassistant/components/rachio/entity.py @@ -1,7 +1,7 @@ """Adapter to wrap the rachiopy api for home assistant.""" from homeassistant.helpers import device_registry -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DEFAULT_NAME, DOMAIN @@ -20,23 +20,23 @@ class RachioDevice(Entity): return False @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" - return { - "identifiers": { + return DeviceInfo( + identifiers={ ( DOMAIN, self._controller.serial_number, ) }, - "connections": { + connections={ ( device_registry.CONNECTION_NETWORK_MAC, self._controller.mac_address, ) }, - "name": self._controller.name, - "model": self._controller.model, - "manufacturer": DEFAULT_NAME, - "configuration_url": "https://app.rach.io", - } + name=self._controller.name, + model=self._controller.model, + manufacturer=DEFAULT_NAME, + configuration_url="https://app.rach.io", + ) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 2c2cb0ef3c1..d8eea8b3df5 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv import homeassistant.helpers.device_registry as dr -from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -332,18 +332,18 @@ class RainMachineEntity(CoordinatorEntity): """Initialize.""" super().__init__(coordinator) - self._attr_device_info = { - "identifiers": {(DOMAIN, 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), - "manufacturer": "RainMachine", - "model": ( + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, 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), + manufacturer="RainMachine", + model=( f"Version {controller.hardware_version} " f"(API: {controller.api_version})" ), - "sw_version": controller.software_version, - } + sw_version=controller.software_version, + ) self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} self._attr_name = f"{controller.name} {description.name}" # The colons are removed from the device MAC simply because that value diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py index 1e3cb3d8aca..a8695e32b57 100644 --- a/homeassistant/components/sense/sensor.py +++ b/homeassistant/components/sense/sensor.py @@ -15,6 +15,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -280,13 +281,13 @@ class SenseTrendsSensor(CoordinatorEntity, SensorEntity): self._attr_entity_registry_enabled_default = False self._attr_state_class = None self._attr_device_class = None - self._attr_device_info = { - "name": f"Sense {sense_monitor_id}", - "identifiers": {(DOMAIN, sense_monitor_id)}, - "model": "Sense", - "manufacturer": "Sense Labs, Inc.", - "configuration_url": "https://home.sense.com", - } + self._attr_device_info = DeviceInfo( + name=f"Sense {sense_monitor_id}", + identifiers={(DOMAIN, sense_monitor_id)}, + model="Sense", + manufacturer="Sense Labs, Inc.", + configuration_url="https://home.sense.com", + ) @property def native_value(self): diff --git a/homeassistant/components/sonarr/entity.py b/homeassistant/components/sonarr/entity.py index 4d6eae8a669..8f3f1188bac 100644 --- a/homeassistant/components/sonarr/entity.py +++ b/homeassistant/components/sonarr/entity.py @@ -3,12 +3,6 @@ from __future__ import annotations from sonarr import Sonarr -from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_NAME, - ATTR_SW_VERSION, -) from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN @@ -39,11 +33,11 @@ class SonarrEntity(Entity): configuration_url += f"{self.sonarr.host}:{self.sonarr.port}" configuration_url += self.sonarr.base_path.replace("/api", "") - return { - ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)}, - ATTR_NAME: "Activity Sensor", - ATTR_MANUFACTURER: "Sonarr", - ATTR_SW_VERSION: self.sonarr.app.info.version, - "entry_type": "service", - "configuration_url": configuration_url, - } + return DeviceInfo( + identifiers={(DOMAIN, self._device_id)}, + name="Activity Sensor", + manufacturer="Sonarr", + sw_version=self.sonarr.app.info.version, + entry_type="service", + configuration_url=configuration_url, + ) diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py index 0d53c848ae9..1d4eb6e1cdd 100644 --- a/homeassistant/components/sonos/entity.py +++ b/homeassistant/components/sonos/entity.py @@ -90,19 +90,19 @@ class SonosEntity(Entity): @property def device_info(self) -> DeviceInfo: """Return information about the device.""" - return { - "identifiers": {(DOMAIN, self.soco.uid)}, - "name": self.speaker.zone_name, - "model": self.speaker.model_name.replace("Sonos ", ""), - "sw_version": self.speaker.version, - "connections": { + return DeviceInfo( + identifiers={(DOMAIN, self.soco.uid)}, + name=self.speaker.zone_name, + model=self.speaker.model_name.replace("Sonos ", ""), + sw_version=self.speaker.version, + connections={ (dr.CONNECTION_NETWORK_MAC, self.speaker.mac_address), (dr.CONNECTION_UPNP, f"uuid:{self.speaker.uid}"), }, - "manufacturer": "Sonos", - "suggested_area": self.speaker.zone_name, - "configuration_url": f"http://{self.soco.ip_address}:1400/support/review", - } + manufacturer="Sonos", + suggested_area=self.speaker.zone_name, + configuration_url=f"http://{self.soco.ip_address}:1400/support/review", + ) @property def available(self) -> bool: diff --git a/homeassistant/components/uptimerobot/entity.py b/homeassistant/components/uptimerobot/entity.py index 8dde12c6091..eee3d774d98 100644 --- a/homeassistant/components/uptimerobot/entity.py +++ b/homeassistant/components/uptimerobot/entity.py @@ -3,7 +3,7 @@ from __future__ import annotations from pyuptimerobot import UptimeRobotMonitor -from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -27,14 +27,14 @@ class UptimeRobotEntity(CoordinatorEntity): super().__init__(coordinator) self.entity_description = description self._monitor = monitor - self._attr_device_info = { - "identifiers": {(DOMAIN, str(self.monitor.id))}, - "name": self.monitor.friendly_name, - "manufacturer": "UptimeRobot Team", - "entry_type": "service", - "model": self.monitor.type.name, - "configuration_url": f"https://uptimerobot.com/dashboard#{self.monitor.id}", - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, str(self.monitor.id))}, + name=self.monitor.friendly_name, + manufacturer="UptimeRobot Team", + entry_type="service", + model=self.monitor.type.name, + configuration_url=f"https://uptimerobot.com/dashboard#{self.monitor.id}", + ) self._attr_extra_state_attributes = { ATTR_TARGET: self.monitor.url, } From da7b67cc29ecd4e952a885777adffdd33b4ef136 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 22 Oct 2021 12:20:39 -0400 Subject: [PATCH 0679/1038] Add strict typing to efergy (#57682) --- .strict-typing | 1 + homeassistant/components/efergy/config_flow.py | 2 +- homeassistant/components/efergy/sensor.py | 6 +++--- mypy.ini | 11 +++++++++++ 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.strict-typing b/.strict-typing index 5b0bbd569d7..e764449d1d6 100644 --- a/.strict-typing +++ b/.strict-typing @@ -35,6 +35,7 @@ homeassistant.components.dlna_dmr.* homeassistant.components.dnsip.* homeassistant.components.dsmr.* homeassistant.components.dunehd.* +homeassistant.components.efergy.* homeassistant.components.elgato.* homeassistant.components.esphome.* homeassistant.components.energy.* diff --git a/homeassistant/components/efergy/config_flow.py b/homeassistant/components/efergy/config_flow.py index 3fb5fbec4a6..f386b539768 100644 --- a/homeassistant/components/efergy/config_flow.py +++ b/homeassistant/components/efergy/config_flow.py @@ -56,7 +56,7 @@ class EfergyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, import_config: ConfigType): + async def async_step_import(self, import_config: ConfigType) -> FlowResult: """Import a config entry from configuration.yaml.""" for entry in self._async_current_entries(): if entry.data[CONF_API_KEY] == import_config[CONF_APPTOKEN]: diff --git a/homeassistant/components/efergy/sensor.py b/homeassistant/components/efergy/sensor.py index cc12fd8486e..1f0db12f449 100644 --- a/homeassistant/components/efergy/sensor.py +++ b/homeassistant/components/efergy/sensor.py @@ -143,7 +143,7 @@ async def async_setup_platform( hass: HomeAssistant, config: ConfigType, add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType = None, + discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Efergy sensor from yaml.""" hass.async_create_task( @@ -194,8 +194,8 @@ class EfergySensor(EfergyEntity, SensorEntity): api: Efergy, description: SensorEntityDescription, server_unique_id: str, - period: str = None, - currency: str = None, + period: str | None = None, + currency: str | None = None, sid: str = "", ) -> None: """Initialize the sensor.""" diff --git a/mypy.ini b/mypy.ini index 5335cde9be2..a429c04caa8 100644 --- a/mypy.ini +++ b/mypy.ini @@ -396,6 +396,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.efergy.*] +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.elgato.*] check_untyped_defs = true disallow_incomplete_defs = true From a3b3c4ebad51226bc357814ba4008696fd315f61 Mon Sep 17 00:00:00 2001 From: Paul Monigatti Date: Sat, 23 Oct 2021 05:21:41 +1300 Subject: [PATCH 0680/1038] Consolidate ESPHome icon-handling code into EsphomeEntity (#57744) --- homeassistant/components/esphome/__init__.py | 11 +++++++++++ homeassistant/components/esphome/number.py | 12 ------------ homeassistant/components/esphome/select.py | 13 ------------- homeassistant/components/esphome/sensor.py | 17 ----------------- homeassistant/components/esphome/switch.py | 5 ----- 5 files changed, 11 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index cffb8dc8ad9..f5fd765dd71 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -637,6 +637,9 @@ class EsphomeEnumMapper(Generic[_EnumT, _ValT]): return self._inverse[value] +ICON_SCHEMA = vol.Schema(cv.icon) + + class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): """Define a base esphome entity.""" @@ -761,6 +764,14 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): """Return the name of the entity.""" return self._static_info.name + @property + def icon(self) -> str | None: + """Return the icon.""" + if not self._static_info.icon: + return None + + return cast(str, ICON_SCHEMA(self._static_info.icon)) + @property def should_poll(self) -> bool: """Disable polling.""" diff --git a/homeassistant/components/esphome/number.py b/homeassistant/components/esphome/number.py index 1a90cdbeb24..c8baa1f112e 100644 --- a/homeassistant/components/esphome/number.py +++ b/homeassistant/components/esphome/number.py @@ -2,21 +2,16 @@ from __future__ import annotations import math -from typing import cast from aioesphomeapi import NumberInfo, NumberState -import voluptuous as vol from homeassistant.components.number import NumberEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry -ICON_SCHEMA = vol.Schema(cv.icon) - async def async_setup_entry( hass: HomeAssistant, @@ -42,13 +37,6 @@ async def async_setup_entry( class EsphomeNumber(EsphomeEntity[NumberInfo, NumberState], NumberEntity): """A number implementation for esphome.""" - @property - def icon(self) -> str | None: - """Return the icon.""" - if not self._static_info.icon: - return None - return cast(str, ICON_SCHEMA(self._static_info.icon)) - @property def min_value(self) -> float: """Return the minimum value.""" diff --git a/homeassistant/components/esphome/select.py b/homeassistant/components/esphome/select.py index 6ba6ba4c594..f3bfcb982ea 100644 --- a/homeassistant/components/esphome/select.py +++ b/homeassistant/components/esphome/select.py @@ -1,21 +1,15 @@ """Support for esphome selects.""" from __future__ import annotations -from typing import cast - from aioesphomeapi import SelectInfo, SelectState -import voluptuous as vol from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry -ICON_SCHEMA = vol.Schema(cv.icon) - async def async_setup_entry( hass: HomeAssistant, @@ -41,13 +35,6 @@ async def async_setup_entry( class EsphomeSelect(EsphomeEntity[SelectInfo, SelectState], SelectEntity): """A select implementation for esphome.""" - @property - def icon(self) -> str | None: - """Return the icon.""" - if not self._static_info.icon: - return None - return cast(str, ICON_SCHEMA(self._static_info.icon)) - @property def options(self) -> list[str]: """Return a set of selectable options.""" diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index c0ea9f0f9c5..b2758c91b68 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations import math -from typing import cast from aioesphomeapi import ( SensorInfo, @@ -12,7 +11,6 @@ from aioesphomeapi import ( TextSensorState, ) from aioesphomeapi.model import LastResetType -import voluptuous as vol from homeassistant.components.sensor import ( DEVICE_CLASS_TIMESTAMP, @@ -23,7 +21,6 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt @@ -34,8 +31,6 @@ from . import ( platform_async_setup_entry, ) -ICON_SCHEMA = vol.Schema(cv.icon) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -77,13 +72,6 @@ _STATE_CLASSES: EsphomeEnumMapper[SensorStateClass, str | None] = EsphomeEnumMap class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity): """A sensor implementation for esphome.""" - @property - def icon(self) -> str | None: - """Return the icon.""" - if not self._static_info.icon or self._static_info.device_class: - return None - return cast(str, ICON_SCHEMA(self._static_info.icon)) - @property def force_update(self) -> bool: """Return if this sensor should force a state update.""" @@ -133,11 +121,6 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity): class EsphomeTextSensor(EsphomeEntity[TextSensorInfo, TextSensorState], SensorEntity): """A text sensor implementation for ESPHome.""" - @property - def icon(self) -> str: - """Return the icon.""" - return self._static_info.icon - @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 218cd3905b0..a8f0febf5b0 100644 --- a/homeassistant/components/esphome/switch.py +++ b/homeassistant/components/esphome/switch.py @@ -35,11 +35,6 @@ async def async_setup_entry( class EsphomeSwitch(EsphomeEntity[SwitchInfo, SwitchState], SwitchEntity): """A switch implementation for ESPHome.""" - @property - def icon(self) -> str: - """Return the icon.""" - return self._static_info.icon - @property def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" From efbe8a8689e63ae8cc71cc890b084845eddcecb8 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Fri, 22 Oct 2021 18:24:12 +0200 Subject: [PATCH 0681/1038] Fix unit of measurement for P1 Montior (#57495) --- homeassistant/components/p1_monitor/sensor.py | 21 +++++++++---------- tests/components/p1_monitor/test_sensor.py | 15 ++++++++----- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/p1_monitor/sensor.py b/homeassistant/components/p1_monitor/sensor.py index ea18854f748..8c86d3d4529 100644 --- a/homeassistant/components/p1_monitor/sensor.py +++ b/homeassistant/components/p1_monitor/sensor.py @@ -19,7 +19,6 @@ from homeassistant.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, DEVICE_CLASS_GAS, - DEVICE_CLASS_MONETARY, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, ELECTRIC_CURRENT_AMPERE, @@ -195,32 +194,32 @@ SENSORS: dict[ key="gas_consumption_price", name="Gas Consumption Price", entity_registry_enabled_default=False, - device_class=DEVICE_CLASS_MONETARY, - native_unit_of_measurement=CURRENCY_EURO, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=f"{CURRENCY_EURO}/{VOLUME_CUBIC_METERS}", ), SensorEntityDescription( key="energy_consumption_price_low", name="Energy Consumption Price - Low", - device_class=DEVICE_CLASS_MONETARY, - native_unit_of_measurement=CURRENCY_EURO, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}", ), SensorEntityDescription( key="energy_consumption_price_high", name="Energy Consumption Price - High", - device_class=DEVICE_CLASS_MONETARY, - native_unit_of_measurement=CURRENCY_EURO, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}", ), SensorEntityDescription( key="energy_production_price_low", name="Energy Production Price - Low", - device_class=DEVICE_CLASS_MONETARY, - native_unit_of_measurement=CURRENCY_EURO, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}", ), SensorEntityDescription( key="energy_production_price_high", name="Energy Production Price - High", - device_class=DEVICE_CLASS_MONETARY, - native_unit_of_measurement=CURRENCY_EURO, + state_class=STATE_CLASS_MEASUREMENT, + native_unit_of_measurement=f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}", ), ), } diff --git a/tests/components/p1_monitor/test_sensor.py b/tests/components/p1_monitor/test_sensor.py index 90733ce8941..960f68315e5 100644 --- a/tests/components/p1_monitor/test_sensor.py +++ b/tests/components/p1_monitor/test_sensor.py @@ -15,7 +15,6 @@ from homeassistant.const import ( CURRENCY_EURO, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, - DEVICE_CLASS_MONETARY, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, ELECTRIC_CURRENT_AMPERE, @@ -158,8 +157,11 @@ async def test_settings( assert entry.unique_id == f"{entry_id}_settings_energy_consumption_price_low" assert state.state == "0.20522" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy Consumption Price - Low" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_MONETARY - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CURRENCY_EURO + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == 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") @@ -168,8 +170,11 @@ async def test_settings( assert entry.unique_id == f"{entry_id}_settings_energy_production_price_low" assert state.state == "0.20522" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy Production Price - Low" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_MONETARY - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CURRENCY_EURO + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}" + ) assert entry.device_id device_entry = device_registry.async_get(entry.device_id) From 5a102d793e8cd62b9b99b4388f291d09a81cc987 Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Fri, 22 Oct 2021 19:25:01 +0300 Subject: [PATCH 0682/1038] Bump pylgnetcast to 0.3.4 (#58233) --- homeassistant/components/lg_netcast/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lg_netcast/manifest.json b/homeassistant/components/lg_netcast/manifest.json index 7ee8eb063ff..556dbec6cd6 100644 --- a/homeassistant/components/lg_netcast/manifest.json +++ b/homeassistant/components/lg_netcast/manifest.json @@ -2,7 +2,7 @@ "domain": "lg_netcast", "name": "LG Netcast", "documentation": "https://www.home-assistant.io/integrations/lg_netcast", - "requirements": ["pylgnetcast==0.3.3"], + "requirements": ["pylgnetcast==0.3.4"], "codeowners": ["@Drafteed"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 18783fa64ec..281b47e3471 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1589,7 +1589,7 @@ pylast==4.2.1 pylaunches==1.0.0 # homeassistant.components.lg_netcast -pylgnetcast==0.3.3 +pylgnetcast==0.3.4 # homeassistant.components.forked_daapd pylibrespot-java==0.1.0 From 1aa7a8170ca3a507f7a861953824beeeb981edfb Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Fri, 22 Oct 2021 18:46:21 +0200 Subject: [PATCH 0683/1038] Fix plugwise longterm statistics (#58127) Co-authored-by: Paulus Schoutsen --- homeassistant/components/plugwise/const.py | 2 +- homeassistant/components/plugwise/sensor.py | 84 ++++++++++++++++++--- 2 files changed, 75 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py index a7a5c92a21c..2c1867346c4 100644 --- a/homeassistant/components/plugwise/const.py +++ b/homeassistant/components/plugwise/const.py @@ -31,8 +31,8 @@ ZEROCONF_MAP = { # Sensor mapping SENSOR_MAP_DEVICE_CLASS = 2 -SENSOR_MAP_ICON = 3 SENSOR_MAP_MODEL = 0 +SENSOR_MAP_STATE_CLASS = 3 SENSOR_MAP_UOM = 1 # Default directives diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index 888f5bac5c2..6b33eccc753 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -2,14 +2,20 @@ import logging -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ( +from homeassistant.components.sensor import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_ENERGY, + DEVICE_CLASS_GAS, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL, + STATE_CLASS_TOTAL_INCREASING, + SensorEntity, +) +from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR, PERCENTAGE, @@ -29,6 +35,7 @@ from .const import ( IDLE_ICON, SENSOR_MAP_DEVICE_CLASS, SENSOR_MAP_MODEL, + SENSOR_MAP_STATE_CLASS, SENSOR_MAP_UOM, UNIT_LUMEN, ) @@ -40,18 +47,26 @@ ATTR_TEMPERATURE = [ "Temperature", TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, ] ATTR_BATTERY_LEVEL = [ "Charge", PERCENTAGE, DEVICE_CLASS_BATTERY, + STATE_CLASS_MEASUREMENT, ] ATTR_ILLUMINANCE = [ "Illuminance", UNIT_LUMEN, DEVICE_CLASS_ILLUMINANCE, + STATE_CLASS_MEASUREMENT, +] +ATTR_PRESSURE = [ + "Pressure", + PRESSURE_BAR, + DEVICE_CLASS_PRESSURE, + STATE_CLASS_MEASUREMENT, ] -ATTR_PRESSURE = ["Pressure", PRESSURE_BAR, DEVICE_CLASS_PRESSURE] TEMP_SENSOR_MAP = { "setpoint": ATTR_TEMPERATURE, @@ -64,97 +79,138 @@ TEMP_SENSOR_MAP = { } ENERGY_SENSOR_MAP = { - "electricity_consumed": ["Current Consumed Power", POWER_WATT, DEVICE_CLASS_POWER], - "electricity_produced": ["Current Produced Power", POWER_WATT, DEVICE_CLASS_POWER], + "electricity_consumed": [ + "Current Consumed Power", + POWER_WATT, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, + ], + "electricity_produced": [ + "Current Produced Power", + POWER_WATT, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, + ], "electricity_consumed_interval": [ "Consumed Power Interval", ENERGY_WATT_HOUR, DEVICE_CLASS_ENERGY, + STATE_CLASS_TOTAL, ], "electricity_consumed_peak_interval": [ "Consumed Power Interval", ENERGY_WATT_HOUR, DEVICE_CLASS_ENERGY, + STATE_CLASS_TOTAL, ], "electricity_consumed_off_peak_interval": [ "Consumed Power Interval (off peak)", ENERGY_WATT_HOUR, DEVICE_CLASS_ENERGY, + STATE_CLASS_TOTAL, ], "electricity_produced_interval": [ "Produced Power Interval", ENERGY_WATT_HOUR, DEVICE_CLASS_ENERGY, + STATE_CLASS_TOTAL, ], "electricity_produced_peak_interval": [ "Produced Power Interval", ENERGY_WATT_HOUR, DEVICE_CLASS_ENERGY, + STATE_CLASS_TOTAL, ], "electricity_produced_off_peak_interval": [ "Produced Power Interval (off peak)", ENERGY_WATT_HOUR, DEVICE_CLASS_ENERGY, + STATE_CLASS_TOTAL, ], "electricity_consumed_off_peak_point": [ "Current Consumed Power (off peak)", POWER_WATT, DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, ], "electricity_consumed_peak_point": [ "Current Consumed Power", POWER_WATT, DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, ], "electricity_consumed_off_peak_cumulative": [ "Cumulative Consumed Power (off peak)", ENERGY_KILO_WATT_HOUR, DEVICE_CLASS_ENERGY, + STATE_CLASS_TOTAL_INCREASING, ], "electricity_consumed_peak_cumulative": [ "Cumulative Consumed Power", ENERGY_KILO_WATT_HOUR, DEVICE_CLASS_ENERGY, + STATE_CLASS_TOTAL_INCREASING, ], "electricity_produced_off_peak_point": [ "Current Produced Power (off peak)", POWER_WATT, DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, ], "electricity_produced_peak_point": [ "Current Produced Power", POWER_WATT, DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, ], "electricity_produced_off_peak_cumulative": [ "Cumulative Produced Power (off peak)", ENERGY_KILO_WATT_HOUR, DEVICE_CLASS_ENERGY, + STATE_CLASS_TOTAL_INCREASING, ], "electricity_produced_peak_cumulative": [ "Cumulative Produced Power", ENERGY_KILO_WATT_HOUR, DEVICE_CLASS_ENERGY, + STATE_CLASS_TOTAL_INCREASING, ], "gas_consumed_interval": [ "Current Consumed Gas Interval", VOLUME_CUBIC_METERS, - None, + DEVICE_CLASS_GAS, + STATE_CLASS_TOTAL, + ], + "gas_consumed_cumulative": [ + "Consumed Gas", + VOLUME_CUBIC_METERS, + DEVICE_CLASS_GAS, + STATE_CLASS_TOTAL_INCREASING, + ], + "net_electricity_point": [ + "Current net Power", + POWER_WATT, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, ], - "gas_consumed_cumulative": ["Cumulative Consumed Gas", VOLUME_CUBIC_METERS, None], - "net_electricity_point": ["Current net Power", POWER_WATT, DEVICE_CLASS_POWER], "net_electricity_cumulative": [ "Cumulative net Power", ENERGY_KILO_WATT_HOUR, DEVICE_CLASS_ENERGY, + STATE_CLASS_TOTAL, ], } MISC_SENSOR_MAP = { "battery": ATTR_BATTERY_LEVEL, "illuminance": ATTR_ILLUMINANCE, - "modulation_level": ["Heater Modulation Level", PERCENTAGE, None], - "valve_position": ["Valve Position", PERCENTAGE, None], + "modulation_level": [ + "Heater Modulation Level", + PERCENTAGE, + None, + STATE_CLASS_MEASUREMENT, + ], + "valve_position": ["Valve Position", PERCENTAGE, None, STATE_CLASS_MEASUREMENT], "water_pressure": ATTR_PRESSURE, } @@ -249,6 +305,7 @@ class SmileSensor(SmileGateway, SensorEntity): self._dev_class = None self._icon = None self._state = None + self._state_class = None self._unit_of_measurement = None if dev_id == self._api.heater_id: @@ -282,6 +339,11 @@ class SmileSensor(SmileGateway, SensorEntity): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement + @property + def state_class(self): + """Return the state_class of this entity.""" + return self._state_class + class PwThermostatSensor(SmileSensor): """Thermostat (or generic) sensor devices.""" @@ -294,6 +356,7 @@ class PwThermostatSensor(SmileSensor): self._model = sensor_type[SENSOR_MAP_MODEL] self._unit_of_measurement = sensor_type[SENSOR_MAP_UOM] self._dev_class = sensor_type[SENSOR_MAP_DEVICE_CLASS] + self._state_class = sensor_type[SENSOR_MAP_STATE_CLASS] @callback def _async_process_data(self): @@ -363,6 +426,7 @@ class PwPowerSensor(SmileSensor): self._unit_of_measurement = sensor_type[SENSOR_MAP_UOM] self._dev_class = sensor_type[SENSOR_MAP_DEVICE_CLASS] + self._state_class = sensor_type[SENSOR_MAP_STATE_CLASS] if dev_id == self._api.gateway_id: self._model = "P1 DSMR" From fa56be7cc0f7bf9e8c6688abea531e9bf992ef97 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 22 Oct 2021 18:55:13 +0200 Subject: [PATCH 0684/1038] Use value_fn for bmw_connected_drive binary_sensor (#57540) --- .../bmw_connected_drive/binary_sensor.py | 246 ++++++++++++------ 1 file changed, 165 insertions(+), 81 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index d2d2aa9d42f..14d75a70591 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -1,9 +1,14 @@ """Reads vehicle status from BMW connected drive portal.""" from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass import logging +from typing import Any from bimmer_connected.state import ChargingState, LockState +from bimmer_connected.vehicle import ConnectedDriveVehicle +from bimmer_connected.vehicle_status import ConditionBasedServiceReport, VehicleStatus from homeassistant.components.binary_sensor import ( DEVICE_CLASS_OPENING, @@ -12,72 +17,205 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import LENGTH_KILOMETERS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util.unit_system import UnitSystem -from . import DOMAIN as BMW_DOMAIN, BMWConnectedDriveBaseEntity +from . import ( + DOMAIN as BMW_DOMAIN, + BMWConnectedDriveAccount, + BMWConnectedDriveBaseEntity, +) from .const import CONF_ACCOUNT, DATA_ENTRIES _LOGGER = logging.getLogger(__name__) -SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( - BinarySensorEntityDescription( + +def _are_doors_closed( + vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], *args: Any +) -> bool: + # device class opening: On means open, Off means closed + _LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed) + for lid in vehicle_state.lids: + extra_attributes[lid.name] = lid.state.value + return not vehicle_state.all_lids_closed + + +def _are_windows_closed( + vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], *args: Any +) -> bool: + # device class opening: On means open, Off means closed + for window in vehicle_state.windows: + extra_attributes[window.name] = window.state.value + return not vehicle_state.all_windows_closed + + +def _are_doors_locked( + vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], *args: Any +) -> bool: + # device class lock: On means unlocked, Off means locked + # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED + extra_attributes["door_lock_state"] = vehicle_state.door_lock_state.value + extra_attributes["last_update_reason"] = vehicle_state.last_update_reason + return vehicle_state.door_lock_state not in {LockState.LOCKED, LockState.SECURED} + + +def _are_parking_lights_on( + vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], *args: Any +) -> bool: + # device class light: On means light detected, Off means no light + extra_attributes["lights_parking"] = vehicle_state.parking_lights.value + return vehicle_state.are_parking_lights_on + + +def _are_problems_detected( + vehicle_state: VehicleStatus, + extra_attributes: dict[str, Any], + unit_system: UnitSystem, +) -> bool: + # device class problem: On means problem detected, Off means no problem + for report in vehicle_state.condition_based_services: + extra_attributes.update(_format_cbs_report(report, unit_system)) + return not vehicle_state.are_all_cbs_ok + + +def _check_control_messages( + vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], *args: Any +) -> bool: + # device class problem: On means problem detected, Off means no problem + check_control_messages = vehicle_state.check_control_messages + has_check_control_messages = vehicle_state.has_check_control_messages + if has_check_control_messages: + cbs_list = [message.description_short for message in check_control_messages] + extra_attributes["check_control_messages"] = cbs_list + else: + extra_attributes["check_control_messages"] = "OK" + return vehicle_state.has_check_control_messages + + +def _is_vehicle_charging( + vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], *args: Any +) -> bool: + # device class power: On means power detected, Off means no power + extra_attributes["charging_status"] = vehicle_state.charging_status.value + extra_attributes[ + "last_charging_end_result" + ] = vehicle_state.last_charging_end_result + return vehicle_state.charging_status == ChargingState.CHARGING + + +def _is_vehicle_plugged_in( + vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], *args: Any +) -> bool: + # device class plug: On means device is plugged in, + # Off means device is unplugged + extra_attributes["connection_status"] = vehicle_state.connection_status + return vehicle_state.connection_status == "CONNECTED" + + +def _format_cbs_report( + report: ConditionBasedServiceReport, 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 + if report.due_date is not None: + result[f"{service_type} date"] = report.due_date.strftime("%Y-%m-%d") + if report.due_distance is not None: + distance = round(unit_system.length(report.due_distance, LENGTH_KILOMETERS)) + result[f"{service_type} distance"] = f"{distance} {unit_system.length_unit}" + return result + + +@dataclass +class BMWRequiredKeysMixin: + """Mixin for required keys.""" + + value_fn: Callable[[VehicleStatus, dict[str, Any], UnitSystem], bool] + + +@dataclass +class BMWBinarySensorEntityDescription( + BinarySensorEntityDescription, BMWRequiredKeysMixin +): + """Describes BMW binary_sensor entity.""" + + +SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = ( + BMWBinarySensorEntityDescription( key="lids", name="Doors", device_class=DEVICE_CLASS_OPENING, icon="mdi:car-door-lock", + value_fn=_are_doors_closed, ), - BinarySensorEntityDescription( + BMWBinarySensorEntityDescription( key="windows", name="Windows", device_class=DEVICE_CLASS_OPENING, icon="mdi:car-door", + value_fn=_are_windows_closed, ), - BinarySensorEntityDescription( + BMWBinarySensorEntityDescription( key="door_lock_state", name="Door lock state", device_class="lock", icon="mdi:car-key", + value_fn=_are_doors_locked, ), - BinarySensorEntityDescription( + BMWBinarySensorEntityDescription( key="lights_parking", name="Parking lights", device_class="light", icon="mdi:car-parking-lights", + value_fn=_are_parking_lights_on, ), - BinarySensorEntityDescription( + BMWBinarySensorEntityDescription( key="condition_based_services", name="Condition based services", device_class=DEVICE_CLASS_PROBLEM, icon="mdi:wrench", + value_fn=_are_problems_detected, ), - BinarySensorEntityDescription( + BMWBinarySensorEntityDescription( key="check_control_messages", name="Control messages", device_class=DEVICE_CLASS_PROBLEM, icon="mdi:car-tire-alert", + value_fn=_check_control_messages, ), # electric - BinarySensorEntityDescription( + BMWBinarySensorEntityDescription( key="charging_status", name="Charging status", device_class="power", icon="mdi:ev-station", + value_fn=_is_vehicle_charging, ), - BinarySensorEntityDescription( + BMWBinarySensorEntityDescription( key="connection_status", name="Connection status", device_class=DEVICE_CLASS_PLUG, icon="mdi:car-electric", + value_fn=_is_vehicle_plugged_in, ), ) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the BMW ConnectedDrive binary sensors from config entry.""" - account = hass.data[BMW_DOMAIN][DATA_ENTRIES][config_entry.entry_id][CONF_ACCOUNT] + account: BMWConnectedDriveAccount = hass.data[BMW_DOMAIN][DATA_ENTRIES][ + config_entry.entry_id + ][CONF_ACCOUNT] entities = [ - BMWConnectedDriveSensor(account, vehicle, description) + BMWConnectedDriveSensor(account, vehicle, description, hass.config.units) for vehicle in account.account.vehicles for description in SENSOR_TYPES if description.key in vehicle.available_attributes @@ -88,83 +226,29 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity): """Representation of a BMW vehicle binary sensor.""" - def __init__(self, account, vehicle, description: BinarySensorEntityDescription): + entity_description: BMWBinarySensorEntityDescription + + def __init__( + self, + account: BMWConnectedDriveAccount, + vehicle: ConnectedDriveVehicle, + description: BMWBinarySensorEntityDescription, + unit_system: UnitSystem, + ) -> None: """Initialize sensor.""" super().__init__(account, 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}" - def update(self): + def update(self) -> None: """Read new state data from the library.""" - sensor_type = self.entity_description.key vehicle_state = self._vehicle.state result = self._attrs.copy() - # device class opening: On means open, Off means closed - if sensor_type == "lids": - _LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed) - self._attr_is_on = not vehicle_state.all_lids_closed - for lid in vehicle_state.lids: - result[lid.name] = lid.state.value - elif sensor_type == "windows": - self._attr_is_on = not vehicle_state.all_windows_closed - for window in vehicle_state.windows: - result[window.name] = window.state.value - # device class lock: On means unlocked, Off means locked - elif sensor_type == "door_lock_state": - # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED - self._attr_is_on = vehicle_state.door_lock_state not in [ - LockState.LOCKED, - LockState.SECURED, - ] - result["door_lock_state"] = vehicle_state.door_lock_state.value - result["last_update_reason"] = vehicle_state.last_update_reason - # device class light: On means light detected, Off means no light - elif sensor_type == "lights_parking": - self._attr_is_on = vehicle_state.are_parking_lights_on - result["lights_parking"] = vehicle_state.parking_lights.value - # device class problem: On means problem detected, Off means no problem - elif sensor_type == "condition_based_services": - self._attr_is_on = not vehicle_state.are_all_cbs_ok - for report in vehicle_state.condition_based_services: - result.update(self._format_cbs_report(report)) - elif sensor_type == "check_control_messages": - self._attr_is_on = vehicle_state.has_check_control_messages - check_control_messages = vehicle_state.check_control_messages - has_check_control_messages = vehicle_state.has_check_control_messages - if has_check_control_messages: - cbs_list = [] - for message in check_control_messages: - cbs_list.append(message.description_short) - result["check_control_messages"] = cbs_list - else: - result["check_control_messages"] = "OK" - # device class power: On means power detected, Off means no power - elif sensor_type == "charging_status": - self._attr_is_on = vehicle_state.charging_status in [ChargingState.CHARGING] - result["charging_status"] = vehicle_state.charging_status.value - result["last_charging_end_result"] = vehicle_state.last_charging_end_result - # device class plug: On means device is plugged in, - # Off means device is unplugged - elif sensor_type == "connection_status": - self._attr_is_on = vehicle_state.connection_status == "CONNECTED" - result["connection_status"] = vehicle_state.connection_status - + self._attr_is_on = self.entity_description.value_fn( + vehicle_state, result, self._unit_system + ) self._attr_extra_state_attributes = result - - def _format_cbs_report(self, report): - result = {} - service_type = report.service_type.lower().replace("_", " ") - result[f"{service_type} status"] = report.state.value - if report.due_date is not None: - result[f"{service_type} date"] = report.due_date.strftime("%Y-%m-%d") - if report.due_distance is not None: - distance = round( - self.hass.config.units.length(report.due_distance, LENGTH_KILOMETERS) - ) - result[ - f"{service_type} distance" - ] = f"{distance} {self.hass.config.units.length_unit}" - return result From 3b7dce8b95c925c21e599a0d88a9b738f1ff89b1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 22 Oct 2021 07:19:49 -1000 Subject: [PATCH 0685/1038] Index in-progress flows to avoid linear search (#58146) Co-authored-by: Steven Looman --- homeassistant/components/auth/login_flow.py | 11 +-- homeassistant/components/point/config_flow.py | 2 +- .../components/smartthings/__init__.py | 6 +- .../components/smartthings/smartapp.py | 4 +- homeassistant/components/withings/common.py | 4 +- homeassistant/components/zha/config_flow.py | 5 +- homeassistant/config_entries.py | 42 +++++---- homeassistant/data_entry_flow.py | 90 ++++++++++++++----- tests/components/auth/test_login_flow.py | 40 +++++++++ tests/test_config_entries.py | 12 +-- tests/test_data_entry_flow.py | 38 ++++++++ 11 files changed, 190 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index e660832487a..ed5c544499e 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -231,14 +231,9 @@ class LoginFlowResourceView(HomeAssistantView): try: # do not allow change ip during login flow - for flow in self._flow_mgr.async_progress(): - if flow["flow_id"] == flow_id and flow["context"][ - "ip_address" - ] != ip_address(request.remote): - return self.json_message( - "IP address changed", HTTPStatus.BAD_REQUEST - ) - + flow = self._flow_mgr.async_get(flow_id) + if flow["context"]["ip_address"] != ip_address(request.remote): + 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: return self.json_message("Invalid flow specified", HTTPStatus.NOT_FOUND) diff --git a/homeassistant/components/point/config_flow.py b/homeassistant/components/point/config_flow.py index 3b9ba84fab5..fbcbcd02a2b 100644 --- a/homeassistant/components/point/config_flow.py +++ b/homeassistant/components/point/config_flow.py @@ -131,7 +131,7 @@ class PointFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.debug( "Should close all flows below %s", - self.hass.config_entries.flow.async_progress(), + self._async_in_progress(), ) # Remove notification if no other discovery config entries in progress diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 44a360a2e55..5612cd65732 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -73,8 +73,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Remove the entry which will invoke the callback to delete the app. hass.async_create_task(hass.config_entries.async_remove(entry.entry_id)) # only create new flow if there isn't a pending one for SmartThings. - flows = hass.config_entries.flow.async_progress() - if not [flow for flow in flows if flow["handler"] == DOMAIN]: + if not hass.config_entries.flow.async_progress_by_handler(DOMAIN): hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT} @@ -181,8 +180,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if remove_entry: hass.async_create_task(hass.config_entries.async_remove(entry.entry_id)) # only create new flow if there isn't a pending one for SmartThings. - flows = hass.config_entries.flow.async_progress() - if not [flow for flow in flows if flow["handler"] == DOMAIN]: + if not hass.config_entries.flow.async_progress_by_handler(DOMAIN): hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT} diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index 36f2610b981..2086d564753 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -406,8 +406,8 @@ async def _continue_flow( flow = next( ( flow - for flow in hass.config_entries.flow.async_progress() - if flow["handler"] == DOMAIN and flow["context"]["unique_id"] == unique_id + for flow in hass.config_entries.flow.async_progress_by_handler(DOMAIN) + if flow["context"]["unique_id"] == unique_id ), None, ) diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index 9e4beff8c38..83c5653afdf 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -745,7 +745,9 @@ class DataManager: flow = next( iter( flow - for flow in self._hass.config_entries.flow.async_progress() + for flow in self._hass.config_entries.flow.async_progress_by_handler( + const.DOMAIN + ) if flow.context == context ), None, diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 2b867366453..a3eaffdf1ba 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -120,9 +120,8 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # If they already have a discovery for deconz # we ignore the usb discovery as they probably # want to use it there instead - for flow in self.hass.config_entries.flow.async_progress(): - if flow["handler"] == DECONZ_DOMAIN: - return self.async_abort(reason="not_zha_device") + 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): if entry.source != config_entries.SOURCE_IGNORE: return self.async_abort(reason="not_zha_device") diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 03d7df740ba..17f8b1396ed 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -586,7 +586,7 @@ class ConfigEntry: "unique_id": self.unique_id, } - for flow in hass.config_entries.flow.async_progress(): + for flow in hass.config_entries.flow.async_progress_by_handler(self.domain): if flow["context"] == flow_context: return @@ -618,6 +618,14 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): self.config_entries = config_entries self._hass_config = hass_config + @callback + def _async_has_other_discovery_flows(self, flow_id: str) -> bool: + """Check if there are any other discovery flows in progress.""" + return any( + flow.context["source"] in DISCOVERY_SOURCES and flow.flow_id != flow_id + for flow in self._progress.values() + ) + async def async_finish_flow( self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResult ) -> data_entry_flow.FlowResult: @@ -625,11 +633,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): flow = cast(ConfigFlow, flow) # Remove notification if no other discovery config entries in progress - if not any( - ent["context"]["source"] in DISCOVERY_SOURCES - for ent in self.hass.config_entries.flow.async_progress() - if ent["flow_id"] != flow.flow_id - ): + if not self._async_has_other_discovery_flows(flow.flow_id): self.hass.components.persistent_notification.async_dismiss( DISCOVERY_NOTIFICATION_ID ) @@ -642,15 +646,11 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): # Abort all flows in progress with same unique ID # or the default discovery ID - for progress_flow in self.async_progress(): + for progress_flow in self.async_progress_by_handler(flow.handler): progress_unique_id = progress_flow["context"].get("unique_id") - if ( - progress_flow["handler"] == flow.handler - and progress_flow["flow_id"] != flow.flow_id - and ( - (flow.unique_id and progress_unique_id == flow.unique_id) - or progress_unique_id == DEFAULT_DISCOVERY_UNIQUE_ID - ) + if progress_flow["flow_id"] != flow.flow_id and ( + (flow.unique_id and progress_unique_id == flow.unique_id) + or progress_unique_id == DEFAULT_DISCOVERY_UNIQUE_ID ): self.async_abort(progress_flow["flow_id"]) @@ -837,7 +837,9 @@ class ConfigEntries: # If the configuration entry is removed during reauth, it should # abort any reauth flow that is active for the removed entry. - for progress_flow in self.hass.config_entries.flow.async_progress(): + for progress_flow in self.hass.config_entries.flow.async_progress_by_handler( + entry.domain + ): context = progress_flow.get("context") if ( context @@ -1265,10 +1267,10 @@ class ConfigFlow(data_entry_flow.FlowHandler): """Return other in progress flows for current domain.""" return [ flw - for flw in self.hass.config_entries.flow.async_progress( - include_uninitialized=include_uninitialized + for flw in self.hass.config_entries.flow.async_progress_by_handler( + self.handler, include_uninitialized=include_uninitialized ) - if flw["handler"] == self.handler and flw["flow_id"] != self.flow_id + if flw["flow_id"] != self.flow_id ] async def async_step_ignore( @@ -1329,7 +1331,9 @@ class ConfigFlow(data_entry_flow.FlowHandler): # Remove reauth notification if no reauth flows are in progress if self.source == SOURCE_REAUTH and not any( ent["context"]["source"] == SOURCE_REAUTH - for ent in self.hass.config_entries.flow.async_progress() + for ent in self.hass.config_entries.flow.async_progress_by_handler( + self.handler + ) if ent["flow_id"] != self.flow_id ): self.hass.components.persistent_notification.async_dismiss( diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index c82ec3acfd7..c1f798fcc32 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations import abc import asyncio -from collections.abc import Mapping +from collections.abc import Iterable, Mapping from types import MappingProxyType from typing import Any, TypedDict import uuid @@ -78,6 +78,23 @@ class FlowResult(TypedDict, total=False): options: Mapping[str, Any] +@callback +def _async_flow_handler_to_flow_result( + flows: Iterable[FlowHandler], include_uninitialized: bool +) -> list[FlowResult]: + """Convert a list of FlowHandler to a partial FlowResult that can be serialized.""" + return [ + { + "flow_id": flow.flow_id, + "handler": flow.handler, + "context": flow.context, + "step_id": flow.cur_step["step_id"] if flow.cur_step else None, + } + for flow in flows + if include_uninitialized or flow.cur_step is not None + ] + + class FlowManager(abc.ABC): """Manage all the flows that are in progress.""" @@ -89,7 +106,8 @@ class FlowManager(abc.ABC): self.hass = hass self._initializing: dict[str, list[asyncio.Future]] = {} self._initialize_tasks: dict[str, list[asyncio.Task]] = {} - self._progress: dict[str, Any] = {} + self._progress: dict[str, FlowHandler] = {} + self._handler_progress_index: dict[str, set[str]] = {} async def async_wait_init_flow_finish(self, handler: str) -> None: """Wait till all flows in progress are initialized.""" @@ -127,24 +145,39 @@ class FlowManager(abc.ABC): """Check if an existing matching flow is in progress with the same handler, context, and data.""" return any( flow - for flow in self._progress.values() - if flow.handler == handler - and flow.context["source"] == context["source"] - and flow.init_data == data + for flow in self._async_progress_by_handler(handler) + if flow.context["source"] == context["source"] and flow.init_data == data ) + @callback + def async_get(self, flow_id: str) -> FlowResult | None: + """Return a flow in progress as a partial FlowResult.""" + if (flow := self._progress.get(flow_id)) is None: + raise UnknownFlow + return _async_flow_handler_to_flow_result([flow], False)[0] + @callback def async_progress(self, include_uninitialized: bool = False) -> list[FlowResult]: - """Return the flows in progress.""" + """Return the flows in progress as a partial FlowResult.""" + return _async_flow_handler_to_flow_result( + self._progress.values(), include_uninitialized + ) + + @callback + def async_progress_by_handler( + self, handler: str, include_uninitialized: bool = False + ) -> list[FlowResult]: + """Return the flows in progress by handler as a partial FlowResult.""" + return _async_flow_handler_to_flow_result( + self._async_progress_by_handler(handler), include_uninitialized + ) + + @callback + def _async_progress_by_handler(self, handler: str) -> list[FlowHandler]: + """Return the flows in progress by handler.""" return [ - { - "flow_id": flow.flow_id, - "handler": flow.handler, - "context": flow.context, - "step_id": flow.cur_step["step_id"] if flow.cur_step else None, - } - for flow in self._progress.values() - if include_uninitialized or flow.cur_step is not None + self._progress[flow_id] + for flow_id in self._handler_progress_index.get(handler, {}) ] async def async_init( @@ -187,7 +220,7 @@ class FlowManager(abc.ABC): flow.flow_id = uuid.uuid4().hex flow.context = context flow.init_data = data - self._progress[flow.flow_id] = flow + self._async_add_flow_progress(flow) result = await self._async_handle_step(flow, flow.init_step, data, init_done) return flow, result @@ -205,6 +238,7 @@ class FlowManager(abc.ABC): raise UnknownFlow cur_step = flow.cur_step + assert cur_step is not None if cur_step.get("data_schema") is not None and user_input is not None: user_input = cur_step["data_schema"](user_input) @@ -245,8 +279,24 @@ class FlowManager(abc.ABC): @callback def async_abort(self, flow_id: str) -> None: """Abort a flow.""" - if self._progress.pop(flow_id, None) is None: + self._async_remove_flow_progress(flow_id) + + @callback + def _async_add_flow_progress(self, flow: FlowHandler) -> None: + """Add a flow to in progress.""" + self._progress[flow.flow_id] = flow + self._handler_progress_index.setdefault(flow.handler, set()).add(flow.flow_id) + + @callback + def _async_remove_flow_progress(self, flow_id: str) -> None: + """Remove a flow from in progress.""" + flow = self._progress.pop(flow_id, None) + if flow is None: raise UnknownFlow + handler = flow.handler + self._handler_progress_index[handler].remove(flow.flow_id) + if not self._handler_progress_index[handler]: + del self._handler_progress_index[handler] async def _async_handle_step( self, @@ -259,7 +309,7 @@ class FlowManager(abc.ABC): method = f"async_step_{step_id}" if not hasattr(flow, method): - self._progress.pop(flow.flow_id) + self._async_remove_flow_progress(flow.flow_id) if step_done: step_done.set_result(None) raise UnknownStep( @@ -310,7 +360,7 @@ class FlowManager(abc.ABC): return result # Abort and Success results both finish the flow - self._progress.pop(flow.flow_id) + self._async_remove_flow_progress(flow.flow_id) return result @@ -319,7 +369,7 @@ class FlowHandler: """Handle the configuration flow of a component.""" # Set by flow manager - cur_step: dict[str, str] | None = None + cur_step: dict[str, Any] | None = None # While not purely typed, it makes typehinting more useful for us # and removes the need for constant None checks or asserts. diff --git a/tests/components/auth/test_login_flow.py b/tests/components/auth/test_login_flow.py index ce3d37598d7..72881023fe5 100644 --- a/tests/components/auth/test_login_flow.py +++ b/tests/components/auth/test_login_flow.py @@ -114,3 +114,43 @@ async def test_login_exist_user(hass, aiohttp_client): step = await resp.json() assert step["type"] == "create_entry" assert len(step["result"]) > 1 + + +async def test_login_exist_user_ip_changes(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) + cred = await hass.auth.auth_providers[0].async_get_or_create_credentials( + {"username": "test-user"} + ) + await hass.auth.async_get_or_create_user(cred) + + resp = await client.post( + "/auth/login_flow", + json={ + "client_id": CLIENT_ID, + "handler": ["insecure_example", None], + "redirect_uri": CLIENT_REDIRECT_URI, + }, + ) + assert resp.status == 200 + step = await resp.json() + + # + # Here we modify the ip_address in the context to make sure + # when ip address changes in the middle of the login flow we prevent logins. + # + # This method was chosen because it seemed less likely to break + # vs patching aiohttp internals to fake the ip address + # + for flow_id, flow in hass.auth.login_flow._progress.items(): + assert flow_id == step["flow_id"] + flow.context["ip_address"] = "10.2.3.1" + + resp = await client.post( + f"/auth/login_flow/{step['flow_id']}", + json={"client_id": CLIENT_ID, "username": "test-user", "password": "test-pass"}, + ) + + assert resp.status == 400 + response = await resp.json() + assert response == {"message": "IP address changed"} diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 0b146c2f612..85d64de70a2 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -349,7 +349,7 @@ async def test_remove_entry_cancels_reauth(hass, manager): await entry.async_setup(hass) await hass.async_block_till_done() - flows = hass.config_entries.flow.async_progress() + flows = hass.config_entries.flow.async_progress_by_handler("test") assert len(flows) == 1 assert flows[0]["context"]["entry_id"] == entry.entry_id assert flows[0]["context"]["source"] == config_entries.SOURCE_REAUTH @@ -357,7 +357,7 @@ async def test_remove_entry_cancels_reauth(hass, manager): await manager.async_remove(entry.entry_id) - flows = hass.config_entries.flow.async_progress() + flows = hass.config_entries.flow.async_progress_by_handler("test") assert len(flows) == 0 @@ -2100,11 +2100,11 @@ async def test_unignore_step_form(hass, manager): # Right after removal there shouldn't be an entry or active flows assert len(hass.config_entries.async_entries("comp")) == 0 - assert len(hass.config_entries.flow.async_progress()) == 0 + assert len(hass.config_entries.flow.async_progress_by_handler("comp")) == 0 # But after a 'tick' the unignore step has run and we can see an active flow again. await hass.async_block_till_done() - assert len(hass.config_entries.flow.async_progress()) == 1 + assert len(hass.config_entries.flow.async_progress_by_handler("comp")) == 1 # and still not config entries assert len(hass.config_entries.async_entries("comp")) == 0 @@ -2144,7 +2144,7 @@ async def test_unignore_create_entry(hass, manager): await manager.async_remove(entry.entry_id) # Right after removal there shouldn't be an entry or flow - assert len(hass.config_entries.flow.async_progress()) == 0 + assert len(hass.config_entries.flow.async_progress_by_handler("comp")) == 0 assert len(hass.config_entries.async_entries("comp")) == 0 # But after a 'tick' the unignore step has run and we can see a config entry. @@ -2155,7 +2155,7 @@ async def test_unignore_create_entry(hass, manager): assert entry.title == "yo" # And still no active flow - assert len(hass.config_entries.flow.async_progress()) == 0 + assert len(hass.config_entries.flow.async_progress_by_handler("comp")) == 0 async def test_unignore_default_impl(hass, manager): diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 0aa3c01d50f..b4b40b6b6c6 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -271,6 +271,8 @@ async def test_external_step(hass, manager): result = await manager.async_init("test") assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP assert len(manager.async_progress()) == 1 + assert len(manager.async_progress_by_handler("test")) == 1 + assert manager.async_get(result["flow_id"])["handler"] == "test" # Mimic external step # Called by integrations: `hass.config_entries.flow.async_configure(…)` @@ -327,6 +329,8 @@ async def test_show_progress(hass, manager): assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS assert result["progress_action"] == "task_one" assert len(manager.async_progress()) == 1 + assert len(manager.async_progress_by_handler("test")) == 1 + assert manager.async_get(result["flow_id"])["handler"] == "test" # Mimic task one done and moving to task two # Called by integrations: `hass.config_entries.flow.async_configure(…)` @@ -400,6 +404,13 @@ async def test_init_unknown_flow(manager): await manager.async_init("test") +async def test_async_get_unknown_flow(manager): + """Test that UnknownFlow is raised when async_get is called with a flow_id that does not exist.""" + + with pytest.raises(data_entry_flow.UnknownFlow): + await manager.async_get("does_not_exist") + + async def test_async_has_matching_flow( hass: HomeAssistant, manager: data_entry_flow.FlowManager ): @@ -424,6 +435,8 @@ async def test_async_has_matching_flow( assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS assert result["progress_action"] == "task_one" assert len(manager.async_progress()) == 1 + assert len(manager.async_progress_by_handler("test")) == 1 + assert manager.async_get(result["flow_id"])["handler"] == "test" assert ( manager.async_has_matching_flow( @@ -449,3 +462,28 @@ async def test_async_has_matching_flow( ) is False ) + + +async def test_move_to_unknown_step_raises_and_removes_from_in_progress(manager): + """Test that moving to an unknown step raises and removes the flow from in progress.""" + + @manager.mock_reg_handler("test") + class TestFlow(data_entry_flow.FlowHandler): + VERSION = 1 + + with pytest.raises(data_entry_flow.UnknownStep): + await manager.async_init("test", context={"init_step": "does_not_exist"}) + + assert manager.async_progress() == [] + + +async def test_configure_raises_unknown_flow_if_not_in_progress(manager): + """Test configure raises UnknownFlow if the flow is not in progress.""" + with pytest.raises(data_entry_flow.UnknownFlow): + await manager.async_configure("wrong_flow_id") + + +async def test_abort_raises_unknown_flow_if_not_in_progress(manager): + """Test abort raises UnknownFlow if the flow is not in progress.""" + with pytest.raises(data_entry_flow.UnknownFlow): + await manager.async_abort("wrong_flow_id") From 843296c1a3cf23ea60a8aa3c9bb55773ccaaa606 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 22 Oct 2021 19:39:31 +0200 Subject: [PATCH 0686/1038] Add product ID to model for Tuya (#58235) --- homeassistant/components/tuya/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index f0bcb8e537f..c5b2aac729d 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -117,7 +117,7 @@ class TuyaEntity(Entity): identifiers={(DOMAIN, self.device.id)}, manufacturer="Tuya", name=self.device.name, - model=self.device.product_name, + model=f"{self.device.product_name} ({self.device.product_id})", ) @property From ab7a34fc7134c2be483cfac9062048f79830658f Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 22 Oct 2021 19:41:49 +0200 Subject: [PATCH 0687/1038] Add support for device configuration URL to deCONZ gateway (#58184) --- homeassistant/components/deconz/gateway.py | 6 +++++ tests/components/deconz/test_gateway.py | 30 +++++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 1199884fb5a..ddb0d47190c 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -4,6 +4,7 @@ import asyncio import async_timeout from pydeconz import DeconzSession, errors, group, light, sensor +from homeassistant.config_entries import SOURCE_HASSIO from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady @@ -149,8 +150,13 @@ class DeconzGateway: ) # Gateway service + configuration_url = f"http://{self.host}:{self.config_entry.data[CONF_PORT]}" + if self.config_entry.source == SOURCE_HASSIO: + configuration_url = None device_registry.async_get_or_create( config_entry_id=self.config_entry.entry_id, + configuration_url=configuration_url, + entry_type="service", identifiers={(DECONZ_DOMAIN, self.api.config.bridge_id)}, manufacturer="Dresden Elektronik", model=self.api.config.model_id, diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index dc57c679fb7..2cb73102bf1 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -34,7 +34,7 @@ from homeassistant.components.ssdp import ( ATTR_UPNP_UDN, ) from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER +from homeassistant.config_entries import SOURCE_HASSIO, SOURCE_SSDP, SOURCE_USER from homeassistant.const import ( CONF_API_KEY, CONF_HOST, @@ -43,6 +43,7 @@ from homeassistant.const import ( STATE_OFF, STATE_UNAVAILABLE, ) +from homeassistant.helpers import device_registry as dr from tests.common import MockConfigEntry @@ -169,6 +170,33 @@ async def test_gateway_setup(hass, aioclient_mock): assert forward_entry_setup.mock_calls[10][1] == (config_entry, SIREN_DOMAIN) assert forward_entry_setup.mock_calls[11][1] == (config_entry, SWITCH_DOMAIN) + device_registry = dr.async_get(hass) + gateway_entry = device_registry.async_get_device( + identifiers={(DECONZ_DOMAIN, gateway.bridgeid)} + ) + + assert gateway_entry.configuration_url == f"http://{HOST}:{PORT}" + assert gateway_entry.entry_type == "service" + + +async def test_gateway_device_no_configuration_url_when_addon(hass, aioclient_mock): + """Successful setup.""" + with patch( + "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", + return_value=True, + ): + config_entry = await setup_deconz_integration( + hass, aioclient_mock, source=SOURCE_HASSIO + ) + gateway = get_gateway_from_config_entry(hass, config_entry) + + device_registry = dr.async_get(hass) + gateway_entry = device_registry.async_get_device( + identifiers={(DECONZ_DOMAIN, gateway.bridgeid)} + ) + + assert not gateway_entry.configuration_url + async def test_gateway_retry(hass): """Retry setup.""" From 73d192b3f36ea3c87280cc68ecc85a3b16a810a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 22 Oct 2021 20:43:40 +0300 Subject: [PATCH 0688/1038] Use HTTPStatus instead of HTTP_ consts and magic values in comp.../[bc]* (#57989) --- homeassistant/components/bloomsky/__init__.py | 14 ++--- .../components/bluesound/media_player.py | 6 +- homeassistant/components/bond/__init__.py | 10 +-- homeassistant/components/bond/config_flow.py | 10 +-- homeassistant/components/buienradar/util.py | 5 +- homeassistant/components/clicksend/notify.py | 8 +-- .../components/clicksend_tts/notify.py | 9 +-- .../components/cloud/alexa_config.py | 4 +- homeassistant/components/cloud/client.py | 4 +- .../components/cloud/google_config.py | 4 +- homeassistant/components/cloud/http_api.py | 27 ++++---- tests/components/buienradar/test_camera.py | 4 +- tests/components/calendar/test_init.py | 7 ++- tests/components/camera/test_init.py | 12 ++-- tests/components/cloud/test_google_config.py | 9 ++- tests/components/cloud/test_http_api.py | 61 +++++++++---------- tests/components/config/test_automation.py | 9 +-- .../components/config/test_config_entries.py | 47 +++++++------- tests/components/config/test_core.py | 5 +- tests/components/config/test_customize.py | 9 +-- tests/components/config/test_group.py | 11 ++-- tests/components/config/test_scene.py | 9 +-- tests/components/config/test_script.py | 3 +- tests/components/config/test_zwave.py | 52 ++++++++-------- tests/components/conversation/test_init.py | 12 ++-- 25 files changed, 169 insertions(+), 182 deletions(-) diff --git a/homeassistant/components/bloomsky/__init__.py b/homeassistant/components/bloomsky/__init__.py index fa8d3160dc8..e04f4731918 100644 --- a/homeassistant/components/bloomsky/__init__.py +++ b/homeassistant/components/bloomsky/__init__.py @@ -1,17 +1,13 @@ """Support for BloomSky weather station.""" from datetime import timedelta +from http import HTTPStatus import logging from aiohttp.hdrs import AUTHORIZATION import requests import voluptuous as vol -from homeassistant.const import ( - CONF_API_KEY, - HTTP_METHOD_NOT_ALLOWED, - HTTP_OK, - HTTP_UNAUTHORIZED, -) +from homeassistant.const import CONF_API_KEY from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle @@ -72,12 +68,12 @@ class BloomSky: headers={AUTHORIZATION: self._api_key}, timeout=10, ) - if response.status_code == HTTP_UNAUTHORIZED: + if response.status_code == HTTPStatus.UNAUTHORIZED: raise RuntimeError("Invalid API_KEY") - if response.status_code == HTTP_METHOD_NOT_ALLOWED: + if response.status_code == HTTPStatus.METHOD_NOT_ALLOWED: _LOGGER.error("You have no bloomsky devices configured") return - if response.status_code != HTTP_OK: + if response.status_code != HTTPStatus.OK: _LOGGER.error("Invalid HTTP response: %s", response.status_code) return # Create dictionary keyed off of the device unique id diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 04261a4137c..ddc67bed6ab 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -2,6 +2,7 @@ import asyncio from asyncio import CancelledError from datetime import timedelta +from http import HTTPStatus import logging from urllib import parse @@ -38,7 +39,6 @@ from homeassistant.const import ( CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - HTTP_OK, STATE_IDLE, STATE_OFF, STATE_PAUSED, @@ -351,7 +351,7 @@ class BluesoundPlayer(MediaPlayerEntity): with async_timeout.timeout(10): response = await websession.get(url) - if response.status == HTTP_OK: + if response.status == HTTPStatus.OK: result = await response.text() if result: data = xmltodict.parse(result) @@ -395,7 +395,7 @@ class BluesoundPlayer(MediaPlayerEntity): url, headers={CONNECTION: KEEP_ALIVE} ) - if response.status == HTTP_OK: + if response.status == HTTPStatus.OK: result = await response.text() self._is_online = True self._last_status_update = dt_util.utcnow() diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index ecf5ea526b9..20b6b0a2ea5 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -1,5 +1,6 @@ """The Bond integration.""" from asyncio import TimeoutError as AsyncIOTimeoutError +from http import HTTPStatus import logging from typing import Any @@ -7,12 +8,7 @@ from aiohttp import ClientError, ClientResponseError, ClientTimeout from bond_api import Bond, BPUPSubscriptions, start_bpup from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_ACCESS_TOKEN, - CONF_HOST, - EVENT_HOMEASSISTANT_STOP, - HTTP_UNAUTHORIZED, -) +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -44,7 +40,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await hub.setup() except ClientResponseError as ex: - if ex.status == HTTP_UNAUTHORIZED: + if ex.status == HTTPStatus.UNAUTHORIZED: _LOGGER.error("Bond token no longer valid: %s", ex) return False raise ConfigEntryNotReady from ex diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index 073a91d54fb..6f70d37e0a1 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Bond integration.""" from __future__ import annotations +from http import HTTPStatus import logging from typing import Any @@ -10,12 +11,7 @@ import voluptuous as vol from homeassistant import config_entries, exceptions from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import ( - CONF_ACCESS_TOKEN, - CONF_HOST, - CONF_NAME, - HTTP_UNAUTHORIZED, -) +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -56,7 +52,7 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> tuple[st except ClientConnectionError as error: raise InputValidationError("cannot_connect") from error except ClientResponseError as error: - if error.status == HTTP_UNAUTHORIZED: + if error.status == HTTPStatus.UNAUTHORIZED: raise InputValidationError("invalid_auth") from error raise InputValidationError("unknown") from error except Exception as error: diff --git a/homeassistant/components/buienradar/util.py b/homeassistant/components/buienradar/util.py index 8934a7a6833..63c585f8c2f 100644 --- a/homeassistant/components/buienradar/util.py +++ b/homeassistant/components/buienradar/util.py @@ -1,6 +1,7 @@ """Shared utilities for different supported platforms.""" import asyncio from datetime import datetime, timedelta +from http import HTTPStatus import logging import aiohttp @@ -25,7 +26,7 @@ from buienradar.constants import ( ) from buienradar.urls import JSON_FEED_URL, json_precipitation_forecast_url -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, HTTP_OK +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util import dt as dt_util @@ -92,7 +93,7 @@ class BrData: result[STATUS_CODE] = resp.status result[CONTENT] = await resp.text() - if resp.status == HTTP_OK: + if resp.status == HTTPStatus.OK: result[SUCCESS] = True else: result[MESSAGE] = "Got http statuscode: %d" % (resp.status) diff --git a/homeassistant/components/clicksend/notify.py b/homeassistant/components/clicksend/notify.py index 18562260431..74f1c2e1ae5 100644 --- a/homeassistant/components/clicksend/notify.py +++ b/homeassistant/components/clicksend/notify.py @@ -1,4 +1,5 @@ """Clicksend platform for notify component.""" +from http import HTTPStatus import json import logging @@ -13,7 +14,6 @@ from homeassistant.const import ( CONF_SENDER, CONF_USERNAME, CONTENT_TYPE_JSON, - HTTP_OK, ) import homeassistant.helpers.config_validation as cv @@ -81,7 +81,7 @@ class ClicksendNotificationService(BaseNotificationService): auth=(self.username, self.api_key), timeout=TIMEOUT, ) - if resp.status_code == HTTP_OK: + if resp.status_code == HTTPStatus.OK: return obj = json.loads(resp.text) @@ -101,6 +101,4 @@ def _authenticate(config): auth=(config[CONF_USERNAME], config[CONF_API_KEY]), timeout=TIMEOUT, ) - if resp.status_code != HTTP_OK: - return False - return True + return resp.status_code == HTTPStatus.OK diff --git a/homeassistant/components/clicksend_tts/notify.py b/homeassistant/components/clicksend_tts/notify.py index 6648333bb54..712787c34e6 100644 --- a/homeassistant/components/clicksend_tts/notify.py +++ b/homeassistant/components/clicksend_tts/notify.py @@ -1,4 +1,5 @@ """clicksend_tts platform for notify component.""" +from http import HTTPStatus import json import logging @@ -12,7 +13,6 @@ from homeassistant.const import ( CONF_RECIPIENT, CONF_USERNAME, CONTENT_TYPE_JSON, - HTTP_OK, ) import homeassistant.helpers.config_validation as cv @@ -88,7 +88,7 @@ class ClicksendNotificationService(BaseNotificationService): timeout=TIMEOUT, ) - if resp.status_code == HTTP_OK: + if resp.status_code == HTTPStatus.OK: return obj = json.loads(resp.text) response_msg = obj["response_msg"] @@ -108,7 +108,4 @@ def _authenticate(config): timeout=TIMEOUT, ) - if resp.status_code != HTTP_OK: - return False - - return True + return resp.status_code == HTTPStatus.OK diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 3e82e662fe9..41bab5e0bd4 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -2,6 +2,7 @@ import asyncio from contextlib import suppress from datetime import timedelta +from http import HTTPStatus import logging import aiohttp @@ -19,7 +20,6 @@ from homeassistant.const import ( CLOUD_NEVER_EXPOSED_ENTITIES, ENTITY_CATEGORY_CONFIG, ENTITY_CATEGORY_DIAGNOSTIC, - HTTP_BAD_REQUEST, ) from homeassistant.core import HomeAssistant, callback, split_entity_id from homeassistant.helpers import entity_registry as er, start @@ -161,7 +161,7 @@ class AlexaConfig(alexa_config.AbstractConfig): resp = await cloud_api.async_alexa_access_token(self._cloud) body = await resp.json() - if resp.status == HTTP_BAD_REQUEST: + if resp.status == HTTPStatus.BAD_REQUEST: if body["reason"] in ("RefreshTokenNotFound", "UnknownRegion"): if self.should_report_state: await self._prefs.async_update(alexa_report_state=False) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 54c471e2a83..5a10e1d1e5c 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from http import HTTPStatus import logging from pathlib import Path from typing import Any @@ -14,7 +15,6 @@ from homeassistant.components.alexa import ( smart_home as alexa_sh, ) from homeassistant.components.google_assistant import const as gc, smart_home as ga -from homeassistant.const import HTTP_OK from homeassistant.core import Context, HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later @@ -210,7 +210,7 @@ class CloudClient(Interface): break if found is None: - return {"status": HTTP_OK} + return {"status": HTTPStatus.OK} request = MockRequest( content=payload["body"].encode("utf-8"), diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 4f71aeeb9a0..f3f5a64bbd6 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -1,5 +1,6 @@ """Google config for Cloud.""" import asyncio +from http import HTTPStatus import logging from hass_nabucasa import Cloud, cloud_api @@ -11,7 +12,6 @@ from homeassistant.const import ( CLOUD_NEVER_EXPOSED_ENTITIES, ENTITY_CATEGORY_CONFIG, ENTITY_CATEGORY_DIAGNOSTIC, - HTTP_OK, ) from homeassistant.core import CoreState, split_entity_id from homeassistant.helpers import entity_registry as er, start @@ -178,7 +178,7 @@ class CloudGoogleConfig(AbstractConfig): async def _async_request_sync_devices(self, agent_user_id: str): """Trigger a sync with Google.""" if self._sync_entities_lock.locked(): - return HTTP_OK + return HTTPStatus.OK async with self._sync_entities_lock: resp = await cloud_api.async_google_actions_request_sync(self._cloud) diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 1f17f46013e..5bfebec40a3 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -1,6 +1,7 @@ """The HTTP api to control the cloud integration.""" import asyncio from functools import wraps +from http import HTTPStatus import logging import aiohttp @@ -20,12 +21,6 @@ from homeassistant.components.google_assistant import helpers as google_helpers from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.websocket_api import const as ws_const -from homeassistant.const import ( - HTTP_BAD_GATEWAY, - HTTP_BAD_REQUEST, - HTTP_INTERNAL_SERVER_ERROR, - HTTP_UNAUTHORIZED, -) from .const import ( DOMAIN, @@ -48,19 +43,19 @@ _LOGGER = logging.getLogger(__name__) _CLOUD_ERRORS = { InvalidTrustedNetworks: ( - HTTP_INTERNAL_SERVER_ERROR, + HTTPStatus.INTERNAL_SERVER_ERROR, "Remote UI not compatible with 127.0.0.1/::1 as a trusted network.", ), InvalidTrustedProxies: ( - HTTP_INTERNAL_SERVER_ERROR, + HTTPStatus.INTERNAL_SERVER_ERROR, "Remote UI not compatible with 127.0.0.1/::1 as trusted proxies.", ), asyncio.TimeoutError: ( - HTTP_BAD_GATEWAY, + HTTPStatus.BAD_GATEWAY, "Unable to reach the Home Assistant cloud.", ), aiohttp.ClientError: ( - HTTP_INTERNAL_SERVER_ERROR, + HTTPStatus.INTERNAL_SERVER_ERROR, "Error making internal request", ), } @@ -96,15 +91,15 @@ async def async_setup(hass): _CLOUD_ERRORS.update( { - auth.UserNotFound: (HTTP_BAD_REQUEST, "User does not exist."), - auth.UserNotConfirmed: (HTTP_BAD_REQUEST, "Email not confirmed."), + auth.UserNotFound: (HTTPStatus.BAD_REQUEST, "User does not exist."), + auth.UserNotConfirmed: (HTTPStatus.BAD_REQUEST, "Email not confirmed."), auth.UserExists: ( - HTTP_BAD_REQUEST, + HTTPStatus.BAD_REQUEST, "An account with the given email already exists.", ), - auth.Unauthenticated: (HTTP_UNAUTHORIZED, "Authentication failed."), + auth.Unauthenticated: (HTTPStatus.UNAUTHORIZED, "Authentication failed."), auth.PasswordChangeRequired: ( - HTTP_BAD_REQUEST, + HTTPStatus.BAD_REQUEST, "Password change required.", ), } @@ -157,7 +152,7 @@ def _process_cloud_exception(exc, where): if err_info is None: _LOGGER.exception("Unexpected error processing request for %s", where) - err_info = (HTTP_BAD_GATEWAY, f"Unexpected error: {exc}") + err_info = (HTTPStatus.BAD_GATEWAY, f"Unexpected error: {exc}") return err_info diff --git a/tests/components/buienradar/test_camera.py b/tests/components/buienradar/test_camera.py index 1688dd83d2c..9267caff959 100644 --- a/tests/components/buienradar/test_camera.py +++ b/tests/components/buienradar/test_camera.py @@ -56,7 +56,7 @@ async def test_fetching_url_and_caching(aioclient_mock, hass, hass_client): resp = await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert aioclient_mock.call_count == 1 body = await resp.text() assert body == "hello world" @@ -86,7 +86,7 @@ async def test_expire_delta(aioclient_mock, hass, hass_client): resp = await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert aioclient_mock.call_count == 1 body = await resp.text() assert body == "hello world" diff --git a/tests/components/calendar/test_init.py b/tests/components/calendar/test_init.py index 1f87c38c6bf..8ab210e7180 100644 --- a/tests/components/calendar/test_init.py +++ b/tests/components/calendar/test_init.py @@ -1,5 +1,6 @@ """The tests for the calendar component.""" from datetime import timedelta +from http import HTTPStatus from homeassistant.bootstrap import async_setup_component import homeassistant.util.dt as dt_util @@ -11,7 +12,7 @@ async def test_events_http_api(hass, hass_client): await hass.async_block_till_done() client = await hass_client() response = await client.get("/api/calendars/calendar.calendar_2") - assert response.status == 400 + assert response.status == HTTPStatus.BAD_REQUEST start = dt_util.now() end = start + timedelta(days=1) response = await client.get( @@ -19,7 +20,7 @@ async def test_events_http_api(hass, hass_client): start.isoformat(), end.isoformat() ) ) - assert response.status == 200 + assert response.status == HTTPStatus.OK events = await response.json() assert events[0]["summary"] == "Future Event" assert events[0]["title"] == "Future Event" @@ -31,7 +32,7 @@ async def test_calendars_http_api(hass, hass_client): await hass.async_block_till_done() client = await hass_client() response = await client.get("/api/calendars") - assert response.status == 200 + assert response.status == HTTPStatus.OK data = await response.json() assert data == [ {"entity_id": "calendar.calendar_1", "name": "Calendar 1"}, diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index c37c1b2909a..3f8f62449ba 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -1,6 +1,7 @@ """The tests for the camera component.""" import asyncio import base64 +from http import HTTPStatus import io from unittest.mock import Mock, PropertyMock, mock_open, patch @@ -15,12 +16,7 @@ from homeassistant.components.camera.const import ( from homeassistant.components.camera.prefs import CameraEntityPreferences 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, - HTTP_BAD_GATEWAY, - HTTP_OK, -) +from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component @@ -481,14 +477,14 @@ async def test_camera_proxy_stream(hass, mock_camera, hass_client): client = await hass_client() response = await client.get("/api/camera_proxy_stream/camera.demo_camera") - assert response.status == HTTP_OK + assert response.status == HTTPStatus.OK with patch( "homeassistant.components.demo.camera.DemoCamera.handle_async_mjpeg_stream", return_value=None, ): response = await client.get("/api/camera_proxy_stream/camera.demo_camera") - assert response.status == HTTP_BAD_GATEWAY + assert response.status == HTTPStatus.BAD_GATEWAY async def test_websocket_web_rtc_offer( diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 61d93b5bc85..99fa24a6cb9 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -1,4 +1,5 @@ """Test the Cloud Google Config.""" +from http import HTTPStatus from unittest.mock import Mock, patch import pytest @@ -6,7 +7,7 @@ import pytest from homeassistant.components.cloud import GACTIONS_SCHEMA from homeassistant.components.cloud.google_config import CloudGoogleConfig from homeassistant.components.google_assistant import helpers as ga_helpers -from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, HTTP_NOT_FOUND +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import CoreState, State from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED from homeassistant.util.dt import utcnow @@ -71,9 +72,11 @@ async def test_sync_entities(mock_conf, hass, cloud_prefs): with patch( "hass_nabucasa.cloud_api.async_google_actions_request_sync", - return_value=Mock(status=HTTP_NOT_FOUND), + return_value=Mock(status=HTTPStatus.NOT_FOUND), ) as mock_request_sync: - assert await mock_conf.async_sync_entities("mock-user-id") == HTTP_NOT_FOUND + assert ( + await mock_conf.async_sync_entities("mock-user-id") == HTTPStatus.NOT_FOUND + ) assert len(mock_conf._store.agent_user_ids) == 0 assert len(mock_request_sync.mock_calls) == 1 diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index a7181ea4a73..566f2041fdd 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -16,7 +16,6 @@ from homeassistant.components.alexa import errors as alexa_errors from homeassistant.components.alexa.entities import LightCapabilities from homeassistant.components.cloud.const import DOMAIN, RequireRelink from homeassistant.components.google_assistant.helpers import GoogleEntity -from homeassistant.const import HTTP_INTERNAL_SERVER_ERROR from homeassistant.core import State from . import mock_cloud, mock_cloud_prefs @@ -90,7 +89,7 @@ async def test_google_actions_sync(mock_cognito, mock_cloud_login, cloud_client) return_value=Mock(status=200), ) as mock_request_sync: req = await cloud_client.post("/api/cloud/google_actions/sync") - assert req.status == 200 + assert req.status == HTTPStatus.OK assert len(mock_request_sync.mock_calls) == 1 @@ -98,10 +97,10 @@ async def test_google_actions_sync_fails(mock_cognito, mock_cloud_login, cloud_c """Test syncing Google Actions gone bad.""" with patch( "hass_nabucasa.cloud_api.async_google_actions_request_sync", - return_value=Mock(status=HTTP_INTERNAL_SERVER_ERROR), + return_value=Mock(status=HTTPStatus.INTERNAL_SERVER_ERROR), ) as mock_request_sync: req = await cloud_client.post("/api/cloud/google_actions/sync") - assert req.status == HTTP_INTERNAL_SERVER_ERROR + assert req.status == HTTPStatus.INTERNAL_SERVER_ERROR assert len(mock_request_sync.mock_calls) == 1 @@ -113,7 +112,7 @@ async def test_login_view(hass, cloud_client): "/api/cloud/login", json={"email": "my_username", "password": "my_password"} ) - assert req.status == 200 + assert req.status == HTTPStatus.OK result = await req.json() assert result == {"success": True} @@ -124,7 +123,7 @@ async def test_login_view_random_exception(cloud_client): req = await cloud_client.post( "/api/cloud/login", json={"email": "my_username", "password": "my_password"} ) - assert req.status == 502 + assert req.status == HTTPStatus.BAD_GATEWAY resp = await req.json() assert resp == {"code": "valueerror", "message": "Unexpected error: Boom"} @@ -133,7 +132,7 @@ async def test_login_view_invalid_json(cloud_client): """Try logging in with invalid JSON.""" with patch("hass_nabucasa.auth.CognitoAuth.async_login") as mock_login: req = await cloud_client.post("/api/cloud/login", data="Not JSON") - assert req.status == 400 + assert req.status == HTTPStatus.BAD_REQUEST assert len(mock_login.mock_calls) == 0 @@ -141,7 +140,7 @@ async def test_login_view_invalid_schema(cloud_client): """Try logging in with invalid schema.""" with patch("hass_nabucasa.auth.CognitoAuth.async_login") as mock_login: req = await cloud_client.post("/api/cloud/login", json={"invalid": "schema"}) - assert req.status == 400 + assert req.status == HTTPStatus.BAD_REQUEST assert len(mock_login.mock_calls) == 0 @@ -154,7 +153,7 @@ async def test_login_view_request_timeout(cloud_client): "/api/cloud/login", json={"email": "my_username", "password": "my_password"} ) - assert req.status == 502 + assert req.status == HTTPStatus.BAD_GATEWAY async def test_login_view_invalid_credentials(cloud_client): @@ -166,7 +165,7 @@ async def test_login_view_invalid_credentials(cloud_client): "/api/cloud/login", json={"email": "my_username", "password": "my_password"} ) - assert req.status == 401 + assert req.status == HTTPStatus.UNAUTHORIZED async def test_login_view_unknown_error(cloud_client): @@ -176,7 +175,7 @@ async def test_login_view_unknown_error(cloud_client): "/api/cloud/login", json={"email": "my_username", "password": "my_password"} ) - assert req.status == 502 + assert req.status == HTTPStatus.BAD_GATEWAY async def test_logout_view(hass, cloud_client): @@ -184,7 +183,7 @@ async def test_logout_view(hass, cloud_client): cloud = hass.data["cloud"] = MagicMock() cloud.logout = AsyncMock(return_value=None) req = await cloud_client.post("/api/cloud/logout") - assert req.status == 200 + assert req.status == HTTPStatus.OK data = await req.json() assert data == {"message": "ok"} assert len(cloud.logout.mock_calls) == 1 @@ -195,7 +194,7 @@ async def test_logout_view_request_timeout(hass, cloud_client): cloud = hass.data["cloud"] = MagicMock() cloud.logout.side_effect = asyncio.TimeoutError req = await cloud_client.post("/api/cloud/logout") - assert req.status == 502 + assert req.status == HTTPStatus.BAD_GATEWAY async def test_logout_view_unknown_error(hass, cloud_client): @@ -203,7 +202,7 @@ async def test_logout_view_unknown_error(hass, cloud_client): cloud = hass.data["cloud"] = MagicMock() cloud.logout.side_effect = UnknownError req = await cloud_client.post("/api/cloud/logout") - assert req.status == 502 + assert req.status == HTTPStatus.BAD_GATEWAY async def test_register_view(mock_cognito, cloud_client): @@ -211,7 +210,7 @@ async def test_register_view(mock_cognito, cloud_client): req = await cloud_client.post( "/api/cloud/register", json={"email": "hello@bla.com", "password": "falcon42"} ) - assert req.status == 200 + assert req.status == HTTPStatus.OK assert len(mock_cognito.register.mock_calls) == 1 result_email, result_pass = mock_cognito.register.mock_calls[0][1] assert result_email == "hello@bla.com" @@ -223,7 +222,7 @@ async def test_register_view_bad_data(mock_cognito, cloud_client): req = await cloud_client.post( "/api/cloud/register", json={"email": "hello@bla.com", "not_password": "falcon"} ) - assert req.status == 400 + assert req.status == HTTPStatus.BAD_REQUEST assert len(mock_cognito.logout.mock_calls) == 0 @@ -233,7 +232,7 @@ async def test_register_view_request_timeout(mock_cognito, cloud_client): req = await cloud_client.post( "/api/cloud/register", json={"email": "hello@bla.com", "password": "falcon42"} ) - assert req.status == 502 + assert req.status == HTTPStatus.BAD_GATEWAY async def test_register_view_unknown_error(mock_cognito, cloud_client): @@ -242,7 +241,7 @@ async def test_register_view_unknown_error(mock_cognito, cloud_client): req = await cloud_client.post( "/api/cloud/register", json={"email": "hello@bla.com", "password": "falcon42"} ) - assert req.status == 502 + assert req.status == HTTPStatus.BAD_GATEWAY async def test_forgot_password_view(mock_cognito, cloud_client): @@ -250,7 +249,7 @@ async def test_forgot_password_view(mock_cognito, cloud_client): req = await cloud_client.post( "/api/cloud/forgot_password", json={"email": "hello@bla.com"} ) - assert req.status == 200 + assert req.status == HTTPStatus.OK assert len(mock_cognito.initiate_forgot_password.mock_calls) == 1 @@ -259,7 +258,7 @@ async def test_forgot_password_view_bad_data(mock_cognito, cloud_client): req = await cloud_client.post( "/api/cloud/forgot_password", json={"not_email": "hello@bla.com"} ) - assert req.status == 400 + assert req.status == HTTPStatus.BAD_REQUEST assert len(mock_cognito.initiate_forgot_password.mock_calls) == 0 @@ -269,7 +268,7 @@ async def test_forgot_password_view_request_timeout(mock_cognito, cloud_client): req = await cloud_client.post( "/api/cloud/forgot_password", json={"email": "hello@bla.com"} ) - assert req.status == 502 + assert req.status == HTTPStatus.BAD_GATEWAY async def test_forgot_password_view_unknown_error(mock_cognito, cloud_client): @@ -278,7 +277,7 @@ async def test_forgot_password_view_unknown_error(mock_cognito, cloud_client): req = await cloud_client.post( "/api/cloud/forgot_password", json={"email": "hello@bla.com"} ) - assert req.status == 502 + assert req.status == HTTPStatus.BAD_GATEWAY async def test_forgot_password_view_aiohttp_error(mock_cognito, cloud_client): @@ -289,7 +288,7 @@ async def test_forgot_password_view_aiohttp_error(mock_cognito, cloud_client): req = await cloud_client.post( "/api/cloud/forgot_password", json={"email": "hello@bla.com"} ) - assert req.status == 500 + assert req.status == HTTPStatus.INTERNAL_SERVER_ERROR async def test_resend_confirm_view(mock_cognito, cloud_client): @@ -297,7 +296,7 @@ async def test_resend_confirm_view(mock_cognito, cloud_client): req = await cloud_client.post( "/api/cloud/resend_confirm", json={"email": "hello@bla.com"} ) - assert req.status == 200 + assert req.status == HTTPStatus.OK assert len(mock_cognito.client.resend_confirmation_code.mock_calls) == 1 @@ -306,7 +305,7 @@ async def test_resend_confirm_view_bad_data(mock_cognito, cloud_client): req = await cloud_client.post( "/api/cloud/resend_confirm", json={"not_email": "hello@bla.com"} ) - assert req.status == 400 + assert req.status == HTTPStatus.BAD_REQUEST assert len(mock_cognito.client.resend_confirmation_code.mock_calls) == 0 @@ -316,7 +315,7 @@ async def test_resend_confirm_view_request_timeout(mock_cognito, cloud_client): req = await cloud_client.post( "/api/cloud/resend_confirm", json={"email": "hello@bla.com"} ) - assert req.status == 502 + assert req.status == HTTPStatus.BAD_GATEWAY async def test_resend_confirm_view_unknown_error(mock_cognito, cloud_client): @@ -325,7 +324,7 @@ async def test_resend_confirm_view_unknown_error(mock_cognito, cloud_client): req = await cloud_client.post( "/api/cloud/resend_confirm", json={"email": "hello@bla.com"} ) - assert req.status == 502 + assert req.status == HTTPStatus.BAD_GATEWAY async def test_websocket_status( @@ -583,7 +582,7 @@ async def test_enabling_remote_trusted_networks_local4( response = await client.receive_json() assert not response["success"] - assert response["error"]["code"] == HTTP_INTERNAL_SERVER_ERROR + assert response["error"]["code"] == HTTPStatus.INTERNAL_SERVER_ERROR assert ( response["error"]["message"] == "Remote UI not compatible with 127.0.0.1/::1 as a trusted network." @@ -616,7 +615,7 @@ async def test_enabling_remote_trusted_networks_local6( response = await client.receive_json() assert not response["success"] - assert response["error"]["code"] == HTTP_INTERNAL_SERVER_ERROR + assert response["error"]["code"] == HTTPStatus.INTERNAL_SERVER_ERROR assert ( response["error"]["message"] == "Remote UI not compatible with 127.0.0.1/::1 as a trusted network." @@ -745,7 +744,7 @@ async def test_enabling_remote_trusted_proxies_local4( response = await client.receive_json() assert not response["success"] - assert response["error"]["code"] == HTTP_INTERNAL_SERVER_ERROR + assert response["error"]["code"] == HTTPStatus.INTERNAL_SERVER_ERROR assert ( response["error"]["message"] == "Remote UI not compatible with 127.0.0.1/::1 as trusted proxies." @@ -769,7 +768,7 @@ async def test_enabling_remote_trusted_proxies_local6( response = await client.receive_json() assert not response["success"] - assert response["error"]["code"] == HTTP_INTERNAL_SERVER_ERROR + assert response["error"]["code"] == HTTPStatus.INTERNAL_SERVER_ERROR assert ( response["error"]["message"] == "Remote UI not compatible with 127.0.0.1/::1 as trusted proxies." diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py index 6aeb71a7fd0..0950e3d0358 100644 --- a/tests/components/config/test_automation.py +++ b/tests/components/config/test_automation.py @@ -1,4 +1,5 @@ """Test Automation config panel.""" +from http import HTTPStatus import json from unittest.mock import patch @@ -23,7 +24,7 @@ async def test_get_device_config(hass, hass_client): with patch("homeassistant.components.config._read", mock_read): resp = await client.get("/api/config/automation/config/moon") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"id": "moon"} @@ -56,7 +57,7 @@ async def test_update_device_config(hass, hass_client): data=json.dumps({"trigger": [], "action": [], "condition": []}), ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"result": "ok"} @@ -99,7 +100,7 @@ async def test_bad_formatted_automations(hass, hass_client): ) await hass.async_block_till_done() - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"result": "ok"} @@ -157,7 +158,7 @@ async def test_delete_automation(hass, hass_client): resp = await client.delete("/api/config/automation/config/sun") await hass.async_block_till_done() - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"result": "ok"} diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 0e1b471cbd5..04444e40f5d 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -1,6 +1,7 @@ """Test config entries API.""" from collections import OrderedDict +from http import HTTPStatus from unittest.mock import AsyncMock, patch import pytest @@ -75,7 +76,7 @@ async def test_get_entries(hass, client): ).add_to_hass(hass) resp = await client.get("/api/config/config_entries/entry") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK data = await resp.json() for entry in data: entry.pop("entry_id") @@ -124,7 +125,7 @@ async def test_remove_entry(hass, client): entry = MockConfigEntry(domain="demo", state=core_ce.ConfigEntryState.LOADED) entry.add_to_hass(hass) resp = await client.delete(f"/api/config/config_entries/entry/{entry.entry_id}") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK data = await resp.json() assert data == {"require_restart": True} assert len(hass.config_entries.async_entries()) == 0 @@ -137,7 +138,7 @@ async def test_reload_entry(hass, client): resp = await client.post( f"/api/config/config_entries/entry/{entry.entry_id}/reload" ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK data = await resp.json() assert data == {"require_restart": True} assert len(hass.config_entries.async_entries()) == 1 @@ -146,7 +147,7 @@ async def test_reload_entry(hass, client): async def test_reload_invalid_entry(hass, client): """Test reloading an invalid entry via the API.""" resp = await client.post("/api/config/config_entries/entry/invalid/reload") - assert resp.status == 404 + assert resp.status == HTTPStatus.NOT_FOUND async def test_remove_entry_unauth(hass, client, hass_admin_user): @@ -155,7 +156,7 @@ async def test_remove_entry_unauth(hass, client, hass_admin_user): entry = MockConfigEntry(domain="demo", state=core_ce.ConfigEntryState.LOADED) entry.add_to_hass(hass) resp = await client.delete(f"/api/config/config_entries/entry/{entry.entry_id}") - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED assert len(hass.config_entries.async_entries()) == 1 @@ -167,7 +168,7 @@ async def test_reload_entry_unauth(hass, client, hass_admin_user): resp = await client.post( f"/api/config/config_entries/entry/{entry.entry_id}/reload" ) - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED assert len(hass.config_entries.async_entries()) == 1 @@ -178,7 +179,7 @@ async def test_reload_entry_in_failed_state(hass, client, hass_admin_user): resp = await client.post( f"/api/config/config_entries/entry/{entry.entry_id}/reload" ) - assert resp.status == 403 + assert resp.status == HTTPStatus.FORBIDDEN assert len(hass.config_entries.async_entries()) == 1 @@ -186,7 +187,7 @@ async def test_available_flows(hass, client): """Test querying the available flows.""" with patch.object(config_flows, "FLOWS", ["hello", "world"]): resp = await client.get("/api/config/config_entries/flow_handlers") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK data = await resp.json() assert set(data) == {"hello", "world"} @@ -222,7 +223,7 @@ async def test_initialize_flow(hass, client): json={"handler": "test", "show_advanced_options": True}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK data = await resp.json() data.pop("flow_id") @@ -266,7 +267,7 @@ async def test_initialize_flow_unauth(hass, client, hass_admin_user): "/api/config/config_entries/flow", json={"handler": "test"} ) - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED async def test_abort(hass, client): @@ -282,7 +283,7 @@ async def test_abort(hass, client): "/api/config/config_entries/flow", json={"handler": "test"} ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK data = await resp.json() data.pop("flow_id") assert data == { @@ -314,7 +315,7 @@ async def test_create_account(hass, client, enable_custom_integrations): "/api/config/config_entries/flow", json={"handler": "test"} ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK entries = hass.config_entries.async_entries("test") assert len(entries) == 1 @@ -369,7 +370,7 @@ async def test_two_step_flow(hass, client, enable_custom_integrations): resp = await client.post( "/api/config/config_entries/flow", json={"handler": "test"} ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK data = await resp.json() flow_id = data.pop("flow_id") assert data == { @@ -387,7 +388,7 @@ async def test_two_step_flow(hass, client, enable_custom_integrations): f"/api/config/config_entries/flow/{flow_id}", json={"user_title": "user-title"}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK entries = hass.config_entries.async_entries("test") assert len(entries) == 1 @@ -442,7 +443,7 @@ async def test_continue_flow_unauth(hass, client, hass_admin_user): resp = await client.post( "/api/config/config_entries/flow", json={"handler": "test"} ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK data = await resp.json() flow_id = data.pop("flow_id") assert data == { @@ -461,7 +462,7 @@ async def test_continue_flow_unauth(hass, client, hass_admin_user): f"/api/config/config_entries/flow/{flow_id}", json={"user_title": "user-title"}, ) - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED async def test_get_progress_index(hass, hass_ws_client): @@ -532,14 +533,14 @@ async def test_get_progress_flow(hass, client): "/api/config/config_entries/flow", json={"handler": "test"} ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK data = await resp.json() resp2 = await client.get( "/api/config/config_entries/flow/{}".format(data["flow_id"]) ) - assert resp2.status == 200 + assert resp2.status == HTTPStatus.OK data2 = await resp2.json() assert data == data2 @@ -566,7 +567,7 @@ async def test_get_progress_flow_unauth(hass, client, hass_admin_user): "/api/config/config_entries/flow", json={"handler": "test"} ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK data = await resp.json() hass_admin_user.groups = [] @@ -575,7 +576,7 @@ async def test_get_progress_flow_unauth(hass, client, hass_admin_user): "/api/config/config_entries/flow/{}".format(data["flow_id"]) ) - assert resp2.status == 401 + assert resp2.status == HTTPStatus.UNAUTHORIZED async def test_options_flow(hass, client): @@ -608,7 +609,7 @@ async def test_options_flow(hass, client): url = "/api/config/config_entries/options/flow" resp = await client.post(url, json={"handler": entry.entry_id}) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK data = await resp.json() data.pop("flow_id") @@ -657,7 +658,7 @@ async def test_two_step_options_flow(hass, client): url = "/api/config/config_entries/options/flow" resp = await client.post(url, json={"handler": entry.entry_id}) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK data = await resp.json() flow_id = data.pop("flow_id") assert data == { @@ -675,7 +676,7 @@ async def test_two_step_options_flow(hass, client): f"/api/config/config_entries/options/flow/{flow_id}", json={"enabled": True}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK data = await resp.json() data.pop("flow_id") assert data == { diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index 9c86a3f2d1b..b78ed50cdf2 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -1,4 +1,5 @@ """Test core config.""" +from http import HTTPStatus from unittest.mock import patch import pytest @@ -33,7 +34,7 @@ async def test_validate_config_ok(hass, hass_client): ): resp = await client.post("/api/config/core/check_config") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result["result"] == "valid" assert result["errors"] is None @@ -44,7 +45,7 @@ async def test_validate_config_ok(hass, hass_client): ): resp = await client.post("/api/config/core/check_config") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result["result"] == "invalid" assert result["errors"] == "beer" diff --git a/tests/components/config/test_customize.py b/tests/components/config/test_customize.py index aac18bc379e..d5b4c788bcf 100644 --- a/tests/components/config/test_customize.py +++ b/tests/components/config/test_customize.py @@ -1,4 +1,5 @@ """Test Customize config panel.""" +from http import HTTPStatus import json from unittest.mock import patch @@ -22,7 +23,7 @@ async def test_get_entity(hass, hass_client): with patch("homeassistant.components.config._read", mock_read): resp = await client.get("/api/config/customize/config/hello.beer") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"local": {"free": "beer"}, "global": {"cold": "beer"}} @@ -65,7 +66,7 @@ async def test_update_entity(hass, hass_client): ) await hass.async_block_till_done() - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"result": "ok"} @@ -94,7 +95,7 @@ async def test_update_entity_invalid_key(hass, hass_client): "/api/config/customize/config/not_entity", data=json.dumps({"name": "YO"}) ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST async def test_update_entity_invalid_json(hass, hass_client): @@ -106,4 +107,4 @@ async def test_update_entity_invalid_json(hass, hass_client): resp = await client.post("/api/config/customize/config/hello.beer", data="not json") - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST diff --git a/tests/components/config/test_group.py b/tests/components/config/test_group.py index c4b7cf25800..72a9a00cbea 100644 --- a/tests/components/config/test_group.py +++ b/tests/components/config/test_group.py @@ -1,4 +1,5 @@ """Test Group config panel.""" +from http import HTTPStatus import json from unittest.mock import AsyncMock, patch @@ -22,7 +23,7 @@ async def test_get_device_config(hass, hass_client): with patch("homeassistant.components.config._read", mock_read): resp = await client.get("/api/config/group/config/hello.beer") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"free": "beer"} @@ -63,7 +64,7 @@ async def test_update_device_config(hass, hass_client): ) await hass.async_block_till_done() - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"result": "ok"} @@ -85,7 +86,7 @@ async def test_update_device_config_invalid_key(hass, hass_client): "/api/config/group/config/not a slug", data=json.dumps({"name": "YO"}) ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST async def test_update_device_config_invalid_data(hass, hass_client): @@ -99,7 +100,7 @@ async def test_update_device_config_invalid_data(hass, hass_client): "/api/config/group/config/hello_beer", data=json.dumps({"invalid_option": 2}) ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST async def test_update_device_config_invalid_json(hass, hass_client): @@ -111,4 +112,4 @@ async def test_update_device_config_invalid_json(hass, hass_client): resp = await client.post("/api/config/group/config/hello_beer", data="not json") - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST diff --git a/tests/components/config/test_scene.py b/tests/components/config/test_scene.py index 429fb807883..db938638d01 100644 --- a/tests/components/config/test_scene.py +++ b/tests/components/config/test_scene.py @@ -1,4 +1,5 @@ """Test Automation config panel.""" +from http import HTTPStatus import json from unittest.mock import patch @@ -40,7 +41,7 @@ async def test_create_scene(hass, hass_client): ), ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"result": "ok"} @@ -91,7 +92,7 @@ async def test_update_scene(hass, hass_client): ), ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"result": "ok"} @@ -148,7 +149,7 @@ async def test_bad_formatted_scene(hass, hass_client): ), ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"result": "ok"} @@ -202,7 +203,7 @@ async def test_delete_scene(hass, hass_client): resp = await client.delete("/api/config/scene/config/light_on") await hass.async_block_till_done() - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"result": "ok"} diff --git a/tests/components/config/test_script.py b/tests/components/config/test_script.py index 0026729766c..18ab87b8c40 100644 --- a/tests/components/config/test_script.py +++ b/tests/components/config/test_script.py @@ -1,4 +1,5 @@ """Tests for config/script.""" +from http import HTTPStatus from unittest.mock import patch from homeassistant.bootstrap import async_setup_component @@ -29,7 +30,7 @@ async def test_delete_script(hass, hass_client): ): resp = await client.delete("/api/config/script/config/two") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"result": "ok"} diff --git a/tests/components/config/test_zwave.py b/tests/components/config/test_zwave.py index 06dd3434738..bc7f22c104f 100644 --- a/tests/components/config/test_zwave.py +++ b/tests/components/config/test_zwave.py @@ -1,4 +1,5 @@ """Test Z-Wave config panel.""" +from http import HTTPStatus import json from unittest.mock import MagicMock, patch @@ -7,7 +8,6 @@ import pytest from homeassistant.bootstrap import async_setup_component from homeassistant.components import config from homeassistant.components.zwave import DATA_NETWORK, const -from homeassistant.const import HTTP_NOT_FOUND from tests.mock.zwave import MockEntityValues, MockNode, MockValue @@ -33,7 +33,7 @@ async def test_get_device_config(client): with patch("homeassistant.components.config._read", mock_read): resp = await client.get("/api/config/zwave/device_config/hello.beer") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"free": "beer"} @@ -64,7 +64,7 @@ async def test_update_device_config(client): data=json.dumps({"polling_intensity": 2}), ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"result": "ok"} @@ -80,7 +80,7 @@ async def test_update_device_config_invalid_key(client): data=json.dumps({"polling_intensity": 2}), ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST async def test_update_device_config_invalid_data(client): @@ -90,7 +90,7 @@ async def test_update_device_config_invalid_data(client): data=json.dumps({"invalid_option": 2}), ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST async def test_update_device_config_invalid_json(client): @@ -99,7 +99,7 @@ async def test_update_device_config_invalid_json(client): "/api/config/zwave/device_config/hello.beer", data="not json" ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST async def test_get_values(hass, client): @@ -121,7 +121,7 @@ async def test_get_values(hass, client): resp = await client.get("/api/zwave/values/1") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == { @@ -147,7 +147,7 @@ async def test_get_groups(hass, client): resp = await client.get("/api/zwave/groups/2") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == { @@ -169,7 +169,7 @@ async def test_get_groups_nogroups(hass, client): resp = await client.get("/api/zwave/groups/2") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {} @@ -182,7 +182,7 @@ async def test_get_groups_nonode(hass, client): resp = await client.get("/api/zwave/groups/2") - assert resp.status == HTTP_NOT_FOUND + assert resp.status == HTTPStatus.NOT_FOUND result = await resp.json() assert result == {"message": "Node not found"} @@ -206,7 +206,7 @@ async def test_get_config(hass, client): resp = await client.get("/api/zwave/config/2") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == { @@ -232,7 +232,7 @@ async def test_get_config_noconfig_node(hass, client): resp = await client.get("/api/zwave/config/2") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {} @@ -245,7 +245,7 @@ async def test_get_config_nonode(hass, client): resp = await client.get("/api/zwave/config/2") - assert resp.status == HTTP_NOT_FOUND + assert resp.status == HTTPStatus.NOT_FOUND result = await resp.json() assert result == {"message": "Node not found"} @@ -258,7 +258,7 @@ async def test_get_usercodes_nonode(hass, client): resp = await client.get("/api/zwave/usercodes/2") - assert resp.status == HTTP_NOT_FOUND + assert resp.status == HTTPStatus.NOT_FOUND result = await resp.json() assert result == {"message": "Node not found"} @@ -278,7 +278,7 @@ async def test_get_usercodes(hass, client): resp = await client.get("/api/zwave/usercodes/18") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"0": {"code": "1234", "label": "label", "length": 4}} @@ -294,7 +294,7 @@ async def test_get_usercode_nousercode_node(hass, client): resp = await client.get("/api/zwave/usercodes/18") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {} @@ -314,7 +314,7 @@ async def test_get_usercodes_no_genreuser(hass, client): resp = await client.get("/api/zwave/usercodes/18") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {} @@ -324,7 +324,7 @@ async def test_save_config_no_network(hass, client): """Test saving configuration without network data.""" resp = await client.post("/api/zwave/saveconfig") - assert resp.status == HTTP_NOT_FOUND + assert resp.status == HTTPStatus.NOT_FOUND result = await resp.json() assert result == {"message": "No Z-Wave network data found"} @@ -335,7 +335,7 @@ async def test_save_config(hass, client): resp = await client.post("/api/zwave/saveconfig") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert network.write_config.called assert result == {"message": "Z-Wave configuration saved to file"} @@ -367,7 +367,7 @@ async def test_get_protection_values(hass, client): resp = await client.get("/api/zwave/protection/18") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert node.get_protections.called assert node.get_protection_item.called @@ -401,7 +401,7 @@ async def test_get_protection_values_nonexisting_node(hass, client): resp = await client.get("/api/zwave/protection/18") - assert resp.status == HTTP_NOT_FOUND + assert resp.status == HTTPStatus.NOT_FOUND result = await resp.json() assert not node.get_protections.called assert not node.get_protection_item.called @@ -419,7 +419,7 @@ async def test_get_protection_values_without_protectionclass(hass, client): resp = await client.get("/api/zwave/protection/18") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert not node.get_protections.called assert not node.get_protection_item.called @@ -452,7 +452,7 @@ async def test_set_protection_value(hass, client): data=json.dumps({"value_id": "123456", "selection": "Protection by Sequence"}), ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert node.set_protection.called assert result == {"message": "Protection setting successfully set"} @@ -484,7 +484,7 @@ async def test_set_protection_value_failed(hass, client): data=json.dumps({"value_id": "123456", "selection": "Protecton by Sequence"}), ) - assert resp.status == 202 + assert resp.status == HTTPStatus.ACCEPTED result = await resp.json() assert node.set_protection.called assert result == {"message": "Protection setting did not complete"} @@ -516,7 +516,7 @@ async def test_set_protection_value_nonexisting_node(hass, client): data=json.dumps({"value_id": "123456", "selection": "Protecton by Sequence"}), ) - assert resp.status == HTTP_NOT_FOUND + assert resp.status == HTTPStatus.NOT_FOUND result = await resp.json() assert not node.set_protection.called assert result == {"message": "Node not found"} @@ -536,7 +536,7 @@ async def test_set_protection_value_missing_class(hass, client): data=json.dumps({"value_id": "123456", "selection": "Protecton by Sequence"}), ) - assert resp.status == HTTP_NOT_FOUND + assert resp.status == HTTPStatus.NOT_FOUND result = await resp.json() assert not node.set_protection.called assert result == {"message": "No protection commandclass on this node"} diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index 737d99cbddd..cbaa3278e9c 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -1,4 +1,6 @@ """The tests for the Conversation component.""" +from http import HTTPStatus + import pytest from homeassistant.components import conversation @@ -116,7 +118,7 @@ async def test_http_processing_intent(hass, hass_client, hass_admin_user): "/api/conversation/process", json={"text": "I would like the Grolsch beer"} ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK data = await resp.json() assert data == { @@ -212,7 +214,7 @@ async def test_http_api(hass, hass_client): resp = await client.post( "/api/conversation/process", json={"text": "Turn the kitchen on"} ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert len(calls) == 1 call = calls[0] @@ -232,10 +234,10 @@ async def test_http_api_wrong_data(hass, hass_client): client = await hass_client() resp = await client.post("/api/conversation/process", json={"text": 123}) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST resp = await client.post("/api/conversation/process", json={}) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST async def test_custom_agent(hass, hass_client, hass_admin_user): @@ -263,7 +265,7 @@ async def test_custom_agent(hass, hass_client, hass_admin_user): "/api/conversation/process", json={"text": "Test Text", "conversation_id": "test-conv-id"}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert await resp.json() == { "card": {}, "speech": {"plain": {"extra_data": None, "speech": "Test response"}}, From 001a452bb7988b61a7369464d40fa27a66ad3f90 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 22 Oct 2021 19:46:02 +0200 Subject: [PATCH 0689/1038] Update naming scheme for Renault entities (#57922) Co-authored-by: epenet --- .../components/renault/renault_entities.py | 9 ++ tests/components/renault/const.py | 108 +++++++++--------- tests/components/renault/test_select.py | 2 +- 3 files changed, 64 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/renault/renault_entities.py b/homeassistant/components/renault/renault_entities.py index 2ea823c25c2..b963edbc81f 100644 --- a/homeassistant/components/renault/renault_entities.py +++ b/homeassistant/components/renault/renault_entities.py @@ -4,6 +4,7 @@ from __future__ import annotations from dataclasses import dataclass from typing import Optional, cast +from homeassistant.const import ATTR_NAME from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -46,3 +47,11 @@ class RenaultDataEntity(CoordinatorEntity[Optional[T]], Entity): if self.coordinator.data is None: return None return cast(StateType, getattr(self.coordinator.data, key)) + + @property + def name(self) -> str: + """Return the name of the entity. + + Overridden to include the device name. + """ + return f"{self.vehicle.device_info[ATTR_NAME]} {self.entity_description.name}" diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py index 2e411500d62..f1296ba2ce3 100644 --- a/tests/components/renault/const.py +++ b/tests/components/renault/const.py @@ -56,9 +56,9 @@ FIXED_ATTRIBUTES = ( DYNAMIC_ATTRIBUTES = (ATTR_ICON,) ICON_FOR_EMPTY_VALUES = { - "select.charge_mode": "mdi:calendar-remove", - "sensor.charge_state": "mdi:flash-off", - "sensor.plug_state": "mdi:power-plug-off", + "select.reg_number_charge_mode": "mdi:calendar-remove", + "sensor.reg_number_charge_state": "mdi:flash-off", + "sensor.reg_number_plug_state": "mdi:power-plug-off", } MOCK_ACCOUNT_ID = "account_id_1" @@ -95,13 +95,13 @@ MOCK_VEHICLES = { }, BINARY_SENSOR_DOMAIN: [ { - "entity_id": "binary_sensor.plugged_in", + "entity_id": "binary_sensor.reg_number_plugged_in", "unique_id": "vf1aaaaa555777999_plugged_in", "result": STATE_ON, ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG, }, { - "entity_id": "binary_sensor.charging", + "entity_id": "binary_sensor.reg_number_charging", "unique_id": "vf1aaaaa555777999_charging", "result": STATE_ON, ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY_CHARGING, @@ -110,7 +110,7 @@ MOCK_VEHICLES = { DEVICE_TRACKER_DOMAIN: [], SELECT_DOMAIN: [ { - "entity_id": "select.charge_mode", + "entity_id": "select.reg_number_charge_mode", "unique_id": "vf1aaaaa555777999_charge_mode", "result": "always", ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_MODE, @@ -120,7 +120,7 @@ MOCK_VEHICLES = { ], SENSOR_DOMAIN: [ { - "entity_id": "sensor.battery_autonomy", + "entity_id": "sensor.reg_number_battery_autonomy", "unique_id": "vf1aaaaa555777999_battery_autonomy", "result": "141", ATTR_ICON: "mdi:ev-station", @@ -128,7 +128,7 @@ MOCK_VEHICLES = { ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { - "entity_id": "sensor.battery_available_energy", + "entity_id": "sensor.reg_number_battery_available_energy", "unique_id": "vf1aaaaa555777999_battery_available_energy", "result": "31", ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, @@ -136,7 +136,7 @@ MOCK_VEHICLES = { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, }, { - "entity_id": "sensor.battery_level", + "entity_id": "sensor.reg_number_battery_level", "unique_id": "vf1aaaaa555777999_battery_level", "result": "60", ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY, @@ -144,14 +144,14 @@ MOCK_VEHICLES = { ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, { - "entity_id": "sensor.battery_last_activity", + "entity_id": "sensor.reg_number_battery_last_activity", "unique_id": "vf1aaaaa555777999_battery_last_activity", "result": "2020-01-12T21:40:16+00:00", "default_disabled": True, ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, }, { - "entity_id": "sensor.battery_temperature", + "entity_id": "sensor.reg_number_battery_temperature", "unique_id": "vf1aaaaa555777999_battery_temperature", "result": "20", ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, @@ -159,14 +159,14 @@ MOCK_VEHICLES = { ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, { - "entity_id": "sensor.charge_state", + "entity_id": "sensor.reg_number_charge_state", "unique_id": "vf1aaaaa555777999_charge_state", "result": "charge_in_progress", ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_STATE, ATTR_ICON: "mdi:flash", }, { - "entity_id": "sensor.charging_power", + "entity_id": "sensor.reg_number_charging_power", "unique_id": "vf1aaaaa555777999_charging_power", "result": "0.027", ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, @@ -174,7 +174,7 @@ MOCK_VEHICLES = { ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT, }, { - "entity_id": "sensor.charging_remaining_time", + "entity_id": "sensor.reg_number_charging_remaining_time", "unique_id": "vf1aaaaa555777999_charging_remaining_time", "result": "145", ATTR_ICON: "mdi:timer", @@ -182,7 +182,7 @@ MOCK_VEHICLES = { ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, }, { - "entity_id": "sensor.mileage", + "entity_id": "sensor.reg_number_mileage", "unique_id": "vf1aaaaa555777999_mileage", "result": "49114", ATTR_ICON: "mdi:sign-direction", @@ -190,7 +190,7 @@ MOCK_VEHICLES = { ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { - "entity_id": "sensor.outside_temperature", + "entity_id": "sensor.reg_number_outside_temperature", "unique_id": "vf1aaaaa555777999_outside_temperature", "result": "8.0", ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, @@ -198,7 +198,7 @@ MOCK_VEHICLES = { ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, { - "entity_id": "sensor.plug_state", + "entity_id": "sensor.reg_number_plug_state", "unique_id": "vf1aaaaa555777999_plug_state", "result": "plugged", ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG_STATE, @@ -229,13 +229,13 @@ MOCK_VEHICLES = { }, BINARY_SENSOR_DOMAIN: [ { - "entity_id": "binary_sensor.plugged_in", + "entity_id": "binary_sensor.reg_number_plugged_in", "unique_id": "vf1aaaaa555777999_plugged_in", "result": STATE_OFF, ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG, }, { - "entity_id": "binary_sensor.charging", + "entity_id": "binary_sensor.reg_number_charging", "unique_id": "vf1aaaaa555777999_charging", "result": STATE_OFF, ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY_CHARGING, @@ -243,7 +243,7 @@ MOCK_VEHICLES = { ], DEVICE_TRACKER_DOMAIN: [ { - "entity_id": "device_tracker.location", + "entity_id": "device_tracker.reg_number_location", "unique_id": "vf1aaaaa555777999_location", "result": STATE_NOT_HOME, ATTR_ICON: "mdi:car", @@ -251,7 +251,7 @@ MOCK_VEHICLES = { ], SELECT_DOMAIN: [ { - "entity_id": "select.charge_mode", + "entity_id": "select.reg_number_charge_mode", "unique_id": "vf1aaaaa555777999_charge_mode", "result": "schedule_mode", ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_MODE, @@ -261,7 +261,7 @@ MOCK_VEHICLES = { ], SENSOR_DOMAIN: [ { - "entity_id": "sensor.battery_autonomy", + "entity_id": "sensor.reg_number_battery_autonomy", "unique_id": "vf1aaaaa555777999_battery_autonomy", "result": "128", ATTR_ICON: "mdi:ev-station", @@ -269,7 +269,7 @@ MOCK_VEHICLES = { ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { - "entity_id": "sensor.battery_available_energy", + "entity_id": "sensor.reg_number_battery_available_energy", "unique_id": "vf1aaaaa555777999_battery_available_energy", "result": "0", ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, @@ -277,7 +277,7 @@ MOCK_VEHICLES = { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, }, { - "entity_id": "sensor.battery_level", + "entity_id": "sensor.reg_number_battery_level", "unique_id": "vf1aaaaa555777999_battery_level", "result": "50", ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY, @@ -285,14 +285,14 @@ MOCK_VEHICLES = { ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, { - "entity_id": "sensor.battery_last_activity", + "entity_id": "sensor.reg_number_battery_last_activity", "unique_id": "vf1aaaaa555777999_battery_last_activity", "result": "2020-11-17T08:06:48+00:00", "default_disabled": True, ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, }, { - "entity_id": "sensor.battery_temperature", + "entity_id": "sensor.reg_number_battery_temperature", "unique_id": "vf1aaaaa555777999_battery_temperature", "result": STATE_UNKNOWN, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, @@ -300,14 +300,14 @@ MOCK_VEHICLES = { ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, { - "entity_id": "sensor.charge_state", + "entity_id": "sensor.reg_number_charge_state", "unique_id": "vf1aaaaa555777999_charge_state", "result": "charge_error", ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_STATE, ATTR_ICON: "mdi:flash-off", }, { - "entity_id": "sensor.charging_power", + "entity_id": "sensor.reg_number_charging_power", "unique_id": "vf1aaaaa555777999_charging_power", "result": STATE_UNKNOWN, ATTR_DEVICE_CLASS: DEVICE_CLASS_CURRENT, @@ -315,7 +315,7 @@ MOCK_VEHICLES = { ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, }, { - "entity_id": "sensor.charging_remaining_time", + "entity_id": "sensor.reg_number_charging_remaining_time", "unique_id": "vf1aaaaa555777999_charging_remaining_time", "result": STATE_UNKNOWN, ATTR_ICON: "mdi:timer", @@ -323,7 +323,7 @@ MOCK_VEHICLES = { ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, }, { - "entity_id": "sensor.mileage", + "entity_id": "sensor.reg_number_mileage", "unique_id": "vf1aaaaa555777999_mileage", "result": "49114", ATTR_ICON: "mdi:sign-direction", @@ -331,14 +331,14 @@ MOCK_VEHICLES = { ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { - "entity_id": "sensor.plug_state", + "entity_id": "sensor.reg_number_plug_state", "unique_id": "vf1aaaaa555777999_plug_state", "result": "unplugged", ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG_STATE, ATTR_ICON: "mdi:power-plug-off", }, { - "entity_id": "sensor.location_last_activity", + "entity_id": "sensor.reg_number_location_last_activity", "unique_id": "vf1aaaaa555777999_location_last_activity", "result": "2020-02-18T16:58:38+00:00", "default_disabled": True, @@ -369,13 +369,13 @@ MOCK_VEHICLES = { }, BINARY_SENSOR_DOMAIN: [ { - "entity_id": "binary_sensor.plugged_in", + "entity_id": "binary_sensor.reg_number_plugged_in", "unique_id": "vf1aaaaa555777123_plugged_in", "result": STATE_ON, ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG, }, { - "entity_id": "binary_sensor.charging", + "entity_id": "binary_sensor.reg_number_charging", "unique_id": "vf1aaaaa555777123_charging", "result": STATE_ON, ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY_CHARGING, @@ -383,7 +383,7 @@ MOCK_VEHICLES = { ], DEVICE_TRACKER_DOMAIN: [ { - "entity_id": "device_tracker.location", + "entity_id": "device_tracker.reg_number_location", "unique_id": "vf1aaaaa555777123_location", "result": STATE_NOT_HOME, ATTR_ICON: "mdi:car", @@ -391,7 +391,7 @@ MOCK_VEHICLES = { ], SELECT_DOMAIN: [ { - "entity_id": "select.charge_mode", + "entity_id": "select.reg_number_charge_mode", "unique_id": "vf1aaaaa555777123_charge_mode", "result": "always", ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_MODE, @@ -401,7 +401,7 @@ MOCK_VEHICLES = { ], SENSOR_DOMAIN: [ { - "entity_id": "sensor.battery_autonomy", + "entity_id": "sensor.reg_number_battery_autonomy", "unique_id": "vf1aaaaa555777123_battery_autonomy", "result": "141", ATTR_ICON: "mdi:ev-station", @@ -409,7 +409,7 @@ MOCK_VEHICLES = { ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { - "entity_id": "sensor.battery_available_energy", + "entity_id": "sensor.reg_number_battery_available_energy", "unique_id": "vf1aaaaa555777123_battery_available_energy", "result": "31", ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, @@ -417,7 +417,7 @@ MOCK_VEHICLES = { ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, }, { - "entity_id": "sensor.battery_level", + "entity_id": "sensor.reg_number_battery_level", "unique_id": "vf1aaaaa555777123_battery_level", "result": "60", ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY, @@ -425,14 +425,14 @@ MOCK_VEHICLES = { ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, { - "entity_id": "sensor.battery_last_activity", + "entity_id": "sensor.reg_number_battery_last_activity", "unique_id": "vf1aaaaa555777123_battery_last_activity", "result": "2020-01-12T21:40:16+00:00", "default_disabled": True, ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, }, { - "entity_id": "sensor.battery_temperature", + "entity_id": "sensor.reg_number_battery_temperature", "unique_id": "vf1aaaaa555777123_battery_temperature", "result": "20", ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, @@ -440,14 +440,14 @@ MOCK_VEHICLES = { ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, { - "entity_id": "sensor.charge_state", + "entity_id": "sensor.reg_number_charge_state", "unique_id": "vf1aaaaa555777123_charge_state", "result": "charge_in_progress", ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_STATE, ATTR_ICON: "mdi:flash", }, { - "entity_id": "sensor.charging_power", + "entity_id": "sensor.reg_number_charging_power", "unique_id": "vf1aaaaa555777123_charging_power", "result": "27.0", ATTR_DEVICE_CLASS: DEVICE_CLASS_CURRENT, @@ -455,7 +455,7 @@ MOCK_VEHICLES = { ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, }, { - "entity_id": "sensor.charging_remaining_time", + "entity_id": "sensor.reg_number_charging_remaining_time", "unique_id": "vf1aaaaa555777123_charging_remaining_time", "result": "145", ATTR_ICON: "mdi:timer", @@ -463,7 +463,7 @@ MOCK_VEHICLES = { ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, }, { - "entity_id": "sensor.fuel_autonomy", + "entity_id": "sensor.reg_number_fuel_autonomy", "unique_id": "vf1aaaaa555777123_fuel_autonomy", "result": "35", ATTR_ICON: "mdi:gas-station", @@ -471,7 +471,7 @@ MOCK_VEHICLES = { ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { - "entity_id": "sensor.fuel_quantity", + "entity_id": "sensor.reg_number_fuel_quantity", "unique_id": "vf1aaaaa555777123_fuel_quantity", "result": "3", ATTR_ICON: "mdi:fuel", @@ -479,7 +479,7 @@ MOCK_VEHICLES = { ATTR_UNIT_OF_MEASUREMENT: VOLUME_LITERS, }, { - "entity_id": "sensor.mileage", + "entity_id": "sensor.reg_number_mileage", "unique_id": "vf1aaaaa555777123_mileage", "result": "5567", ATTR_ICON: "mdi:sign-direction", @@ -487,14 +487,14 @@ MOCK_VEHICLES = { ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { - "entity_id": "sensor.plug_state", + "entity_id": "sensor.reg_number_plug_state", "unique_id": "vf1aaaaa555777123_plug_state", "result": "plugged", ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG_STATE, ATTR_ICON: "mdi:power-plug", }, { - "entity_id": "sensor.location_last_activity", + "entity_id": "sensor.reg_number_location_last_activity", "unique_id": "vf1aaaaa555777123_location_last_activity", "result": "2020-02-18T16:58:38+00:00", "default_disabled": True, @@ -524,7 +524,7 @@ MOCK_VEHICLES = { BINARY_SENSOR_DOMAIN: [], DEVICE_TRACKER_DOMAIN: [ { - "entity_id": "device_tracker.location", + "entity_id": "device_tracker.reg_number_location", "unique_id": "vf1aaaaa555777123_location", "result": STATE_NOT_HOME, ATTR_ICON: "mdi:car", @@ -533,7 +533,7 @@ MOCK_VEHICLES = { SELECT_DOMAIN: [], SENSOR_DOMAIN: [ { - "entity_id": "sensor.fuel_autonomy", + "entity_id": "sensor.reg_number_fuel_autonomy", "unique_id": "vf1aaaaa555777123_fuel_autonomy", "result": "35", ATTR_ICON: "mdi:gas-station", @@ -541,7 +541,7 @@ MOCK_VEHICLES = { ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { - "entity_id": "sensor.fuel_quantity", + "entity_id": "sensor.reg_number_fuel_quantity", "unique_id": "vf1aaaaa555777123_fuel_quantity", "result": "3", ATTR_ICON: "mdi:fuel", @@ -549,7 +549,7 @@ MOCK_VEHICLES = { ATTR_UNIT_OF_MEASUREMENT: VOLUME_LITERS, }, { - "entity_id": "sensor.mileage", + "entity_id": "sensor.reg_number_mileage", "unique_id": "vf1aaaaa555777123_mileage", "result": "5567", ATTR_ICON: "mdi:sign-direction", @@ -557,7 +557,7 @@ MOCK_VEHICLES = { ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { - "entity_id": "sensor.location_last_activity", + "entity_id": "sensor.reg_number_location_last_activity", "unique_id": "vf1aaaaa555777123_location_last_activity", "result": "2020-02-18T16:58:38+00:00", "default_disabled": True, diff --git a/tests/components/renault/test_select.py b/tests/components/renault/test_select.py index b7adaa0c637..9d2655bfe1c 100644 --- a/tests/components/renault/test_select.py +++ b/tests/components/renault/test_select.py @@ -133,7 +133,7 @@ async def test_select_charge_mode(hass: HomeAssistant, config_entry: ConfigEntry await hass.async_block_till_done() data = { - ATTR_ENTITY_ID: "select.charge_mode", + ATTR_ENTITY_ID: "select.reg_number_charge_mode", ATTR_OPTION: "always", } From 806dc51125aa183158577d35db256da81c2c7527 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Fri, 22 Oct 2021 13:51:22 -0400 Subject: [PATCH 0690/1038] Add datetime_today template method (#57435) --- homeassistant/helpers/template.py | 16 +++++++++++++ tests/helpers/test_template.py | 38 +++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 1c6fcce9e5e..f6c88f16688 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1576,6 +1576,20 @@ def random_every_time(context, values): return random.choice(values) +def today_at(time_str: str = "") -> datetime: + """Record fetching now where the time has been replaced with value.""" + start = dt_util.start_of_local_day(datetime.now()) + + dttime = start.time() if time_str == "" else dt_util.parse_time(time_str) + + if dttime: + return datetime.combine(start.date(), dttime, tzinfo=dt_util.DEFAULT_TIME_ZONE) + + raise ValueError( + f"could not convert {type(time_str).__name__} to datetime: '{time_str}'" + ) + + def relative_time(value): """ Take a datetime and return its "age" as a string. @@ -1685,6 +1699,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.filters["sqrt"] = square_root self.filters["as_datetime"] = dt_util.parse_datetime self.filters["as_timestamp"] = forgiving_as_timestamp + self.filters["today_at"] = today_at self.filters["as_local"] = dt_util.as_local self.filters["timestamp_custom"] = timestamp_custom self.filters["timestamp_local"] = timestamp_local @@ -1725,6 +1740,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.globals["as_datetime"] = dt_util.parse_datetime self.globals["as_local"] = dt_util.as_local self.globals["as_timestamp"] = forgiving_as_timestamp + self.globals["today_at"] = today_at self.globals["relative_time"] = relative_time self.globals["timedelta"] = timedelta self.globals["strptime"] = strptime diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 4be9d527d31..66199e2a94c 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -1080,6 +1080,44 @@ def test_utcnow(mock_is_safe, hass): assert info.has_time is True +@patch( + "homeassistant.helpers.template.TemplateEnvironment.is_safe_callable", + return_value=True, +) +def test_today_at(mock_is_safe, hass): + """Test today_at method.""" + now = dt_util.now() + with patch("homeassistant.util.dt.now", return_value=now): + now = now.replace(hour=10, minute=0, second=0, microsecond=0) + result = template.Template( + "{{ today_at('10:00').isoformat() }}", + hass, + ).async_render() + assert result == now.isoformat() + + result = template.Template( + "{{ today_at('10:00:00').isoformat() }}", + hass, + ).async_render() + assert result == now.isoformat() + + result = template.Template( + "{{ ('10:00:00' | today_at).isoformat() }}", + hass, + ).async_render() + assert result == now.isoformat() + + now = now.replace(hour=0) + result = template.Template( + "{{ today_at().isoformat() }}", + hass, + ).async_render() + assert result == now.isoformat() + + with pytest.raises(TemplateError): + template.Template("{{ today_at('bad') }}", hass).async_render() + + @patch( "homeassistant.helpers.template.TemplateEnvironment.is_safe_callable", return_value=True, From e5255cf21f2011db7aabea0aea9dd613eadd0fab Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 22 Oct 2021 13:59:01 -0400 Subject: [PATCH 0691/1038] Add area_entities and area_devices template functions/filters (#55228) --- homeassistant/helpers/template.py | 52 +++++++++++++++++++ tests/helpers/test_template.py | 85 +++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index f6c88f16688..6cfae98a2ac 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1038,6 +1038,52 @@ def area_name(hass: HomeAssistant, lookup_value: str) -> str | None: return None +def area_entities(hass: HomeAssistant, area_id_or_name: str) -> Iterable[str]: + """Return entities for a given area ID or name.""" + _area_id: str | None + # if area_name returns a value, we know the input was an ID, otherwise we + # assume it's a name, and if it's neither, we return early + if area_name(hass, area_id_or_name) is None: + _area_id = area_id(hass, area_id_or_name) + else: + _area_id = area_id_or_name + if _area_id is None: + return [] + ent_reg = entity_registry.async_get(hass) + entity_ids = [ + entry.entity_id + for entry in entity_registry.async_entries_for_area(ent_reg, _area_id) + ] + dev_reg = device_registry.async_get(hass) + # We also need to add entities tied to a device in the area that don't themselves + # have an area specified since they inherit the area from the device. + entity_ids.extend( + [ + entity.entity_id + for device in device_registry.async_entries_for_area(dev_reg, _area_id) + for entity in entity_registry.async_entries_for_device(ent_reg, device.id) + if entity.area_id is None + ] + ) + return entity_ids + + +def area_devices(hass: HomeAssistant, area_id_or_name: str) -> Iterable[str]: + """Return device IDs for a given area ID or name.""" + _area_id: str | None + # if area_name returns a value, we know the input was an ID, otherwise we + # assume it's a name, and if it's neither, we return early + if area_name(hass, area_id_or_name) is not None: + _area_id = area_id_or_name + else: + _area_id = area_id(hass, area_id_or_name) + if _area_id is None: + return [] + dev_reg = device_registry.async_get(hass) + entries = device_registry.async_entries_for_area(dev_reg, _area_id) + return [entry.id for entry in entries] + + def closest(hass, *args): """Find closest entity. @@ -1783,6 +1829,12 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.globals["area_name"] = hassfunction(area_name) self.filters["area_name"] = pass_context(self.globals["area_name"]) + self.globals["area_entities"] = hassfunction(area_entities) + self.filters["area_entities"] = pass_context(self.globals["area_entities"]) + + self.globals["area_devices"] = hassfunction(area_devices) + self.filters["area_devices"] = pass_context(self.globals["area_devices"]) + if limited: # Only device_entities is available to limited templates, mark other # functions and filters as unsupported. diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 66199e2a94c..305f8a6717b 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -2143,6 +2143,91 @@ async def test_area_name(hass): assert info.rate_limit is None +async def test_area_entities(hass): + """Test area_entities function.""" + config_entry = MockConfigEntry(domain="light") + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + area_registry = mock_area_registry(hass) + + # Test non existing device id + info = render_to_info(hass, "{{ area_entities('deadbeef') }}") + assert_result_info(info, []) + assert info.rate_limit is None + + # Test wrong value type + info = render_to_info(hass, "{{ area_entities(56) }}") + assert_result_info(info, []) + assert info.rate_limit is None + + area_entry = area_registry.async_get_or_create("sensor.fake") + entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=config_entry, + area_id=area_entry.id, + ) + + info = render_to_info(hass, f"{{{{ area_entities('{area_entry.id}') }}}}") + assert_result_info(info, ["light.hue_5678"]) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ '{area_entry.name}' | area_entities }}}}") + assert_result_info(info, ["light.hue_5678"]) + assert info.rate_limit is None + + # Test for entities that inherit area from device + device_entry = device_registry.async_get_or_create( + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + config_entry_id=config_entry.entry_id, + suggested_area="sensor.fake", + ) + entity_registry.async_get_or_create( + "light", + "hue_light", + "5678", + config_entry=config_entry, + device_id=device_entry.id, + ) + + info = render_to_info(hass, f"{{{{ '{area_entry.name}' | area_entities }}}}") + assert_result_info(info, ["light.hue_5678", "light.hue_light_5678"]) + assert info.rate_limit is None + + +async def test_area_devices(hass): + """Test area_devices function.""" + config_entry = MockConfigEntry(domain="light") + device_registry = mock_device_registry(hass) + area_registry = mock_area_registry(hass) + + # Test non existing device id + info = render_to_info(hass, "{{ area_devices('deadbeef') }}") + assert_result_info(info, []) + assert info.rate_limit is None + + # Test wrong value type + info = render_to_info(hass, "{{ area_devices(56) }}") + assert_result_info(info, []) + assert info.rate_limit is None + + area_entry = area_registry.async_get_or_create("sensor.fake") + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + suggested_area=area_entry.name, + ) + + info = render_to_info(hass, f"{{{{ area_devices('{area_entry.id}') }}}}") + assert_result_info(info, [device_entry.id]) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ '{area_entry.name}' | area_devices }}}}") + assert_result_info(info, [device_entry.id]) + assert info.rate_limit is None + + def test_closest_function_to_coord(hass): """Test closest function to coord.""" hass.states.async_set( From 9c1bee9c16ffeb103d7179833aa8cd7e98e65992 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 22 Oct 2021 20:04:25 +0200 Subject: [PATCH 0692/1038] Add support for device configuration URL to UniFi Controller (#58237) --- homeassistant/components/unifi/__init__.py | 1 + tests/components/unifi/test_controller.py | 10 ++++++++++ tests/components/unifi/test_init.py | 2 ++ 3 files changed, 13 insertions(+) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index e7364e6665f..b935a7d01da 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -61,6 +61,7 @@ async def async_setup_entry(hass, config_entry): device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, + configuration_url=controller.api.url, connections={(CONNECTION_NETWORK_MAC, controller.mac)}, default_manufacturer=ATTR_MANUFACTURER, default_model="UniFi Controller", diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 02745dd60fb..869d33037bb 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -40,6 +40,8 @@ from homeassistant.const import ( CONF_VERIFY_SSL, CONTENT_TYPE_JSON, ) +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -256,6 +258,14 @@ async def test_controller_mac(hass, aioclient_mock): controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] assert controller.mac == CONTROLLER_HOST["mac"] + device_registry = dr.async_get(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(CONNECTION_NETWORK_MAC, controller.mac)}, + ) + + assert device_entry.configuration_url == controller.api.url + async def test_controller_not_accessible(hass): """Retry to login gets scheduled when connection fails.""" diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index 591165dabf2..1d1fcad9cbe 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -48,6 +48,7 @@ async def test_controller_mac(hass): with patch("homeassistant.components.unifi.UniFiController") as mock_controller: mock_controller.return_value.async_setup = AsyncMock(return_value=True) mock_controller.return_value.mac = "mac1" + mock_controller.return_value.api.url = "https://123:443" assert await unifi.async_setup_entry(hass, entry) is True assert len(mock_controller.mock_calls) == 2 @@ -56,6 +57,7 @@ async def test_controller_mac(hass): device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, connections={(CONNECTION_NETWORK_MAC, "mac1")} ) + assert device.configuration_url == "https://123:443" assert device.manufacturer == "Ubiquiti Networks" assert device.model == "UniFi Controller" assert device.name == "UniFi Controller" From d9b87ee5c5cb9e3911dbfc03ff9a7172f3ef046a Mon Sep 17 00:00:00 2001 From: avee87 <6134677+avee87@users.noreply.github.com> Date: Fri, 22 Oct 2021 19:09:54 +0100 Subject: [PATCH 0693/1038] Add warning when entity used in template doesn't exist (#57316) --- homeassistant/helpers/template.py | 7 ++++++- tests/helpers/test_template.py | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 6cfae98a2ac..ef66154ee91 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -831,7 +831,12 @@ def _get_state_if_valid(hass: HomeAssistant, entity_id: str) -> TemplateState | def _get_state(hass: HomeAssistant, entity_id: str) -> TemplateState | None: - return _get_template_state_from_state(hass, entity_id, hass.states.get(entity_id)) + state_obj = _get_template_state_from_state( + hass, entity_id, hass.states.get(entity_id) + ) + if state_obj is None: + _LOGGER.warning("Template warning: entity '%s' doesn't exist", entity_id) + return state_obj def _get_template_state_from_state( diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 305f8a6717b..c1f023071d0 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -3206,3 +3206,21 @@ async def test_undefined_variable(hass, caplog): "Template variable warning: 'no_such_variable' is undefined when rendering '{{ no_such_variable }}'" in caplog.text ) + + +async def test_missing_entity(hass, caplog): + """Test a warning is logged on missing entity.""" + hass.states.async_set("binary_sensor.active", "on") + valid_template = template.Template( + "{{ is_state('binary_sensor.active', 'on') }}", hass + ) + invalid_template = template.Template( + "{{ is_state('binary_sensor.abcde', 'on') }}", hass + ) + assert valid_template.async_render() is True + assert invalid_template.async_render() is False + assert ( + "Template warning: entity 'binary_sensor.active' doesn't exist" + not in caplog.text + ) + assert "Template warning: entity 'binary_sensor.abcde' doesn't exist" in caplog.text From c7b4542624f60b4a272c752ae0fb6ae7dc9d0a1b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 22 Oct 2021 08:14:33 -1000 Subject: [PATCH 0694/1038] Enable strict typing in lookin (#58238) --- .strict-typing | 1 + homeassistant/components/lookin/climate.py | 4 +++- homeassistant/components/lookin/entity.py | 2 +- mypy.ini | 11 +++++++++++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.strict-typing b/.strict-typing index e764449d1d6..8e6e59c11d6 100644 --- a/.strict-typing +++ b/.strict-typing @@ -66,6 +66,7 @@ homeassistant.components.lcn.* homeassistant.components.light.* homeassistant.components.local_ip.* homeassistant.components.lock.* +homeassistant.components.lookin.* homeassistant.components.mailbox.* homeassistant.components.media_player.* homeassistant.components.modbus.* diff --git a/homeassistant/components/lookin/climate.py b/homeassistant/components/lookin/climate.py index 536d4ca4016..7bb48350eef 100644 --- a/homeassistant/components/lookin/climate.py +++ b/homeassistant/components/lookin/climate.py @@ -79,7 +79,9 @@ async def async_setup_entry( continue uuid = remote["UUID"] - def _wrap_async_update(uuid) -> Callable[[], Coroutine[None, Any, Climate]]: + def _wrap_async_update( + uuid: str, + ) -> Callable[[], Coroutine[None, Any, Climate]]: """Create a function to capture the uuid cell variable.""" async def _async_update() -> Climate: diff --git a/homeassistant/components/lookin/entity.py b/homeassistant/components/lookin/entity.py index 228d69f8341..ad532889771 100644 --- a/homeassistant/components/lookin/entity.py +++ b/homeassistant/components/lookin/entity.py @@ -26,7 +26,7 @@ def _lookin_device_to_device_info(lookin_device: Device) -> DeviceInfo: def _lookin_controlled_device_to_device_info( - lookin_device: Device, uuid: str, device: Device + lookin_device: Device, uuid: str, device: Climate | Remote ) -> DeviceInfo: return DeviceInfo( identifiers={(DOMAIN, uuid)}, diff --git a/mypy.ini b/mypy.ini index a429c04caa8..a4740a7fedc 100644 --- a/mypy.ini +++ b/mypy.ini @@ -737,6 +737,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.lookin.*] +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.mailbox.*] check_untyped_defs = true disallow_incomplete_defs = true From b413f7434f84cacb3e8aabab8325712c5ffdb25a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 22 Oct 2021 20:28:16 +0200 Subject: [PATCH 0695/1038] Add support for min/max dimmer brightness in Tuya (#58165) --- .coveragerc | 1 + homeassistant/components/tuya/base.py | 11 +-- homeassistant/components/tuya/const.py | 6 ++ homeassistant/components/tuya/light.py | 106 ++++++++++++++++++++++-- homeassistant/components/tuya/number.py | 68 +++++++++++++++ homeassistant/components/tuya/util.py | 16 ++++ 6 files changed, 194 insertions(+), 14 deletions(-) create mode 100644 homeassistant/components/tuya/util.py diff --git a/.coveragerc b/.coveragerc index 3bf6aa04d35..06a0f84e032 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1136,6 +1136,7 @@ omit = homeassistant/components/tuya/sensor.py homeassistant/components/tuya/siren.py homeassistant/components/tuya/switch.py + homeassistant/components/tuya/util.py homeassistant/components/tuya/vacuum.py homeassistant/components/twentemilieu/const.py homeassistant/components/twentemilieu/sensor.py diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index c5b2aac729d..d61c83b17ad 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -12,6 +12,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN, TUYA_HA_SIGNAL_UPDATE_ENTITY +from .util import remap_value _LOGGER = logging.getLogger(__name__) @@ -53,9 +54,7 @@ class IntegerTypeData: reverse: bool = False, ) -> float: """Remap a value from this range to a new range.""" - if reverse: - value = self.max - value + self.min - return ((value - self.min) / (self.max - self.min)) * (to_max - to_min) + to_min + return remap_value(value, self.min, self.max, to_min, to_max, reverse) def remap_value_from( self, @@ -65,11 +64,7 @@ class IntegerTypeData: reverse: bool = False, ) -> float: """Remap a value from its current range to this range.""" - if reverse: - value = from_max - value + from_min - return ((value - from_min) / (from_max - from_min)) * ( - self.max - self.min - ) + self.min + return remap_value(value, from_min, from_max, self.min, self.max, reverse) @classmethod def from_json(cls, data: str) -> IntegerTypeData: diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 2c6bb67fa16..276debe6b96 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -140,6 +140,12 @@ class DPCode(str, Enum): BRIGHT_VALUE_2 = "bright_value_2" BRIGHT_VALUE_3 = "bright_value_3" BRIGHT_VALUE_V2 = "bright_value_v2" + BRIGHTNESS_MAX_1 = "brightness_max_1" + BRIGHTNESS_MAX_2 = "brightness_max_2" + BRIGHTNESS_MAX_3 = "brightness_max_3" + BRIGHTNESS_MIN_1 = "brightness_min_1" + BRIGHTNESS_MIN_2 = "brightness_min_2" + BRIGHTNESS_MIN_3 = "brightness_min_3" C_F = "c_f" # Temperature unit switching CH2O_STATE = "ch2o_state" CH2O_VALUE = "ch2o_value" diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 308c0037d0b..6a194ed94b2 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -26,16 +26,19 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantTuyaData from .base import IntegerTypeData, TuyaEntity from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode, WorkMode +from .util import remap_value @dataclass class TuyaLightEntityDescription(LightEntityDescription): """Describe an Tuya light entity.""" - color_mode: DPCode | None = None + brightness_max: DPCode | None = None + brightness_min: DPCode | None = None brightness: DPCode | tuple[DPCode, ...] | None = None - color_temp: DPCode | tuple[DPCode, ...] | None = None color_data: DPCode | tuple[DPCode, ...] | None = None + color_mode: DPCode | None = None + color_temp: DPCode | tuple[DPCode, ...] | None = None LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { @@ -120,16 +123,22 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { key=DPCode.SWITCH_LED_1, name="Light", brightness=DPCode.BRIGHT_VALUE_1, + brightness_max=DPCode.BRIGHTNESS_MAX_1, + brightness_min=DPCode.BRIGHTNESS_MIN_1, ), TuyaLightEntityDescription( key=DPCode.SWITCH_LED_2, name="Light 2", brightness=DPCode.BRIGHT_VALUE_2, + brightness_max=DPCode.BRIGHTNESS_MAX_2, + brightness_min=DPCode.BRIGHTNESS_MIN_2, ), TuyaLightEntityDescription( key=DPCode.SWITCH_LED_3, name="Light 3", brightness=DPCode.BRIGHT_VALUE_3, + brightness_max=DPCode.BRIGHTNESS_MAX_3, + brightness_min=DPCode.BRIGHTNESS_MIN_3, ), ), # Dimmer @@ -276,6 +285,8 @@ class TuyaLightEntity(TuyaEntity, LightEntity): entity_description: TuyaLightEntityDescription _brightness_dpcode: DPCode | None = None + _brightness_max_type: IntegerTypeData | None = None + _brightness_min_type: IntegerTypeData | None = None _brightness_type: IntegerTypeData | None = None _color_data_dpcode: DPCode | None = None _color_data_type: ColorTypeData | None = None @@ -349,6 +360,20 @@ class TuyaLightEntity(TuyaEntity, LightEntity): device.status_range[self._brightness_dpcode].values ) + # Check if min/max capable + if ( + description.brightness_max is not None + and description.brightness_min is not None + and description.brightness_max in device.function + and description.brightness_min in device.function + ): + self._brightness_max_type = IntegerTypeData.from_json( + device.status_range[description.brightness_max].values + ) + self._brightness_min_type = IntegerTypeData.from_json( + device.status_range[description.brightness_min].values + ) + # Update internals based on found color temperature dpcode if self._color_temp_dpcode: self._attr_supported_color_modes.add(COLOR_MODE_COLOR_TEMP) @@ -456,12 +481,47 @@ class TuyaLightEntity(TuyaEntity, LightEntity): and self.color_mode != COLOR_MODE_HS and self._brightness_type ): + brightness = kwargs[ATTR_BRIGHTNESS] + + # If there is a min/max value, the brightness is actually limited. + # Meaning it is actually not on a 0-255 scale. + if ( + self._brightness_max_type is not None + and self._brightness_min_type is not None + and self.entity_description.brightness_max is not None + and self.entity_description.brightness_min is not None + and ( + brightness_max := self.device.status.get( + self.entity_description.brightness_max + ) + ) + is not None + and ( + brightness_min := self.device.status.get( + self.entity_description.brightness_min + ) + ) + is not None + ): + # Remap values onto our scale + brightness_max = self._brightness_max_type.remap_value_to( + brightness_max + ) + brightness_min = self._brightness_min_type.remap_value_to( + brightness_min + ) + + # Remap the brightness value from their min-max to our 0-255 scale + brightness = remap_value( + brightness, + to_min=brightness_min, + to_max=brightness_max, + ) + commands += [ { "code": self._brightness_dpcode, - "value": round( - self._brightness_type.remap_value_from(kwargs[ATTR_BRIGHTNESS]) - ), + "value": round(self._brightness_type.remap_value_from(brightness)), }, ] @@ -485,7 +545,41 @@ class TuyaLightEntity(TuyaEntity, LightEntity): if brightness is None: return None - return round(self._brightness_type.remap_value_to(brightness)) + # Remap value to our scale + brightness = self._brightness_type.remap_value_to(brightness) + + # If there is a min/max value, the brightness is actually limited. + # Meaning it is actually not on a 0-255 scale. + if ( + self._brightness_max_type is not None + and self._brightness_min_type is not None + and self.entity_description.brightness_max is not None + and self.entity_description.brightness_min is not None + and ( + brightness_max := self.device.status.get( + self.entity_description.brightness_max + ) + ) + is not None + and ( + brightness_min := self.device.status.get( + self.entity_description.brightness_min + ) + ) + is not None + ): + # Remap values onto our scale + brightness_max = self._brightness_max_type.remap_value_to(brightness_max) + brightness_min = self._brightness_min_type.remap_value_to(brightness_min) + + # Remap the brightness value from their min-max to our 0-255 scale + brightness = remap_value( + brightness, + from_min=brightness_min, + from_max=brightness_max, + ) + + return round(brightness) @property def color_temp(self) -> int | None: diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index 0c5aa288f29..922012412b7 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -78,6 +78,74 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { entity_category=ENTITY_CATEGORY_CONFIG, ), ), + # Dimmer Switch + # https://developer.tuya.com/en/docs/iot/categorytgkg?id=Kaiuz0ktx7m0o + "tgkg": ( + NumberEntityDescription( + key=DPCode.BRIGHTNESS_MIN_1, + name="Minimum Brightness", + icon="mdi:lightbulb-outline", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + NumberEntityDescription( + key=DPCode.BRIGHTNESS_MAX_1, + name="Maximum Brightness", + icon="mdi:lightbulb-on-outline", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + NumberEntityDescription( + key=DPCode.BRIGHTNESS_MIN_2, + name="Minimum Brightness 2", + icon="mdi:lightbulb-outline", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + NumberEntityDescription( + key=DPCode.BRIGHTNESS_MAX_2, + name="Maximum Brightness 2", + icon="mdi:lightbulb-on-outline", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + NumberEntityDescription( + key=DPCode.BRIGHTNESS_MIN_3, + name="Minimum Brightness 3", + icon="mdi:lightbulb-outline", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + NumberEntityDescription( + key=DPCode.BRIGHTNESS_MAX_3, + name="Maximum Brightness 3", + icon="mdi:lightbulb-on-outline", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), + # Dimmer Switch + # https://developer.tuya.com/en/docs/iot/categorytgkg?id=Kaiuz0ktx7m0o + "tgq": ( + NumberEntityDescription( + key=DPCode.BRIGHTNESS_MIN_1, + name="Minimum Brightness", + icon="mdi:lightbulb-outline", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + NumberEntityDescription( + key=DPCode.BRIGHTNESS_MAX_1, + name="Maximum Brightness", + icon="mdi:lightbulb-on-outline", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + NumberEntityDescription( + key=DPCode.BRIGHTNESS_MIN_2, + name="Minimum Brightness 2", + icon="mdi:lightbulb-outline", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + NumberEntityDescription( + key=DPCode.BRIGHTNESS_MAX_2, + name="Maximum Brightness 2", + icon="mdi:lightbulb-on-outline", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), # Vibration Sensor # https://developer.tuya.com/en/docs/iot/categoryzd?id=Kaiuz3a5vrzno "zd": ( diff --git a/homeassistant/components/tuya/util.py b/homeassistant/components/tuya/util.py new file mode 100644 index 00000000000..3b29a3e13cf --- /dev/null +++ b/homeassistant/components/tuya/util.py @@ -0,0 +1,16 @@ +"""Utility methods for the Tuya integration.""" +from __future__ import annotations + + +def remap_value( + value: float | int, + from_min: float | int = 0, + from_max: float | int = 255, + to_min: float | int = 0, + to_max: float | int = 255, + reverse: bool = False, +) -> float: + """Remap a value from its current range, to a new range.""" + if reverse: + value = from_max - value + from_min + return ((value - from_min) / (from_max - from_min)) * (to_max - to_min) + to_min From 5193e3115d8a9dabef3b7b6cd423b8df5eaaca1b Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Fri, 22 Oct 2021 20:52:41 +0200 Subject: [PATCH 0696/1038] Restore the previous state of a KNX binary sensor (#57891) --- homeassistant/components/knx/binary_sensor.py | 19 ++++- tests/components/knx/test_binary_sensor.py | 69 ++++++++++++++++++- 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index 9a9c9627670..7a3de7e1ec0 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -7,9 +7,16 @@ from xknx import XKNX from xknx.devices import BinarySensor as XknxBinarySensor from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME +from homeassistant.const import ( + CONF_DEVICE_CLASS, + CONF_NAME, + STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt @@ -36,7 +43,7 @@ async def async_setup_platform( ) -class KNXBinarySensor(KnxEntity, BinarySensorEntity): +class KNXBinarySensor(KnxEntity, BinarySensorEntity, RestoreEntity): """Representation of a KNX binary sensor.""" _device: XknxBinarySensor @@ -61,6 +68,14 @@ class KNXBinarySensor(KnxEntity, BinarySensorEntity): self._attr_force_update = self._device.ignore_internal_state self._attr_unique_id = str(self._device.remote_value.group_address_state) + async def async_added_to_hass(self) -> None: + """Restore last state.""" + await super().async_added_to_hass() + if ( + last_state := await self.async_get_last_state() + ) and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE): + await self._device.remote_value.update_value(last_state.state == STATE_ON) + @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" diff --git a/tests/components/knx/test_binary_sensor.py b/tests/components/knx/test_binary_sensor.py index 1cb9f14e61f..811c8ab1341 100644 --- a/tests/components/knx/test_binary_sensor.py +++ b/tests/components/knx/test_binary_sensor.py @@ -1,10 +1,11 @@ """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 from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, State from homeassistant.util import dt from .conftest import KNXTestKit @@ -213,3 +214,69 @@ async def test_binary_sensor_reset(hass: HomeAssistant, knx: KNXTestKit): # state reset after after timeout state = hass.states.get("binary_sensor.test") assert state.state is STATE_OFF + + +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) + + with patch( + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", + return_value=fake_state, + ): + await knx.setup_integration( + { + BinarySensorSchema.PLATFORM_NAME: [ + { + CONF_NAME: "test", + CONF_STATE_ADDRESS: _ADDRESS, + CONF_SYNC_STATE: False, + }, + ] + } + ) + + # restored state - doesn't send telegram + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_ON + await knx.assert_telegram_count(0) + + await knx.receive_write(_ADDRESS, False) + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.test") + assert state.state is STATE_OFF + + +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) + + with patch( + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", + return_value=fake_state, + ): + await knx.setup_integration( + { + BinarySensorSchema.PLATFORM_NAME: [ + { + 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") + assert state.state == STATE_ON + await knx.assert_telegram_count(0) + + # inverted is on, make sure the state is off after it + await knx.receive_write(_ADDRESS, True) + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.test") + assert state.state is STATE_OFF From b0b49c611e84cab8e5f109249bda6121b0a19ff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren?= Date: Fri, 22 Oct 2021 21:40:39 +0200 Subject: [PATCH 0697/1038] Nello removal (#57926) --- .coveragerc | 1 - CODEOWNERS | 1 - homeassistant/components/nello/__init__.py | 1 - homeassistant/components/nello/lock.py | 97 -------------------- homeassistant/components/nello/manifest.json | 8 -- requirements_all.txt | 3 - 6 files changed, 111 deletions(-) delete mode 100644 homeassistant/components/nello/__init__.py delete mode 100644 homeassistant/components/nello/lock.py delete mode 100644 homeassistant/components/nello/manifest.json diff --git a/.coveragerc b/.coveragerc index 06a0f84e032..713f7e98cc3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -701,7 +701,6 @@ omit = homeassistant/components/neato/switch.py homeassistant/components/neato/vacuum.py homeassistant/components/nederlandse_spoorwegen/sensor.py - homeassistant/components/nello/lock.py homeassistant/components/nest/legacy/* homeassistant/components/netdata/sensor.py homeassistant/components/netgear/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index 3f67d50ccd0..79a602545d7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -337,7 +337,6 @@ homeassistant/components/nam/* @bieniu homeassistant/components/nanoleaf/* @milanmeu homeassistant/components/neato/* @dshokouhi @Santobert homeassistant/components/nederlandse_spoorwegen/* @YarmoM -homeassistant/components/nello/* @pschmitt homeassistant/components/ness_alarm/* @nickw444 homeassistant/components/nest/* @allenporter homeassistant/components/netatmo/* @cgtobi diff --git a/homeassistant/components/nello/__init__.py b/homeassistant/components/nello/__init__.py deleted file mode 100644 index dfe556f7f29..00000000000 --- a/homeassistant/components/nello/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The nello component.""" diff --git a/homeassistant/components/nello/lock.py b/homeassistant/components/nello/lock.py deleted file mode 100644 index 93e63b05da9..00000000000 --- a/homeassistant/components/nello/lock.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Nello.io lock platform.""" -from itertools import filterfalse -import logging - -from pynello.private import Nello -import voluptuous as vol - -from homeassistant.components.lock import PLATFORM_SCHEMA, SUPPORT_OPEN, LockEntity -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -ATTR_ADDRESS = "address" -ATTR_LOCATION_ID = "location_id" -EVENT_DOOR_BELL = "nello_bell_ring" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Nello lock platform.""" - - nello = Nello(config.get(CONF_USERNAME), config.get(CONF_PASSWORD)) - add_entities([NelloLock(lock) for lock in nello.locations], True) - - -class NelloLock(LockEntity): - """Representation of a Nello lock.""" - - def __init__(self, nello_lock): - """Initialize the lock.""" - self._nello_lock = nello_lock - self._device_attrs = None - self._activity = None - self._name = None - - @property - def name(self): - """Return the name of the lock.""" - return self._name - - @property - def is_locked(self): - """Return true if lock is locked.""" - return True - - @property - def extra_state_attributes(self): - """Return the device specific state attributes.""" - return self._device_attrs - - def update(self): - """Update the nello lock properties.""" - self._nello_lock.update() - # Location identifiers - location_id = self._nello_lock.location_id - short_id = self._nello_lock.short_id - address = self._nello_lock.address - self._name = f"Nello {short_id}" - self._device_attrs = {ATTR_ADDRESS: address, ATTR_LOCATION_ID: location_id} - # Process recent activity - activity = self._nello_lock.activity - if self._activity: - # Filter out old events - new_activity = list(filterfalse(lambda x: x in self._activity, activity)) - if new_activity: - for act in new_activity: - activity_type = act.get("type") - if activity_type == "bell.ring.denied": - event_data = { - "address": address, - "date": act.get("date"), - "description": act.get("description"), - "location_id": location_id, - "short_id": short_id, - } - self.hass.bus.fire(EVENT_DOOR_BELL, event_data) - # Save the activity history so that we don't trigger an event twice - self._activity = activity - - def unlock(self, **kwargs): - """Unlock the device.""" - if not self._nello_lock.open_door(): - _LOGGER.error("Failed to unlock") - - def open(self, **kwargs): - """Unlock the device.""" - if not self._nello_lock.open_door(): - _LOGGER.error("Failed to open") - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_OPEN diff --git a/homeassistant/components/nello/manifest.json b/homeassistant/components/nello/manifest.json deleted file mode 100644 index 790b8610543..00000000000 --- a/homeassistant/components/nello/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "nello", - "name": "Nello", - "documentation": "https://www.home-assistant.io/integrations/nello", - "requirements": ["pynello==2.0.3"], - "codeowners": ["@pschmitt"], - "iot_class": "cloud_polling" -} diff --git a/requirements_all.txt b/requirements_all.txt index 281b47e3471..d40d4f7d2d2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1651,9 +1651,6 @@ pymyq==3.1.4 # homeassistant.components.mysensors pymysensors==0.21.0 -# homeassistant.components.nello -pynello==2.0.3 - # homeassistant.components.netgear pynetgear==0.7.0 From 823ca7ee402f71f227d61eb559e6eef62dfda71c Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 22 Oct 2021 21:41:06 +0200 Subject: [PATCH 0698/1038] Use attributes octoprint (#58241) --- .../components/octoprint/binary_sensor.py | 14 +---- homeassistant/components/octoprint/sensor.py | 63 +++++-------------- 2 files changed, 17 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/octoprint/binary_sensor.py b/homeassistant/components/octoprint/binary_sensor.py index e7806999698..fe18af4f808 100644 --- a/homeassistant/components/octoprint/binary_sensor.py +++ b/homeassistant/components/octoprint/binary_sensor.py @@ -52,9 +52,9 @@ class OctoPrintBinarySensorBase(CoordinatorEntity, BinarySensorEntity): ) -> None: """Initialize a new OctoPrint sensor.""" super().__init__(coordinator) - self._name = f"Octoprint {sensor_type}" - self.sensor_type = sensor_type self._device_id = device_id + self._attr_name = f"Octoprint {sensor_type}" + self._attr_unique_id = f"{sensor_type}-{device_id}" @property def device_info(self): @@ -65,16 +65,6 @@ class OctoPrintBinarySensorBase(CoordinatorEntity, BinarySensorEntity): "name": "Octoprint", } - @property - def unique_id(self): - """Return a unique id.""" - return f"{self.sensor_type}-{self._device_id}" - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - @property def is_on(self): """Return true if binary sensor is on.""" diff --git a/homeassistant/components/octoprint/sensor.py b/homeassistant/components/octoprint/sensor.py index 3eef0654870..3feff099297 100644 --- a/homeassistant/components/octoprint/sensor.py +++ b/homeassistant/components/octoprint/sensor.py @@ -75,9 +75,9 @@ class OctoPrintSensorBase(CoordinatorEntity, SensorEntity): ) -> None: """Initialize a new OctoPrint sensor.""" super().__init__(coordinator) - self._sensor_type = sensor_type - self._name = f"Octoprint {sensor_type}" self._device_id = device_id + self._attr_name = f"Octoprint {sensor_type}" + self._attr_unique_id = f"{sensor_type}-{device_id}" @property def device_info(self): @@ -88,20 +88,12 @@ class OctoPrintSensorBase(CoordinatorEntity, SensorEntity): "name": "Octoprint", } - @property - def unique_id(self): - """Return a unique id.""" - return f"{self._sensor_type}-{self._device_id}" - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - class OctoPrintStatusSensor(OctoPrintSensorBase): """Representation of an OctoPrint sensor.""" + _attr_icon = "mdi:printer-3d" + def __init__(self, coordinator: DataUpdateCoordinator, device_id: str) -> None: """Initialize a new OctoPrint sensor.""" super().__init__(coordinator, "Current State", device_id) @@ -115,11 +107,6 @@ class OctoPrintStatusSensor(OctoPrintSensorBase): return printer.state.text - @property - def icon(self): - """Icon to use in the frontend.""" - return "mdi:printer-3d" - @property def available(self) -> bool: """Return if entity is available.""" @@ -129,6 +116,9 @@ class OctoPrintStatusSensor(OctoPrintSensorBase): class OctoPrintJobPercentageSensor(OctoPrintSensorBase): """Representation of an OctoPrint sensor.""" + _attr_native_unit_of_measurement = PERCENTAGE + _attr_icon = "mdi:file-percent" + def __init__(self, coordinator: DataUpdateCoordinator, device_id: str) -> None: """Initialize a new OctoPrint sensor.""" super().__init__(coordinator, "Job Percentage", device_id) @@ -146,20 +136,12 @@ class OctoPrintJobPercentageSensor(OctoPrintSensorBase): return round(state, 2) - @property - def native_unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return PERCENTAGE - - @property - def icon(self): - """Icon to use in the frontend.""" - return "mdi:file-percent" - class OctoPrintEstimatedFinishTimeSensor(OctoPrintSensorBase): """Representation of an OctoPrint sensor.""" + _attr_device_class = DEVICE_CLASS_TIMESTAMP + def __init__(self, coordinator: DataUpdateCoordinator, device_id: str) -> None: """Initialize a new OctoPrint sensor.""" super().__init__(coordinator, "Estimated Finish Time", device_id) @@ -175,15 +157,12 @@ class OctoPrintEstimatedFinishTimeSensor(OctoPrintSensorBase): return (read_time + timedelta(seconds=job.progress.print_time_left)).isoformat() - @property - def device_class(self): - """Return the device class of the sensor.""" - return DEVICE_CLASS_TIMESTAMP - class OctoPrintStartTimeSensor(OctoPrintSensorBase): """Representation of an OctoPrint sensor.""" + _attr_device_class = DEVICE_CLASS_TIMESTAMP + def __init__(self, coordinator: DataUpdateCoordinator, device_id: str) -> None: """Initialize a new OctoPrint sensor.""" super().__init__(coordinator, "Start Time", device_id) @@ -200,15 +179,14 @@ class OctoPrintStartTimeSensor(OctoPrintSensorBase): return (read_time - timedelta(seconds=job.progress.print_time)).isoformat() - @property - def device_class(self): - """Return the device class of the sensor.""" - return DEVICE_CLASS_TIMESTAMP - class OctoPrintTemperatureSensor(OctoPrintSensorBase): """Representation of an OctoPrint sensor.""" + _attr_native_unit_of_measurement = TEMP_CELSIUS + _attr_device_class = DEVICE_CLASS_TEMPERATURE + _attr_state_class = STATE_CLASS_MEASUREMENT + def __init__( self, coordinator: DataUpdateCoordinator, @@ -220,17 +198,6 @@ class OctoPrintTemperatureSensor(OctoPrintSensorBase): super().__init__(coordinator, f"{temp_type} {tool} temp", device_id) self._temp_type = temp_type self._api_tool = tool - self._attr_state_class = STATE_CLASS_MEASUREMENT - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return TEMP_CELSIUS - - @property - def device_class(self): - """Return the device class of this entity.""" - return DEVICE_CLASS_TEMPERATURE @property def native_value(self): From f1091b80a7f57dea941e11de49ab98687c69dd00 Mon Sep 17 00:00:00 2001 From: Dennis Schroer Date: Fri, 22 Oct 2021 22:00:44 +0200 Subject: [PATCH 0699/1038] Add statistics support to Huisbaasje (#54651) --- CODEOWNERS | 2 +- homeassistant/components/huisbaasje/const.py | 61 +++- .../components/huisbaasje/manifest.json | 10 +- homeassistant/components/huisbaasje/sensor.py | 8 +- tests/components/huisbaasje/test_data.py | 16 +- tests/components/huisbaasje/test_init.py | 6 +- tests/components/huisbaasje/test_sensor.py | 312 ++++++++++++++++-- 7 files changed, 367 insertions(+), 48 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 79a602545d7..eeac3b6af78 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -229,7 +229,7 @@ homeassistant/components/http/* @home-assistant/core homeassistant/components/huawei_lte/* @scop @fphammerle homeassistant/components/huawei_router/* @abmantis homeassistant/components/hue/* @balloob @frenck -homeassistant/components/huisbaasje/* @denniss17 +homeassistant/components/huisbaasje/* @dennisschroer homeassistant/components/humidifier/* @home-assistant/core @Shulyaka homeassistant/components/hunterdouglas_powerview/* @bdraco homeassistant/components/hvv_departures/* @vigonotion diff --git a/homeassistant/components/huisbaasje/const.py b/homeassistant/components/huisbaasje/const.py index f2565a15ce2..e989b7949e2 100644 --- a/homeassistant/components/huisbaasje/const.py +++ b/homeassistant/components/huisbaasje/const.py @@ -8,9 +8,10 @@ from huisbaasje.const import ( SOURCE_TYPE_GAS, ) -from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT +from homeassistant.components.sensor import STATE_CLASS_TOTAL_INCREASING from homeassistant.const import ( DEVICE_CLASS_ENERGY, + DEVICE_CLASS_GAS, DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, TIME_HOURS, @@ -49,31 +50,62 @@ SENSORS_INFO = [ "name": "Huisbaasje Current Power", "device_class": DEVICE_CLASS_POWER, "source_type": SOURCE_TYPE_ELECTRICITY, - "state_class": STATE_CLASS_MEASUREMENT, }, { - "name": "Huisbaasje Current Power In", + "name": "Huisbaasje Current Power In Peak", "device_class": DEVICE_CLASS_POWER, "source_type": SOURCE_TYPE_ELECTRICITY_IN, - "state_class": STATE_CLASS_MEASUREMENT, }, { - "name": "Huisbaasje Current Power In Low", + "name": "Huisbaasje Current Power In Off Peak", "device_class": DEVICE_CLASS_POWER, "source_type": SOURCE_TYPE_ELECTRICITY_IN_LOW, - "state_class": STATE_CLASS_MEASUREMENT, }, { - "name": "Huisbaasje Current Power Out", + "name": "Huisbaasje Current Power Out Peak", "device_class": DEVICE_CLASS_POWER, "source_type": SOURCE_TYPE_ELECTRICITY_OUT, - "state_class": STATE_CLASS_MEASUREMENT, }, { - "name": "Huisbaasje Current Power Out Low", + "name": "Huisbaasje Current Power Out Off Peak", "device_class": DEVICE_CLASS_POWER, "source_type": SOURCE_TYPE_ELECTRICITY_OUT_LOW, - "state_class": STATE_CLASS_MEASUREMENT, + }, + { + "name": "Huisbaasje Energy Consumption Peak Today", + "device_class": DEVICE_CLASS_ENERGY, + "unit_of_measurement": ENERGY_KILO_WATT_HOUR, + "source_type": SOURCE_TYPE_ELECTRICITY_IN, + "sensor_type": SENSOR_TYPE_THIS_DAY, + "state_class": STATE_CLASS_TOTAL_INCREASING, + "precision": 3, + }, + { + "name": "Huisbaasje Energy Consumption Off Peak Today", + "device_class": DEVICE_CLASS_ENERGY, + "unit_of_measurement": ENERGY_KILO_WATT_HOUR, + "source_type": SOURCE_TYPE_ELECTRICITY_IN_LOW, + "sensor_type": SENSOR_TYPE_THIS_DAY, + "state_class": STATE_CLASS_TOTAL_INCREASING, + "precision": 3, + }, + { + "name": "Huisbaasje Energy Production Peak Today", + "device_class": DEVICE_CLASS_ENERGY, + "unit_of_measurement": ENERGY_KILO_WATT_HOUR, + "source_type": SOURCE_TYPE_ELECTRICITY_OUT, + "sensor_type": SENSOR_TYPE_THIS_DAY, + "state_class": STATE_CLASS_TOTAL_INCREASING, + "precision": 3, + }, + { + "name": "Huisbaasje Energy Production Off Peak Today", + "device_class": DEVICE_CLASS_ENERGY, + "unit_of_measurement": ENERGY_KILO_WATT_HOUR, + "source_type": SOURCE_TYPE_ELECTRICITY_OUT_LOW, + "sensor_type": SENSOR_TYPE_THIS_DAY, + "state_class": STATE_CLASS_TOTAL_INCREASING, + "precision": 3, }, { "name": "Huisbaasje Energy Today", @@ -113,37 +145,44 @@ SENSORS_INFO = [ "source_type": SOURCE_TYPE_GAS, "icon": "mdi:fire", "precision": 1, - "state_class": STATE_CLASS_MEASUREMENT, }, { "name": "Huisbaasje Gas Today", + "device_class": DEVICE_CLASS_GAS, "unit_of_measurement": VOLUME_CUBIC_METERS, "source_type": SOURCE_TYPE_GAS, "sensor_type": SENSOR_TYPE_THIS_DAY, + "state_class": STATE_CLASS_TOTAL_INCREASING, "icon": "mdi:counter", "precision": 1, }, { "name": "Huisbaasje Gas This Week", + "device_class": DEVICE_CLASS_GAS, "unit_of_measurement": VOLUME_CUBIC_METERS, "source_type": SOURCE_TYPE_GAS, "sensor_type": SENSOR_TYPE_THIS_WEEK, + "state_class": STATE_CLASS_TOTAL_INCREASING, "icon": "mdi:counter", "precision": 1, }, { "name": "Huisbaasje Gas This Month", + "device_class": DEVICE_CLASS_GAS, "unit_of_measurement": VOLUME_CUBIC_METERS, "source_type": SOURCE_TYPE_GAS, "sensor_type": SENSOR_TYPE_THIS_MONTH, + "state_class": STATE_CLASS_TOTAL_INCREASING, "icon": "mdi:counter", "precision": 1, }, { "name": "Huisbaasje Gas This Year", + "device_class": DEVICE_CLASS_GAS, "unit_of_measurement": VOLUME_CUBIC_METERS, "source_type": SOURCE_TYPE_GAS, "sensor_type": SENSOR_TYPE_THIS_YEAR, + "state_class": STATE_CLASS_TOTAL_INCREASING, "icon": "mdi:counter", "precision": 1, }, diff --git a/homeassistant/components/huisbaasje/manifest.json b/homeassistant/components/huisbaasje/manifest.json index d0182733750..6b9981fee23 100644 --- a/homeassistant/components/huisbaasje/manifest.json +++ b/homeassistant/components/huisbaasje/manifest.json @@ -3,7 +3,11 @@ "name": "Huisbaasje", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/huisbaasje", - "requirements": ["huisbaasje-client==0.1.0"], - "codeowners": ["@denniss17"], + "requirements": [ + "huisbaasje-client==0.1.0" + ], + "codeowners": [ + "@dennisschroer" + ], "iot_class": "cloud_polling" -} +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/sensor.py b/homeassistant/components/huisbaasje/sensor.py index 6f18ad27796..4ffc0048079 100644 --- a/homeassistant/components/huisbaasje/sensor.py +++ b/homeassistant/components/huisbaasje/sensor.py @@ -1,7 +1,9 @@ """Platform for sensor integration.""" from __future__ import annotations -from homeassistant.components.sensor import SensorEntity +import logging + +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID, POWER_WATT from homeassistant.core import HomeAssistant @@ -12,6 +14,8 @@ from homeassistant.helpers.update_coordinator import ( from .const import DATA_COORDINATOR, DOMAIN, SENSOR_TYPE_RATE, SENSORS_INFO +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities @@ -40,7 +44,7 @@ class HuisbaasjeSensor(CoordinatorEntity, SensorEntity): unit_of_measurement: str = POWER_WATT, icon: str = "mdi:lightning-bolt", precision: int = 0, - state_class: str | None = None, + state_class: str | None = STATE_CLASS_MEASUREMENT, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) diff --git a/tests/components/huisbaasje/test_data.py b/tests/components/huisbaasje/test_data.py index 5752be11c51..e14976443f3 100644 --- a/tests/components/huisbaasje/test_data.py +++ b/tests/components/huisbaasje/test_data.py @@ -35,17 +35,17 @@ MOCK_CURRENT_MEASUREMENTS = { }, "electricityOut": { "measurement": None, - "thisDay": {"value": 0.0, "cost": 0.0}, - "thisWeek": {"value": 0.0, "cost": 0.0}, - "thisMonth": {"value": 0.0, "cost": 0.0}, - "thisYear": {"value": 0.0, "cost": 0.0}, + "thisDay": {"value": 1.51234, "cost": 0.0}, + "thisWeek": {"value": 2.5, "cost": 0.0}, + "thisMonth": {"value": 3.5, "cost": 0.0}, + "thisYear": {"value": 4.5, "cost": 0.0}, }, "electricityOutLow": { "measurement": None, - "thisDay": {"value": 0.0, "cost": 0.0}, - "thisWeek": {"value": 0.0, "cost": 0.0}, - "thisMonth": {"value": 0.0, "cost": 0.0}, - "thisYear": {"value": 0.0, "cost": 0.0}, + "thisDay": {"value": 1.09281, "cost": 0.0}, + "thisWeek": {"value": 2.0, "cost": 0.0}, + "thisMonth": {"value": 3.0, "cost": 0.0}, + "thisYear": {"value": 4.0, "cost": 0.0}, }, "gas": { "measurement": { diff --git a/tests/components/huisbaasje/test_init.py b/tests/components/huisbaasje/test_init.py index 390dc6c304d..ba2022f7583 100644 --- a/tests/components/huisbaasje/test_init.py +++ b/tests/components/huisbaasje/test_init.py @@ -56,7 +56,7 @@ async def test_setup_entry(hass: HomeAssistant): # Assert entities are loaded entities = hass.states.async_entity_ids("sensor") - assert len(entities) == 14 + assert len(entities) == 18 # Assert mocks are called assert len(mock_authenticate.mock_calls) == 1 @@ -128,13 +128,13 @@ async def test_unload_entry(hass: HomeAssistant): await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED entities = hass.states.async_entity_ids("sensor") - assert len(entities) == 14 + assert len(entities) == 18 # Unload config entry await hass.config_entries.async_unload(config_entry.entry_id) assert config_entry.state is ConfigEntryState.NOT_LOADED entities = hass.states.async_entity_ids("sensor") - assert len(entities) == 14 + assert len(entities) == 18 for entity in entities: assert hass.states.get(entity).state == STATE_UNAVAILABLE diff --git a/tests/components/huisbaasje/test_sensor.py b/tests/components/huisbaasje/test_sensor.py index 45ce20af628..f6e7d1c4609 100644 --- a/tests/components/huisbaasje/test_sensor.py +++ b/tests/components/huisbaasje/test_sensor.py @@ -2,7 +2,26 @@ from unittest.mock import patch from homeassistant.components import huisbaasje -from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME +from homeassistant.components.huisbaasje.const import FLOW_CUBIC_METERS_PER_HOUR +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + CONF_ID, + CONF_PASSWORD, + CONF_USERNAME, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_GAS, + DEVICE_CLASS_POWER, + ENERGY_KILO_WATT_HOUR, + POWER_WATT, + VOLUME_CUBIC_METERS, +) from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -41,25 +60,255 @@ async def test_setup_entry(hass: HomeAssistant): await hass.async_block_till_done() # Assert data is loaded - assert hass.states.get("sensor.huisbaasje_current_power").state == "1012.0" - assert hass.states.get("sensor.huisbaasje_current_power_in").state == "1012.0" + current_power = hass.states.get("sensor.huisbaasje_current_power") + assert current_power.state == "1012.0" + assert current_power.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert current_power.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" + assert current_power.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert current_power.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + + current_power_in = hass.states.get("sensor.huisbaasje_current_power_in_peak") + assert current_power_in.state == "1012.0" + assert current_power_in.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert current_power_in.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" assert ( - hass.states.get("sensor.huisbaasje_current_power_in_low").state == "unknown" + current_power_in.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT ) - assert hass.states.get("sensor.huisbaasje_current_power_out").state == "unknown" + assert current_power_in.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + + current_power_in_low = hass.states.get( + "sensor.huisbaasje_current_power_in_off_peak" + ) + assert current_power_in_low.state == "unknown" assert ( - hass.states.get("sensor.huisbaasje_current_power_out_low").state - == "unknown" + current_power_in_low.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + ) + assert current_power_in_low.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" + assert ( + current_power_in_low.attributes.get(ATTR_STATE_CLASS) + == STATE_CLASS_MEASUREMENT + ) + assert ( + current_power_in_low.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + ) + + current_power_out = hass.states.get("sensor.huisbaasje_current_power_out_peak") + assert current_power_out.state == "unknown" + assert current_power_out.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER + assert current_power_out.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" + assert ( + current_power_out.attributes.get(ATTR_STATE_CLASS) + == STATE_CLASS_MEASUREMENT + ) + assert current_power_out.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + + current_power_out_low = hass.states.get( + "sensor.huisbaasje_current_power_out_off_peak" + ) + assert current_power_out_low.state == "unknown" + assert ( + current_power_out_low.attributes.get(ATTR_DEVICE_CLASS) + == DEVICE_CLASS_POWER + ) + assert current_power_out_low.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" + assert ( + current_power_out_low.attributes.get(ATTR_STATE_CLASS) + == STATE_CLASS_MEASUREMENT + ) + assert ( + current_power_out_low.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + ) + + energy_consumption_peak_today = hass.states.get( + "sensor.huisbaasje_energy_consumption_peak_today" + ) + assert energy_consumption_peak_today.state == "2.67" + assert ( + energy_consumption_peak_today.attributes.get(ATTR_DEVICE_CLASS) + == DEVICE_CLASS_ENERGY + ) + assert ( + energy_consumption_peak_today.attributes.get(ATTR_ICON) + == "mdi:lightning-bolt" + ) + assert ( + energy_consumption_peak_today.attributes.get(ATTR_STATE_CLASS) + is STATE_CLASS_TOTAL_INCREASING + ) + assert ( + energy_consumption_peak_today.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == ENERGY_KILO_WATT_HOUR + ) + + energy_consumption_off_peak_today = hass.states.get( + "sensor.huisbaasje_energy_consumption_off_peak_today" + ) + assert energy_consumption_off_peak_today.state == "0.627" + assert ( + energy_consumption_off_peak_today.attributes.get(ATTR_DEVICE_CLASS) + == DEVICE_CLASS_ENERGY + ) + assert ( + energy_consumption_off_peak_today.attributes.get(ATTR_ICON) + == "mdi:lightning-bolt" + ) + assert ( + energy_consumption_off_peak_today.attributes.get(ATTR_STATE_CLASS) + is STATE_CLASS_TOTAL_INCREASING + ) + assert ( + energy_consumption_off_peak_today.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == ENERGY_KILO_WATT_HOUR + ) + + energy_production_peak_today = hass.states.get( + "sensor.huisbaasje_energy_production_peak_today" + ) + assert energy_production_peak_today.state == "1.512" + assert ( + energy_production_peak_today.attributes.get(ATTR_DEVICE_CLASS) + == DEVICE_CLASS_ENERGY + ) + assert ( + energy_production_peak_today.attributes.get(ATTR_ICON) + == "mdi:lightning-bolt" + ) + assert ( + energy_production_peak_today.attributes.get(ATTR_STATE_CLASS) + is STATE_CLASS_TOTAL_INCREASING + ) + assert ( + energy_production_peak_today.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == ENERGY_KILO_WATT_HOUR + ) + + energy_production_off_peak_today = hass.states.get( + "sensor.huisbaasje_energy_production_off_peak_today" + ) + assert energy_production_off_peak_today.state == "1.093" + assert ( + energy_production_off_peak_today.attributes.get(ATTR_DEVICE_CLASS) + == DEVICE_CLASS_ENERGY + ) + assert ( + energy_production_off_peak_today.attributes.get(ATTR_ICON) + == "mdi:lightning-bolt" + ) + assert ( + energy_production_off_peak_today.attributes.get(ATTR_STATE_CLASS) + is STATE_CLASS_TOTAL_INCREASING + ) + assert ( + energy_production_off_peak_today.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == ENERGY_KILO_WATT_HOUR + ) + + energy_today = hass.states.get("sensor.huisbaasje_energy_today") + assert energy_today.state == "3.3" + assert energy_today.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert energy_today.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" + assert energy_today.attributes.get(ATTR_STATE_CLASS) is STATE_CLASS_MEASUREMENT + assert ( + energy_today.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == ENERGY_KILO_WATT_HOUR + ) + + energy_this_week = hass.states.get("sensor.huisbaasje_energy_this_week") + assert energy_this_week.state == "17.5" + assert energy_this_week.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert energy_this_week.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" + assert ( + energy_this_week.attributes.get(ATTR_STATE_CLASS) is STATE_CLASS_MEASUREMENT + ) + assert ( + energy_this_week.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == ENERGY_KILO_WATT_HOUR + ) + + energy_this_month = hass.states.get("sensor.huisbaasje_energy_this_month") + assert energy_this_month.state == "103.3" + assert ( + energy_this_month.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + ) + assert energy_this_month.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" + assert ( + energy_this_month.attributes.get(ATTR_STATE_CLASS) + is STATE_CLASS_MEASUREMENT + ) + assert ( + energy_this_month.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == ENERGY_KILO_WATT_HOUR + ) + + energy_this_year = hass.states.get("sensor.huisbaasje_energy_this_year") + assert energy_this_year.state == "673.0" + assert energy_this_year.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_ENERGY + assert energy_this_year.attributes.get(ATTR_ICON) == "mdi:lightning-bolt" + assert ( + energy_this_year.attributes.get(ATTR_STATE_CLASS) is STATE_CLASS_MEASUREMENT + ) + assert ( + energy_this_year.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == ENERGY_KILO_WATT_HOUR + ) + + current_gas = hass.states.get("sensor.huisbaasje_current_gas") + assert current_gas.state == "0.0" + assert current_gas.attributes.get(ATTR_DEVICE_CLASS) is None + assert current_gas.attributes.get(ATTR_ICON) == "mdi:fire" + assert current_gas.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + current_gas.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == FLOW_CUBIC_METERS_PER_HOUR + ) + + gas_today = hass.states.get("sensor.huisbaasje_gas_today") + assert gas_today.state == "1.1" + assert gas_today.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS + assert gas_today.attributes.get(ATTR_ICON) == "mdi:counter" + assert ( + gas_today.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + ) + assert gas_today.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + + gas_this_week = hass.states.get("sensor.huisbaasje_gas_this_week") + assert gas_this_week.state == "5.6" + assert gas_this_week.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS + assert gas_this_week.attributes.get(ATTR_ICON) == "mdi:counter" + assert ( + gas_this_week.attributes.get(ATTR_STATE_CLASS) + is STATE_CLASS_TOTAL_INCREASING + ) + assert ( + gas_this_week.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == VOLUME_CUBIC_METERS + ) + + gas_this_month = hass.states.get("sensor.huisbaasje_gas_this_month") + assert gas_this_month.state == "39.1" + assert gas_this_month.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS + assert gas_this_month.attributes.get(ATTR_ICON) == "mdi:counter" + assert ( + gas_this_month.attributes.get(ATTR_STATE_CLASS) + is STATE_CLASS_TOTAL_INCREASING + ) + assert ( + gas_this_month.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == VOLUME_CUBIC_METERS + ) + + gas_this_year = hass.states.get("sensor.huisbaasje_gas_this_year") + assert gas_this_year.state == "116.7" + assert gas_this_year.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_GAS + assert gas_this_year.attributes.get(ATTR_ICON) == "mdi:counter" + assert ( + gas_this_year.attributes.get(ATTR_STATE_CLASS) + is STATE_CLASS_TOTAL_INCREASING + ) + assert ( + gas_this_year.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == VOLUME_CUBIC_METERS ) - assert hass.states.get("sensor.huisbaasje_current_gas").state == "0.0" - assert hass.states.get("sensor.huisbaasje_energy_today").state == "3.3" - assert hass.states.get("sensor.huisbaasje_energy_this_week").state == "17.5" - assert hass.states.get("sensor.huisbaasje_energy_this_month").state == "103.3" - assert hass.states.get("sensor.huisbaasje_energy_this_year").state == "673.0" - assert hass.states.get("sensor.huisbaasje_gas_today").state == "1.1" - assert hass.states.get("sensor.huisbaasje_gas_this_week").state == "5.6" - assert hass.states.get("sensor.huisbaasje_gas_this_month").state == "39.1" - assert hass.states.get("sensor.huisbaasje_gas_this_year").state == "116.7" # Assert mocks are called assert len(mock_authenticate.mock_calls) == 1 @@ -97,17 +346,40 @@ async def test_setup_entry_absent_measurement(hass: HomeAssistant): # Assert data is loaded assert hass.states.get("sensor.huisbaasje_current_power").state == "1012.0" - assert hass.states.get("sensor.huisbaasje_current_power_in").state == "unknown" assert ( - hass.states.get("sensor.huisbaasje_current_power_in_low").state == "unknown" + hass.states.get("sensor.huisbaasje_current_power_in_peak").state + == "unknown" ) - assert hass.states.get("sensor.huisbaasje_current_power_out").state == "unknown" assert ( - hass.states.get("sensor.huisbaasje_current_power_out_low").state + hass.states.get("sensor.huisbaasje_current_power_in_off_peak").state + == "unknown" + ) + assert ( + hass.states.get("sensor.huisbaasje_current_power_out_peak").state + == "unknown" + ) + assert ( + hass.states.get("sensor.huisbaasje_current_power_out_off_peak").state == "unknown" ) assert hass.states.get("sensor.huisbaasje_current_gas").state == "unknown" assert hass.states.get("sensor.huisbaasje_energy_today").state == "3.3" + assert ( + hass.states.get("sensor.huisbaasje_energy_consumption_peak_today").state + == "unknown" + ) + assert ( + hass.states.get("sensor.huisbaasje_energy_consumption_off_peak_today").state + == "unknown" + ) + assert ( + hass.states.get("sensor.huisbaasje_energy_production_peak_today").state + == "unknown" + ) + assert ( + hass.states.get("sensor.huisbaasje_energy_production_off_peak_today").state + == "unknown" + ) assert hass.states.get("sensor.huisbaasje_gas_today").state == "unknown" # Assert mocks are called From 23710d14969c8280279efa5e8a776459ca6a6509 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 22 Oct 2021 16:04:06 -0400 Subject: [PATCH 0700/1038] Add strict typing to modem_callerid (#57683) --- .strict-typing | 1 + .../components/modem_callerid/config_flow.py | 3 ++- homeassistant/components/modem_callerid/sensor.py | 4 ++-- mypy.ini | 11 +++++++++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.strict-typing b/.strict-typing index 8e6e59c11d6..295a0a352e0 100644 --- a/.strict-typing +++ b/.strict-typing @@ -70,6 +70,7 @@ homeassistant.components.lookin.* homeassistant.components.mailbox.* homeassistant.components.media_player.* homeassistant.components.modbus.* +homeassistant.components.modem_callerid.* homeassistant.components.mysensors.* homeassistant.components.nam.* homeassistant.components.nanoleaf.* diff --git a/homeassistant/components/modem_callerid/config_flow.py b/homeassistant/components/modem_callerid/config_flow.py index fd90f46d94a..da552b26beb 100644 --- a/homeassistant/components/modem_callerid/config_flow.py +++ b/homeassistant/components/modem_callerid/config_flow.py @@ -6,6 +6,7 @@ from typing import Any from phone_modem import DEFAULT_PORT, PhoneModem import serial.tools.list_ports +from serial.tools.list_ports_common import ListPortInfo import voluptuous as vol from homeassistant import config_entries @@ -20,7 +21,7 @@ _LOGGER = logging.getLogger(__name__) DATA_SCHEMA = vol.Schema({"name": str, "device": str}) -def _generate_unique_id(port: Any) -> str: +def _generate_unique_id(port: ListPortInfo) -> str: """Generate unique id from usb attributes.""" return f"{port.vid}:{port.pid}_{port.serial_number}_{port.manufacturer}_{port.description}" diff --git a/homeassistant/components/modem_callerid/sensor.py b/homeassistant/components/modem_callerid/sensor.py index 469e8ecb994..372f9700201 100644 --- a/homeassistant/components/modem_callerid/sensor.py +++ b/homeassistant/components/modem_callerid/sensor.py @@ -63,7 +63,7 @@ async def async_setup_entry( ] ) - async def _async_on_hass_stop(self) -> None: + async def _async_on_hass_stop() -> None: """HA is shutting down, close modem port.""" if hass.data[DOMAIN][entry.entry_id][DATA_KEY_API]: await hass.data[DOMAIN][entry.entry_id][DATA_KEY_API].close() @@ -104,7 +104,7 @@ class ModemCalleridSensor(SensorEntity): await super().async_added_to_hass() @callback - def _async_incoming_call(self, new_state) -> None: + def _async_incoming_call(self, new_state: str) -> None: """Handle new states.""" self._attr_extra_state_attributes = {} if self.api.cid_name: diff --git a/mypy.ini b/mypy.ini index a4740a7fedc..8114e3d39f5 100644 --- a/mypy.ini +++ b/mypy.ini @@ -781,6 +781,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.modem_callerid.*] +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.mysensors.*] check_untyped_defs = true disallow_incomplete_defs = true From 4369b0b8bede44ed9100f2fcae4bc21b02071e59 Mon Sep 17 00:00:00 2001 From: rik-v Date: Fri, 22 Oct 2021 22:09:19 +0200 Subject: [PATCH 0701/1038] Fix Fibaro light features (#56385) --- homeassistant/components/fibaro/light.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fibaro/light.py b/homeassistant/components/fibaro/light.py index aed1da543ee..fa9248dc11e 100644 --- a/homeassistant/components/fibaro/light.py +++ b/homeassistant/components/fibaro/light.py @@ -60,10 +60,25 @@ class FibaroLight(FibaroDevice, LightEntity): devconf = fibaro_device.device_config self._reset_color = devconf.get(CONF_RESET_COLOR, False) supports_color = ( - "color" in fibaro_device.properties and "setColor" in fibaro_device.actions + "color" in fibaro_device.properties + or "colorComponents" in fibaro_device.properties + or "RGB" in fibaro_device.type + or "rgb" in fibaro_device.type + or "color" in fibaro_device.baseType + ) and ( + "setColor" in fibaro_device.actions + or "setColorComponents" in fibaro_device.actions + ) + supports_white_v = ( + "setW" in fibaro_device.actions + or "RGBW" in fibaro_device.type + or "rgbw" in fibaro_device.type + ) + supports_dimming = ( + "levelChange" in fibaro_device.interfaces + or supports_color + or supports_white_v ) - supports_dimming = "levelChange" in fibaro_device.interfaces - supports_white_v = "setW" in fibaro_device.actions # Configuration can override default capability detection if devconf.get(CONF_DIMMING, supports_dimming): From b1360ffafb24d97132aed343115e92a17a447902 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 22 Oct 2021 23:10:47 +0300 Subject: [PATCH 0702/1038] Use http.HTTPStatus in components/f* (#58244) --- homeassistant/components/facebook/notify.py | 5 ++-- .../components/facebox/image_processing.py | 14 +++++----- homeassistant/components/flock/notify.py | 5 ++-- .../components/foursquare/__init__.py | 4 +-- .../components/free_mobile/notify.py | 17 +++++------- tests/components/facebook/test_notify.py | 14 ++++++---- .../facebox/test_image_processing.py | 16 +++++------ tests/components/flo/conftest.py | 27 ++++++++++--------- tests/components/flo/test_config_flow.py | 3 ++- tests/components/foobot/test_sensor.py | 7 +++-- tests/components/frontend/test_init.py | 12 ++++----- 11 files changed, 61 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/facebook/notify.py b/homeassistant/components/facebook/notify.py index 0ce2fbfc665..ea8848a5af2 100644 --- a/homeassistant/components/facebook/notify.py +++ b/homeassistant/components/facebook/notify.py @@ -1,4 +1,5 @@ """Facebook platform for notify component.""" +from http import HTTPStatus import json import logging @@ -12,7 +13,7 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) -from homeassistant.const import CONTENT_TYPE_JSON, HTTP_OK +from homeassistant.const import CONTENT_TYPE_JSON import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -76,7 +77,7 @@ class FacebookNotificationService(BaseNotificationService): headers={CONTENT_TYPE: CONTENT_TYPE_JSON}, timeout=10, ) - if resp.status_code != HTTP_OK: + if resp.status_code != HTTPStatus.OK: log_error(resp) diff --git a/homeassistant/components/facebox/image_processing.py b/homeassistant/components/facebox/image_processing.py index 5c90ce73560..ba95d1cd476 100644 --- a/homeassistant/components/facebox/image_processing.py +++ b/homeassistant/components/facebox/image_processing.py @@ -1,5 +1,6 @@ """Component for facial detection and identification via facebox.""" import base64 +from http import HTTPStatus import logging import requests @@ -21,9 +22,6 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_PORT, CONF_USERNAME, - HTTP_BAD_REQUEST, - HTTP_OK, - HTTP_UNAUTHORIZED, ) from homeassistant.core import split_entity_id import homeassistant.helpers.config_validation as cv @@ -67,10 +65,10 @@ def check_box_health(url, username, password): kwargs["auth"] = requests.auth.HTTPBasicAuth(username, password) try: response = requests.get(url, **kwargs) - if response.status_code == HTTP_UNAUTHORIZED: + if response.status_code == HTTPStatus.UNAUTHORIZED: _LOGGER.error("AuthenticationError on %s", CLASSIFIER) return None - if response.status_code == HTTP_OK: + if response.status_code == HTTPStatus.OK: return response.json()["hostname"] except requests.exceptions.ConnectionError: _LOGGER.error("ConnectionError: Is %s running?", CLASSIFIER) @@ -115,7 +113,7 @@ def post_image(url, image, username, password): kwargs["auth"] = requests.auth.HTTPBasicAuth(username, password) try: response = requests.post(url, json={"base64": encode_image(image)}, **kwargs) - if response.status_code == HTTP_UNAUTHORIZED: + if response.status_code == HTTPStatus.UNAUTHORIZED: _LOGGER.error("AuthenticationError on %s", CLASSIFIER) return None return response @@ -137,9 +135,9 @@ def teach_file(url, name, file_path, username, password): files={"file": open_file}, **kwargs, ) - if response.status_code == HTTP_UNAUTHORIZED: + if response.status_code == HTTPStatus.UNAUTHORIZED: _LOGGER.error("AuthenticationError on %s", CLASSIFIER) - elif response.status_code == HTTP_BAD_REQUEST: + elif response.status_code == HTTPStatus.BAD_REQUEST: _LOGGER.error( "%s teaching of file %s failed with message:%s", CLASSIFIER, diff --git a/homeassistant/components/flock/notify.py b/homeassistant/components/flock/notify.py index 7bdd1b33c5b..de5c078f714 100644 --- a/homeassistant/components/flock/notify.py +++ b/homeassistant/components/flock/notify.py @@ -1,12 +1,13 @@ """Flock platform for notify component.""" import asyncio +from http import HTTPStatus import logging import async_timeout import voluptuous as vol from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService -from homeassistant.const import CONF_ACCESS_TOKEN, HTTP_OK +from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -44,7 +45,7 @@ class FlockNotificationService(BaseNotificationService): response = await self._session.post(self._url, json=payload) result = await response.json() - if response.status != HTTP_OK or "error" in result: + if response.status != HTTPStatus.OK or "error" in result: _LOGGER.error( "Flock service returned HTTP status %d, response %s", response.status, diff --git a/homeassistant/components/foursquare/__init__.py b/homeassistant/components/foursquare/__init__.py index 6dc0d1c8228..59f3811a14b 100644 --- a/homeassistant/components/foursquare/__init__.py +++ b/homeassistant/components/foursquare/__init__.py @@ -6,7 +6,7 @@ import requests import voluptuous as vol from homeassistant.components.http import HomeAssistantView -from homeassistant.const import CONF_ACCESS_TOKEN, HTTP_CREATED, HTTP_OK +from homeassistant.const import CONF_ACCESS_TOKEN import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -56,7 +56,7 @@ def setup(hass, config): url = f"https://api.foursquare.com/v2/checkins/add?oauth_token={config[CONF_ACCESS_TOKEN]}&v=20160802&m=swarm" response = requests.post(url, data=call.data, timeout=10) - if response.status_code not in (HTTP_OK, HTTP_CREATED): + if response.status_code not in (HTTPStatus.OK, HTTPStatus.CREATED): _LOGGER.exception( "Error checking in user. Response %d: %s:", response.status_code, diff --git a/homeassistant/components/free_mobile/notify.py b/homeassistant/components/free_mobile/notify.py index a4351bfe678..61733237807 100644 --- a/homeassistant/components/free_mobile/notify.py +++ b/homeassistant/components/free_mobile/notify.py @@ -1,17 +1,12 @@ """Support for Free Mobile SMS platform.""" +from http import HTTPStatus import logging from freesms import FreeClient import voluptuous as vol from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService -from homeassistant.const import ( - CONF_ACCESS_TOKEN, - CONF_USERNAME, - HTTP_BAD_REQUEST, - HTTP_FORBIDDEN, - HTTP_INTERNAL_SERVER_ERROR, -) +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -37,11 +32,11 @@ class FreeSMSNotificationService(BaseNotificationService): """Send a message to the Free Mobile user cell.""" resp = self.free_client.send_sms(message) - if resp.status_code == HTTP_BAD_REQUEST: + if resp.status_code == HTTPStatus.BAD_REQUEST: _LOGGER.error("At least one parameter is missing") - elif resp.status_code == 402: + elif resp.status_code == HTTPStatus.PAYMENT_REQUIRED: _LOGGER.error("Too much SMS send in a few time") - elif resp.status_code == HTTP_FORBIDDEN: + elif resp.status_code == HTTPStatus.FORBIDDEN: _LOGGER.error("Wrong Username/Password") - elif resp.status_code == HTTP_INTERNAL_SERVER_ERROR: + elif resp.status_code == HTTPStatus.INTERNAL_SERVER_ERROR: _LOGGER.error("Server error, try later") diff --git a/tests/components/facebook/test_notify.py b/tests/components/facebook/test_notify.py index c9fddc7fcc4..27121539f5b 100644 --- a/tests/components/facebook/test_notify.py +++ b/tests/components/facebook/test_notify.py @@ -1,4 +1,6 @@ """The test for the Facebook notify module.""" +from http import HTTPStatus + import pytest import requests_mock @@ -15,7 +17,7 @@ def facebook(): async def test_send_simple_message(hass, facebook): """Test sending a simple message with success.""" with requests_mock.Mocker() as mock: - mock.register_uri(requests_mock.POST, fb.BASE_URL, status_code=200) + mock.register_uri(requests_mock.POST, fb.BASE_URL, status_code=HTTPStatus.OK) message = "This is just a test" target = ["+15555551234"] @@ -39,7 +41,7 @@ async def test_send_simple_message(hass, facebook): async def test_send_multiple_message(hass, facebook): """Test sending a message to multiple targets.""" with requests_mock.Mocker() as mock: - mock.register_uri(requests_mock.POST, fb.BASE_URL, status_code=200) + mock.register_uri(requests_mock.POST, fb.BASE_URL, status_code=HTTPStatus.OK) message = "This is just a test" targets = ["+15555551234", "+15555551235"] @@ -65,7 +67,7 @@ async def test_send_multiple_message(hass, facebook): async def test_send_message_attachment(hass, facebook): """Test sending a message with a remote attachment.""" with requests_mock.Mocker() as mock: - mock.register_uri(requests_mock.POST, fb.BASE_URL, status_code=200) + mock.register_uri(requests_mock.POST, fb.BASE_URL, status_code=HTTPStatus.OK) message = "This will be thrown away." data = { @@ -94,7 +96,9 @@ async def test_send_message_attachment(hass, facebook): async def test_send_targetless_message(hass, facebook): """Test sending a message without a target.""" with requests_mock.Mocker() as mock: - mock.register_uri(requests_mock.POST, fb.BASE_URL, status_code=200) + mock.register_uri( + requests_mock.POST, fb.BASE_URL, status_code=HTTPStatus.OK + ) facebook.send_message(message="going nowhere") assert not mock.called @@ -105,7 +109,7 @@ async def test_send_message_attachment(hass, facebook): mock.register_uri( requests_mock.POST, fb.BASE_URL, - status_code=400, + status_code=HTTPStatus.BAD_REQUEST, json={ "error": { "message": "Invalid OAuth access token.", diff --git a/tests/components/facebox/test_image_processing.py b/tests/components/facebox/test_image_processing.py index b0e482a89f5..a4f1ccf739e 100644 --- a/tests/components/facebox/test_image_processing.py +++ b/tests/components/facebox/test_image_processing.py @@ -1,4 +1,5 @@ """The tests for the facebox component.""" +from http import HTTPStatus from unittest.mock import Mock, mock_open, patch import pytest @@ -15,9 +16,6 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_PORT, CONF_USERNAME, - HTTP_BAD_REQUEST, - HTTP_OK, - HTTP_UNAUTHORIZED, STATE_UNKNOWN, ) from homeassistant.core import callback @@ -120,10 +118,10 @@ def test_check_box_health(caplog): """Test check box health.""" with requests_mock.Mocker() as mock_req: url = f"http://{MOCK_IP}:{MOCK_PORT}/healthz" - mock_req.get(url, status_code=HTTP_OK, json=MOCK_HEALTH) + mock_req.get(url, status_code=HTTPStatus.OK, json=MOCK_HEALTH) assert fb.check_box_health(url, "user", "pass") == MOCK_BOX_ID - mock_req.get(url, status_code=HTTP_UNAUTHORIZED) + mock_req.get(url, status_code=HTTPStatus.UNAUTHORIZED) assert fb.check_box_health(url, None, None) is None assert "AuthenticationError on facebox" in caplog.text @@ -238,7 +236,7 @@ async def test_process_image_errors(hass, mock_healthybox, mock_image, caplog): # Now test with bad auth. with requests_mock.Mocker() as mock_req: url = f"http://{MOCK_IP}:{MOCK_PORT}/facebox/check" - mock_req.register_uri("POST", url, status_code=HTTP_UNAUTHORIZED) + mock_req.register_uri("POST", url, status_code=HTTPStatus.UNAUTHORIZED) data = {ATTR_ENTITY_ID: VALID_ENTITY_ID} await hass.services.async_call(ip.DOMAIN, ip.SERVICE_SCAN, service_data=data) await hass.async_block_till_done() @@ -259,7 +257,7 @@ async def test_teach_service( # Test successful teach. with requests_mock.Mocker() as mock_req: url = f"http://{MOCK_IP}:{MOCK_PORT}/facebox/teach" - mock_req.post(url, status_code=HTTP_OK) + mock_req.post(url, status_code=HTTPStatus.OK) data = { ATTR_ENTITY_ID: VALID_ENTITY_ID, ATTR_NAME: MOCK_NAME, @@ -273,7 +271,7 @@ async def test_teach_service( # Now test with bad auth. with requests_mock.Mocker() as mock_req: url = f"http://{MOCK_IP}:{MOCK_PORT}/facebox/teach" - mock_req.post(url, status_code=HTTP_UNAUTHORIZED) + mock_req.post(url, status_code=HTTPStatus.UNAUTHORIZED) data = { ATTR_ENTITY_ID: VALID_ENTITY_ID, ATTR_NAME: MOCK_NAME, @@ -288,7 +286,7 @@ async def test_teach_service( # Now test the failed teaching. with requests_mock.Mocker() as mock_req: url = f"http://{MOCK_IP}:{MOCK_PORT}/facebox/teach" - mock_req.post(url, status_code=HTTP_BAD_REQUEST, text=MOCK_ERROR_NO_FACE) + mock_req.post(url, status_code=HTTPStatus.BAD_REQUEST, text=MOCK_ERROR_NO_FACE) data = { ATTR_ENTITY_ID: VALID_ENTITY_ID, ATTR_NAME: MOCK_NAME, diff --git a/tests/components/flo/conftest.py b/tests/components/flo/conftest.py index d4ba80e6406..6a82c0f4791 100644 --- a/tests/components/flo/conftest.py +++ b/tests/components/flo/conftest.py @@ -1,4 +1,5 @@ """Define fixtures available for all tests.""" +from http import HTTPStatus import json import time @@ -41,40 +42,40 @@ def aioclient_mock_fixture(aioclient_mock): } ), headers={"Content-Type": CONTENT_TYPE_JSON}, - status=200, + status=HTTPStatus.OK, ) # Mocks the devices for flo. aioclient_mock.get( "https://api-gw.meetflo.com/api/v2/devices/98765", text=load_fixture("flo/device_info_response.json"), - status=200, + status=HTTPStatus.OK, headers={"Content-Type": CONTENT_TYPE_JSON}, ) aioclient_mock.get( "https://api-gw.meetflo.com/api/v2/devices/32839", text=load_fixture("flo/device_info_response_detector.json"), - status=200, + status=HTTPStatus.OK, headers={"Content-Type": CONTENT_TYPE_JSON}, ) # Mocks the water consumption for flo. aioclient_mock.get( "https://api-gw.meetflo.com/api/v2/water/consumption", text=load_fixture("flo/water_consumption_info_response.json"), - status=200, + status=HTTPStatus.OK, headers={"Content-Type": CONTENT_TYPE_JSON}, ) # Mocks the location info for flo. aioclient_mock.get( "https://api-gw.meetflo.com/api/v2/locations/mmnnoopp", text=load_fixture("flo/location_info_expand_devices_response.json"), - status=200, + status=HTTPStatus.OK, headers={"Content-Type": CONTENT_TYPE_JSON}, ) # Mocks the user info for flo. aioclient_mock.get( "https://api-gw.meetflo.com/api/v2/users/12345abcde", text=load_fixture("flo/user_info_expand_locations_response.json"), - status=200, + status=HTTPStatus.OK, headers={"Content-Type": CONTENT_TYPE_JSON}, params={"expand": "locations"}, ) @@ -82,14 +83,14 @@ def aioclient_mock_fixture(aioclient_mock): aioclient_mock.get( "https://api-gw.meetflo.com/api/v2/users/12345abcde", text=load_fixture("flo/user_info_expand_locations_response.json"), - status=200, + status=HTTPStatus.OK, headers={"Content-Type": CONTENT_TYPE_JSON}, ) # Mocks the valve open call for flo. aioclient_mock.post( "https://api-gw.meetflo.com/api/v2/devices/98765", text=load_fixture("flo/device_info_response.json"), - status=200, + status=HTTPStatus.OK, headers={"Content-Type": CONTENT_TYPE_JSON}, json={"valve": {"target": "open"}}, ) @@ -97,7 +98,7 @@ def aioclient_mock_fixture(aioclient_mock): aioclient_mock.post( "https://api-gw.meetflo.com/api/v2/devices/98765", text=load_fixture("flo/device_info_response_closed.json"), - status=200, + status=HTTPStatus.OK, headers={"Content-Type": CONTENT_TYPE_JSON}, json={"valve": {"target": "closed"}}, ) @@ -105,14 +106,14 @@ def aioclient_mock_fixture(aioclient_mock): aioclient_mock.post( "https://api-gw.meetflo.com/api/v2/devices/98765/healthTest/run", text=load_fixture("flo/user_info_expand_locations_response.json"), - status=200, + status=HTTPStatus.OK, headers={"Content-Type": CONTENT_TYPE_JSON}, ) # Mocks the health test call for flo. aioclient_mock.post( "https://api-gw.meetflo.com/api/v2/locations/mmnnoopp/systemMode", text=load_fixture("flo/user_info_expand_locations_response.json"), - status=200, + status=HTTPStatus.OK, headers={"Content-Type": CONTENT_TYPE_JSON}, json={"systemMode": {"target": "home"}}, ) @@ -120,7 +121,7 @@ def aioclient_mock_fixture(aioclient_mock): aioclient_mock.post( "https://api-gw.meetflo.com/api/v2/locations/mmnnoopp/systemMode", text=load_fixture("flo/user_info_expand_locations_response.json"), - status=200, + status=HTTPStatus.OK, headers={"Content-Type": CONTENT_TYPE_JSON}, json={"systemMode": {"target": "away"}}, ) @@ -128,7 +129,7 @@ def aioclient_mock_fixture(aioclient_mock): aioclient_mock.post( "https://api-gw.meetflo.com/api/v2/locations/mmnnoopp/systemMode", text=load_fixture("flo/user_info_expand_locations_response.json"), - status=200, + status=HTTPStatus.OK, headers={"Content-Type": CONTENT_TYPE_JSON}, json={ "systemMode": { diff --git a/tests/components/flo/test_config_flow.py b/tests/components/flo/test_config_flow.py index f1cbd46ba70..b7c6e0d4acb 100644 --- a/tests/components/flo/test_config_flow.py +++ b/tests/components/flo/test_config_flow.py @@ -1,4 +1,5 @@ """Test the flo config flow.""" +from http import HTTPStatus import json import time from unittest.mock import patch @@ -51,7 +52,7 @@ async def test_form_cannot_connect(hass, aioclient_mock): } ), headers={"Content-Type": CONTENT_TYPE_JSON}, - status=400, + status=HTTPStatus.BAD_REQUEST, ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} diff --git a/tests/components/foobot/test_sensor.py b/tests/components/foobot/test_sensor.py index f3bf961fdc8..ec19fb7b94f 100644 --- a/tests/components/foobot/test_sensor.py +++ b/tests/components/foobot/test_sensor.py @@ -1,6 +1,7 @@ """The tests for the Foobot sensor platform.""" import asyncio +from http import HTTPStatus import re from unittest.mock import MagicMock @@ -12,8 +13,6 @@ from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, - HTTP_FORBIDDEN, - HTTP_INTERNAL_SERVER_ERROR, PERCENTAGE, TEMP_CELSIUS, ) @@ -73,7 +72,7 @@ async def test_setup_permanent_error(hass, aioclient_mock): """Expected failures caused by permanent errors in API response.""" fake_async_add_entities = MagicMock() - errors = [400, 401, HTTP_FORBIDDEN] + errors = [HTTPStatus.BAD_REQUEST, HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN] for error in errors: aioclient_mock.get(re.compile("api.foobot.io/v2/owner/.*"), status=error) result = await foobot.async_setup_platform( @@ -86,7 +85,7 @@ async def test_setup_temporary_error(hass, aioclient_mock): """Expected failures caused by temporary errors in API response.""" fake_async_add_entities = MagicMock() - errors = [429, HTTP_INTERNAL_SERVER_ERROR] + errors = [HTTPStatus.TOO_MANY_REQUESTS, HTTPStatus.INTERNAL_SERVER_ERROR] for error in errors: aioclient_mock.get(re.compile("api.foobot.io/v2/owner/.*"), status=error) with pytest.raises(PlatformNotReady): diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index c508175a846..8c8fd3bf671 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -1,5 +1,6 @@ """The tests for Home Assistant frontend.""" from datetime import timedelta +from http import HTTPStatus import re from unittest.mock import patch @@ -16,7 +17,6 @@ from homeassistant.components.frontend import ( THEMES_STORAGE_KEY, ) from homeassistant.components.websocket_api.const import TYPE_RESULT -from homeassistant.const import HTTP_NOT_FOUND, HTTP_OK from homeassistant.loader import async_get_integration from homeassistant.setup import async_setup_component from homeassistant.util import dt @@ -156,7 +156,7 @@ async def test_dont_cache_service_worker(mock_http_client): async def test_404(mock_http_client): """Test for HTTP 404 error.""" resp = await mock_http_client.get("/not-existing") - assert resp.status == HTTP_NOT_FOUND + assert resp.status == HTTPStatus.NOT_FOUND async def test_we_cannot_POST_to_root(mock_http_client): @@ -365,7 +365,7 @@ async def test_get_panels(hass, hass_ws_client, mock_http_client): events = async_capture_events(hass, EVENT_PANELS_UPDATED) resp = await mock_http_client.get("/map") - assert resp.status == HTTP_NOT_FOUND + assert resp.status == HTTPStatus.NOT_FOUND hass.components.frontend.async_register_built_in_panel( "map", "Map", "mdi:tooltip-account", require_admin=True @@ -393,7 +393,7 @@ async def test_get_panels(hass, hass_ws_client, mock_http_client): hass.components.frontend.async_remove_panel("map") resp = await mock_http_client.get("/map") - assert resp.status == HTTP_NOT_FOUND + assert resp.status == HTTPStatus.NOT_FOUND assert len(events) == 2 @@ -509,7 +509,7 @@ async def test_static_paths(hass, mock_http_client): async def test_manifest_json(hass, frontend_themes, mock_http_client): """Test for fetching manifest.json.""" resp = await mock_http_client.get("/manifest.json") - assert resp.status == HTTP_OK + assert resp.status == HTTPStatus.OK assert "cache-control" not in resp.headers json = await resp.json() @@ -521,7 +521,7 @@ async def test_manifest_json(hass, frontend_themes, mock_http_client): await hass.async_block_till_done() resp = await mock_http_client.get("/manifest.json") - assert resp.status == HTTP_OK + assert resp.status == HTTPStatus.OK assert "cache-control" not in resp.headers json = await resp.json() From 8da3b4c79f553a556603efbb4e832c6559c5ea05 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 22 Oct 2021 22:13:17 +0200 Subject: [PATCH 0703/1038] Bump arcam library to 0.12 with new series support (#53843) --- .../components/arcam_fmj/manifest.json | 2 +- .../components/arcam_fmj/media_player.py | 43 +++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/arcam_fmj/conftest.py | 2 + .../components/arcam_fmj/test_media_player.py | 84 +++++++------------ 6 files changed, 44 insertions(+), 91 deletions(-) diff --git a/homeassistant/components/arcam_fmj/manifest.json b/homeassistant/components/arcam_fmj/manifest.json index 6685ea240eb..08545f4c5b0 100644 --- a/homeassistant/components/arcam_fmj/manifest.json +++ b/homeassistant/components/arcam_fmj/manifest.json @@ -3,7 +3,7 @@ "name": "Arcam FMJ Receivers", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/arcam_fmj", - "requirements": ["arcam-fmj==0.7.0"], + "requirements": ["arcam-fmj==0.12.0"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index acccb91c98e..b63279d9c26 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -1,7 +1,7 @@ """Arcam media player.""" import logging -from arcam.fmj import DecodeMode2CH, DecodeModeMCH, IncomingAudioFormat, SourceCodes +from arcam.fmj import SourceCodes from arcam.fmj.state import State from homeassistant import config_entries @@ -92,19 +92,6 @@ class ArcamFmj(MediaPlayerEntity): self._attr_unique_id = f"{uuid}-{state.zn}" self._attr_entity_registry_enabled_default = state.zn == 1 - def _get_2ch(self): - """Return if source is 2 channel or not.""" - audio_format, _ = self._state.get_incoming_audio_format() - return bool( - audio_format - in ( - IncomingAudioFormat.PCM, - IncomingAudioFormat.ANALOGUE_DIRECT, - IncomingAudioFormat.UNDETECTED, - None, - ) - ) - @property def state(self): """Return the state of the device.""" @@ -128,6 +115,7 @@ class ArcamFmj(MediaPlayerEntity): async def async_added_to_hass(self): """Once registered, add listener for events.""" await self._state.start() + await self._state.update() @callback def _data(host): @@ -186,11 +174,8 @@ class ArcamFmj(MediaPlayerEntity): async def async_select_sound_mode(self, sound_mode): """Select a specific source.""" try: - if self._get_2ch(): - await self._state.set_decode_mode_2ch(DecodeMode2CH[sound_mode]) - else: - await self._state.set_decode_mode_mch(DecodeModeMCH[sound_mode]) - except KeyError: + await self._state.set_decode_mode(sound_mode) + except (KeyError, ValueError): _LOGGER.error("Unsupported sound_mode %s", sound_mode) return @@ -283,26 +268,18 @@ class ArcamFmj(MediaPlayerEntity): @property def sound_mode(self): """Name of the current sound mode.""" - if self._state.zn != 1: + value = self._state.get_decode_mode() + if value is None: return None - - if self._get_2ch(): - value = self._state.get_decode_mode_2ch() - else: - value = self._state.get_decode_mode_mch() - if value: - return value.name - return None + return value.name @property def sound_mode_list(self): """List of available sound modes.""" - if self._state.zn != 1: + values = self._state.get_decode_modes() + if values is None: return None - - if self._get_2ch(): - return [x.name for x in DecodeMode2CH] - return [x.name for x in DecodeModeMCH] + return [x.name for x in values] @property def is_volume_muted(self): diff --git a/requirements_all.txt b/requirements_all.txt index d40d4f7d2d2..58d4a230d3c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -318,7 +318,7 @@ aprslib==0.6.46 aqualogic==2.6 # homeassistant.components.arcam_fmj -arcam-fmj==0.7.0 +arcam-fmj==0.12.0 # homeassistant.components.arris_tg2492lg arris-tg2492lg==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 81e969b6d74..7e672541079 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -224,7 +224,7 @@ apprise==0.9.5.1 aprslib==0.6.46 # homeassistant.components.arcam_fmj -arcam-fmj==0.7.0 +arcam-fmj==0.12.0 # homeassistant.components.dlna_dmr # homeassistant.components.ssdp diff --git a/tests/components/arcam_fmj/conftest.py b/tests/components/arcam_fmj/conftest.py index 2ef0df9511e..39b44d2ad9e 100644 --- a/tests/components/arcam_fmj/conftest.py +++ b/tests/components/arcam_fmj/conftest.py @@ -44,6 +44,7 @@ def state_1_fixture(client): state.get_source_list.return_value = [] state.get_incoming_audio_format.return_value = (0, 0) state.get_mute.return_value = None + state.get_decode_modes.return_value = [] return state @@ -58,6 +59,7 @@ def state_2_fixture(client): state.get_source_list.return_value = [] state.get_incoming_audio_format.return_value = (0, 0) state.get_mute.return_value = None + state.get_decode_modes.return_value = [] return state diff --git a/tests/components/arcam_fmj/test_media_player.py b/tests/components/arcam_fmj/test_media_player.py index 58609da6d6a..db12f715b3c 100644 --- a/tests/components/arcam_fmj/test_media_player.py +++ b/tests/components/arcam_fmj/test_media_player.py @@ -1,12 +1,14 @@ """Tests for arcam fmj receivers.""" from math import isclose -from unittest.mock import ANY, MagicMock, Mock, PropertyMock, patch +from unittest.mock import ANY, MagicMock, PropertyMock, patch -from arcam.fmj import DecodeMode2CH, DecodeModeMCH, IncomingAudioFormat, SourceCodes +from arcam.fmj import DecodeMode2CH, DecodeModeMCH, SourceCodes import pytest from homeassistant.components.media_player.const import ( ATTR_INPUT_SOURCE, + ATTR_SOUND_MODE, + ATTR_SOUND_MODE_LIST, MEDIA_TYPE_MUSIC, SERVICE_SELECT_SOURCE, ) @@ -100,21 +102,6 @@ async def test_update(player, state): state.update.assert_called_with() -@pytest.mark.parametrize( - "fmt, result", - [ - (None, True), - (IncomingAudioFormat.PCM, True), - (IncomingAudioFormat.ANALOGUE_DIRECT, True), - (IncomingAudioFormat.DOLBY_DIGITAL, False), - ], -) -async def test_2ch(player, state, fmt, result): - """Test selection of 2ch mode.""" - state.get_incoming_audio_format.return_value = (fmt, None) - assert player._get_2ch() == result # pylint: disable=W0212 - - @pytest.mark.parametrize( "source, value", [("PVR", SourceCodes.PVR), ("BD", SourceCodes.BD), ("INVALID", None)], @@ -142,27 +129,16 @@ async def test_source_list(player, state): @pytest.mark.parametrize( - "mode, mode_sel, mode_2ch, mode_mch", + "mode", [ - ("STEREO", True, DecodeMode2CH.STEREO, None), - ("STEREO", False, None, None), - ("STEREO", False, None, None), + ("STEREO"), + ("DOLBY_PL"), ], ) -async def test_select_sound_mode(player, state, mode, mode_sel, mode_2ch, mode_mch): +async def test_select_sound_mode(player, state, mode): """Test selection sound mode.""" - player._get_2ch = Mock(return_value=mode_sel) # pylint: disable=W0212 - await player.async_select_sound_mode(mode) - if mode_2ch: - state.set_decode_mode_2ch.assert_called_with(mode_2ch) - else: - state.set_decode_mode_2ch.assert_not_called() - - if mode_mch: - state.set_decode_mode_mch.assert_called_with(mode_mch) - else: - state.set_decode_mode_mch.assert_not_called() + state.set_decode_mode.assert_called_with(mode) async def test_volume_up(player, state): @@ -180,35 +156,33 @@ async def test_volume_down(player, state): @pytest.mark.parametrize( - "mode, mode_sel, mode_2ch, mode_mch", + "mode, mode_enum", [ - ("STEREO", True, DecodeMode2CH.STEREO, None), - ("STEREO_DOWNMIX", False, None, DecodeModeMCH.STEREO_DOWNMIX), - (None, False, None, None), + ("STEREO", DecodeMode2CH.STEREO), + ("STEREO_DOWNMIX", DecodeModeMCH.STEREO_DOWNMIX), + (None, None), ], ) -async def test_sound_mode(player, state, mode, mode_sel, mode_2ch, mode_mch): +async def test_sound_mode(player, state, mode, mode_enum): """Test selection sound mode.""" - player._get_2ch = Mock(return_value=mode_sel) # pylint: disable=W0212 - state.get_decode_mode_2ch.return_value = mode_2ch - state.get_decode_mode_mch.return_value = mode_mch - - assert player.sound_mode == mode + state.get_decode_mode.return_value = mode_enum + data = await update(player) + assert data.attributes.get(ATTR_SOUND_MODE) == mode -async def test_sound_mode_list(player, state): +@pytest.mark.parametrize( + "modes, modes_enum", + [ + (["STEREO", "DOLBY_PL"], [DecodeMode2CH.STEREO, DecodeMode2CH.DOLBY_PL]), + (["STEREO_DOWNMIX"], [DecodeModeMCH.STEREO_DOWNMIX]), + (None, None), + ], +) +async def test_sound_mode_list(player, state, modes, modes_enum): """Test sound mode list.""" - player._get_2ch = Mock(return_value=True) # pylint: disable=W0212 - assert sorted(player.sound_mode_list) == sorted(x.name for x in DecodeMode2CH) - player._get_2ch = Mock(return_value=False) # pylint: disable=W0212 - assert sorted(player.sound_mode_list) == sorted(x.name for x in DecodeModeMCH) - - -async def test_sound_mode_zone_x(player, state): - """Test second zone sound mode.""" - state.zn = 2 - assert player.sound_mode is None - assert player.sound_mode_list is None + state.get_decode_modes.return_value = modes_enum + data = await update(player) + assert data.attributes.get(ATTR_SOUND_MODE_LIST) == modes async def test_is_volume_muted(player, state): From a195418dd34cab1a39e5380bedcee40131755cd1 Mon Sep 17 00:00:00 2001 From: Yuval Aboulafia Date: Fri, 22 Oct 2021 23:17:05 +0300 Subject: [PATCH 0704/1038] ISS cleanup (#55801) --- homeassistant/components/iss/binary_sensor.py | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/iss/binary_sensor.py b/homeassistant/components/iss/binary_sensor.py index 787d7471d43..eab1294ad10 100644 --- a/homeassistant/components/iss/binary_sensor.py +++ b/homeassistant/components/iss/binary_sensor.py @@ -1,9 +1,12 @@ -"""Support for International Space Station data sensor.""" +"""Support for International Space Station binary sensor.""" +from __future__ import annotations + from datetime import timedelta import logging import pyiss import requests +from requests.exceptions import HTTPError import voluptuous as vol from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity @@ -13,7 +16,10 @@ from homeassistant.const import ( CONF_NAME, CONF_SHOW_ON_MAP, ) +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 from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -34,18 +40,23 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the ISS sensor.""" +def setup_platform( + hass: HomeAssistant, + config: ConfigType, + add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the ISS binary sensor.""" if None in (hass.config.latitude, hass.config.longitude): _LOGGER.error("Latitude or longitude not set in Home Assistant config") - return False + return try: iss_data = IssData(hass.config.latitude, hass.config.longitude) iss_data.update() - except requests.exceptions.HTTPError as error: + except HTTPError as error: _LOGGER.error(error) - return False + return name = config.get(CONF_NAME) show_on_map = config.get(CONF_SHOW_ON_MAP) @@ -56,28 +67,20 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class IssBinarySensor(BinarySensorEntity): """Implementation of the ISS binary sensor.""" + _attr_device_class = DEFAULT_DEVICE_CLASS + def __init__(self, iss_data, name, show): """Initialize the sensor.""" self.iss_data = iss_data self._state = None - self._name = name + self._attr_name = name self._show_on_map = show @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def is_on(self): + 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 - @property - def device_class(self): - """Return the class of this sensor.""" - return DEFAULT_DEVICE_CLASS - @property def extra_state_attributes(self): """Return the state attributes.""" @@ -92,6 +95,7 @@ class IssBinarySensor(BinarySensorEntity): else: attrs["long"] = self.iss_data.position.get("longitude") attrs["lat"] = self.iss_data.position.get("latitude") + return attrs def update(self): @@ -120,6 +124,6 @@ class IssData: 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 (requests.exceptions.HTTPError, requests.exceptions.ConnectionError): + except (HTTPError, requests.exceptions.ConnectionError): _LOGGER.error("Unable to retrieve data") return False From ee087c7a0582a6d88d0e0980b40ae6c5456b158b Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Sat, 23 Oct 2021 07:26:33 +1100 Subject: [PATCH 0705/1038] Discovery ignores DLNA DMR devices when they are better supported by another integration (#57363) --- .../components/dlna_dmr/config_flow.py | 105 ++++++++- .../components/dlna_dmr/manifest.json | 26 +++ .../components/dlna_dmr/strings.json | 14 +- .../components/dlna_dmr/translations/en.json | 16 +- homeassistant/components/ssdp/__init__.py | 3 + homeassistant/generated/ssdp.py | 26 +++ tests/components/dlna_dmr/test_config_flow.py | 204 ++++++++++++++++-- tests/components/ssdp/test_init.py | 2 + 8 files changed, 365 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/dlna_dmr/config_flow.py b/homeassistant/components/dlna_dmr/config_flow.py index 3958ca9e3e5..ee81d1be88f 100644 --- a/homeassistant/components/dlna_dmr/config_flow.py +++ b/homeassistant/components/dlna_dmr/config_flow.py @@ -14,7 +14,13 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import ssdp -from homeassistant.const import CONF_DEVICE_ID, CONF_NAME, CONF_TYPE, CONF_URL +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_HOST, + CONF_NAME, + CONF_TYPE, + CONF_URL, +) from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import IntegrationError @@ -51,7 +57,7 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize flow.""" - self._discoveries: list[Mapping[str, str]] = [] + self._discoveries: dict[str, Mapping[str, Any]] = {} self._location: str | None = None self._udn: str | None = None self._device_type: str | None = None @@ -67,13 +73,43 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return DlnaDmrOptionsFlowHandler(config_entry) async def async_step_user(self, user_input: FlowInput = None) -> FlowResult: - """Handle a flow initialized by the user: manual URL entry. + """Handle a flow initialized by the user. - Discovered devices will already be displayed, no need to prompt user - with them here. + Let user choose from a list of found and unconfigured devices or to + enter an URL manually. """ LOGGER.debug("async_step_user: user_input: %s", user_input) + if user_input is not None: + host = user_input.get(CONF_HOST) + if not host: + # No device chosen, user might want to directly enter an URL + return await self.async_step_manual() + # User has chosen a device, ask for confirmation + discovery = self._discoveries[host] + await self._async_set_info_from_discovery(discovery) + return self._create_entry() + + discoveries = await self._async_get_discoveries() + if not discoveries: + # Nothing found, maybe the user knows an URL to try + return await self.async_step_manual() + + self._discoveries = { + discovery.get(ssdp.ATTR_UPNP_FRIENDLY_NAME) + or urlparse(discovery[ssdp.ATTR_SSDP_LOCATION]).hostname: discovery + for discovery in discoveries + } + + data_schema = vol.Schema( + {vol.Optional(CONF_HOST): vol.In(self._discoveries.keys())} + ) + return self.async_show_form(step_id="user", data_schema=data_schema) + + async def async_step_manual(self, user_input: FlowInput = None) -> FlowResult: + """Manual URL entry by the user.""" + LOGGER.debug("async_step_manual: user_input: %s", user_input) + # Device setup manually, assume we don't get SSDP broadcast notifications self._options[CONF_POLL_AVAILABILITY] = True @@ -89,7 +125,7 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data_schema = vol.Schema({CONF_URL: str}) return self.async_show_form( - step_id="user", data_schema=data_schema, errors=errors + step_id="manual", data_schema=data_schema, errors=errors ) async def async_step_import(self, import_data: FlowInput = None) -> FlowResult: @@ -177,6 +213,9 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self._async_set_info_from_discovery(discovery_info) + if _is_ignored_device(discovery_info): + return self.async_abort(reason="alternative_integration") + # Abort if a migration flow for the device's location is in progress for progress in self._async_in_progress(include_uninitialized=True): if progress["context"].get("unique_id") == self._location: @@ -190,6 +229,29 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_confirm() + async def async_step_unignore(self, user_input: Mapping[str, Any]) -> FlowResult: + """Rediscover previously ignored devices by their unique_id.""" + LOGGER.debug("async_step_unignore: user_input: %s", user_input) + self._udn = user_input["unique_id"] + assert self._udn + await self.async_set_unique_id(self._udn) + + # Find a discovery matching the unignored unique_id for a DMR device + for dev_type in DmrDevice.DEVICE_TYPES: + discovery = await ssdp.async_get_discovery_info_by_udn_st( + self.hass, self._udn, dev_type + ) + if discovery: + break + else: + return self.async_abort(reason="discovery_error") + + await self._async_set_info_from_discovery(discovery, abort_if_configured=False) + + self.context["title_placeholders"] = {"name": self._name} + + return await self.async_step_confirm() + async def async_step_confirm(self, user_input: FlowInput = None) -> FlowResult: """Allow the user to confirm adding the device.""" LOGGER.debug("async_step_confirm: %s", user_input) @@ -213,7 +275,7 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): try: device = await domain_data.upnp_factory.async_create_device(self._location) except UpnpError as err: - raise ConnectError("could_not_connect") from err + raise ConnectError("cannot_connect") from err try: device = find_device_of_type(device, DmrDevice.DEVICE_TYPES) @@ -284,12 +346,12 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): or DEFAULT_NAME ) - async def _async_get_discoveries(self) -> list[Mapping[str, str]]: + async def _async_get_discoveries(self) -> list[Mapping[str, Any]]: """Get list of unconfigured DLNA devices discovered by SSDP.""" LOGGER.debug("_get_discoveries") # Get all compatible devices from ssdp's cache - discoveries: list[Mapping[str, str]] = [] + discoveries: list[Mapping[str, Any]] = [] for udn_st in DmrDevice.DEVICE_TYPES: st_discoveries = await ssdp.async_get_discovery_info_by_st( self.hass, udn_st @@ -298,7 +360,8 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Filter out devices already configured current_unique_ids = { - entry.unique_id for entry in self._async_current_entries() + entry.unique_id + for entry in self._async_current_entries(include_ignore=False) } discoveries = [ disc @@ -374,3 +437,25 @@ class DlnaDmrOptionsFlowHandler(config_entries.OptionsFlow): data_schema=vol.Schema(fields), errors=errors, ) + + +def _is_ignored_device(discovery_info: Mapping[str, Any]) -> bool: + """Return True if this device should be ignored for discovery. + + These devices are supported better by other integrations, so don't bug + the user about them. The user can add them if desired by via the user config + flow, which will list all discovered but unconfigured devices. + """ + # Did the discovery trigger more than just this flow? + if len(discovery_info.get(ssdp.ATTR_HA_MATCHING_DOMAINS, set())) > 1: + LOGGER.debug( + "Ignoring device supported by multiple integrations: %s", + discovery_info[ssdp.ATTR_HA_MATCHING_DOMAINS], + ) + return True + + # Is the root device not a DMR? + if discovery_info.get(ssdp.ATTR_UPNP_DEVICE_TYPE) not in DmrDevice.DEVICE_TYPES: + return True + + return False diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 18e50ba1035..a7c0a674853 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -5,6 +5,32 @@ "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", "requirements": ["async-upnp-client==0.22.9"], "dependencies": ["ssdp"], + "ssdp": [ + { + "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", + "st": "urn:schemas-upnp-org:device:MediaRenderer:1" + }, + { + "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:2", + "st": "urn:schemas-upnp-org:device:MediaRenderer:2" + }, + { + "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:3", + "st": "urn:schemas-upnp-org:device:MediaRenderer:3" + }, + { + "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", + "nt": "urn:schemas-upnp-org:device:MediaRenderer:1" + }, + { + "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:2", + "nt": "urn:schemas-upnp-org:device:MediaRenderer:2" + }, + { + "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:3", + "nt": "urn:schemas-upnp-org:device:MediaRenderer:3" + } + ], "codeowners": ["@StevenLooman", "@chishm"], "iot_class": "local_push" } diff --git a/homeassistant/components/dlna_dmr/strings.json b/homeassistant/components/dlna_dmr/strings.json index c418305d2e6..ac6a35194fe 100644 --- a/homeassistant/components/dlna_dmr/strings.json +++ b/homeassistant/components/dlna_dmr/strings.json @@ -3,7 +3,14 @@ "flow_title": "{name}", "step": { "user": { - "title": "DLNA Digital Media Renderer", + "title": "Discovered DLNA DMR devices", + "description": "Choose a device to configure or leave blank to enter a URL", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "manual": { + "title": "Manual DLNA DMR device connection", "description": "URL to a device description XML file", "data": { "url": "[%key:common::config_flow::data::url%]" @@ -18,14 +25,15 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "could_not_connect": "Failed to connect to DLNA device", + "alternative_integration": "Device is better supported by another integration", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "discovery_error": "Failed to discover a matching DLNA device", "incomplete_config": "Configuration is missing a required variable", "non_unique_id": "Multiple devices found with the same unique ID", "not_dmr": "Device is not a Digital Media Renderer" }, "error": { - "could_not_connect": "Failed to connect to DLNA device", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "not_dmr": "Device is not a Digital Media Renderer" } }, diff --git a/homeassistant/components/dlna_dmr/translations/en.json b/homeassistant/components/dlna_dmr/translations/en.json index 470328d0c27..3cdec814178 100644 --- a/homeassistant/components/dlna_dmr/translations/en.json +++ b/homeassistant/components/dlna_dmr/translations/en.json @@ -2,14 +2,15 @@ "config": { "abort": { "already_configured": "Device is already configured", - "could_not_connect": "Failed to connect to DLNA device", + "alternative_integration": "Device is better supported by another integration", + "cannot_connect": "Failed to connect", "discovery_error": "Failed to discover a matching DLNA device", "incomplete_config": "Configuration is missing a required variable", "non_unique_id": "Multiple devices found with the same unique ID", "not_dmr": "Device is not a Digital Media Renderer" }, "error": { - "could_not_connect": "Failed to connect to DLNA device", + "cannot_connect": "Failed to connect", "not_dmr": "Device is not a Digital Media Renderer" }, "flow_title": "{name}", @@ -20,12 +21,19 @@ "import_turn_on": { "description": "Please turn on the device and click submit to continue migration" }, - "user": { + "manual": { "data": { "url": "URL" }, "description": "URL to a device description XML file", - "title": "DLNA Digital Media Renderer" + "title": "Manual DLNA DMR device connection" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Choose a device to configure or leave blank to enter a URL", + "title": "Discovered DLNA DMR devices" } } }, diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index af3f09f560d..f5f4feaa70a 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -54,6 +54,8 @@ ATTR_UPNP_SERIAL = "serialNumber" ATTR_UPNP_UDN = "UDN" ATTR_UPNP_UPC = "UPC" ATTR_UPNP_PRESENTATION_URL = "presentationURL" +# Attributes for accessing info added by Home Assistant +ATTR_HA_MATCHING_DOMAINS = "x-homeassistant-matching-domains" PRIMARY_MATCH_KEYS = [ATTR_UPNP_MANUFACTURER, "st", ATTR_UPNP_DEVICE_TYPE, "nt"] @@ -398,6 +400,7 @@ class Scanner: return discovery_info = discovery_info_from_headers_and_description(info_with_desc) + discovery_info[ATTR_HA_MATCHING_DOMAINS] = matching_domains ssdp_change = SSDP_SOURCE_SSDP_CHANGE_MAPPING[source] await _async_process_callbacks(callbacks, discovery_info, ssdp_change) diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 1676037bbf2..925ba5b82fe 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -83,6 +83,32 @@ SSDP = { "manufacturer": "DIRECTV" } ], + "dlna_dmr": [ + { + "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", + "st": "urn:schemas-upnp-org:device:MediaRenderer:1" + }, + { + "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:2", + "st": "urn:schemas-upnp-org:device:MediaRenderer:2" + }, + { + "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:3", + "st": "urn:schemas-upnp-org:device:MediaRenderer:3" + }, + { + "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", + "nt": "urn:schemas-upnp-org:device:MediaRenderer:1" + }, + { + "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:2", + "nt": "urn:schemas-upnp-org:device:MediaRenderer:2" + }, + { + "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:3", + "nt": "urn:schemas-upnp-org:device:MediaRenderer:3" + } + ], "fritz": [ { "st": "urn:schemas-upnp-org:device:fritzbox:1" diff --git a/tests/components/dlna_dmr/test_config_flow.py b/tests/components/dlna_dmr/test_config_flow.py index 0586a43422a..245d97be4aa 100644 --- a/tests/components/dlna_dmr/test_config_flow.py +++ b/tests/components/dlna_dmr/test_config_flow.py @@ -16,6 +16,7 @@ from homeassistant.components.dlna_dmr.const import ( ) from homeassistant.const import ( CONF_DEVICE_ID, + CONF_HOST, CONF_NAME, CONF_PLATFORM, CONF_TYPE, @@ -49,26 +50,26 @@ MOCK_CONFIG_IMPORT_DATA = { } MOCK_ROOT_DEVICE_UDN = "ROOT_DEVICE" -MOCK_ROOT_DEVICE_TYPE = "ROOT_DEVICE_TYPE" MOCK_DISCOVERY = { ssdp.ATTR_SSDP_LOCATION: MOCK_DEVICE_LOCATION, ssdp.ATTR_SSDP_UDN: MOCK_DEVICE_UDN, ssdp.ATTR_SSDP_ST: MOCK_DEVICE_TYPE, ssdp.ATTR_UPNP_UDN: MOCK_ROOT_DEVICE_UDN, - ssdp.ATTR_UPNP_DEVICE_TYPE: MOCK_ROOT_DEVICE_TYPE, + ssdp.ATTR_UPNP_DEVICE_TYPE: MOCK_DEVICE_TYPE, ssdp.ATTR_UPNP_FRIENDLY_NAME: MOCK_DEVICE_NAME, + ssdp.ATTR_HA_MATCHING_DOMAINS: {DLNA_DOMAIN}, } -async def test_user_flow(hass: HomeAssistant) -> None: - """Test user-init'd config flow with user entering a valid URL.""" +async def test_user_flow_undiscovered_manual(hass: HomeAssistant) -> None: + """Test user-init'd flow, no discovered devices, user entering a valid URL.""" result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} - assert result["step_id"] == "user" + assert result["step_id"] == "manual" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} @@ -87,6 +88,79 @@ async def test_user_flow(hass: HomeAssistant) -> None: await hass.async_block_till_done() +async def test_user_flow_discovered_manual( + hass: HomeAssistant, ssdp_scanner_mock: Mock +) -> None: + """Test user-init'd flow, with discovered devices, user entering a valid URL.""" + ssdp_scanner_mock.async_get_discovery_info_by_st.side_effect = [ + [MOCK_DISCOVERY], + [], + [], + ] + + result = await hass.config_entries.flow.async_init( + DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + assert result["step_id"] == "manual" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == MOCK_DEVICE_NAME + assert result["data"] == { + CONF_URL: MOCK_DEVICE_LOCATION, + CONF_DEVICE_ID: MOCK_DEVICE_UDN, + CONF_TYPE: MOCK_DEVICE_TYPE, + } + assert result["options"] == {CONF_POLL_AVAILABILITY: True} + + # Wait for platform to be fully setup + await hass.async_block_till_done() + + +async def test_user_flow_selected(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> None: + """Test user-init'd flow, user selects discovered device.""" + ssdp_scanner_mock.async_get_discovery_info_by_st.side_effect = [ + [MOCK_DISCOVERY], + [], + [], + ] + + result = await hass.config_entries.flow.async_init( + DLNA_DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_HOST: MOCK_DEVICE_NAME} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == MOCK_DEVICE_NAME + assert result["data"] == { + CONF_URL: MOCK_DEVICE_LOCATION, + CONF_DEVICE_ID: MOCK_DEVICE_UDN, + CONF_TYPE: MOCK_DEVICE_TYPE, + } + assert result["options"] == {} + + await hass.async_block_till_done() + + async def test_user_flow_uncontactable( hass: HomeAssistant, domain_data_mock: Mock ) -> None: @@ -99,15 +173,15 @@ async def test_user_flow_uncontactable( ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} - assert result["step_id"] == "user" + assert result["step_id"] == "manual" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "could_not_connect"} - assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + assert result["step_id"] == "manual" async def test_user_flow_embedded_st( @@ -117,7 +191,7 @@ async def test_user_flow_embedded_st( # Device is the wrong type upnp_device = domain_data_mock.upnp_factory.async_create_device.return_value upnp_device.udn = MOCK_ROOT_DEVICE_UDN - upnp_device.device_type = MOCK_ROOT_DEVICE_TYPE + upnp_device.device_type = "ROOT_DEVICE_TYPE" upnp_device.name = "ROOT_DEVICE_NAME" embedded_device = Mock(spec=UpnpDevice) embedded_device.udn = MOCK_DEVICE_UDN @@ -130,7 +204,7 @@ async def test_user_flow_embedded_st( ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} - assert result["step_id"] == "user" + assert result["step_id"] == "manual" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} @@ -160,7 +234,7 @@ async def test_user_flow_wrong_st(hass: HomeAssistant, domain_data_mock: Mock) - ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} - assert result["step_id"] == "user" + assert result["step_id"] == "manual" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} @@ -168,7 +242,7 @@ async def test_user_flow_wrong_st(hass: HomeAssistant, domain_data_mock: Mock) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "not_dmr"} - assert result["step_id"] == "user" + assert result["step_id"] == "manual" async def test_import_flow_invalid(hass: HomeAssistant, domain_data_mock: Mock) -> None: @@ -298,7 +372,7 @@ async def test_import_flow_offline( ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "could_not_connect"} + assert result["errors"] == {"base": "cannot_connect"} assert result["step_id"] == "import_turn_on" # Device is discovered via SSDP, new flow should not be initialized @@ -469,7 +543,7 @@ async def test_ssdp_flow_upnp_udn( ssdp.ATTR_SSDP_UDN: MOCK_DEVICE_UDN, ssdp.ATTR_SSDP_ST: MOCK_DEVICE_TYPE, ssdp.ATTR_UPNP_UDN: "DIFFERENT_ROOT_DEVICE", - ssdp.ATTR_UPNP_DEVICE_TYPE: "DIFFERENT_ROOT_DEVICE_TYPE", + ssdp.ATTR_UPNP_DEVICE_TYPE: MOCK_DEVICE_TYPE, ssdp.ATTR_UPNP_FRIENDLY_NAME: MOCK_DEVICE_NAME, }, ) @@ -478,6 +552,108 @@ async def test_ssdp_flow_upnp_udn( assert config_entry_mock.data[CONF_URL] == NEW_DEVICE_LOCATION +async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: + """Test SSDP discovery ignores certain devices.""" + discovery = MOCK_DISCOVERY.copy() + discovery[ssdp.ATTR_HA_MATCHING_DOMAINS] = {DLNA_DOMAIN, "other_domain"} + result = await hass.config_entries.flow.async_init( + DLNA_DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=discovery, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "alternative_integration" + + discovery = MOCK_DISCOVERY.copy() + discovery[ssdp.ATTR_UPNP_DEVICE_TYPE] = "urn:schemas-upnp-org:device:ZonePlayer:1" + result = await hass.config_entries.flow.async_init( + DLNA_DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=discovery, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "alternative_integration" + + +async def test_unignore_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> None: + """Test a config flow started by unignoring a device.""" + # Create ignored entry + result = await hass.config_entries.flow.async_init( + DLNA_DOMAIN, + context={"source": config_entries.SOURCE_IGNORE}, + data={"unique_id": MOCK_DEVICE_UDN, "title": MOCK_DEVICE_NAME}, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == MOCK_DEVICE_NAME + assert result["data"] == {} + + # Device was found via SSDP, matching the 2nd device type tried + ssdp_scanner_mock.async_get_discovery_info_by_udn_st.side_effect = [ + None, + MOCK_DISCOVERY, + None, + None, + None, + ] + + # Unignore it and expect config flow to start + result = await hass.config_entries.flow.async_init( + DLNA_DOMAIN, + context={"source": config_entries.SOURCE_UNIGNORE}, + data={"unique_id": MOCK_DEVICE_UDN}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == MOCK_DEVICE_NAME + assert result["data"] == { + CONF_URL: MOCK_DEVICE_LOCATION, + CONF_DEVICE_ID: MOCK_DEVICE_UDN, + CONF_TYPE: MOCK_DEVICE_TYPE, + } + assert result["options"] == {} + + # Wait for platform to be fully setup + await hass.async_block_till_done() + + +async def test_unignore_flow_offline( + hass: HomeAssistant, ssdp_scanner_mock: Mock +) -> None: + """Test a config flow started by unignoring a device, but the device is offline.""" + # Create ignored entry + result = await hass.config_entries.flow.async_init( + DLNA_DOMAIN, + context={"source": config_entries.SOURCE_IGNORE}, + data={"unique_id": MOCK_DEVICE_UDN, "title": MOCK_DEVICE_NAME}, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == MOCK_DEVICE_NAME + assert result["data"] == {} + + # Device is not in the SSDP discoveries (perhaps HA restarted between ignore and unignore) + ssdp_scanner_mock.async_get_discovery_info_by_udn_st.return_value = None + + # Unignore it and expect config flow to start then abort + result = await hass.config_entries.flow.async_init( + DLNA_DOMAIN, + context={"source": config_entries.SOURCE_UNIGNORE}, + data={"unique_id": MOCK_DEVICE_UDN}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "discovery_error" + + async def test_options_flow( hass: HomeAssistant, config_entry_mock: MockConfigEntry ) -> None: diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index e72b25715fe..c9098726ab4 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -71,6 +71,7 @@ async def test_ssdp_flow_dispatched_on_st(mock_get_ssdp, hass, caplog, mock_flow ssdp.ATTR_UPNP_UDN: "uuid:mock-udn", ssdp.ATTR_SSDP_UDN: ANY, "_timestamp": ANY, + ssdp.ATTR_HA_MATCHING_DOMAINS: {"mock-domain"}, } assert "Failed to fetch ssdp data" not in caplog.text @@ -463,6 +464,7 @@ async def test_scan_with_registered_callback( "x-rincon-bootseq": "55", ssdp.ATTR_SSDP_UDN: ANY, "_timestamp": ANY, + ssdp.ATTR_HA_MATCHING_DOMAINS: set(), }, ssdp.SsdpChange.ALIVE, ) From 1867d24b185db52c35226c631c7bfacfe4f237b6 Mon Sep 17 00:00:00 2001 From: Chris Browet Date: Fri, 22 Oct 2021 22:48:13 +0200 Subject: [PATCH 0706/1038] Add state_class support to Rest (#58026) --- homeassistant/components/rest/binary_sensor.py | 6 +++--- homeassistant/components/rest/entity.py | 7 ------- homeassistant/components/rest/schema.py | 3 +++ homeassistant/components/rest/sensor.py | 15 ++++++++------- homeassistant/components/rest/switch.py | 13 ++++++++++++- tests/components/rest/test_binary_sensor.py | 6 ++++++ tests/components/rest/test_sensor.py | 13 +++++++++++-- tests/components/rest/test_switch.py | 10 +++++++++- 8 files changed, 52 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/rest/binary_sensor.py b/homeassistant/components/rest/binary_sensor.py index a90c5bd7c77..d6bdefee563 100644 --- a/homeassistant/components/rest/binary_sensor.py +++ b/homeassistant/components/rest/binary_sensor.py @@ -85,14 +85,14 @@ class RestBinarySensor(RestEntity, BinarySensorEntity): resource_template, ): """Initialize a REST binary sensor.""" - super().__init__( - coordinator, rest, name, device_class, resource_template, force_update - ) + super().__init__(coordinator, rest, name, resource_template, force_update) self._state = False self._previous_data = None self._value_template = value_template self._is_on = None + self._attr_device_class = device_class + @property def is_on(self): """Return true if the binary sensor is on.""" diff --git a/homeassistant/components/rest/entity.py b/homeassistant/components/rest/entity.py index acfe5a2dfc5..064396af415 100644 --- a/homeassistant/components/rest/entity.py +++ b/homeassistant/components/rest/entity.py @@ -18,7 +18,6 @@ class RestEntity(Entity): coordinator: DataUpdateCoordinator[Any], rest: RestData, name, - device_class, resource_template, force_update, ) -> None: @@ -26,7 +25,6 @@ class RestEntity(Entity): self.coordinator = coordinator self.rest = rest self._name = name - self._device_class = device_class self._resource_template = resource_template self._force_update = force_update super().__init__() @@ -36,11 +34,6 @@ class RestEntity(Entity): """Return the name of the sensor.""" return self._name - @property - def device_class(self): - """Return the class of this sensor.""" - return self._device_class - @property def force_update(self): """Force update.""" diff --git a/homeassistant/components/rest/schema.py b/homeassistant/components/rest/schema.py index bedd02d272a..a4b87051c4b 100644 --- a/homeassistant/components/rest/schema.py +++ b/homeassistant/components/rest/schema.py @@ -9,7 +9,9 @@ from homeassistant.components.binary_sensor import ( from homeassistant.components.sensor import ( DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA, DOMAIN as SENSOR_DOMAIN, + STATE_CLASSES_SCHEMA, ) +from homeassistant.components.sensor.const import CONF_STATE_CLASS from homeassistant.const import ( CONF_AUTHENTICATION, CONF_DEVICE_CLASS, @@ -66,6 +68,7 @@ SENSOR_SCHEMA = { vol.Optional(CONF_NAME, default=DEFAULT_SENSOR_NAME): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, vol.Optional(CONF_JSON_ATTRS, default=[]): cv.ensure_list_csv, vol.Optional(CONF_JSON_ATTRS_PATH): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index f0355014986..9f8c33ad6df 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -8,6 +8,7 @@ import voluptuous as vol import xmltodict from homeassistant.components.sensor import ( + CONF_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, PLATFORM_SCHEMA, SensorEntity, @@ -60,6 +61,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= name = conf.get(CONF_NAME) unit = conf.get(CONF_UNIT_OF_MEASUREMENT) device_class = conf.get(CONF_DEVICE_CLASS) + state_class = conf.get(CONF_STATE_CLASS) json_attrs = conf.get(CONF_JSON_ATTRS) json_attrs_path = conf.get(CONF_JSON_ATTRS_PATH) value_template = conf.get(CONF_VALUE_TEMPLATE) @@ -77,6 +79,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= name, unit, device_class, + state_class, value_template, json_attrs, force_update, @@ -97,6 +100,7 @@ class RestSensor(RestEntity, SensorEntity): name, unit_of_measurement, device_class, + state_class, value_template, json_attrs, force_update, @@ -104,9 +108,7 @@ class RestSensor(RestEntity, SensorEntity): json_attrs_path, ): """Initialize the REST sensor.""" - super().__init__( - coordinator, rest, name, device_class, resource_template, force_update - ) + super().__init__(coordinator, rest, name, resource_template, force_update) self._state = None self._unit_of_measurement = unit_of_measurement self._value_template = value_template @@ -114,10 +116,9 @@ class RestSensor(RestEntity, SensorEntity): self._attributes = None self._json_attrs_path = json_attrs_path - @property - def native_unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit_of_measurement + self._attr_native_unit_of_measurement = self._unit_of_measurement + self._attr_device_class = device_class + self._attr_state_class = state_class @property def native_value(self): diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index e8ae1dee015..66a5f21cc40 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -6,8 +6,13 @@ import aiohttp import async_timeout import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + DEVICE_CLASSES_SCHEMA, + PLATFORM_SCHEMA, + SwitchEntity, +) from homeassistant.const import ( + CONF_DEVICE_CLASS, CONF_HEADERS, CONF_METHOD, CONF_NAME, @@ -51,6 +56,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Lower, vol.In(SUPPORT_REST_METHODS) ), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, vol.Inclusive(CONF_USERNAME, "authentication"): cv.string, vol.Inclusive(CONF_PASSWORD, "authentication"): cv.string, @@ -68,6 +74,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= headers = config.get(CONF_HEADERS) params = config.get(CONF_PARAMS) name = config.get(CONF_NAME) + device_class = config.get(CONF_DEVICE_CLASS) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) resource = config.get(CONF_RESOURCE) @@ -89,6 +96,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= try: switch = RestSwitch( name, + device_class, resource, state_resource, method, @@ -122,6 +130,7 @@ class RestSwitch(SwitchEntity): def __init__( self, name, + device_class, resource, state_resource, method, @@ -149,6 +158,8 @@ class RestSwitch(SwitchEntity): self._timeout = timeout self._verify_ssl = verify_ssl + self._attr_device_class = device_class + @property def name(self): """Return the name of the switch.""" diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py index d443710f9b2..0af82f9ab5a 100644 --- a/tests/components/rest/test_binary_sensor.py +++ b/tests/components/rest/test_binary_sensor.py @@ -10,6 +10,7 @@ import respx from homeassistant import config as hass_config import homeassistant.components.binary_sensor as binary_sensor from homeassistant.const import ( + ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, CONTENT_TYPE_JSON, SERVICE_RELOAD, @@ -164,6 +165,7 @@ async def test_setup_get(hass): "username": "my username", "password": "my password", "headers": {"Accept": CONTENT_TYPE_JSON}, + "device_class": binary_sensor.DEVICE_CLASS_PLUG, } }, ) @@ -171,6 +173,10 @@ async def test_setup_get(hass): await hass.async_block_till_done() assert len(hass.states.async_all("binary_sensor")) == 1 + state = hass.states.get("binary_sensor.foo") + assert state.state == STATE_OFF + assert state.attributes[ATTR_DEVICE_CLASS] == binary_sensor.DEVICE_CLASS_PLUG + @respx.mock async def test_setup_get_digest_auth(hass): diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index f50f5aba3bc..a59cb99bcdf 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -10,12 +10,15 @@ from homeassistant import config as hass_config from homeassistant.components.homeassistant import SERVICE_UPDATE_ENTITY import homeassistant.components.sensor as sensor from homeassistant.const import ( + ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONTENT_TYPE_JSON, DATA_MEGABYTES, + DEVICE_CLASS_TEMPERATURE, SERVICE_RELOAD, STATE_UNKNOWN, + TEMP_CELSIUS, ) from homeassistant.setup import async_setup_component @@ -177,13 +180,15 @@ async def test_setup_get(hass): "method": "GET", "value_template": "{{ value_json.key }}", "name": "foo", - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": TEMP_CELSIUS, "verify_ssl": "true", "timeout": 30, "authentication": "basic", "username": "my username", "password": "my password", "headers": {"Accept": CONTENT_TYPE_JSON}, + "device_class": DEVICE_CLASS_TEMPERATURE, + "state_class": sensor.STATE_CLASS_MEASUREMENT, } }, ) @@ -200,7 +205,11 @@ async def test_setup_get(hass): blocking=True, ) await hass.async_block_till_done() - assert hass.states.get("sensor.foo").state == "" + state = hass.states.get("sensor.foo") + assert state.state == "" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TEMPERATURE + assert state.attributes[sensor.ATTR_STATE_CLASS] == sensor.STATE_CLASS_MEASUREMENT @respx.mock diff --git a/tests/components/rest/test_switch.py b/tests/components/rest/test_switch.py index 48f63ddabc7..4370386dcff 100644 --- a/tests/components/rest/test_switch.py +++ b/tests/components/rest/test_switch.py @@ -6,7 +6,7 @@ import aiohttp from homeassistant.components.rest import DOMAIN import homeassistant.components.rest.switch as rest -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.switch import DEVICE_CLASS_SWITCH, DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( CONF_HEADERS, CONF_NAME, @@ -23,6 +23,7 @@ from tests.common import assert_setup_component """Tests for setting up the REST switch platform.""" NAME = "foo" +DEVICE_CLASS = DEVICE_CLASS_SWITCH METHOD = "post" RESOURCE = "http://localhost/" STATE_RESOURCE = RESOURCE @@ -158,6 +159,7 @@ def _setup_test_switch(hass): body_off = Template("off", hass) switch = rest.RestSwitch( NAME, + DEVICE_CLASS, RESOURCE, STATE_RESOURCE, METHOD, @@ -180,6 +182,12 @@ def test_name(hass): assert switch.name == NAME +def test_device_class(hass): + """Test the name.""" + switch, body_on, body_off = _setup_test_switch(hass) + assert switch.device_class == DEVICE_CLASS + + def test_is_on_before_update(hass): """Test is_on in initial state.""" switch, body_on, body_off = _setup_test_switch(hass) From ce1eda98098cc699c64e310777e1b8003785f86b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 23 Oct 2021 00:06:18 +0300 Subject: [PATCH 0707/1038] Use http.HTTPStatus in components/[ikl]* (#58248) --- homeassistant/components/ifttt/__init__.py | 5 +- homeassistant/components/ios/notify.py | 6 +- homeassistant/components/lifx_cloud/scene.py | 13 ++--- .../linksys_smart/device_tracker.py | 7 ++- .../components/llamalab_automate/notify.py | 5 +- homeassistant/components/london_air/sensor.py | 4 +- tests/components/influxdb/test_init.py | 7 ++- tests/components/influxdb/test_sensor.py | 3 +- tests/components/kmtronic/test_config_flow.py | 3 +- tests/components/kmtronic/test_switch.py | 3 +- tests/components/konnected/test_init.py | 56 +++++++++---------- tests/components/local_file/test_camera.py | 11 ++-- tests/components/locative/test_init.py | 42 +++++++------- tests/components/logbook/test_init.py | 55 +++++++++--------- .../logi_circle/test_config_flow.py | 5 +- tests/components/london_air/test_sensor.py | 9 ++- tests/components/lyric/test_config_flow.py | 3 +- 17 files changed, 124 insertions(+), 113 deletions(-) diff --git a/homeassistant/components/ifttt/__init__.py b/homeassistant/components/ifttt/__init__.py index 9e77e49709c..68d8159b6e4 100644 --- a/homeassistant/components/ifttt/__init__.py +++ b/homeassistant/components/ifttt/__init__.py @@ -1,4 +1,5 @@ """Support to trigger Maker IFTTT recipes.""" +from http import HTTPStatus import json import logging @@ -6,7 +7,7 @@ import pyfttt import requests import voluptuous as vol -from homeassistant.const import CONF_WEBHOOK_ID, HTTP_OK +from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.helpers import config_entry_flow import homeassistant.helpers.config_validation as cv @@ -75,7 +76,7 @@ async def async_setup(hass, config): for target, key in target_keys.items(): res = pyfttt.send_event(key, event, value1, value2, value3) - if res.status_code != HTTP_OK: + if res.status_code != HTTPStatus.OK: _LOGGER.error("IFTTT reported error sending event to %s", target) except requests.exceptions.RequestException: _LOGGER.exception("Error communicating with IFTTT") diff --git a/homeassistant/components/ios/notify.py b/homeassistant/components/ios/notify.py index 0c7ba59b533..2e27271841a 100644 --- a/homeassistant/components/ios/notify.py +++ b/homeassistant/components/ios/notify.py @@ -1,4 +1,5 @@ """Support for iOS push notifications.""" +from http import HTTPStatus import logging import requests @@ -12,7 +13,6 @@ from homeassistant.components.notify import ( ATTR_TITLE_DEFAULT, BaseNotificationService, ) -from homeassistant.const import HTTP_CREATED, HTTP_TOO_MANY_REQUESTS import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -91,13 +91,13 @@ class iOSNotificationService(BaseNotificationService): req = requests.post(PUSH_URL, json=data, timeout=10) - if req.status_code != HTTP_CREATED: + if req.status_code != HTTPStatus.CREATED: fallback_error = req.json().get("errorMessage", "Unknown error") fallback_message = ( f"Internal server error, please try again later: {fallback_error}" ) message = req.json().get("message", fallback_message) - if req.status_code == HTTP_TOO_MANY_REQUESTS: + if req.status_code == HTTPStatus.TOO_MANY_REQUESTS: _LOGGER.warning(message) log_rate_limits(self.hass, target, req.json(), 30) else: diff --git a/homeassistant/components/lifx_cloud/scene.py b/homeassistant/components/lifx_cloud/scene.py index cf39d70d89a..ec2aca00aa9 100644 --- a/homeassistant/components/lifx_cloud/scene.py +++ b/homeassistant/components/lifx_cloud/scene.py @@ -1,5 +1,6 @@ """Support for LIFX Cloud scenes.""" import asyncio +from http import HTTPStatus import logging from typing import Any @@ -9,13 +10,7 @@ import async_timeout import voluptuous as vol from homeassistant.components.scene import Scene -from homeassistant.const import ( - CONF_PLATFORM, - CONF_TIMEOUT, - CONF_TOKEN, - HTTP_OK, - HTTP_UNAUTHORIZED, -) +from homeassistant.const import CONF_PLATFORM, CONF_TIMEOUT, CONF_TOKEN from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -51,12 +46,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= return False status = scenes_resp.status - if status == HTTP_OK: + if status == HTTPStatus.OK: data = await scenes_resp.json() devices = [LifxCloudScene(hass, headers, timeout, scene) for scene in data] async_add_entities(devices) return True - if status == HTTP_UNAUTHORIZED: + if status == HTTPStatus.UNAUTHORIZED: _LOGGER.error("Unauthorized (bad token?) on %s", url) return False diff --git a/homeassistant/components/linksys_smart/device_tracker.py b/homeassistant/components/linksys_smart/device_tracker.py index 0ccfe36da24..c43de3141f7 100644 --- a/homeassistant/components/linksys_smart/device_tracker.py +++ b/homeassistant/components/linksys_smart/device_tracker.py @@ -1,4 +1,5 @@ """Support for Linksys Smart Wifi routers.""" +from http import HTTPStatus import logging import requests @@ -9,7 +10,7 @@ from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) -from homeassistant.const import CONF_HOST, HTTP_OK +from homeassistant.const import CONF_HOST import homeassistant.helpers.config_validation as cv DEFAULT_TIMEOUT = 10 @@ -37,7 +38,7 @@ class LinksysSmartWifiDeviceScanner(DeviceScanner): # Check if the access point is accessible response = self._make_request() - if not response.status_code == HTTP_OK: + if response.status_code != HTTPStatus.OK: raise ConnectionError("Cannot connect to Linksys Access Point") def scan_devices(self): @@ -56,7 +57,7 @@ class LinksysSmartWifiDeviceScanner(DeviceScanner): self.last_results = {} response = self._make_request() - if response.status_code != HTTP_OK: + if response.status_code != HTTPStatus.OK: _LOGGER.error( "Got HTTP status code %d when getting device list", response.status_code ) diff --git a/homeassistant/components/llamalab_automate/notify.py b/homeassistant/components/llamalab_automate/notify.py index b94ffa099be..e2850792906 100644 --- a/homeassistant/components/llamalab_automate/notify.py +++ b/homeassistant/components/llamalab_automate/notify.py @@ -1,4 +1,5 @@ """LlamaLab Automate notification service.""" +from http import HTTPStatus import logging import requests @@ -9,7 +10,7 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) -from homeassistant.const import CONF_API_KEY, CONF_DEVICE, HTTP_OK +from homeassistant.const import CONF_API_KEY, CONF_DEVICE from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -66,5 +67,5 @@ class AutomateNotificationService(BaseNotificationService): } response = requests.post(_RESOURCE, json=data) - if response.status_code != HTTP_OK: + if response.status_code != HTTPStatus.OK: _LOGGER.error("Error sending message: %s", response) diff --git a/homeassistant/components/london_air/sensor.py b/homeassistant/components/london_air/sensor.py index 23bc67f46bc..4c9e7a4c4fb 100644 --- a/homeassistant/components/london_air/sensor.py +++ b/homeassistant/components/london_air/sensor.py @@ -1,12 +1,12 @@ """Sensor for checking the status of London air.""" from datetime import timedelta +from http import HTTPStatus import logging import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import HTTP_OK import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle @@ -83,7 +83,7 @@ class APIData: def update(self): """Get the latest data from TFL.""" response = requests.get(URL, timeout=10) - if response.status_code != HTTP_OK: + if response.status_code != HTTPStatus.OK: _LOGGER.warning("Invalid response from API") else: self.data = parse_api_response(response.json()) diff --git a/tests/components/influxdb/test_init.py b/tests/components/influxdb/test_init.py index e589a42e99a..24ef68dd5bd 100644 --- a/tests/components/influxdb/test_init.py +++ b/tests/components/influxdb/test_init.py @@ -1,6 +1,7 @@ """The tests for the InfluxDB component.""" from dataclasses import dataclass import datetime +from http import HTTPStatus from unittest.mock import MagicMock, Mock, call, patch import pytest @@ -1640,14 +1641,16 @@ async def test_connection_failure_on_startup( BASE_V1_CONFIG, _get_write_api_mock_v1, influxdb.DEFAULT_API_VERSION, - influxdb.exceptions.InfluxDBClientError("fail", code=400), + influxdb.exceptions.InfluxDBClientError( + "fail", code=HTTPStatus.BAD_REQUEST + ), ), ( influxdb.API_VERSION_2, BASE_V2_CONFIG, _get_write_api_mock_v2, influxdb.API_VERSION_2, - influxdb.ApiException(status=400), + influxdb.ApiException(status=HTTPStatus.BAD_REQUEST), ), ], indirect=["mock_client", "get_mock_call"], diff --git a/tests/components/influxdb/test_sensor.py b/tests/components/influxdb/test_sensor.py index 1df106473f9..ac88c3ab967 100644 --- a/tests/components/influxdb/test_sensor.py +++ b/tests/components/influxdb/test_sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass from datetime import timedelta +from http import HTTPStatus from unittest.mock import MagicMock, patch from influxdb.exceptions import InfluxDBClientError, InfluxDBServerError @@ -423,7 +424,7 @@ async def test_state_for_no_results( BASE_V2_CONFIG, BASE_V2_QUERY, _set_query_mock_v2, - ApiException(status=400), + ApiException(status=HTTPStatus.BAD_REQUEST), ), ], indirect=["mock_client"], diff --git a/tests/components/kmtronic/test_config_flow.py b/tests/components/kmtronic/test_config_flow.py index 510157ad9dd..fabd8738259 100644 --- a/tests/components/kmtronic/test_config_flow.py +++ b/tests/components/kmtronic/test_config_flow.py @@ -1,4 +1,5 @@ """Test the kmtronic config flow.""" +from http import HTTPStatus from unittest.mock import Mock, patch from aiohttp import ClientConnectorError, ClientResponseError @@ -91,7 +92,7 @@ async def test_form_invalid_auth(hass): with patch( "homeassistant.components.kmtronic.config_flow.KMTronicHubAPI.async_get_status", - side_effect=ClientResponseError(None, None, status=401), + side_effect=ClientResponseError(None, None, status=HTTPStatus.BAD_REQUEST), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/kmtronic/test_switch.py b/tests/components/kmtronic/test_switch.py index 70a298878bd..e8006101103 100644 --- a/tests/components/kmtronic/test_switch.py +++ b/tests/components/kmtronic/test_switch.py @@ -1,6 +1,7 @@ """The tests for the KMtronic switch platform.""" import asyncio from datetime import timedelta +from http import HTTPStatus from homeassistant.components.kmtronic.const import DOMAIN from homeassistant.const import STATE_UNAVAILABLE @@ -128,7 +129,7 @@ async def test_failed_update(hass, aioclient_mock): aioclient_mock.get( "http://1.1.1.1/status.xml", text="401 Unauthorized: Password required", - status=401, + status=HTTPStatus.UNAUTHORIZED, ) async_fire_time_changed(hass, future) diff --git a/tests/components/konnected/test_init.py b/tests/components/konnected/test_init.py index 9be44a5d6da..4b2d09f1027 100644 --- a/tests/components/konnected/test_init.py +++ b/tests/components/konnected/test_init.py @@ -1,4 +1,5 @@ """Test Konnected setup process.""" +from http import HTTPStatus from unittest.mock import patch import pytest @@ -6,7 +7,6 @@ import pytest from homeassistant.components import konnected from homeassistant.components.konnected import config_flow from homeassistant.config import async_process_ha_core_config -from homeassistant.const import HTTP_NOT_FOUND from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -474,44 +474,44 @@ async def test_api(hass, hass_client_no_auth, mock_panel): # Test the get endpoint for switch status polling resp = await client.get("/api/konnected") - assert resp.status == HTTP_NOT_FOUND # no device provided + assert resp.status == HTTPStatus.NOT_FOUND # no device provided resp = await client.get("/api/konnected/223344556677") - assert resp.status == HTTP_NOT_FOUND # unknown device provided + assert resp.status == HTTPStatus.NOT_FOUND # unknown device provided resp = await client.get("/api/konnected/device/112233445566") - assert resp.status == HTTP_NOT_FOUND # no zone provided + assert resp.status == HTTPStatus.NOT_FOUND # no zone provided result = await resp.json() assert result == {"message": "Switch on zone or pin unknown not configured"} resp = await client.get("/api/konnected/device/112233445566?zone=8") - assert resp.status == HTTP_NOT_FOUND # invalid zone + assert resp.status == HTTPStatus.NOT_FOUND # invalid zone result = await resp.json() assert result == {"message": "Switch on zone or pin 8 not configured"} resp = await client.get("/api/konnected/device/112233445566?pin=12") - assert resp.status == HTTP_NOT_FOUND # invalid pin + assert resp.status == HTTPStatus.NOT_FOUND # invalid pin result = await resp.json() assert result == {"message": "Switch on zone or pin 12 not configured"} resp = await client.get("/api/konnected/device/112233445566?zone=out") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"state": 1, "zone": "out"} resp = await client.get("/api/konnected/device/112233445566?pin=8") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"state": 1, "pin": "8"} # Test the post endpoint for sensor updates resp = await client.post("/api/konnected/device", json={"zone": "1", "state": 1}) - assert resp.status == HTTP_NOT_FOUND + assert resp.status == HTTPStatus.NOT_FOUND resp = await client.post( "/api/konnected/device/112233445566", json={"zone": "1", "state": 1} ) - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED result = await resp.json() assert result == {"message": "unauthorized"} @@ -520,14 +520,14 @@ async def test_api(hass, hass_client_no_auth, mock_panel): headers={"Authorization": "Bearer abcdefgh"}, json={"zone": "1", "state": 1}, ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST resp = await client.post( "/api/konnected/device/112233445566", headers={"Authorization": "Bearer abcdefgh"}, json={"zone": "15", "state": 1}, ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST result = await resp.json() assert result == {"message": "unregistered sensor/actuator"} @@ -536,7 +536,7 @@ async def test_api(hass, hass_client_no_auth, mock_panel): headers={"Authorization": "Bearer abcdefgh"}, json={"zone": "1", "state": 1}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"message": "ok"} @@ -545,7 +545,7 @@ async def test_api(hass, hass_client_no_auth, mock_panel): headers={"Authorization": "Bearer globaltoken"}, json={"zone": "1", "state": 1}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"message": "ok"} @@ -554,7 +554,7 @@ async def test_api(hass, hass_client_no_auth, mock_panel): headers={"Authorization": "Bearer abcdefgh"}, json={"zone": "4", "temp": 22, "humi": 20}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"message": "ok"} @@ -564,7 +564,7 @@ async def test_api(hass, hass_client_no_auth, mock_panel): headers={"Authorization": "Bearer abcdefgh"}, json={"zone": "1", "state": 1}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"message": "ok"} @@ -650,7 +650,7 @@ async def test_state_updates_zone(hass, hass_client_no_auth, mock_panel): headers={"Authorization": "Bearer abcdefgh"}, json={"zone": "1", "state": 0}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"message": "ok"} await hass.async_block_till_done() @@ -661,7 +661,7 @@ async def test_state_updates_zone(hass, hass_client_no_auth, mock_panel): headers={"Authorization": "Bearer abcdefgh"}, json={"zone": "1", "state": 1}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"message": "ok"} await hass.async_block_till_done() @@ -673,7 +673,7 @@ async def test_state_updates_zone(hass, hass_client_no_auth, mock_panel): headers={"Authorization": "Bearer abcdefgh"}, json={"zone": "4", "temp": 22, "humi": 20}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"message": "ok"} await hass.async_block_till_done() @@ -687,7 +687,7 @@ async def test_state_updates_zone(hass, hass_client_no_auth, mock_panel): headers={"Authorization": "Bearer abcdefgh"}, json={"zone": "4", "temp": 25, "humi": 23}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"message": "ok"} await hass.async_block_till_done() @@ -702,7 +702,7 @@ async def test_state_updates_zone(hass, hass_client_no_auth, mock_panel): headers={"Authorization": "Bearer abcdefgh"}, json={"zone": "5", "temp": 32, "addr": 1}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"message": "ok"} await hass.async_block_till_done() @@ -713,7 +713,7 @@ async def test_state_updates_zone(hass, hass_client_no_auth, mock_panel): headers={"Authorization": "Bearer abcdefgh"}, json={"zone": "5", "temp": 42, "addr": 1}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"message": "ok"} await hass.async_block_till_done() @@ -805,7 +805,7 @@ async def test_state_updates_pin(hass, hass_client_no_auth, mock_panel): headers={"Authorization": "Bearer abcdefgh"}, json={"pin": "1", "state": 0}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"message": "ok"} await hass.async_block_till_done() @@ -816,7 +816,7 @@ async def test_state_updates_pin(hass, hass_client_no_auth, mock_panel): headers={"Authorization": "Bearer abcdefgh"}, json={"pin": "1", "state": 1}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"message": "ok"} await hass.async_block_till_done() @@ -828,7 +828,7 @@ async def test_state_updates_pin(hass, hass_client_no_auth, mock_panel): headers={"Authorization": "Bearer abcdefgh"}, json={"pin": "6", "temp": 22, "humi": 20}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"message": "ok"} await hass.async_block_till_done() @@ -842,7 +842,7 @@ async def test_state_updates_pin(hass, hass_client_no_auth, mock_panel): headers={"Authorization": "Bearer abcdefgh"}, json={"pin": "6", "temp": 25, "humi": 23}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"message": "ok"} await hass.async_block_till_done() @@ -857,7 +857,7 @@ async def test_state_updates_pin(hass, hass_client_no_auth, mock_panel): headers={"Authorization": "Bearer abcdefgh"}, json={"pin": "7", "temp": 32, "addr": 1}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"message": "ok"} await hass.async_block_till_done() @@ -868,7 +868,7 @@ async def test_state_updates_pin(hass, hass_client_no_auth, mock_panel): headers={"Authorization": "Bearer abcdefgh"}, json={"pin": "7", "temp": 42, "addr": 1}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == {"message": "ok"} await hass.async_block_till_done() diff --git a/tests/components/local_file/test_camera.py b/tests/components/local_file/test_camera.py index b1a9b04412d..b9b2b19832e 100644 --- a/tests/components/local_file/test_camera.py +++ b/tests/components/local_file/test_camera.py @@ -1,4 +1,5 @@ """The tests for local file camera component.""" +from http import HTTPStatus from unittest import mock from homeassistant.components.local_file.const import DOMAIN, SERVICE_UPDATE_FILE_PATH @@ -35,7 +36,7 @@ async def test_loading_file(hass, hass_client): ): resp = await client.get("/api/camera_proxy/camera.config_test") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK body = await resp.text() assert body == "hello" @@ -107,23 +108,23 @@ async def test_camera_content_type(hass, hass_client): resp_3 = await client.get("/api/camera_proxy/camera.test_svg") resp_4 = await client.get("/api/camera_proxy/camera.test_no_ext") - assert resp_1.status == 200 + assert resp_1.status == HTTPStatus.OK assert resp_1.content_type == "image/jpeg" body = await resp_1.text() assert body == image - assert resp_2.status == 200 + assert resp_2.status == HTTPStatus.OK assert resp_2.content_type == "image/png" body = await resp_2.text() assert body == image - assert resp_3.status == 200 + assert resp_3.status == HTTPStatus.OK assert resp_3.content_type == "image/svg+xml" body = await resp_3.text() assert body == image # default mime type - assert resp_4.status == 200 + assert resp_4.status == HTTPStatus.OK assert resp_4.content_type == "image/jpeg" body = await resp_4.text() assert body == image diff --git a/tests/components/locative/test_init.py b/tests/components/locative/test_init.py index bd39ec42978..f65e9d8a6af 100644 --- a/tests/components/locative/test_init.py +++ b/tests/components/locative/test_init.py @@ -1,4 +1,5 @@ """The tests the for Locative device tracker platform.""" +from http import HTTPStatus from unittest.mock import patch import pytest @@ -8,7 +9,6 @@ from homeassistant.components import locative from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.locative import DOMAIN, TRACKER_UPDATE from homeassistant.config import async_process_ha_core_config -from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component @@ -64,50 +64,50 @@ async def test_missing_data(locative_client, webhook_id): # No data req = await locative_client.post(url) - assert req.status == HTTP_UNPROCESSABLE_ENTITY + assert req.status == HTTPStatus.UNPROCESSABLE_ENTITY # No latitude copy = data.copy() del copy["latitude"] req = await locative_client.post(url, data=copy) - assert req.status == HTTP_UNPROCESSABLE_ENTITY + assert req.status == HTTPStatus.UNPROCESSABLE_ENTITY # No device copy = data.copy() del copy["device"] req = await locative_client.post(url, data=copy) - assert req.status == HTTP_UNPROCESSABLE_ENTITY + assert req.status == HTTPStatus.UNPROCESSABLE_ENTITY # No location copy = data.copy() del copy["id"] req = await locative_client.post(url, data=copy) - assert req.status == HTTP_UNPROCESSABLE_ENTITY + assert req.status == HTTPStatus.UNPROCESSABLE_ENTITY # No trigger copy = data.copy() del copy["trigger"] req = await locative_client.post(url, data=copy) - assert req.status == HTTP_UNPROCESSABLE_ENTITY + assert req.status == HTTPStatus.UNPROCESSABLE_ENTITY # Test message copy = data.copy() copy["trigger"] = "test" req = await locative_client.post(url, data=copy) - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK # Test message, no location copy = data.copy() copy["trigger"] = "test" del copy["id"] req = await locative_client.post(url, data=copy) - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK # Unknown trigger copy = data.copy() copy["trigger"] = "foobar" req = await locative_client.post(url, data=copy) - assert req.status == HTTP_UNPROCESSABLE_ENTITY + assert req.status == HTTPStatus.UNPROCESSABLE_ENTITY async def test_enter_and_exit(hass, locative_client, webhook_id): @@ -125,7 +125,7 @@ async def test_enter_and_exit(hass, locative_client, webhook_id): # Enter the Home req = await locative_client.post(url, data=data) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK state_name = hass.states.get( "{}.{}".format(DEVICE_TRACKER_DOMAIN, data["device"]) ).state @@ -137,7 +137,7 @@ async def test_enter_and_exit(hass, locative_client, webhook_id): # Exit Home req = await locative_client.post(url, data=data) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK state_name = hass.states.get( "{}.{}".format(DEVICE_TRACKER_DOMAIN, data["device"]) ).state @@ -149,7 +149,7 @@ async def test_enter_and_exit(hass, locative_client, webhook_id): # Enter Home again req = await locative_client.post(url, data=data) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK state_name = hass.states.get( "{}.{}".format(DEVICE_TRACKER_DOMAIN, data["device"]) ).state @@ -160,7 +160,7 @@ async def test_enter_and_exit(hass, locative_client, webhook_id): # Exit Home req = await locative_client.post(url, data=data) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK state_name = hass.states.get( "{}.{}".format(DEVICE_TRACKER_DOMAIN, data["device"]) ).state @@ -172,7 +172,7 @@ async def test_enter_and_exit(hass, locative_client, webhook_id): # Enter Work req = await locative_client.post(url, data=data) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK state_name = hass.states.get( "{}.{}".format(DEVICE_TRACKER_DOMAIN, data["device"]) ).state @@ -194,7 +194,7 @@ async def test_exit_after_enter(hass, locative_client, webhook_id): # Enter Home req = await locative_client.post(url, data=data) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK state = hass.states.get("{}.{}".format(DEVICE_TRACKER_DOMAIN, data["device"])) assert state.state == "home" @@ -204,7 +204,7 @@ async def test_exit_after_enter(hass, locative_client, webhook_id): # Enter Work req = await locative_client.post(url, data=data) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK state = hass.states.get("{}.{}".format(DEVICE_TRACKER_DOMAIN, data["device"])) assert state.state == "work" @@ -215,7 +215,7 @@ async def test_exit_after_enter(hass, locative_client, webhook_id): # Exit Home req = await locative_client.post(url, data=data) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK state = hass.states.get("{}.{}".format(DEVICE_TRACKER_DOMAIN, data["device"])) assert state.state == "work" @@ -236,7 +236,7 @@ async def test_exit_first(hass, locative_client, webhook_id): # Exit Home req = await locative_client.post(url, data=data) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK state = hass.states.get("{}.{}".format(DEVICE_TRACKER_DOMAIN, data["device"])) assert state.state == "not_home" @@ -257,7 +257,7 @@ async def test_two_devices(hass, locative_client, webhook_id): # Exit Home req = await locative_client.post(url, data=data_device_1) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK state = hass.states.get( "{}.{}".format(DEVICE_TRACKER_DOMAIN, data_device_1["device"]) @@ -270,7 +270,7 @@ async def test_two_devices(hass, locative_client, webhook_id): data_device_2["trigger"] = "enter" req = await locative_client.post(url, data=data_device_2) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK state = hass.states.get( "{}.{}".format(DEVICE_TRACKER_DOMAIN, data_device_2["device"]) @@ -300,7 +300,7 @@ async def test_load_unload_entry(hass, locative_client, webhook_id): # Exit Home req = await locative_client.post(url, data=data) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK state = hass.states.get("{}.{}".format(DEVICE_TRACKER_DOMAIN, data["device"])) assert state.state == "not_home" diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index b95ef2e148a..39277ef7aa7 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -2,6 +2,7 @@ # pylint: disable=protected-access,invalid-name import collections from datetime import datetime, timedelta +from http import HTTPStatus import json from unittest.mock import Mock, patch @@ -314,7 +315,7 @@ async def test_logbook_view(hass, hass_client): await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) client = await hass_client() response = await client.get(f"/api/logbook/{dt_util.utcnow().isoformat()}") - assert response.status == 200 + assert response.status == HTTPStatus.OK async def test_logbook_view_period_entity(hass, hass_client): @@ -341,7 +342,7 @@ async def test_logbook_view_period_entity(hass, hass_client): # Test today entries without filters response = await client.get(f"/api/logbook/{start_date.isoformat()}") - assert response.status == 200 + assert response.status == HTTPStatus.OK response_json = await response.json() assert len(response_json) == 2 assert response_json[0]["entity_id"] == entity_id_test @@ -349,7 +350,7 @@ async def test_logbook_view_period_entity(hass, hass_client): # Test today entries with filter by period response = await client.get(f"/api/logbook/{start_date.isoformat()}?period=1") - assert response.status == 200 + assert response.status == HTTPStatus.OK response_json = await response.json() assert len(response_json) == 2 assert response_json[0]["entity_id"] == entity_id_test @@ -359,7 +360,7 @@ async def test_logbook_view_period_entity(hass, hass_client): response = await client.get( f"/api/logbook/{start_date.isoformat()}?entity=switch.test" ) - assert response.status == 200 + assert response.status == HTTPStatus.OK response_json = await response.json() assert len(response_json) == 1 assert response_json[0]["entity_id"] == entity_id_test @@ -368,7 +369,7 @@ async def test_logbook_view_period_entity(hass, hass_client): response = await client.get( f"/api/logbook/{start_date.isoformat()}?period=3&entity=switch.test" ) - assert response.status == 200 + assert response.status == HTTPStatus.OK response_json = await response.json() assert len(response_json) == 1 assert response_json[0]["entity_id"] == entity_id_test @@ -379,7 +380,7 @@ async def test_logbook_view_period_entity(hass, hass_client): # Test tomorrow entries without filters response = await client.get(f"/api/logbook/{start_date.isoformat()}") - assert response.status == 200 + assert response.status == HTTPStatus.OK response_json = await response.json() assert len(response_json) == 0 @@ -387,7 +388,7 @@ async def test_logbook_view_period_entity(hass, hass_client): response = await client.get( f"/api/logbook/{start_date.isoformat()}?entity=switch.test" ) - assert response.status == 200 + assert response.status == HTTPStatus.OK response_json = await response.json() assert len(response_json) == 0 @@ -395,7 +396,7 @@ async def test_logbook_view_period_entity(hass, hass_client): response = await client.get( f"/api/logbook/{start_date.isoformat()}?period=3&entity=switch.test" ) - assert response.status == 200 + assert response.status == HTTPStatus.OK response_json = await response.json() assert len(response_json) == 1 assert response_json[0]["entity_id"] == entity_id_test @@ -539,7 +540,7 @@ async def test_logbook_view_end_time_entity(hass, hass_client): response = await client.get( f"/api/logbook/{start_date.isoformat()}?end_time={end_time}" ) - assert response.status == 200 + assert response.status == HTTPStatus.OK response_json = await response.json() assert len(response_json) == 2 assert response_json[0]["entity_id"] == entity_id_test @@ -550,7 +551,7 @@ async def test_logbook_view_end_time_entity(hass, hass_client): response = await client.get( f"/api/logbook/{start_date.isoformat()}?end_time={end_time}&entity=switch.test" ) - assert response.status == 200 + assert response.status == HTTPStatus.OK response_json = await response.json() assert len(response_json) == 1 assert response_json[0]["entity_id"] == entity_id_test @@ -564,7 +565,7 @@ async def test_logbook_view_end_time_entity(hass, hass_client): response = await client.get( f"/api/logbook/{start_date.isoformat()}?end_time={end_time}&entity=switch.test" ) - assert response.status == 200 + assert response.status == HTTPStatus.OK response_json = await response.json() assert len(response_json) == 1 assert response_json[0]["entity_id"] == entity_id_test @@ -611,7 +612,7 @@ async def test_logbook_entity_filter_with_automations(hass, hass_client): response = await client.get( f"/api/logbook/{start_date.isoformat()}?end_time={end_time}" ) - assert response.status == 200 + assert response.status == HTTPStatus.OK json_dict = await response.json() assert json_dict[0]["entity_id"] == entity_id_test @@ -625,7 +626,7 @@ async def test_logbook_entity_filter_with_automations(hass, hass_client): response = await client.get( f"/api/logbook/{start_date.isoformat()}?end_time={end_time}&entity=alarm_control_panel.area_001" ) - assert response.status == 200 + assert response.status == HTTPStatus.OK json_dict = await response.json() assert len(json_dict) == 1 assert json_dict[0]["entity_id"] == entity_id_test @@ -639,7 +640,7 @@ async def test_logbook_entity_filter_with_automations(hass, hass_client): response = await client.get( f"/api/logbook/{start_date.isoformat()}?end_time={end_time}&entity=alarm_control_panel.area_002" ) - assert response.status == 200 + assert response.status == HTTPStatus.OK json_dict = await response.json() assert len(json_dict) == 1 assert json_dict[0]["entity_id"] == entity_id_second @@ -673,7 +674,7 @@ async def test_filter_continuous_sensor_values(hass, hass_client): # Test today entries without filters response = await client.get(f"/api/logbook/{start_date.isoformat()}") - assert response.status == 200 + assert response.status == HTTPStatus.OK response_json = await response.json() assert len(response_json) == 2 @@ -707,7 +708,7 @@ async def test_exclude_new_entities(hass, hass_client): # Test today entries without filters response = await client.get(f"/api/logbook/{start_date.isoformat()}") - assert response.status == 200 + assert response.status == HTTPStatus.OK response_json = await response.json() assert len(response_json) == 2 @@ -748,7 +749,7 @@ async def test_exclude_removed_entities(hass, hass_client): # Test today entries without filters response = await client.get(f"/api/logbook/{start_date.isoformat()}") - assert response.status == 200 + assert response.status == HTTPStatus.OK response_json = await response.json() assert len(response_json) == 3 @@ -787,7 +788,7 @@ async def test_exclude_attribute_changes(hass, hass_client): # Test today entries without filters response = await client.get(f"/api/logbook/{start_date.isoformat()}") - assert response.status == 200 + assert response.status == HTTPStatus.OK response_json = await response.json() assert len(response_json) == 3 @@ -904,7 +905,7 @@ async def test_logbook_entity_context_id(hass, hass_client): response = await client.get( f"/api/logbook/{start_date.isoformat()}?end_time={end_time}" ) - assert response.status == 200 + assert response.status == HTTPStatus.OK json_dict = await response.json() assert json_dict[0]["entity_id"] == "automation.alarm" @@ -1077,7 +1078,7 @@ async def test_logbook_entity_context_parent_id(hass, hass_client): response = await client.get( f"/api/logbook/{start_date.isoformat()}?end_time={end_time}" ) - assert response.status == 200 + assert response.status == HTTPStatus.OK json_dict = await response.json() assert json_dict[0]["entity_id"] == "automation.alarm" @@ -1192,7 +1193,7 @@ async def test_logbook_context_from_template(hass, hass_client): response = await client.get( f"/api/logbook/{start_date.isoformat()}?end_time={end_time}" ) - assert response.status == 200 + assert response.status == HTTPStatus.OK json_dict = await response.json() assert json_dict[0]["domain"] == "homeassistant" @@ -1278,7 +1279,7 @@ async def test_logbook_entity_matches_only(hass, hass_client): response = await client.get( f"/api/logbook/{start_date.isoformat()}?end_time={end_time}&entity=switch.test_state&entity_matches_only" ) - assert response.status == 200 + assert response.status == HTTPStatus.OK json_dict = await response.json() assert len(json_dict) == 2 @@ -1318,7 +1319,7 @@ async def test_custom_log_entry_discoverable_via_entity_matches_only(hass, hass_ response = await client.get( f"/api/logbook/{start_date.isoformat()}?end_time={end_time.isoformat()}&entity=switch.test_switch&entity_matches_only" ) - assert response.status == 200 + assert response.status == HTTPStatus.OK json_dict = await response.json() assert len(json_dict) == 1 @@ -1396,7 +1397,7 @@ async def test_logbook_entity_matches_only_multiple(hass, hass_client): response = await client.get( f"/api/logbook/{start_date.isoformat()}?end_time={end_time}&entity=switch.test_state,light.test_state&entity_matches_only" ) - assert response.status == 200 + assert response.status == HTTPStatus.OK json_dict = await response.json() assert len(json_dict) == 4 @@ -1428,7 +1429,7 @@ async def test_logbook_invalid_entity(hass, hass_client): response = await client.get( f"/api/logbook/{start_date.isoformat()}?end_time={end_time}&entity=invalid&entity_matches_only" ) - assert response.status == 500 + assert response.status == HTTPStatus.INTERNAL_SERVER_ERROR async def test_icon_and_state(hass, hass_client): @@ -1870,7 +1871,7 @@ async def test_context_filter(hass, hass_client): response = await client.get( "/api/logbook", params={"context_id": context.id, "entity": entity_id} ) - assert response.status == 400 + assert response.status == HTTPStatus.BAD_REQUEST async def _async_fetch_logbook(client, params=None): @@ -1886,7 +1887,7 @@ async def _async_fetch_logbook(client, params=None): # Test today entries without filters response = await client.get(f"/api/logbook/{start_date.isoformat()}", params=params) - assert response.status == 200 + assert response.status == HTTPStatus.OK return await response.json() diff --git a/tests/components/logi_circle/test_config_flow.py b/tests/components/logi_circle/test_config_flow.py index dbd35469d79..536d370f16a 100644 --- a/tests/components/logi_circle/test_config_flow.py +++ b/tests/components/logi_circle/test_config_flow.py @@ -1,5 +1,6 @@ """Tests for Logi Circle config flow.""" import asyncio +from http import HTTPStatus from unittest.mock import AsyncMock, Mock, patch import pytest @@ -203,7 +204,7 @@ async def test_callback_view_rejects_missing_code(hass): view = LogiCircleAuthCallbackView() resp = await view.get(MockRequest(hass, {})) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST async def test_callback_view_accepts_code( @@ -214,7 +215,7 @@ async def test_callback_view_accepts_code( view = LogiCircleAuthCallbackView() resp = await view.get(MockRequest(hass, {"code": "456"})) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK await hass.async_block_till_done() mock_logi_circle.authorize.assert_called_with("456") diff --git a/tests/components/london_air/test_sensor.py b/tests/components/london_air/test_sensor.py index 4ee28c54329..8843a367a18 100644 --- a/tests/components/london_air/test_sensor.py +++ b/tests/components/london_air/test_sensor.py @@ -1,6 +1,7 @@ """The tests for the london_air platform.""" +from http import HTTPStatus + from homeassistant.components.london_air.sensor import CONF_LOCATIONS, URL -from homeassistant.const import HTTP_OK, HTTP_SERVICE_UNAVAILABLE from homeassistant.setup import async_setup_component from tests.common import load_fixture @@ -10,7 +11,9 @@ VALID_CONFIG = {"sensor": {"platform": "london_air", CONF_LOCATIONS: ["Merton"]} async def test_valid_state(hass, requests_mock): """Test for operational london_air sensor with proper attributes.""" - requests_mock.get(URL, text=load_fixture("london_air.json"), status_code=HTTP_OK) + requests_mock.get( + URL, text=load_fixture("london_air.json"), status_code=HTTPStatus.OK + ) assert await async_setup_component(hass, "sensor", VALID_CONFIG) await hass.async_block_till_done() @@ -41,7 +44,7 @@ async def test_valid_state(hass, requests_mock): async def test_api_failure(hass, requests_mock): """Test for failure in the API.""" - requests_mock.get(URL, status_code=HTTP_SERVICE_UNAVAILABLE) + requests_mock.get(URL, status_code=HTTPStatus.SERVICE_UNAVAILABLE) assert await async_setup_component(hass, "sensor", VALID_CONFIG) await hass.async_block_till_done() diff --git a/tests/components/lyric/test_config_flow.py b/tests/components/lyric/test_config_flow.py index 23f2a42e449..25cc49c6c09 100644 --- a/tests/components/lyric/test_config_flow.py +++ b/tests/components/lyric/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Honeywell Lyric config flow.""" import asyncio +from http import HTTPStatus from unittest.mock import patch import pytest @@ -79,7 +80,7 @@ async def test_full_flow( client = await hass_client_no_auth() resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert resp.headers["content-type"] == "text/html; charset=utf-8" aioclient_mock.post( From fe0151491e55b14c20796ce97a1c73b1032faffd Mon Sep 17 00:00:00 2001 From: Nihaal Sangha Date: Fri, 22 Oct 2021 22:09:30 +0100 Subject: [PATCH 0708/1038] Improve Discord notifier (#52523) --- homeassistant/components/discord/notify.py | 68 +++++++++------------- 1 file changed, 29 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index 6d3dc704d83..10ad1e8e018 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -59,10 +59,22 @@ class DiscordNotificationService(BaseNotificationService): data = kwargs.get(ATTR_DATA) or {} + embed = None if ATTR_EMBED in data: embedding = data[ATTR_EMBED] fields = embedding.get(ATTR_EMBED_FIELDS) + if embedding: + embed = discord.Embed(**embedding) + for field in fields: + embed.add_field(**field) + if ATTR_EMBED_FOOTER in embedding: + embed.set_footer(**embedding[ATTR_EMBED_FOOTER]) + if ATTR_EMBED_AUTHOR in embedding: + embed.set_author(**embedding[ATTR_EMBED_AUTHOR]) + if ATTR_EMBED_THUMBNAIL in embedding: + embed.set_thumbnail(**embedding[ATTR_EMBED_THUMBNAIL]) + if ATTR_IMAGES in data: images = [] @@ -76,43 +88,21 @@ class DiscordNotificationService(BaseNotificationService): else: _LOGGER.warning("Image not found: %s", image) - # pylint: disable=unused-variable - @discord_bot.event - async def on_ready(): - """Send the messages when the bot is ready.""" - try: - for channelid in kwargs[ATTR_TARGET]: - channelid = int(channelid) - channel = discord_bot.get_channel( + await discord_bot.login(self.token) + + try: + for channelid in kwargs[ATTR_TARGET]: + channelid = int(channelid) + try: + channel = discord_bot.fetch_channel( channelid - ) or discord_bot.get_user(channelid) - - if channel is None: - _LOGGER.warning("Channel not found for ID: %s", channelid) - continue - # Must create new instances of File for each channel. - files = None - if images: - files = [] - for image in images: - files.append(discord.File(image)) - if embedding: - embed = discord.Embed(**embedding) - if fields: - for field_num, field_name in enumerate(fields): - embed.add_field(**fields[field_num]) - if ATTR_EMBED_FOOTER in embedding: - embed.set_footer(**embedding[ATTR_EMBED_FOOTER]) - if ATTR_EMBED_AUTHOR in embedding: - embed.set_author(**embedding[ATTR_EMBED_AUTHOR]) - if ATTR_EMBED_THUMBNAIL in embedding: - embed.set_thumbnail(**embedding[ATTR_EMBED_THUMBNAIL]) - await channel.send(message, files=files, embed=embed) - else: - await channel.send(message, files=files) - except (discord.errors.HTTPException, discord.errors.NotFound) as error: - _LOGGER.warning("Communication error: %s", error) - await discord_bot.close() - - # Using reconnect=False prevents multiple ready events to be fired. - await discord_bot.start(self.token, reconnect=False) + ) or discord_bot.fetch_user(channelid) + except discord.NotFound: + _LOGGER.warning("Channel not found for ID: %s", channelid) + continue + # Must create new instances of File for each channel. + files = [discord.File(image) for image in images] if images else None + await channel.send(message, files=files, embed=embed) + except (discord.HTTPException, discord.NotFound) as error: + _LOGGER.warning("Communication error: %s", error) + await discord_bot.close() From a9ccd70e71a90200e7eee3115d75c00cc0e24bbf Mon Sep 17 00:00:00 2001 From: Yuval Aboulafia Date: Sat, 23 Oct 2021 00:11:41 +0300 Subject: [PATCH 0709/1038] Fully type Jewish Calendar (#56232) --- .strict-typing | 1 + .../components/jewish_calendar/__init__.py | 10 ++-- .../jewish_calendar/binary_sensor.py | 23 +++++--- .../components/jewish_calendar/sensor.py | 58 ++++++++++--------- mypy.ini | 11 ++++ 5 files changed, 64 insertions(+), 39 deletions(-) diff --git a/.strict-typing b/.strict-typing index 295a0a352e0..5fb4b1759ee 100644 --- a/.strict-typing +++ b/.strict-typing @@ -60,6 +60,7 @@ homeassistant.components.hyperion.* homeassistant.components.image_processing.* homeassistant.components.integration.* homeassistant.components.iqvia.* +homeassistant.components.jewish_calendar.* homeassistant.components.knx.* homeassistant.components.kraken.* homeassistant.components.lcn.* diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py index bb7cc9d2841..3f211d5d23d 100644 --- a/homeassistant/components/jewish_calendar/__init__.py +++ b/homeassistant/components/jewish_calendar/__init__.py @@ -1,12 +1,14 @@ """The jewish_calendar component.""" from __future__ import annotations -import hdate +from hdate import Location import voluptuous as vol 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.discovery import async_load_platform +from homeassistant.helpers.typing import ConfigType DOMAIN = "jewish_calendar" @@ -43,7 +45,7 @@ CONFIG_SCHEMA = vol.Schema( def get_unique_prefix( - location: hdate.Location, + location: Location, language: str, candle_lighting_offset: int | None, havdalah_offset: int | None, @@ -63,7 +65,7 @@ def get_unique_prefix( return f"{prefix}" -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Jewish Calendar component.""" name = config[DOMAIN][CONF_NAME] language = config[DOMAIN][CONF_LANGUAGE] @@ -75,7 +77,7 @@ async def async_setup(hass, config): candle_lighting_offset = config[DOMAIN][CONF_CANDLE_LIGHT_MINUTES] havdalah_offset = config[DOMAIN][CONF_HAVDALAH_OFFSET_MINUTES] - location = hdate.Location( + location = Location( latitude=latitude, longitude=longitude, timezone=hass.config.time_zone, diff --git a/homeassistant/components/jewish_calendar/binary_sensor.py b/homeassistant/components/jewish_calendar/binary_sensor.py index c4e9b1e347f..f239dfc31b6 100644 --- a/homeassistant/components/jewish_calendar/binary_sensor.py +++ b/homeassistant/components/jewish_calendar/binary_sensor.py @@ -2,14 +2,17 @@ from __future__ import annotations import datetime as dt +from datetime import datetime +from typing import cast import hdate +from hdate.zmanim import Zmanim from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import event from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -29,7 +32,7 @@ async def async_setup_platform( config: ConfigType, async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, -): +) -> None: """Set up the Jewish Calendar binary sensor devices.""" if discovery_info is None: return @@ -42,7 +45,11 @@ class JewishCalendarBinarySensor(BinarySensorEntity): _attr_should_poll = False - def __init__(self, data, description: BinarySensorEntityDescription) -> None: + def __init__( + self, + data: dict[str, str | bool | int | float], + description: BinarySensorEntityDescription, + ) -> None: """Initialize the binary sensor.""" self._attr_name = f"{data['name']} {description.name}" self._attr_unique_id = f"{data['prefix']}_{description.key}" @@ -50,14 +57,14 @@ class JewishCalendarBinarySensor(BinarySensorEntity): self._hebrew = data["language"] == "hebrew" self._candle_lighting_offset = data["candle_lighting_offset"] self._havdalah_offset = data["havdalah_offset"] - self._update_unsub = None + self._update_unsub: CALLBACK_TYPE | None = None @property def is_on(self) -> bool: """Return true if sensor is on.""" - return self._get_zmanim().issur_melacha_in_effect + return cast(bool, self._get_zmanim().issur_melacha_in_effect) - def _get_zmanim(self): + def _get_zmanim(self) -> Zmanim: """Return the Zmanim object for now().""" return hdate.Zmanim( date=dt_util.now(), @@ -73,13 +80,13 @@ class JewishCalendarBinarySensor(BinarySensorEntity): self._schedule_update() @callback - def _update(self, now=None): + def _update(self, now: datetime | None = None) -> None: """Update the state of the sensor.""" self._update_unsub = None self._schedule_update() self.async_write_ha_state() - def _schedule_update(self): + def _schedule_update(self) -> None: """Schedule the next update of the sensor.""" now = dt_util.now() zmanim = self._get_zmanim() diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 6a57eb0eeda..09a56a9e26a 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -1,17 +1,19 @@ """Platform to retrieve Jewish calendar information for Home Assistant.""" from __future__ import annotations -from datetime import datetime +from datetime import date as Date, datetime import logging +from typing import Any -import hdate +from hdate import HDate +from hdate.zmanim import Zmanim from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.const import DEVICE_CLASS_TIMESTAMP, SUN_EVENT_SUNSET from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sun import get_astral_event_date -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType import homeassistant.util.dt as dt_util from . import DOMAIN @@ -130,7 +132,7 @@ async def async_setup_platform( config: ConfigType, async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, -): +) -> None: """Set up the Jewish calendar sensor platform.""" if discovery_info is None: return @@ -150,7 +152,11 @@ async def async_setup_platform( class JewishCalendarSensor(SensorEntity): """Representation of an Jewish calendar sensor.""" - def __init__(self, data, description: SensorEntityDescription) -> None: + def __init__( + self, + data: dict[str, str | bool | int | float], + description: SensorEntityDescription, + ) -> None: """Initialize the Jewish calendar sensor.""" self.entity_description = description self._attr_name = f"{data['name']} {description.name}" @@ -160,29 +166,33 @@ class JewishCalendarSensor(SensorEntity): self._candle_lighting_offset = data["candle_lighting_offset"] self._havdalah_offset = data["havdalah_offset"] self._diaspora = data["diaspora"] - self._state = None + self._state: datetime | None = None self._holiday_attrs: dict[str, str] = {} @property - def native_value(self): + def native_value(self) -> StateType: """Return the state of the sensor.""" if isinstance(self._state, datetime): return self._state.isoformat() return self._state - async def async_update(self): + async def async_update(self) -> None: """Update the state of the sensor.""" now = dt_util.now() _LOGGER.debug("Now: %s Location: %r", now, self._location) today = now.date() - sunset = dt_util.as_local( - get_astral_event_date(self.hass, SUN_EVENT_SUNSET, today) - ) + event_date = get_astral_event_date(self.hass, SUN_EVENT_SUNSET, today) + + if event_date is None: + _LOGGER.error("Can't get sunset event date for %s", today) + return + + sunset = dt_util.as_local(event_date) _LOGGER.debug("Now: %s Sunset: %s", now, sunset) - daytime_date = hdate.HDate(today, diaspora=self._diaspora, hebrew=self._hebrew) + daytime_date = HDate(today, diaspora=self._diaspora, hebrew=self._hebrew) # The Jewish day starts after darkness (called "tzais") and finishes at # sunset ("shkia"). The time in between is a gray area (aka "Bein @@ -203,9 +213,9 @@ class JewishCalendarSensor(SensorEntity): self._state = self.get_state(daytime_date, after_shkia_date, after_tzais_date) _LOGGER.debug("New value for %s: %s", self.entity_description.key, self._state) - def make_zmanim(self, date): + def make_zmanim(self, date: Date) -> Zmanim: """Create a Zmanim object.""" - return hdate.Zmanim( + return Zmanim( date=date, location=self._location, candle_lighting_offset=self._candle_lighting_offset, @@ -220,7 +230,9 @@ class JewishCalendarSensor(SensorEntity): return {} return self._holiday_attrs - def get_state(self, daytime_date, after_shkia_date, after_tzais_date): + def get_state( + self, daytime_date: HDate, after_shkia_date: HDate, after_tzais_date: HDate + ) -> Any | None: """For a given type of sensor, return the state.""" # Terminology note: by convention in py-libhdate library, "upcoming" # refers to "current" or "upcoming" dates. @@ -250,23 +262,15 @@ class JewishCalendarTimeSensor(JewishCalendarSensor): _attr_device_class = DEVICE_CLASS_TIMESTAMP @property - def native_value(self): + def native_value(self) -> StateType | None: """Return the state of the sensor.""" if self._state is None: return None return dt_util.as_utc(self._state).isoformat() - @property - def extra_state_attributes(self) -> dict[str, str]: - """Return the state attributes.""" - attrs: dict[str, str] = {} - - if self._state is None: - return attrs - - return attrs - - def get_state(self, daytime_date, after_shkia_date, after_tzais_date): + def get_state( + self, daytime_date: HDate, after_shkia_date: HDate, after_tzais_date: HDate + ) -> Any | None: """For a given type of sensor, return the state.""" if self.entity_description.key == "upcoming_shabbat_candle_lighting": times = self.make_zmanim( diff --git a/mypy.ini b/mypy.ini index 8114e3d39f5..c338ebe5a31 100644 --- a/mypy.ini +++ b/mypy.ini @@ -671,6 +671,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.jewish_calendar.*] +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.knx.*] check_untyped_defs = true disallow_incomplete_defs = true From 8d4680255868cdfb8148d51ddc37a717bbab5e10 Mon Sep 17 00:00:00 2001 From: itairaz1 Date: Sat, 23 Oct 2021 00:13:14 +0300 Subject: [PATCH 0710/1038] Apple TV power management (#51520) --- .../components/apple_tv/media_player.py | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index 09ecc01015c..35b9777394e 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -6,6 +6,7 @@ from pyatv.const import ( FeatureName, FeatureState, MediaType, + PowerState, RepeatState, ShuffleState, ) @@ -87,12 +88,14 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): """Handle when connection is made to device.""" self.atv.push_updater.listener = self self.atv.push_updater.start() + self.atv.power.listener = self @callback def async_device_disconnected(self): """Handle when connection was lost to device.""" self.atv.push_updater.stop() self.atv.push_updater.listener = None + self.atv.power.listener = None @property def state(self): @@ -101,6 +104,11 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): return None if self.atv is None: return STATE_OFF + if ( + self._is_feature_available(FeatureName.PowerState) + and self.atv.power.power_state == PowerState.Off + ): + return STATE_STANDBY if self._playing: state = self._playing.device_state if state in (DeviceState.Idle, DeviceState.Loading): @@ -125,6 +133,11 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): self._playing = None self.async_write_ha_state() + @callback + def powerstate_update(self, old_state: PowerState, new_state: PowerState): + """Update power state when it changes.""" + self.async_write_ha_state() + @property def app_id(self): """ID of the current running app.""" @@ -239,12 +252,16 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): async def async_turn_on(self): """Turn the media player on.""" - await self.manager.connect() + if self._is_feature_available(FeatureName.TurnOn): + await self.atv.power.turn_on() async def async_turn_off(self): """Turn the media player off.""" - self._playing = None - await self.manager.disconnect() + if (self._is_feature_available(FeatureName.TurnOff)) and ( + not self._is_feature_available(FeatureName.PowerState) + or self.atv.power.power_state == PowerState.On + ): + await self.atv.power.turn_off() async def async_media_play_pause(self): """Pause media on media player.""" From 750a8f2d371db16919aa2acd604b29de5f477286 Mon Sep 17 00:00:00 2001 From: John James Jacoby Date: Fri, 22 Oct 2021 16:22:19 -0500 Subject: [PATCH 0711/1038] Spelling & grammar improvements to bug_report.yml (#56800) Co-authored-by: Franck Nijhof --- .github/ISSUE_TEMPLATE/bug_report.yml | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 116afec36ee..ac4c8453327 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -15,7 +15,7 @@ body: attributes: label: The problem description: >- - Describe the issue you are experiencing here to communicate to the + Describe the issue you are experiencing here, to communicate to the maintainers. Tell us what you were trying to do and what happened. Provide a clear and concise description of what the problem is. @@ -28,10 +28,12 @@ body: validations: required: true attributes: - label: What is version of Home Assistant Core has the issue? + label: What version of Home Assistant Core has the issue? placeholder: core- description: > - Can be found in the Configuration panel -> Info. + Can be found in: [Configuration panel -> Info](https://my.home-assistant.io/redirect/info/). + + [![Open your Home Assistant instance and show your Home Assistant version information.](https://my.home-assistant.io/badges/info.svg)](https://my.home-assistant.io/redirect/info/) - type: input attributes: label: What was the last working version of Home Assistant Core? @@ -44,7 +46,9 @@ body: attributes: label: What type of installation are you running? description: > - If you don't know, you can find it in: Configuration panel -> Info. + Can be found in: [Configuration panel -> Info](https://my.home-assistant.io/redirect/info/). + + [![Open your Home Assistant instance and show your Home Assistant version information.](https://my.home-assistant.io/badges/info.svg)](https://my.home-assistant.io/redirect/info/) options: - Home Assistant OS - Home Assistant Container @@ -55,15 +59,15 @@ body: attributes: label: Integration causing the issue description: > - The name of the integration, for example, Automation or Philips Hue. + The name of the integration. For example: Automation, Philips Hue - type: input id: integration_link attributes: label: Link to integration documentation on our website placeholder: "https://www.home-assistant.io/integrations/..." description: | - Providing a link [to the documentation][docs] help us categorizing the - issue, while providing a useful reference at the same time. + Providing a link [to the documentation][docs] helps us categorize the + issue, while also providing a useful reference for others. [docs]: https://www.home-assistant.io/integrations @@ -75,8 +79,8 @@ body: attributes: label: Example YAML snippet description: | - If this issue has an example piece of YAML that can help reproducing this problem, please provide. - This can be an piece of YAML from, e.g., an automation, script, scene or configuration. + If applicable, please provide an example piece of YAML that can help reproduce this problem. + This can be from an automation, script, scene or configuration. render: yaml - type: textarea attributes: @@ -88,5 +92,3 @@ body: label: Additional information description: > If you have any additional information for us, use the field below. - Please note, you can attach screenshots or screen recordings here, by - dragging and dropping files in the field below. From 7d6ad6e5e3418bdf1c281c843200f34aa40560f4 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 22 Oct 2021 14:30:15 -0700 Subject: [PATCH 0712/1038] Add additional nest stream test coverage (#58013) --- tests/components/nest/camera_sdm_test.py | 76 +++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/tests/components/nest/camera_sdm_test.py b/tests/components/nest/camera_sdm_test.py index 79b3ef1a929..3cab5eea171 100644 --- a/tests/components/nest/camera_sdm_test.py +++ b/tests/components/nest/camera_sdm_test.py @@ -14,7 +14,12 @@ from google_nest_sdm.event import EventMessage import pytest from homeassistant.components import camera -from homeassistant.components.camera import STATE_IDLE, STATE_STREAMING +from homeassistant.components.camera import ( + STATE_IDLE, + STATE_STREAMING, + STREAM_TYPE_HLS, + STREAM_TYPE_WEB_RTC, +) from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -192,6 +197,7 @@ async def test_camera_stream(hass, auth): cam = hass.states.get("camera.my_camera") assert cam is not None assert cam.state == STATE_STREAMING + assert cam.attributes["frontend_stream_type"] == STREAM_TYPE_HLS stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") assert stream_source == "rtsp://some/url?auth=g.0.streamingToken" @@ -209,6 +215,7 @@ async def test_camera_ws_stream(hass, auth, hass_ws_client): cam = hass.states.get("camera.my_camera") assert cam is not None assert cam.state == STATE_STREAMING + assert cam.attributes["frontend_stream_type"] == STREAM_TYPE_HLS with patch("homeassistant.components.camera.create_stream") as mock_stream: mock_stream().endpoint_url.return_value = "http://home.assistant/playlist.m3u8" @@ -695,6 +702,7 @@ async def test_camera_web_rtc(hass, auth, hass_ws_client): cam = hass.states.get("camera.my_camera") assert cam is not None assert cam.state == STATE_STREAMING + assert cam.attributes["frontend_stream_type"] == STREAM_TYPE_WEB_RTC client = await hass_ws_client(hass) await client.send_json( @@ -725,6 +733,7 @@ async def test_camera_web_rtc_unsupported(hass, auth, hass_ws_client): cam = hass.states.get("camera.my_camera") assert cam is not None assert cam.state == STATE_STREAMING + assert cam.attributes["frontend_stream_type"] == STREAM_TYPE_HLS client = await hass_ws_client(hass) await client.send_json( @@ -786,3 +795,68 @@ async def test_camera_web_rtc_offer_failure(hass, auth, hass_ws_client): assert not msg["success"] assert msg["error"]["code"] == "web_rtc_offer_failed" assert msg["error"]["message"].startswith("Nest API error") + + +async def test_camera_multiple_streams(hass, auth, hass_ws_client): + """Test a camera supporting multiple stream types.""" + expiration = utcnow() + datetime.timedelta(seconds=100) + auth.responses = [ + # RTSP response + make_stream_url_response(), + # WebRTC response + aiohttp.web.json_response( + { + "results": { + "answerSdp": "v=0\r\ns=-\r\n", + "mediaSessionId": "yP2grqz0Y1V_wgiX9KEbMWHoLd...", + "expiresAt": expiration.isoformat(timespec="seconds"), + }, + } + ), + ] + device_traits = { + "sdm.devices.traits.Info": { + "customName": "My Camera", + }, + "sdm.devices.traits.CameraLiveStream": { + "maxVideoResolution": { + "width": 640, + "height": 480, + }, + "videoCodecs": ["H264"], + "audioCodecs": ["AAC"], + "supportedProtocols": ["WEB_RTC", "RTSP"], + }, + } + await async_setup_camera(hass, device_traits, auth=auth) + + assert len(hass.states.async_all()) == 1 + cam = hass.states.get("camera.my_camera") + assert cam is not None + assert cam.state == STATE_STREAMING + # Prefer WebRTC over RTSP/HLS + assert cam.attributes["frontend_stream_type"] == STREAM_TYPE_WEB_RTC + + # RTSP stream + stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") + assert stream_source == "rtsp://some/url?auth=g.0.streamingToken" + + image = await async_get_image(hass) + assert image.content == IMAGE_BYTES_FROM_STREAM + + # WebRTC stream + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 5, + "type": "camera/web_rtc_offer", + "entity_id": "camera.my_camera", + "offer": "a=recvonly", + } + ) + + msg = await client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + assert msg["result"]["answer"] == "v=0\r\ns=-\r\n" From b49b9759999e35bd262f364044e775d88a3b385b Mon Sep 17 00:00:00 2001 From: drinfernoo <2319508+drinfernoo@users.noreply.github.com> Date: Fri, 22 Oct 2021 14:50:32 -0700 Subject: [PATCH 0713/1038] Allow different voices in Watson TTS calls (#56811) --- homeassistant/components/watson_tts/tts.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py index 610ad61132e..b02235d4d45 100644 --- a/homeassistant/components/watson_tts/tts.py +++ b/homeassistant/components/watson_tts/tts.py @@ -191,7 +191,9 @@ class WatsonTTSProvider(Provider): def get_tts_audio(self, message, language=None, options=None): """Request TTS file from Watson TTS.""" response = self.service.synthesize( - text=message, accept=self.output_format, voice=self.default_voice + text=message, + accept=self.output_format, + voice=options.get(CONF_VOICE, self.default_voice), ).get_result() return (CONTENT_TYPE_EXTENSIONS[self.output_format], response.content) From 77120a5137eecddd2ed57203d1a4cd729e0166d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 23 Oct 2021 00:54:36 +0300 Subject: [PATCH 0714/1038] Use http.HTTPStatus in components/m* (#58251) --- .../components/media_player/__init__.py | 11 ++--- .../components/melcloud/config_flow.py | 11 ++--- homeassistant/components/mobile_app/notify.py | 15 +++---- .../components/mystrom/binary_sensor.py | 7 ++- tests/components/mailbox/test_init.py | 20 ++++----- .../media_source/test_local_source.py | 16 ++++--- tests/components/melcloud/test_config_flow.py | 8 ++-- .../components/meraki/test_device_tracker.py | 17 ++++---- tests/components/mobile_app/conftest.py | 8 ++-- .../mobile_app/test_binary_sensor.py | 20 +++++---- .../mobile_app/test_device_tracker.py | 8 ++-- tests/components/mobile_app/test_http_api.py | 7 +-- tests/components/mobile_app/test_sensor.py | 20 +++++---- tests/components/mobile_app/test_webhook.py | 43 ++++++++++--------- tests/components/motioneye/test_web_hooks.py | 17 +++----- tests/components/mqtt/test_camera.py | 3 +- 16 files changed, 119 insertions(+), 112 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 8cf271d09cf..4107b82f723 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -31,9 +31,6 @@ from homeassistant.components.websocket_api.const import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - HTTP_NOT_FOUND, - HTTP_OK, - HTTP_UNAUTHORIZED, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, @@ -978,7 +975,7 @@ class MediaPlayerEntity(Entity): websession = async_get_clientsession(self.hass) with suppress(asyncio.TimeoutError), async_timeout.timeout(10): response = await websession.get(url) - if response.status == HTTP_OK: + if response.status == HTTPStatus.OK: content = await response.read() if content_type := response.headers.get(CONTENT_TYPE): content_type = content_type.split(";")[0] @@ -1031,7 +1028,11 @@ class MediaPlayerImageView(HomeAssistantView): """Start a get request.""" player = self.component.get_entity(entity_id) if player is None: - status = HTTP_NOT_FOUND if request[KEY_AUTHENTICATED] else HTTP_UNAUTHORIZED + status = ( + HTTPStatus.NOT_FOUND + if request[KEY_AUTHENTICATED] + else HTTPStatus.UNAUTHORIZED + ) return web.Response(status=status) authenticated = ( diff --git a/homeassistant/components/melcloud/config_flow.py b/homeassistant/components/melcloud/config_flow.py index a04364ea20f..9c15f5ec242 100644 --- a/homeassistant/components/melcloud/config_flow.py +++ b/homeassistant/components/melcloud/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from http import HTTPStatus from aiohttp import ClientError, ClientResponseError from async_timeout import timeout @@ -9,13 +10,7 @@ import pymelcloud import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import ( - CONF_PASSWORD, - CONF_TOKEN, - CONF_USERNAME, - HTTP_FORBIDDEN, - HTTP_UNAUTHORIZED, -) +from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME from .const import DOMAIN @@ -59,7 +54,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.hass.helpers.aiohttp_client.async_get_clientsession(), ) except ClientResponseError as err: - if err.status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN): + if err.status in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN): return self.async_abort(reason="invalid_auth") return self.async_abort(reason="cannot_connect") except (asyncio.TimeoutError, ClientError): diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py index 21f92d73020..bd5f1354ad3 100644 --- a/homeassistant/components/mobile_app/notify.py +++ b/homeassistant/components/mobile_app/notify.py @@ -1,6 +1,7 @@ """Support for mobile_app push notifications.""" import asyncio from functools import partial +from http import HTTPStatus import logging import aiohttp @@ -14,12 +15,6 @@ from homeassistant.components.notify import ( ATTR_TITLE_DEFAULT, BaseNotificationService, ) -from homeassistant.const import ( - HTTP_ACCEPTED, - HTTP_CREATED, - HTTP_OK, - HTTP_TOO_MANY_REQUESTS, -) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.util.dt as dt_util @@ -160,7 +155,11 @@ class MobileAppNotificationService(BaseNotificationService): ) result = await response.json() - if response.status in (HTTP_OK, HTTP_CREATED, HTTP_ACCEPTED): + if response.status in ( + HTTPStatus.OK, + HTTPStatus.CREATED, + HTTPStatus.ACCEPTED, + ): log_rate_limits(self.hass, entry_data[ATTR_DEVICE_NAME], result) return @@ -175,7 +174,7 @@ class MobileAppNotificationService(BaseNotificationService): message += "." message += " This message is generated externally to Home Assistant." - if response.status == HTTP_TOO_MANY_REQUESTS: + if response.status == HTTPStatus.TOO_MANY_REQUESTS: _LOGGER.warning(message) log_rate_limits( self.hass, entry_data[ATTR_DEVICE_NAME], result, logging.WARNING diff --git a/homeassistant/components/mystrom/binary_sensor.py b/homeassistant/components/mystrom/binary_sensor.py index 87c1a3a2665..c33d6bd62b5 100644 --- a/homeassistant/components/mystrom/binary_sensor.py +++ b/homeassistant/components/mystrom/binary_sensor.py @@ -1,9 +1,9 @@ """Support for the myStrom buttons.""" +from http import HTTPStatus import logging from homeassistant.components.binary_sensor import DOMAIN, BinarySensorEntity from homeassistant.components.http import HomeAssistantView -from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY from homeassistant.core import callback _LOGGER = logging.getLogger(__name__) @@ -42,7 +42,10 @@ class MyStromView(HomeAssistantView): if button_action is None: _LOGGER.error("Received unidentified message from myStrom button: %s", data) - return (f"Received unidentified message: {data}", HTTP_UNPROCESSABLE_ENTITY) + return ( + f"Received unidentified message: {data}", + HTTPStatus.UNPROCESSABLE_ENTITY, + ) button_id = data[button_action] entity_id = f"{DOMAIN}.{button_id}_{button_action}" diff --git a/tests/components/mailbox/test_init.py b/tests/components/mailbox/test_init.py index 8f75085f9ee..c6418911445 100644 --- a/tests/components/mailbox/test_init.py +++ b/tests/components/mailbox/test_init.py @@ -1,11 +1,11 @@ """The tests for the mailbox component.""" from hashlib import sha1 +from http import HTTPStatus import pytest from homeassistant.bootstrap import async_setup_component import homeassistant.components.mailbox as mailbox -from homeassistant.const import HTTP_INTERNAL_SERVER_ERROR, HTTP_NOT_FOUND @pytest.fixture @@ -21,7 +21,7 @@ async def test_get_platforms_from_mailbox(mock_http_client): url = "/api/mailbox/platforms" req = await mock_http_client.get(url) - assert req.status == 200 + assert req.status == HTTPStatus.OK result = await req.json() assert len(result) == 1 assert result[0].get("name") == "DemoMailbox" @@ -32,7 +32,7 @@ async def test_get_messages_from_mailbox(mock_http_client): url = "/api/mailbox/messages/DemoMailbox" req = await mock_http_client.get(url) - assert req.status == 200 + assert req.status == HTTPStatus.OK result = await req.json() assert len(result) == 10 @@ -45,7 +45,7 @@ async def test_get_media_from_mailbox(mock_http_client): url = f"/api/mailbox/media/DemoMailbox/{msgsha}" req = await mock_http_client.get(url) - assert req.status == 200 + assert req.status == HTTPStatus.OK data = await req.read() assert sha1(data).hexdigest() == mp3sha @@ -60,11 +60,11 @@ async def test_delete_from_mailbox(mock_http_client): for msg in [msgsha1, msgsha2]: url = f"/api/mailbox/delete/DemoMailbox/{msg}" req = await mock_http_client.delete(url) - assert req.status == 200 + assert req.status == HTTPStatus.OK url = "/api/mailbox/messages/DemoMailbox" req = await mock_http_client.get(url) - assert req.status == 200 + assert req.status == HTTPStatus.OK result = await req.json() assert len(result) == 8 @@ -74,7 +74,7 @@ async def test_get_messages_from_invalid_mailbox(mock_http_client): url = "/api/mailbox/messages/mailbox.invalid_mailbox" req = await mock_http_client.get(url) - assert req.status == HTTP_NOT_FOUND + assert req.status == HTTPStatus.NOT_FOUND async def test_get_media_from_invalid_mailbox(mock_http_client): @@ -83,7 +83,7 @@ async def test_get_media_from_invalid_mailbox(mock_http_client): url = f"/api/mailbox/media/mailbox.invalid_mailbox/{msgsha}" req = await mock_http_client.get(url) - assert req.status == HTTP_NOT_FOUND + assert req.status == HTTPStatus.NOT_FOUND async def test_get_media_from_invalid_msgid(mock_http_client): @@ -92,7 +92,7 @@ async def test_get_media_from_invalid_msgid(mock_http_client): url = f"/api/mailbox/media/DemoMailbox/{msgsha}" req = await mock_http_client.get(url) - assert req.status == HTTP_INTERNAL_SERVER_ERROR + assert req.status == HTTPStatus.INTERNAL_SERVER_ERROR async def test_delete_from_invalid_mailbox(mock_http_client): @@ -101,4 +101,4 @@ async def test_delete_from_invalid_mailbox(mock_http_client): url = f"/api/mailbox/delete/mailbox.invalid_mailbox/{msgsha}" req = await mock_http_client.delete(url) - assert req.status == HTTP_NOT_FOUND + assert req.status == HTTPStatus.NOT_FOUND diff --git a/tests/components/media_source/test_local_source.py b/tests/components/media_source/test_local_source.py index aff4f92be02..8a9005d7a86 100644 --- a/tests/components/media_source/test_local_source.py +++ b/tests/components/media_source/test_local_source.py @@ -1,4 +1,6 @@ """Test Local Media Source.""" +from http import HTTPStatus + import pytest from homeassistant.components import media_source @@ -78,25 +80,25 @@ async def test_media_view(hass, hass_client): # Protects against non-existent files resp = await client.get("/media/local/invalid.txt") - assert resp.status == 404 + assert resp.status == HTTPStatus.NOT_FOUND resp = await client.get("/media/recordings/invalid.txt") - assert resp.status == 404 + assert resp.status == HTTPStatus.NOT_FOUND # Protects against non-media files resp = await client.get("/media/local/not_media.txt") - assert resp.status == 404 + assert resp.status == HTTPStatus.NOT_FOUND # Protects against unknown local media sources resp = await client.get("/media/unknown_source/not_media.txt") - assert resp.status == 404 + assert resp.status == HTTPStatus.NOT_FOUND # Fetch available media resp = await client.get("/media/local/test.mp3") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK resp = await client.get("/media/local/Epic Sax Guy 10 Hours.mp4") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK resp = await client.get("/media/recordings/test.mp3") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK diff --git a/tests/components/melcloud/test_config_flow.py b/tests/components/melcloud/test_config_flow.py index dbf7a455791..0c4a91a6590 100644 --- a/tests/components/melcloud/test_config_flow.py +++ b/tests/components/melcloud/test_config_flow.py @@ -1,5 +1,6 @@ """Test the MELCloud config flow.""" import asyncio +from http import HTTPStatus from unittest.mock import patch from aiohttp import ClientError, ClientResponseError @@ -8,7 +9,6 @@ import pytest from homeassistant import config_entries from homeassistant.components.melcloud.const import DOMAIN -from homeassistant.const import HTTP_FORBIDDEN, HTTP_INTERNAL_SERVER_ERROR from tests.common import MockConfigEntry @@ -95,9 +95,9 @@ async def test_form_errors(hass, mock_login, mock_get_devices, error, reason): @pytest.mark.parametrize( "error,message", [ - (401, "invalid_auth"), - (HTTP_FORBIDDEN, "invalid_auth"), - (HTTP_INTERNAL_SERVER_ERROR, "cannot_connect"), + (HTTPStatus.UNAUTHORIZED, "invalid_auth"), + (HTTPStatus.FORBIDDEN, "invalid_auth"), + (HTTPStatus.INTERNAL_SERVER_ERROR, "cannot_connect"), ], ) async def test_form_response_errors( diff --git a/tests/components/meraki/test_device_tracker.py b/tests/components/meraki/test_device_tracker.py index 1c22b24411a..47265444105 100644 --- a/tests/components/meraki/test_device_tracker.py +++ b/tests/components/meraki/test_device_tracker.py @@ -1,4 +1,5 @@ """The tests the for Meraki device tracker.""" +from http import HTTPStatus import json import pytest @@ -37,35 +38,35 @@ async def test_invalid_or_missing_data(mock_device_tracker_conf, meraki_client): """Test validator with invalid or missing data.""" req = await meraki_client.get(URL) text = await req.text() - assert req.status == 200 + assert req.status == HTTPStatus.OK assert text == "validator" req = await meraki_client.post(URL, data=b"invalid") text = await req.json() - assert req.status == 400 + assert req.status == HTTPStatus.BAD_REQUEST assert text["message"] == "Invalid JSON" req = await meraki_client.post(URL, data=b"{}") text = await req.json() - assert req.status == 422 + assert req.status == HTTPStatus.UNPROCESSABLE_ENTITY assert text["message"] == "No secret" data = {"version": "1.0", "secret": "secret"} req = await meraki_client.post(URL, data=json.dumps(data)) text = await req.json() - assert req.status == 422 + assert req.status == HTTPStatus.UNPROCESSABLE_ENTITY assert text["message"] == "Invalid version" data = {"version": "2.0", "secret": "invalid"} req = await meraki_client.post(URL, data=json.dumps(data)) text = await req.json() - assert req.status == 422 + assert req.status == HTTPStatus.UNPROCESSABLE_ENTITY assert text["message"] == "Invalid secret" data = {"version": "2.0", "secret": "secret", "type": "InvalidType"} req = await meraki_client.post(URL, data=json.dumps(data)) text = await req.json() - assert req.status == 422 + assert req.status == HTTPStatus.UNPROCESSABLE_ENTITY assert text["message"] == "Invalid device type" data = { @@ -75,7 +76,7 @@ async def test_invalid_or_missing_data(mock_device_tracker_conf, meraki_client): "data": {"observations": []}, } req = await meraki_client.post(URL, data=json.dumps(data)) - assert req.status == 200 + assert req.status == HTTPStatus.OK async def test_data_will_be_saved(mock_device_tracker_conf, hass, meraki_client): @@ -120,7 +121,7 @@ async def test_data_will_be_saved(mock_device_tracker_conf, hass, meraki_client) }, } req = await meraki_client.post(URL, data=json.dumps(data)) - assert req.status == 200 + assert req.status == HTTPStatus.OK await hass.async_block_till_done() state_name = hass.states.get( "{}.{}".format("device_tracker", "00_26_ab_b8_a9_a4") diff --git a/tests/components/mobile_app/conftest.py b/tests/components/mobile_app/conftest.py index db4843c126a..18f425d13c0 100644 --- a/tests/components/mobile_app/conftest.py +++ b/tests/components/mobile_app/conftest.py @@ -1,4 +1,6 @@ """Tests for mobile_app component.""" +from http import HTTPStatus + # pylint: disable=redefined-outer-name,unused-import import pytest @@ -17,14 +19,14 @@ async def create_registrations(hass, authed_api_client): "/api/mobile_app/registrations", json=REGISTER ) - assert enc_reg.status == 201 + assert enc_reg.status == HTTPStatus.CREATED enc_reg_json = await enc_reg.json() clear_reg = await authed_api_client.post( "/api/mobile_app/registrations", json=REGISTER_CLEARTEXT ) - assert clear_reg.status == 201 + assert clear_reg.status == HTTPStatus.CREATED clear_reg_json = await clear_reg.json() await hass.async_block_till_done() @@ -48,7 +50,7 @@ async def push_registration(hass, authed_api_client): }, ) - assert enc_reg.status == 201 + assert enc_reg.status == HTTPStatus.CREATED return await enc_reg.json() diff --git a/tests/components/mobile_app/test_binary_sensor.py b/tests/components/mobile_app/test_binary_sensor.py index 7965bf472cb..e379603e079 100644 --- a/tests/components/mobile_app/test_binary_sensor.py +++ b/tests/components/mobile_app/test_binary_sensor.py @@ -1,4 +1,6 @@ """Entity tests for mobile_app.""" +from http import HTTPStatus + from homeassistant.const import STATE_OFF from homeassistant.helpers import device_registry as dr @@ -24,7 +26,7 @@ async def test_sensor(hass, create_registrations, webhook_client): }, ) - assert reg_resp.status == 201 + assert reg_resp.status == HTTPStatus.CREATED json = await reg_resp.json() assert json == {"success": True} @@ -61,7 +63,7 @@ async def test_sensor(hass, create_registrations, webhook_client): }, ) - assert update_resp.status == 200 + assert update_resp.status == HTTPStatus.OK json = await update_resp.json() assert json["invalid_state"]["success"] is False @@ -101,7 +103,7 @@ async def test_sensor_must_register(hass, create_registrations, webhook_client): }, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK json = await resp.json() assert json["battery_state"]["success"] is False @@ -128,7 +130,7 @@ async def test_sensor_id_no_dupes(hass, create_registrations, webhook_client, ca reg_resp = await webhook_client.post(webhook_url, json=payload) - assert reg_resp.status == 201 + assert reg_resp.status == HTTPStatus.CREATED reg_json = await reg_resp.json() assert reg_json == {"success": True} @@ -149,7 +151,7 @@ async def test_sensor_id_no_dupes(hass, create_registrations, webhook_client, ca payload["data"]["state"] = False dupe_resp = await webhook_client.post(webhook_url, json=payload) - assert dupe_resp.status == 201 + assert dupe_resp.status == HTTPStatus.CREATED dupe_reg_json = await dupe_resp.json() assert dupe_reg_json == {"success": True} await hass.async_block_till_done() @@ -185,7 +187,7 @@ async def test_register_sensor_no_state(hass, create_registrations, webhook_clie }, ) - assert reg_resp.status == 201 + assert reg_resp.status == HTTPStatus.CREATED json = await reg_resp.json() assert json == {"success": True} @@ -210,7 +212,7 @@ async def test_register_sensor_no_state(hass, create_registrations, webhook_clie }, ) - assert reg_resp.status == 201 + assert reg_resp.status == HTTPStatus.CREATED json = await reg_resp.json() assert json == {"success": True} @@ -242,7 +244,7 @@ async def test_update_sensor_no_state(hass, create_registrations, webhook_client }, ) - assert reg_resp.status == 201 + assert reg_resp.status == HTTPStatus.CREATED json = await reg_resp.json() assert json == {"success": True} @@ -262,7 +264,7 @@ async def test_update_sensor_no_state(hass, create_registrations, webhook_client }, ) - assert update_resp.status == 200 + assert update_resp.status == HTTPStatus.OK json = await update_resp.json() assert json == {"is_charging": {"success": True}} diff --git a/tests/components/mobile_app/test_device_tracker.py b/tests/components/mobile_app/test_device_tracker.py index b755a0a8d09..4942144baa5 100644 --- a/tests/components/mobile_app/test_device_tracker.py +++ b/tests/components/mobile_app/test_device_tracker.py @@ -1,5 +1,7 @@ """Test mobile app device tracker.""" +from http import HTTPStatus + async def test_sending_location(hass, create_registrations, webhook_client): """Test sending a location via a webhook.""" @@ -20,7 +22,7 @@ async def test_sending_location(hass, create_registrations, webhook_client): }, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK await hass.async_block_till_done() state = hass.states.get("device_tracker.test_1_2") assert state is not None @@ -53,7 +55,7 @@ async def test_sending_location(hass, create_registrations, webhook_client): }, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK await hass.async_block_till_done() state = hass.states.get("device_tracker.test_1_2") assert state is not None @@ -87,7 +89,7 @@ async def test_restoring_location(hass, create_registrations, webhook_client): }, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK await hass.async_block_till_done() state_1 = hass.states.get("device_tracker.test_1_2") assert state_1 is not None diff --git a/tests/components/mobile_app/test_http_api.py b/tests/components/mobile_app/test_http_api.py index 456f3fab261..5d92418bba2 100644 --- a/tests/components/mobile_app/test_http_api.py +++ b/tests/components/mobile_app/test_http_api.py @@ -1,4 +1,5 @@ """Tests for the mobile_app HTTP API.""" +from http import HTTPStatus import json from unittest.mock import patch @@ -32,7 +33,7 @@ async def test_registration(hass, hass_client, hass_admin_user): assert add_user_dev_track.mock_calls[0][1][1] == hass_admin_user.id assert add_user_dev_track.mock_calls[0][1][2] == "device_tracker.test_1" - assert resp.status == 201 + assert resp.status == HTTPStatus.CREATED register_json = await resp.json() assert CONF_WEBHOOK_ID in register_json assert CONF_SECRET in register_json @@ -71,7 +72,7 @@ async def test_registration_encryption(hass, hass_client): resp = await api_client.post("/api/mobile_app/registrations", json=REGISTER) - assert resp.status == 201 + assert resp.status == HTTPStatus.CREATED register_json = await resp.json() keylen = SecretBox.KEY_SIZE @@ -89,7 +90,7 @@ async def test_registration_encryption(hass, hass_client): f"/api/webhook/{register_json[CONF_WEBHOOK_ID]}", json=container ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK webhook_json = await resp.json() assert "encrypted_data" in webhook_json diff --git a/tests/components/mobile_app/test_sensor.py b/tests/components/mobile_app/test_sensor.py index fea43ffba9e..032870ffb8c 100644 --- a/tests/components/mobile_app/test_sensor.py +++ b/tests/components/mobile_app/test_sensor.py @@ -1,4 +1,6 @@ """Entity tests for mobile_app.""" +from http import HTTPStatus + from homeassistant.const import PERCENTAGE, STATE_UNKNOWN from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -27,7 +29,7 @@ async def test_sensor(hass, create_registrations, webhook_client): }, ) - assert reg_resp.status == 201 + assert reg_resp.status == HTTPStatus.CREATED json = await reg_resp.json() assert json == {"success": True} @@ -67,7 +69,7 @@ async def test_sensor(hass, create_registrations, webhook_client): }, ) - assert update_resp.status == 200 + assert update_resp.status == HTTPStatus.OK json = await update_resp.json() assert json["invalid_state"]["success"] is False @@ -105,7 +107,7 @@ async def test_sensor_must_register(hass, create_registrations, webhook_client): }, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK json = await resp.json() assert json["battery_state"]["success"] is False @@ -133,7 +135,7 @@ async def test_sensor_id_no_dupes(hass, create_registrations, webhook_client, ca reg_resp = await webhook_client.post(webhook_url, json=payload) - assert reg_resp.status == 201 + assert reg_resp.status == HTTPStatus.CREATED reg_json = await reg_resp.json() assert reg_json == {"success": True} @@ -155,7 +157,7 @@ async def test_sensor_id_no_dupes(hass, create_registrations, webhook_client, ca payload["data"]["state"] = 99 dupe_resp = await webhook_client.post(webhook_url, json=payload) - assert dupe_resp.status == 201 + assert dupe_resp.status == HTTPStatus.CREATED dupe_reg_json = await dupe_resp.json() assert dupe_reg_json == {"success": True} await hass.async_block_till_done() @@ -192,7 +194,7 @@ async def test_register_sensor_no_state(hass, create_registrations, webhook_clie }, ) - assert reg_resp.status == 201 + assert reg_resp.status == HTTPStatus.CREATED json = await reg_resp.json() assert json == {"success": True} @@ -217,7 +219,7 @@ async def test_register_sensor_no_state(hass, create_registrations, webhook_clie }, ) - assert reg_resp.status == 201 + assert reg_resp.status == HTTPStatus.CREATED json = await reg_resp.json() assert json == {"success": True} @@ -249,7 +251,7 @@ async def test_update_sensor_no_state(hass, create_registrations, webhook_client }, ) - assert reg_resp.status == 201 + assert reg_resp.status == HTTPStatus.CREATED json = await reg_resp.json() assert json == {"success": True} @@ -267,7 +269,7 @@ async def test_update_sensor_no_state(hass, create_registrations, webhook_client }, ) - assert update_resp.status == 200 + assert update_resp.status == HTTPStatus.OK json = await update_resp.json() assert json == {"battery_state": {"success": True}} diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 8dc2086c495..623abf30e9e 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -1,4 +1,5 @@ """Webhook tests for mobile_app.""" +from http import HTTPStatus from unittest.mock import patch import pytest @@ -76,7 +77,7 @@ async def test_webhook_handle_render_template(create_registrations, webhook_clie }, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK json = await resp.json() assert json == { @@ -97,7 +98,7 @@ async def test_webhook_handle_call_services(hass, create_registrations, webhook_ json=CALL_SERVICE, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert len(calls) == 1 @@ -117,7 +118,7 @@ async def test_webhook_handle_fire_event(hass, create_registrations, webhook_cli "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), json=FIRE_EVENT ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK json = await resp.json() assert json == {} @@ -131,7 +132,7 @@ async def test_webhook_update_registration(webhook_client, authed_api_client): "/api/mobile_app/registrations", json=REGISTER_CLEARTEXT ) - assert register_resp.status == 201 + assert register_resp.status == HTTPStatus.CREATED register_json = await register_resp.json() webhook_id = register_json[CONF_WEBHOOK_ID] @@ -142,7 +143,7 @@ async def test_webhook_update_registration(webhook_client, authed_api_client): f"/api/webhook/{webhook_id}", json=update_container ) - assert update_resp.status == 200 + assert update_resp.status == HTTPStatus.OK update_json = await update_resp.json() assert update_json["app_version"] == "2.0.0" assert CONF_WEBHOOK_ID not in update_json @@ -180,7 +181,7 @@ async def test_webhook_handle_get_zones(hass, create_registrations, webhook_clie json={"type": "get_zones"}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK json = await resp.json() assert len(json) == 3 @@ -206,7 +207,7 @@ async def test_webhook_handle_get_config(hass, create_registrations, webhook_cli json={"type": "get_config"}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK json = await resp.json() if "components" in json: @@ -239,7 +240,7 @@ async def test_webhook_returns_error_incorrect_json( "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), data="not json" ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST json = await resp.json() assert json == {} assert "invalid JSON" in caplog.text @@ -256,7 +257,7 @@ async def test_webhook_handle_decryption(webhook_client, create_registrations): "/api/webhook/{}".format(create_registrations[0]["webhook_id"]), json=container ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK webhook_json = await resp.json() assert "encrypted_data" in webhook_json @@ -273,7 +274,7 @@ async def test_webhook_requires_encryption(webhook_client, create_registrations) json=RENDER_TEMPLATE, ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST webhook_json = await resp.json() assert "error" in webhook_json @@ -291,7 +292,7 @@ async def test_webhook_update_location(hass, webhook_client, create_registration }, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK state = hass.states.get("device_tracker.test_1_2") assert state is not None @@ -310,7 +311,7 @@ async def test_webhook_enable_encryption(hass, webhook_client, create_registrati json={"type": "enable_encryption"}, ) - assert enable_enc_resp.status == 200 + assert enable_enc_resp.status == HTTPStatus.OK enable_enc_json = await enable_enc_resp.json() assert len(enable_enc_json) == 1 @@ -323,7 +324,7 @@ async def test_webhook_enable_encryption(hass, webhook_client, create_registrati json=RENDER_TEMPLATE, ) - assert enc_required_resp.status == 400 + assert enc_required_resp.status == HTTPStatus.BAD_REQUEST enc_required_json = await enc_required_resp.json() assert "error" in enc_required_json @@ -340,7 +341,7 @@ async def test_webhook_enable_encryption(hass, webhook_client, create_registrati enc_resp = await webhook_client.post(f"/api/webhook/{webhook_id}", json=container) - assert enc_resp.status == 200 + assert enc_resp.status == HTTPStatus.OK enc_json = await enc_resp.json() assert "encrypted_data" in enc_json @@ -364,7 +365,7 @@ async def test_webhook_camera_stream_non_existent( }, ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST webhook_json = await resp.json() assert webhook_json["success"] is False @@ -385,7 +386,7 @@ async def test_webhook_camera_stream_non_hls( }, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK webhook_json = await resp.json() assert webhook_json["hls_path"] is None assert ( @@ -416,7 +417,7 @@ async def test_webhook_camera_stream_stream_available( }, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK webhook_json = await resp.json() assert webhook_json["hls_path"] == "/api/streams/some_hls_stream" assert webhook_json["mjpeg_path"] == "/api/camera_proxy_stream/camera.stream_camera" @@ -444,7 +445,7 @@ async def test_webhook_camera_stream_stream_available_but_errors( }, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK webhook_json = await resp.json() assert webhook_json["hls_path"] is None assert webhook_json["mjpeg_path"] == "/api/camera_proxy_stream/camera.stream_camera" @@ -466,7 +467,7 @@ async def test_webhook_handle_scan_tag(hass, create_registrations, webhook_clien json={"type": "scan_tag", "data": {"tag_id": "mock-tag-id"}}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK json = await resp.json() assert json == {} @@ -496,7 +497,7 @@ async def test_register_sensor_limits_state_class( }, ) - assert reg_resp.status == 201 + assert reg_resp.status == HTTPStatus.CREATED reg_resp = await webhook_client.post( webhook_url, @@ -513,4 +514,4 @@ async def test_register_sensor_limits_state_class( ) # This means it was ignored. - assert reg_resp.status == 200 + assert reg_resp.status == HTTPStatus.OK diff --git a/tests/components/motioneye/test_web_hooks.py b/tests/components/motioneye/test_web_hooks.py index 55e7e31d9b9..cf4fe46c73a 100644 --- a/tests/components/motioneye/test_web_hooks.py +++ b/tests/components/motioneye/test_web_hooks.py @@ -1,5 +1,6 @@ """Test the motionEye camera web hooks.""" import copy +from http import HTTPStatus from typing import Any from unittest.mock import AsyncMock, call, patch @@ -22,13 +23,7 @@ from homeassistant.components.motioneye.const import ( EVENT_MOTION_DETECTED, ) from homeassistant.components.webhook import URL_WEBHOOK_PATH -from homeassistant.const import ( - ATTR_DEVICE_ID, - CONF_URL, - CONF_WEBHOOK_ID, - HTTP_BAD_REQUEST, - HTTP_OK, -) +from homeassistant.const import ATTR_DEVICE_ID, CONF_URL, CONF_WEBHOOK_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.helpers.network import NoURLAvailableError @@ -308,7 +303,7 @@ async def test_good_query(hass: HomeAssistant, hass_client_no_auth: Any) -> None ATTR_EVENT_TYPE: event, }, ) - assert resp.status == HTTP_OK + assert resp.status == HTTPStatus.OK assert len(events) == 1 assert events[0].data == { @@ -332,7 +327,7 @@ async def test_bad_query_missing_parameters( resp = await client.post( URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]), json={} ) - assert resp.status == HTTP_BAD_REQUEST + assert resp.status == HTTPStatus.BAD_REQUEST async def test_bad_query_no_such_device( @@ -351,7 +346,7 @@ async def test_bad_query_no_such_device( ATTR_DEVICE_ID: "not-a-real-device", }, ) - assert resp.status == HTTP_BAD_REQUEST + assert resp.status == HTTPStatus.BAD_REQUEST async def test_bad_query_cannot_decode( @@ -370,6 +365,6 @@ async def test_bad_query_cannot_decode( URL_WEBHOOK_PATH.format(webhook_id=config_entry.data[CONF_WEBHOOK_ID]), data=b"this is not json", ) - assert resp.status == HTTP_BAD_REQUEST + assert resp.status == HTTPStatus.BAD_REQUEST assert not motion_events assert not storage_events diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index ed5fa9e3927..2036b486f94 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -1,4 +1,5 @@ """The tests for mqtt camera component.""" +from http import HTTPStatus import json from unittest.mock import patch @@ -56,7 +57,7 @@ async def test_run_camera_setup(hass, hass_client_no_auth, mqtt_mock): client = await hass_client_no_auth() resp = await client.get(url) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK body = await resp.text() assert body == "beer" From 1b42caa34a3a5d384b5cf197c2d8b2ab28fb9247 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 23 Oct 2021 00:11:40 +0000 Subject: [PATCH 0715/1038] [ci skip] Translation update --- .../components/dlna_dmr/translations/bg.json | 11 ++++++- .../components/dlna_dmr/translations/en.json | 5 ++- .../components/hue/translations/ja.json | 12 ++++--- .../components/lookin/translations/et.json | 31 +++++++++++++++++++ .../components/lookin/translations/hu.json | 31 +++++++++++++++++++ .../components/lookin/translations/nl.json | 31 +++++++++++++++++++ .../lookin/translations/zh-Hant.json | 31 +++++++++++++++++++ .../components/octoprint/translations/bg.json | 22 +++++++++++++ .../components/octoprint/translations/ca.json | 29 +++++++++++++++++ .../components/octoprint/translations/et.json | 29 +++++++++++++++++ .../components/octoprint/translations/ru.json | 29 +++++++++++++++++ .../components/shelly/translations/nl.json | 2 +- .../tuya/translations/select.hu.json | 22 +++++++++++++ .../tuya/translations/select.nl.json | 20 ++++++++++++ .../components/withings/translations/bg.json | 1 + 15 files changed, 299 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/lookin/translations/et.json create mode 100644 homeassistant/components/lookin/translations/hu.json create mode 100644 homeassistant/components/lookin/translations/nl.json create mode 100644 homeassistant/components/lookin/translations/zh-Hant.json create mode 100644 homeassistant/components/octoprint/translations/bg.json create mode 100644 homeassistant/components/octoprint/translations/ca.json create mode 100644 homeassistant/components/octoprint/translations/et.json create mode 100644 homeassistant/components/octoprint/translations/ru.json create mode 100644 homeassistant/components/tuya/translations/select.hu.json create mode 100644 homeassistant/components/tuya/translations/select.nl.json diff --git a/homeassistant/components/dlna_dmr/translations/bg.json b/homeassistant/components/dlna_dmr/translations/bg.json index 0d6f344a263..00e64e1568d 100644 --- a/homeassistant/components/dlna_dmr/translations/bg.json +++ b/homeassistant/components/dlna_dmr/translations/bg.json @@ -2,11 +2,13 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "could_not_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 DLNA \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", "incomplete_config": "\u0412 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043b\u0438\u043f\u0441\u0432\u0430 \u0437\u0430\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u0430 \u043f\u0440\u043e\u043c\u0435\u043d\u043b\u0438\u0432\u0430", "non_unique_id": "\u041d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0441\u0430 \u043d\u044f\u043a\u043e\u043b\u043a\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u0435\u0434\u0438\u043d \u0438 \u0441\u044a\u0449 \u0443\u043d\u0438\u043a\u0430\u043b\u0435\u043d \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440" }, "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "could_not_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 DLNA \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" }, "flow_title": "{name}", @@ -14,10 +16,17 @@ "confirm": { "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0442\u0430?" }, - "user": { + "manual": { "data": { "url": "URL" } + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "url": "URL" + }, + "title": "\u041e\u0442\u043a\u0440\u0438\u0442\u0438 DLNA DMR \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" } } }, diff --git a/homeassistant/components/dlna_dmr/translations/en.json b/homeassistant/components/dlna_dmr/translations/en.json index 3cdec814178..6711a861344 100644 --- a/homeassistant/components/dlna_dmr/translations/en.json +++ b/homeassistant/components/dlna_dmr/translations/en.json @@ -4,6 +4,7 @@ "already_configured": "Device is already configured", "alternative_integration": "Device is better supported by another integration", "cannot_connect": "Failed to connect", + "could_not_connect": "Failed to connect to DLNA device", "discovery_error": "Failed to discover a matching DLNA device", "incomplete_config": "Configuration is missing a required variable", "non_unique_id": "Multiple devices found with the same unique ID", @@ -11,6 +12,7 @@ }, "error": { "cannot_connect": "Failed to connect", + "could_not_connect": "Failed to connect to DLNA device", "not_dmr": "Device is not a Digital Media Renderer" }, "flow_title": "{name}", @@ -30,7 +32,8 @@ }, "user": { "data": { - "host": "Host" + "host": "Host", + "url": "URL" }, "description": "Choose a device to configure or leave blank to enter a URL", "title": "Discovered DLNA DMR devices" diff --git a/homeassistant/components/hue/translations/ja.json b/homeassistant/components/hue/translations/ja.json index 0ce555c1f29..f51e0680c67 100644 --- a/homeassistant/components/hue/translations/ja.json +++ b/homeassistant/components/hue/translations/ja.json @@ -1,9 +1,10 @@ { "config": { "abort": { - "all_configured": "\u3059\u3079\u3066\u306e\u3001Philips Hue\u30d6\u30ea\u30c3\u30b8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "discover_timeout": "Hue\u30d6\u30ea\u30c3\u30b8\u3092\u767a\u898b(\u63a2\u308a\u5f53\u3066)\u3067\u304d\u307e\u305b\u3093", - "no_bridges": "Philips Hue\u30d6\u30ea\u30c3\u30b8\u306f\u767a\u898b\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f", + "all_configured": "\u3059\u3079\u3066\u306e\u3001Philips Hue bridge\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "discover_timeout": "Hue bridge\u3092\u767a\u898b(\u63a2\u308a\u5f53\u3066)\u3067\u304d\u307e\u305b\u3093", + "no_bridges": "Hue bridge\u306f\u767a\u898b\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f", + "not_hue_bridge": "Hue bridge\u3067\u306f\u3042\u308a\u307e\u305b\u3093", "unknown": "\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f" }, "error": { @@ -14,10 +15,13 @@ "data": { "host": "\u30db\u30b9\u30c8" }, - "title": "Philips Hue\u30d6\u30ea\u30c3\u30b8\u3092\u30d4\u30c3\u30af\u30a2\u30c3\u30d7" + "title": "Hue bridge\u3092\u30d4\u30c3\u30af\u30a2\u30c3\u30d7" }, "link": { "title": "\u30ea\u30f3\u30af\u30cf\u30d6" + }, + "manual": { + "title": "Hue bridges\u3092\u624b\u52d5\u3067\u8a2d\u5b9a\u3059\u308b" } } } diff --git a/homeassistant/components/lookin/translations/et.json b/homeassistant/components/lookin/translations/et.json new file mode 100644 index 00000000000..c932e28cdd6 --- /dev/null +++ b/homeassistant/components/lookin/translations/et.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "cannot_connect": "\u00dchendamine nurjus", + "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi seadet" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi seadet", + "unknown": "Ootamatu t\u00f5rge" + }, + "flow_title": "{name} ({host})", + "step": { + "device_name": { + "data": { + "name": "Nimi" + } + }, + "discovery_confirm": { + "description": "Kas soovid seadistada {name}({host})?" + }, + "user": { + "data": { + "ip_address": "IP aadress" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lookin/translations/hu.json b/homeassistant/components/lookin/translations/hu.json new file mode 100644 index 00000000000..4d9d53ee33d --- /dev/null +++ b/homeassistant/components/lookin/translations/hu.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "flow_title": "{name} ({host})", + "step": { + "device_name": { + "data": { + "name": "N\u00e9v" + } + }, + "discovery_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name} ({host})?" + }, + "user": { + "data": { + "ip_address": "IP c\u00edm" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lookin/translations/nl.json b/homeassistant/components/lookin/translations/nl.json new file mode 100644 index 00000000000..89f94de97ca --- /dev/null +++ b/homeassistant/components/lookin/translations/nl.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang", + "cannot_connect": "Kan geen verbinding maken", + "no_devices_found": "Geen apparaten gevonden op het netwerk" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "unknown": "Onverwachte fout" + }, + "flow_title": "{name} ({host})", + "step": { + "device_name": { + "data": { + "name": "Naam" + } + }, + "discovery_confirm": { + "description": "Wilt u {name} ({host}) instellen?" + }, + "user": { + "data": { + "ip_address": "IP-adres" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lookin/translations/zh-Hant.json b/homeassistant/components/lookin/translations/zh-Hant.json new file mode 100644 index 00000000000..8bffdc28f3a --- /dev/null +++ b/homeassistant/components/lookin/translations/zh-Hant.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "{name} ({host})", + "step": { + "device_name": { + "data": { + "name": "\u540d\u7a31" + } + }, + "discovery_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name} ({host})\uff1f" + }, + "user": { + "data": { + "ip_address": "IP \u4f4d\u5740" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/octoprint/translations/bg.json b/homeassistant/components/octoprint/translations/bg.json new file mode 100644 index 00000000000..670311552c9 --- /dev/null +++ b/homeassistant/components/octoprint/translations/bg.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041d\u043e\u043c\u0435\u0440 \u043d\u0430 \u043f\u043e\u0440\u0442", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/octoprint/translations/ca.json b/homeassistant/components/octoprint/translations/ca.json new file mode 100644 index 00000000000..5b21931e494 --- /dev/null +++ b/homeassistant/components/octoprint/translations/ca.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "auth_failed": "No s'ha pogut obtenir la clau API de l'aplicaci\u00f3", + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "flow_title": "Impressora OctoPrint: {host}", + "progress": { + "get_api_key": "Obre la interf\u00edcie d'usuari d'OctoPrint i clica a 'Permet' a la sol\u00b7licitud d'acc\u00e9s de 'Home Assistant'." + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "path": "Ruta d'aplicaci\u00f3", + "port": "N\u00famero de port", + "ssl": "Utilitza SSL", + "username": "Nom d'usuari" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/octoprint/translations/et.json b/homeassistant/components/octoprint/translations/et.json new file mode 100644 index 00000000000..df2117cd97c --- /dev/null +++ b/homeassistant/components/octoprint/translations/et.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "auth_failed": "Rakenduse API v\u00f5tme toomine nurjus", + "cannot_connect": "\u00dchendamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "flow_title": "OctoPrint printer: {host}", + "progress": { + "get_api_key": "Ava OctoPrinti kasutajaliides ja kl\u00f5psa Home Assistanti juurdep\u00e4\u00e4sutaotluses nuppu Luba." + }, + "step": { + "user": { + "data": { + "host": "Host", + "path": "Rakenduse tee", + "port": "Pordi number", + "ssl": "Kasuta SSL-i", + "username": "Kasutajanimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/octoprint/translations/ru.json b/homeassistant/components/octoprint/translations/ru.json new file mode 100644 index 00000000000..76f7c19923f --- /dev/null +++ b/homeassistant/components/octoprint/translations/ru.json @@ -0,0 +1,29 @@ +{ + "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.", + "auth_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "OctoPrint: {host}", + "progress": { + "get_api_key": "\u041e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u0432\u0435\u0431-\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 OctoPrint \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 '\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c' \u0432 \u0437\u0430\u043f\u0440\u043e\u0441\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0434\u043b\u044f 'Home Assistant'." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "path": "\u041f\u0443\u0442\u044c \u043a \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044e", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c SSL", + "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/shelly/translations/nl.json b/homeassistant/components/shelly/translations/nl.json index 0251e2e7267..2bc4c1df03e 100644 --- a/homeassistant/components/shelly/translations/nl.json +++ b/homeassistant/components/shelly/translations/nl.json @@ -42,7 +42,7 @@ "double": "{subtype} dubbel geklikt", "double_push": "{subtype} dubbele druk", "long": "{subtype} lang geklikt", - "long_push": " {subtype} lange druk", + "long_push": "{subtype} lange druk", "long_single": "{subtype} lang geklikt en daarna \u00e9\u00e9n keer geklikt", "single": "{subtype} enkel geklikt", "single_long": "{subtype} een keer geklikt en daarna lang geklikt", diff --git a/homeassistant/components/tuya/translations/select.hu.json b/homeassistant/components/tuya/translations/select.hu.json new file mode 100644 index 00000000000..0a76428404b --- /dev/null +++ b/homeassistant/components/tuya/translations/select.hu.json @@ -0,0 +1,22 @@ +{ + "state": { + "tuya__led_type": { + "halogen": "Halog\u00e9n", + "incandescent": "Izz\u00f3", + "led": "LED" + }, + "tuya__light_mode": { + "none": "Ki", + "pos": "A kapcsol\u00f3 hely\u00e9nek jelz\u00e9se", + "relay": "Be-/kikapcsolt \u00e1llapot jelz\u00e9se" + }, + "tuya__relay_status": { + "last": "Utols\u00f3 \u00e1llapot megjegyz\u00e9se", + "memory": "Utols\u00f3 \u00e1llapot megjegyz\u00e9se", + "off": "Ki", + "on": "Be", + "power_off": "Ki", + "power_on": "Be" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.nl.json b/homeassistant/components/tuya/translations/select.nl.json new file mode 100644 index 00000000000..479a6d5e7d3 --- /dev/null +++ b/homeassistant/components/tuya/translations/select.nl.json @@ -0,0 +1,20 @@ +{ + "state": { + "tuya__led_type": { + "halogen": "Halogeen", + "led": "LED" + }, + "tuya__light_mode": { + "none": "Uit", + "relay": "Aan/uit-toestand aangeven" + }, + "tuya__relay_status": { + "last": "Onthoud laatste staat", + "memory": "Onthoud laatste staat", + "off": "Uit", + "on": "Aan", + "power_off": "Uit", + "power_on": "Aan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/bg.json b/homeassistant/components/withings/translations/bg.json index 959b2b99066..d0445aed41f 100644 --- a/homeassistant/components/withings/translations/bg.json +++ b/homeassistant/components/withings/translations/bg.json @@ -6,6 +6,7 @@ "error": { "already_configured": "\u041f\u0440\u043e\u0444\u0438\u043b\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" }, + "flow_title": "{profile}", "step": { "profile": { "data": { From b2d67f8d19e9ecfbd611b526895c1d6234ba2f41 Mon Sep 17 00:00:00 2001 From: some-guy-in-oz <85833124+some-guy-in-oz@users.noreply.github.com> Date: Sat, 23 Oct 2021 10:55:25 +1030 Subject: [PATCH 0716/1038] Add grid services active sensor to telsa powerwall integration (#56317) Co-authored-by: Franck Nijhof --- .../components/powerwall/__init__.py | 2 ++ .../components/powerwall/binary_sensor.py | 26 +++++++++++++++++++ homeassistant/components/powerwall/const.py | 1 + tests/components/powerwall/mocks.py | 3 +++ .../powerwall/test_binary_sensor.py | 10 +++++++ 5 files changed, 42 insertions(+) diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index 6220f81e9a3..8b06b2a9c6d 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -23,6 +23,7 @@ from .const import ( POWERWALL_API_CHANGED, POWERWALL_API_CHARGE, POWERWALL_API_DEVICE_TYPE, + POWERWALL_API_GRID_SERVICES_ACTIVE, POWERWALL_API_GRID_STATUS, POWERWALL_API_METERS, POWERWALL_API_SERIAL_NUMBERS, @@ -225,6 +226,7 @@ def _fetch_powerwall_data(power_wall): POWERWALL_API_CHARGE: power_wall.get_charge(), POWERWALL_API_SITEMASTER: power_wall.get_sitemaster(), POWERWALL_API_METERS: power_wall.get_meters(), + POWERWALL_API_GRID_SERVICES_ACTIVE: power_wall.is_grid_services_active(), POWERWALL_API_GRID_STATUS: power_wall.get_grid_status(), } diff --git a/homeassistant/components/powerwall/binary_sensor.py b/homeassistant/components/powerwall/binary_sensor.py index 1b097b93408..b4104b70f39 100644 --- a/homeassistant/components/powerwall/binary_sensor.py +++ b/homeassistant/components/powerwall/binary_sensor.py @@ -11,6 +11,7 @@ from homeassistant.const import DEVICE_CLASS_POWER from .const import ( DOMAIN, POWERWALL_API_DEVICE_TYPE, + POWERWALL_API_GRID_SERVICES_ACTIVE, POWERWALL_API_GRID_STATUS, POWERWALL_API_METERS, POWERWALL_API_SERIAL_NUMBERS, @@ -35,6 +36,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] for sensor_class in ( PowerWallRunningSensor, + PowerWallGridServicesActiveSensor, PowerWallGridStatusSensor, PowerWallConnectedSensor, PowerWallChargingStatusSensor, @@ -96,6 +98,30 @@ class PowerWallConnectedSensor(PowerWallEntity, BinarySensorEntity): return self.coordinator.data[POWERWALL_API_SITEMASTER].is_connected_to_tesla +class PowerWallGridServicesActiveSensor(PowerWallEntity, BinarySensorEntity): + """Representation of a Powerwall grid services active sensor.""" + + @property + def name(self): + """Device Name.""" + return "Grid Services Active" + + @property + def device_class(self): + """Device Class.""" + return DEVICE_CLASS_POWER + + @property + def unique_id(self): + """Device Uniqueid.""" + return f"{self.base_unique_id}_grid_services_active" + + @property + def is_on(self): + """Grid services is active.""" + return self.coordinator.data[POWERWALL_API_GRID_SERVICES_ACTIVE] + + class PowerWallGridStatusSensor(PowerWallEntity, BinarySensorEntity): """Representation of an Powerwall grid status sensor.""" diff --git a/homeassistant/components/powerwall/const.py b/homeassistant/components/powerwall/const.py index b2cd48df276..8cc0cbc27cd 100644 --- a/homeassistant/components/powerwall/const.py +++ b/homeassistant/components/powerwall/const.py @@ -19,6 +19,7 @@ POWERWALL_SITE_NAME = "site_name" POWERWALL_API_METERS = "meters" POWERWALL_API_CHARGE = "charge" +POWERWALL_API_GRID_SERVICES_ACTIVE = "grid_services_active" POWERWALL_API_GRID_STATUS = "grid_status" POWERWALL_API_SITEMASTER = "sitemaster" POWERWALL_API_STATUS = "status" diff --git a/tests/components/powerwall/mocks.py b/tests/components/powerwall/mocks.py index 0e3cec7f60b..9d253c3a74b 100644 --- a/tests/components/powerwall/mocks.py +++ b/tests/components/powerwall/mocks.py @@ -30,6 +30,7 @@ async def _mock_powerwall_with_fixtures(hass): charge=47.34587394586, sitemaster=SiteMaster(sitemaster), meters=MetersAggregates(meters), + grid_services_active=True, grid_status=GridStatus.CONNECTED, status=PowerwallStatus(status), device_type=DeviceType(device_type["device_type"]), @@ -42,6 +43,7 @@ def _mock_powerwall_return_value( charge=None, sitemaster=None, meters=None, + grid_services_active=None, grid_status=None, status=None, device_type=None, @@ -56,6 +58,7 @@ def _mock_powerwall_return_value( powerwall_mock.get_status = Mock(return_value=status) powerwall_mock.get_device_type = Mock(return_value=device_type) powerwall_mock.get_serial_numbers = Mock(return_value=serial_numbers) + powerwall_mock.is_grid_services_active = Mock(return_value=grid_services_active) return powerwall_mock diff --git a/tests/components/powerwall/test_binary_sensor.py b/tests/components/powerwall/test_binary_sensor.py index 006ecdfb533..8c9168c5f45 100644 --- a/tests/components/powerwall/test_binary_sensor.py +++ b/tests/components/powerwall/test_binary_sensor.py @@ -26,6 +26,16 @@ async def test_sensors(hass): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() + state = hass.states.get("binary_sensor.grid_services_active") + assert state.state == STATE_ON + expected_attributes = { + "friendly_name": "Grid Services Active", + "device_class": "power", + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all(item in state.attributes.items() for item in expected_attributes.items()) + state = hass.states.get("binary_sensor.grid_status") assert state.state == STATE_ON expected_attributes = {"friendly_name": "Grid Status", "device_class": "power"} From 37b7eda37a94a24264a30a6fd261dd6d823545c6 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Sat, 23 Oct 2021 03:42:33 +0200 Subject: [PATCH 0717/1038] Bump velbusaio to 2021.10.6 (#58168) * Bump velbusaio to 2021.10.3 * Bump velbusaio to 2021.10.5 * Bump velbusaio to 2021.10.6 --- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 865663eff6d..8c84c3944f8 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -2,7 +2,7 @@ "domain": "velbus", "name": "Velbus", "documentation": "https://www.home-assistant.io/integrations/velbus", - "requirements": ["velbus-aio==2021.10.1"], + "requirements": ["velbus-aio==2021.10.6"], "config_flow": true, "codeowners": ["@Cereal2nd", "@brefra"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 58d4a230d3c..7bd3003432e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2360,7 +2360,7 @@ uvcclient==0.11.0 vallox-websocket-api==2.8.1 # homeassistant.components.velbus -velbus-aio==2021.10.1 +velbus-aio==2021.10.6 # homeassistant.components.venstar venstarcolortouch==0.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7e672541079..7d6d709abb5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1361,7 +1361,7 @@ url-normalize==1.4.1 uvcclient==0.11.0 # homeassistant.components.velbus -velbus-aio==2021.10.1 +velbus-aio==2021.10.6 # homeassistant.components.venstar venstarcolortouch==0.14 From 93b061e9d96ca962c6735ea19a96ee1261d42359 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Fri, 22 Oct 2021 20:33:30 -0600 Subject: [PATCH 0718/1038] Update ismartgate dependency (#58259) --- homeassistant/components/gogogate2/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gogogate2/manifest.json b/homeassistant/components/gogogate2/manifest.json index 7d88340c597..90d50bdda43 100644 --- a/homeassistant/components/gogogate2/manifest.json +++ b/homeassistant/components/gogogate2/manifest.json @@ -3,7 +3,7 @@ "name": "Gogogate2 and ismartgate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gogogate2", - "requirements": ["ismartgate==4.0.3"], + "requirements": ["ismartgate==4.0.4"], "codeowners": ["@vangorra", "@bdraco"], "homekit": { "models": ["iSmartGate"] diff --git a/requirements_all.txt b/requirements_all.txt index 7bd3003432e..f8d6c7d46f9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -891,7 +891,7 @@ iotawattpy==0.1.0 iperf3==0.1.11 # homeassistant.components.gogogate2 -ismartgate==4.0.3 +ismartgate==4.0.4 # homeassistant.components.rest jsonpath==0.82 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7d6d709abb5..660588b382c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -543,7 +543,7 @@ influxdb==5.2.3 iotawattpy==0.1.0 # homeassistant.components.gogogate2 -ismartgate==4.0.3 +ismartgate==4.0.4 # homeassistant.components.rest jsonpath==0.82 From e481c862a6c6cef671ea0a05a5f0b93da8688d7d Mon Sep 17 00:00:00 2001 From: Michael Davie Date: Fri, 22 Oct 2021 23:31:25 -0400 Subject: [PATCH 0719/1038] Change precision of Nest sensors (#56993) * Change precision of Nest sensors * Add comment to temp rounding Co-authored-by: Allen Porter * Update rounding and tests * Add test for rounding Co-authored-by: Allen Porter --- homeassistant/components/nest/sensor_sdm.py | 10 +++++--- tests/components/nest/sensor_sdm_test.py | 27 ++++++++++++++++++++- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py index 52d81ab7dd9..786efde43f7 100644 --- a/homeassistant/components/nest/sensor_sdm.py +++ b/homeassistant/components/nest/sensor_sdm.py @@ -89,7 +89,10 @@ class TemperatureSensor(SensorBase): def native_value(self) -> float: """Return the state of the sensor.""" trait: TemperatureTrait = self._device.traits[TemperatureTrait.NAME] - return trait.ambient_temperature_celsius + # Round for display purposes because the API returns 5 decimal places. + # This can be removed if the SDM API issue is fixed, or a frontend + # display fix is added for all integrations. + return float(round(trait.ambient_temperature_celsius, 1)) class HumiditySensor(SensorBase): @@ -104,7 +107,8 @@ class HumiditySensor(SensorBase): return f"{self._device_info.device_name} Humidity" @property - def native_value(self) -> float: + def native_value(self) -> int: """Return the state of the sensor.""" trait: HumidityTrait = self._device.traits[HumidityTrait.NAME] - return trait.ambient_humidity_percent + # Cast without loss of precision because the API always returns an integer. + return int(trait.ambient_humidity_percent) diff --git a/tests/components/nest/sensor_sdm_test.py b/tests/components/nest/sensor_sdm_test.py index bfac288742d..72b2ecfc529 100644 --- a/tests/components/nest/sensor_sdm_test.py +++ b/tests/components/nest/sensor_sdm_test.py @@ -64,7 +64,7 @@ async def test_thermostat_device(hass): humidity = hass.states.get("sensor.my_sensor_humidity") assert humidity is not None - assert humidity.state == "35.0" + assert humidity.state == "35" assert humidity.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert humidity.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY assert humidity.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT @@ -230,3 +230,28 @@ async def test_device_with_unknown_type(hass): assert device.name == "My Sensor" assert device.model is None assert device.identifiers == {("nest", "some-device-id")} + + +async def test_temperature_rounding(hass): + """Test the rounding of overly precise temperatures.""" + devices = { + "some-device-id": Device.MakeDevice( + { + "name": "some-device-id", + "type": THERMOSTAT_TYPE, + "traits": { + "sdm.devices.traits.Info": { + "customName": "My Sensor", + }, + "sdm.devices.traits.Temperature": { + "ambientTemperatureCelsius": 25.15678, + }, + }, + }, + auth=None, + ) + } + await async_setup_sensor(hass, devices) + + temperature = hass.states.get("sensor.my_sensor_temperature") + assert temperature.state == "25.2" From 7627b959f890917194d1979276703aa78485d1c9 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 22 Oct 2021 21:33:19 -0700 Subject: [PATCH 0720/1038] Fix format bug in nest log statement (#58263) * Fix format bug in nest log statement * Resolve lint error logging-fstring-interpolation --- homeassistant/components/nest/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 344bdb74970..87682c7043e 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -81,7 +81,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return await async_setup_legacy(hass, config) if CONF_SUBSCRIBER_ID not in config[DOMAIN]: - _LOGGER.error("Configuration option '{CONF_SUBSCRIBER_ID}' required") + _LOGGER.error("Configuration option 'subscriber_id' required") return False # For setup of ConfigEntry below From fc7be8aa00fa0877f84678bd45dd91da4726e064 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 22 Oct 2021 21:33:40 -0700 Subject: [PATCH 0721/1038] Fix a bug in a nest test that causes side effects for other tests (#58264) Fix a bug where a constant configuration variable in the common test library is modified during the test, causing side effects for other tests. This was found by renaming the tests, which caused other tests to fail. --- tests/components/nest/test_init_sdm.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/components/nest/test_init_sdm.py b/tests/components/nest/test_init_sdm.py index db5e0c2fc33..205cc34fe20 100644 --- a/tests/components/nest/test_init_sdm.py +++ b/tests/components/nest/test_init_sdm.py @@ -5,6 +5,7 @@ The tests fake out the subscriber/devicemanager and simulate setup behavior and failure modes. """ +import copy import logging from unittest.mock import patch @@ -41,7 +42,7 @@ async def async_setup_sdm(hass, config=CONFIG): async def test_setup_configuration_failure(hass, caplog): """Test configuration error.""" - config = CONFIG.copy() + config = copy.deepcopy(CONFIG) config[DOMAIN]["subscriber_id"] = "invalid-subscriber-format" result = await async_setup_sdm(hass, config) @@ -107,7 +108,7 @@ async def test_subscriber_auth_failure(hass, caplog): async def test_setup_missing_subscriber_id(hass, caplog): """Test successful setup.""" - config = CONFIG + config = copy.deepcopy(CONFIG) del config[DOMAIN]["subscriber_id"] with caplog.at_level(logging.ERROR, logger="homeassistant.components.nest"): result = await async_setup_sdm(hass, config) From 400e85a299705c9bd5996386a09aeae48837e1e9 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 22 Oct 2021 22:25:55 -0700 Subject: [PATCH 0722/1038] Rename tests filenames to conform to Home Assistant standards (#58266) --- tests/components/nest/{camera_sdm_test.py => test_camera_sdm.py} | 0 .../components/nest/{climate_sdm_test.py => test_climate_sdm.py} | 0 .../components/nest/{device_info_test.py => test_device_info.py} | 0 tests/components/nest/{sensor_sdm_test.py => test_sensor_sdm.py} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename tests/components/nest/{camera_sdm_test.py => test_camera_sdm.py} (100%) rename tests/components/nest/{climate_sdm_test.py => test_climate_sdm.py} (100%) rename tests/components/nest/{device_info_test.py => test_device_info.py} (100%) rename tests/components/nest/{sensor_sdm_test.py => test_sensor_sdm.py} (100%) diff --git a/tests/components/nest/camera_sdm_test.py b/tests/components/nest/test_camera_sdm.py similarity index 100% rename from tests/components/nest/camera_sdm_test.py rename to tests/components/nest/test_camera_sdm.py diff --git a/tests/components/nest/climate_sdm_test.py b/tests/components/nest/test_climate_sdm.py similarity index 100% rename from tests/components/nest/climate_sdm_test.py rename to tests/components/nest/test_climate_sdm.py diff --git a/tests/components/nest/device_info_test.py b/tests/components/nest/test_device_info.py similarity index 100% rename from tests/components/nest/device_info_test.py rename to tests/components/nest/test_device_info.py diff --git a/tests/components/nest/sensor_sdm_test.py b/tests/components/nest/test_sensor_sdm.py similarity index 100% rename from tests/components/nest/sensor_sdm_test.py rename to tests/components/nest/test_sensor_sdm.py From a0c96f2a9a0c1c07b34b4e7b90f23e8ece37b811 Mon Sep 17 00:00:00 2001 From: Clifford Roche Date: Sat, 23 Oct 2021 04:19:19 -0400 Subject: [PATCH 0723/1038] Bump greeclimate to 0.12.2 (#58256) --- homeassistant/components/gree/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index 4dc763411bb..accd02dbe79 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -3,7 +3,7 @@ "name": "Gree Climate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gree", - "requirements": ["greeclimate==0.11.9"], + "requirements": ["greeclimate==0.12.2"], "codeowners": ["@cmroche"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index f8d6c7d46f9..aeff96dfa08 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -750,7 +750,7 @@ gpiozero==1.5.1 gps3==0.33.3 # homeassistant.components.gree -greeclimate==0.11.9 +greeclimate==0.12.2 # homeassistant.components.greeneye_monitor greeneye_monitor==2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 660588b382c..7920f130cb4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -455,7 +455,7 @@ google-nest-sdm==0.3.8 googlemaps==2.5.1 # homeassistant.components.gree -greeclimate==0.11.9 +greeclimate==0.12.2 # homeassistant.components.growatt_server growattServer==1.1.0 From 059880ebdc6fa947b14128055bf9442eb6af5023 Mon Sep 17 00:00:00 2001 From: Ivan Belokobylskiy Date: Sat, 23 Oct 2021 12:14:32 +0300 Subject: [PATCH 0724/1038] Fix yandex captcha detecting (#56132) Yandex recently switched to the new captcha page and the new version of aiomaps supports it. The check for captcha is moved to platform setup. Fixes #56035 --- .../components/yandex_transport/manifest.json | 2 +- .../components/yandex_transport/sensor.py | 23 +++++++++++++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../test_yandex_transport_sensor.py | 1 + 5 files changed, 23 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/yandex_transport/manifest.json b/homeassistant/components/yandex_transport/manifest.json index 79818f8e63e..680336fe47b 100644 --- a/homeassistant/components/yandex_transport/manifest.json +++ b/homeassistant/components/yandex_transport/manifest.json @@ -2,7 +2,7 @@ "domain": "yandex_transport", "name": "Yandex Transport", "documentation": "https://www.home-assistant.io/integrations/yandex_transport", - "requirements": ["aioymaps==1.1.0"], + "requirements": ["aioymaps==1.2.1"], "codeowners": ["@rishatik92", "@devbis"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py index b4f7f986626..0acca753454 100644 --- a/homeassistant/components/yandex_transport/sensor.py +++ b/homeassistant/components/yandex_transport/sensor.py @@ -3,7 +3,7 @@ from datetime import timedelta import logging -from aioymaps import YandexMapsRequester +from aioymaps import CaptchaError, YandexMapsRequester import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity @@ -42,8 +42,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= routes = config[CONF_ROUTE] client_session = async_create_clientsession(hass, requote_redirect_url=False) - data = YandexMapsRequester(user_agent=USER_AGENT, client_session=client_session) - async_add_entities([DiscoverYandexTransport(data, stop_id, routes, name)], True) + ymaps = YandexMapsRequester(user_agent=USER_AGENT, client_session=client_session) + try: + await ymaps.set_new_session() + except CaptchaError as ex: + _LOGGER.error( + "%s. You may need to disable the integration for some time", + ex, + ) + return + async_add_entities([DiscoverYandexTransport(ymaps, stop_id, routes, name)], True) class DiscoverYandexTransport(SensorEntity): @@ -63,7 +71,14 @@ class DiscoverYandexTransport(SensorEntity): """Get the latest data from maps.yandex.ru and update the states.""" attrs = {} closer_time = None - yandex_reply = await self.requester.get_stop_info(self._stop_id) + try: + yandex_reply = await self.requester.get_stop_info(self._stop_id) + except CaptchaError as ex: + _LOGGER.error( + "%s. You may need to disable the integration for some time", + ex, + ) + return try: data = yandex_reply["data"] except KeyError as key_error: diff --git a/requirements_all.txt b/requirements_all.txt index aeff96dfa08..90e7bbd3208 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -264,7 +264,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.yandex_transport -aioymaps==1.1.0 +aioymaps==1.2.1 # homeassistant.components.airly airly==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7920f130cb4..6ea83e4a02c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -191,7 +191,7 @@ aiovlc==0.1.0 aiowatttime==0.1.1 # homeassistant.components.yandex_transport -aioymaps==1.1.0 +aioymaps==1.2.1 # homeassistant.components.airly airly==1.1.0 diff --git a/tests/components/yandex_transport/test_yandex_transport_sensor.py b/tests/components/yandex_transport/test_yandex_transport_sensor.py index a727ea6e6cd..f18bd34e671 100644 --- a/tests/components/yandex_transport/test_yandex_transport_sensor.py +++ b/tests/components/yandex_transport/test_yandex_transport_sensor.py @@ -20,6 +20,7 @@ def mock_requester(): """Create a mock for YandexMapsRequester.""" with patch("aioymaps.YandexMapsRequester") as requester: instance = requester.return_value + instance.set_new_session = AsyncMock() instance.get_stop_info = AsyncMock(return_value=REPLY) yield instance From 137d41d8b447a988a79045469082f34ec773fbee Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 23 Oct 2021 05:41:32 -0400 Subject: [PATCH 0725/1038] Use DeviceInfo Class B-C (#58217) --- homeassistant/components/blebox/__init__.py | 16 ++++++------- .../bmw_connected_drive/__init__.py | 14 +++++------ homeassistant/components/bosch_shc/entity.py | 2 +- .../components/braviatv/media_player.py | 12 +++++----- homeassistant/components/braviatv/remote.py | 12 +++++----- homeassistant/components/broadlink/entity.py | 20 ++++++++-------- homeassistant/components/bsblan/climate.py | 23 +++++++------------ homeassistant/components/canary/camera.py | 13 ++++++----- homeassistant/components/canary/sensor.py | 13 ++++++----- homeassistant/components/cast/media_player.py | 13 ++++++----- .../components/climacell/__init__.py | 14 +++++------ .../components/coolmaster/climate.py | 17 +++++++------- .../components/crownstone/devices.py | 21 ++++++----------- 13 files changed, 90 insertions(+), 100 deletions(-) diff --git a/homeassistant/components/blebox/__init__.py b/homeassistant/components/blebox/__init__.py index 69ca8e23a73..681fff4a9bc 100644 --- a/homeassistant/components/blebox/__init__.py +++ b/homeassistant/components/blebox/__init__.py @@ -10,7 +10,7 @@ from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DEFAULT_SETUP_TIMEOUT, DOMAIN, PRODUCT @@ -82,13 +82,13 @@ class BleBoxEntity(Entity): self._attr_name = feature.full_name self._attr_unique_id = feature.unique_id product = feature.product - self._attr_device_info = { - "identifiers": {(DOMAIN, product.unique_id)}, - "name": product.name, - "manufacturer": product.brand, - "model": product.model, - "sw_version": product.firmware_version, - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, product.unique_id)}, + manufacturer=product.brand, + model=product.model, + name=product.name, + sw_version=product.firmware_version, + ) async def async_update(self): """Update the entity state.""" diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 843fd3a2437..722809dff5c 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -21,7 +21,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry, discovery import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.event import track_utc_time_change from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify @@ -330,12 +330,12 @@ class BMWConnectedDriveBaseEntity(Entity): "vin": self._vehicle.vin, ATTR_ATTRIBUTION: ATTRIBUTION, } - self._attr_device_info = { - "identifiers": {(DOMAIN, vehicle.vin)}, - "name": f'{vehicle.attributes.get("brand")} {vehicle.name}', - "model": vehicle.name, - "manufacturer": vehicle.attributes.get("brand"), - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, vehicle.vin)}, + manufacturer=vehicle.attributes.get("brand"), + model=vehicle.name, + name=f'{vehicle.attributes.get("brand")} {vehicle.name}', + ) def update_callback(self): """Schedule a state update.""" diff --git a/homeassistant/components/bosch_shc/entity.py b/homeassistant/components/bosch_shc/entity.py index 1176c29e351..c3a981aa658 100644 --- a/homeassistant/components/bosch_shc/entity.py +++ b/homeassistant/components/bosch_shc/entity.py @@ -30,9 +30,9 @@ class SHCEntity(Entity): self._attr_unique_id = device.serial self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, device.id)}, - name=device.name, manufacturer=device.manufacturer, model=device.device_model, + name=device.name, via_device=( DOMAIN, device.parent_device_id diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index 8528e3649c1..5451738cec6 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -52,12 +52,12 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN][config_entry.entry_id] unique_id = config_entry.unique_id assert unique_id is not None - device_info: DeviceInfo = { - "identifiers": {(DOMAIN, unique_id)}, - "name": DEFAULT_NAME, - "manufacturer": ATTR_MANUFACTURER, - "model": config_entry.title, - } + device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + manufacturer=ATTR_MANUFACTURER, + model=config_entry.title, + name=DEFAULT_NAME, + ) async_add_entities( [BraviaTVMediaPlayer(coordinator, DEFAULT_NAME, unique_id, device_info)] diff --git a/homeassistant/components/braviatv/remote.py b/homeassistant/components/braviatv/remote.py index 81761240320..7e01f26d0a5 100644 --- a/homeassistant/components/braviatv/remote.py +++ b/homeassistant/components/braviatv/remote.py @@ -25,12 +25,12 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN][config_entry.entry_id] unique_id = config_entry.unique_id assert unique_id is not None - device_info: DeviceInfo = { - "identifiers": {(DOMAIN, unique_id)}, - "name": DEFAULT_NAME, - "manufacturer": ATTR_MANUFACTURER, - "model": config_entry.title, - } + device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + manufacturer=ATTR_MANUFACTURER, + model=config_entry.title, + name=DEFAULT_NAME, + ) async_add_entities( [BraviaTVRemote(coordinator, DEFAULT_NAME, unique_id, device_info)] diff --git a/homeassistant/components/broadlink/entity.py b/homeassistant/components/broadlink/entity.py index bd2f938a2bd..080cd5bab71 100644 --- a/homeassistant/components/broadlink/entity.py +++ b/homeassistant/components/broadlink/entity.py @@ -1,7 +1,7 @@ """Broadlink entities.""" from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN @@ -51,13 +51,13 @@ class BroadlinkEntity(Entity): return self._device.update_manager.available @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info.""" - return { - "identifiers": {(DOMAIN, self._device.unique_id)}, - "connections": {(dr.CONNECTION_NETWORK_MAC, self._device.mac_address)}, - "manufacturer": self._device.api.manufacturer, - "model": self._device.api.model, - "name": self._device.name, - "sw_version": self._device.fw_version, - } + return DeviceInfo( + connections={(dr.CONNECTION_NETWORK_MAC, self._device.mac_address)}, + identifiers={(DOMAIN, self._device.unique_id)}, + manufacturer=self._device.api.manufacturer, + model=self._device.api.model, + name=self._device.name, + sw_version=self._device.fw_version, + ) diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index 23259101224..d7f4972c59e 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -20,16 +20,9 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, - ATTR_TEMPERATURE, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, -) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ATTR_TARGET_TEMPERATURE, DATA_BSBLAN_CLIENT, DOMAIN @@ -98,12 +91,12 @@ class BSBLanClimate(ClimateEntity): self._store_hvac_mode = None self.bsblan = bsblan self._attr_name = self._attr_unique_id = info.device_identification - self._attr_device_info = { - ATTR_IDENTIFIERS: {(DOMAIN, info.device_identification)}, - ATTR_NAME: "BSBLan Device", - ATTR_MANUFACTURER: "BSBLan", - ATTR_MODEL: info.controller_variant, - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, info.device_identification)}, + manufacturer="BSBLan", + model=info.controller_variant, + name="BSBLan Device", + ) async def async_set_preset_mode(self, preset_mode): """Set preset mode.""" diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index a475a27f942..bbdaaf97d0d 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -20,6 +20,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import Throttle @@ -100,12 +101,12 @@ class CanaryCamera(CoordinatorEntity, Camera): self._live_stream_session: LiveStreamSession | None = None self._attr_name = device.name self._attr_unique_id = str(device.device_id) - self._attr_device_info = { - "identifiers": {(DOMAIN, str(device.device_id))}, - "name": device.name, - "model": device.device_type["name"], - "manufacturer": MANUFACTURER, - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, str(device.device_id))}, + manufacturer=MANUFACTURER, + model=device.device_type["name"], + name=device.name, + ) @property def location(self) -> Location: diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index 1e7747039b8..dbd94d98d58 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -17,6 +17,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.update_coordinator import CoordinatorEntity @@ -114,12 +115,12 @@ class CanarySensor(CoordinatorEntity, SensorEntity): self._canary_type = canary_sensor_type self._attr_unique_id = f"{device.device_id}_{sensor_type[0]}" - self._attr_device_info = { - "identifiers": {(DOMAIN, str(device.device_id))}, - "name": device.name, - "model": device.device_type["name"], - "manufacturer": MANUFACTURER, - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, str(device.device_id))}, + model=device.device_type["name"], + manufacturer=MANUFACTURER, + name=device.name, + ) self._attr_native_unit_of_measurement = sensor_type[1] self._attr_device_class = sensor_type[3] self._attr_icon = sensor_type[2] diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 57983808cbd..eca7c69f5d2 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -55,6 +55,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.network import NoURLAvailableError, get_url import homeassistant.util.dt as dt_util from homeassistant.util.logging import async_create_catching_coro @@ -184,12 +185,12 @@ class CastDevice(MediaPlayerEntity): self._attr_unique_id = cast_info.uuid self._attr_name = cast_info.friendly_name if cast_info.model_name != "Google Cast Group": - self._attr_device_info = { - "name": str(cast_info.friendly_name), - "identifiers": {(CAST_DOMAIN, str(cast_info.uuid).replace("-", ""))}, - "model": cast_info.model_name, - "manufacturer": str(cast_info.manufacturer), - } + self._attr_device_info = DeviceInfo( + identifiers={(CAST_DOMAIN, str(cast_info.uuid).replace("-", ""))}, + manufacturer=str(cast_info.manufacturer), + model=cast_info.model_name, + name=str(cast_info.friendly_name), + ) async def async_added_to_hass(self): """Create chromecast object when added to hass.""" diff --git a/homeassistant/components/climacell/__init__.py b/homeassistant/components/climacell/__init__.py index 97090cef31d..c6a40b839f2 100644 --- a/homeassistant/components/climacell/__init__.py +++ b/homeassistant/components/climacell/__init__.py @@ -357,10 +357,10 @@ class ClimaCellEntity(CoordinatorEntity): @property def device_info(self) -> DeviceInfo: """Return device registry information.""" - return { - "identifiers": {(DOMAIN, self._config_entry.data[CONF_API_KEY])}, - "name": "ClimaCell", - "manufacturer": "ClimaCell", - "sw_version": f"v{self.api_version}", - "entry_type": "service", - } + return DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, self._config_entry.data[CONF_API_KEY])}, + manufacturer="ClimaCell", + name="ClimaCell", + sw_version=f"v{self.api_version}", + ) diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py index ceab74b2139..015a68ae18e 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -15,6 +15,7 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import callback +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONF_SUPPORTED_MODES, DATA_COORDINATOR, DATA_INFO, DOMAIN @@ -73,15 +74,15 @@ class CoolmasterClimate(CoordinatorEntity, ClimateEntity): super()._handle_coordinator_update() @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info for this device.""" - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "manufacturer": "CoolAutomation", - "model": "CoolMasterNet", - "sw_version": self._info["version"], - } + return DeviceInfo( + identifiers={(DOMAIN, self.unique_id)}, + manufacturer="CoolAutomation", + model="CoolMasterNet", + name=self.name, + sw_version=self._info["version"], + ) @property def unique_id(self): diff --git a/homeassistant/components/crownstone/devices.py b/homeassistant/components/crownstone/devices.py index 91af18ab15e..ead2c54a58e 100644 --- a/homeassistant/components/crownstone/devices.py +++ b/homeassistant/components/crownstone/devices.py @@ -3,13 +3,6 @@ from __future__ import annotations from crownstone_cloud.cloud_models.crownstones import Crownstone -from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, - ATTR_SW_VERSION, -) from homeassistant.helpers.entity import DeviceInfo, Entity from .const import CROWNSTONE_INCLUDE_TYPES, DOMAIN @@ -36,10 +29,10 @@ class CrownstoneBaseEntity(Entity): @property def device_info(self) -> DeviceInfo: """Return device info.""" - return { - ATTR_IDENTIFIERS: {(DOMAIN, self.cloud_id)}, - ATTR_NAME: self.device.name, - ATTR_MANUFACTURER: "Crownstone", - ATTR_MODEL: CROWNSTONE_INCLUDE_TYPES[self.device.type], - ATTR_SW_VERSION: self.device.sw_version, - } + return DeviceInfo( + identifiers={(DOMAIN, self.cloud_id)}, + manufacturer="Crownstone", + model=CROWNSTONE_INCLUDE_TYPES[self.device.type], + name=self.device.name, + sw_version=self.device.sw_version, + ) From 12c067970ac68c6beb092fc57214f689efba409d Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 23 Oct 2021 05:44:51 -0400 Subject: [PATCH 0726/1038] Use DeviceInfo Class E (#58230) --- homeassistant/components/eafm/sensor.py | 15 ++++++------- .../components/ecobee/binary_sensor.py | 17 ++++++++------- homeassistant/components/ecobee/climate.py | 16 +++++++------- homeassistant/components/ecobee/humidifier.py | 18 +++++++++------- homeassistant/components/ecobee/sensor.py | 15 ++++++------- homeassistant/components/ecobee/weather.py | 18 +++++++++------- homeassistant/components/econet/__init__.py | 14 ++++++------- homeassistant/components/elgato/light.py | 21 +++++++------------ homeassistant/components/elkm1/__init__.py | 16 +++++++------- homeassistant/components/emonitor/sensor.py | 12 +++++------ .../components/enphase_envoy/sensor.py | 15 ++++++------- .../components/epson/media_player.py | 19 ++++++++++------- homeassistant/components/ezviz/entity.py | 14 ++++++------- 13 files changed, 111 insertions(+), 99 deletions(-) diff --git a/homeassistant/components/eafm/sensor.py b/homeassistant/components/eafm/sensor.py index bc2158e4db8..e5edf8e0b99 100644 --- a/homeassistant/components/eafm/sensor.py +++ b/homeassistant/components/eafm/sensor.py @@ -8,6 +8,7 @@ import async_timeout from homeassistant.components.sensor import SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, LENGTH_METERS from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -121,13 +122,13 @@ class Measurement(CoordinatorEntity, SensorEntity): @property def device_info(self): """Return the device info.""" - return { - "identifiers": {(DOMAIN, "measure-id", self.station_id)}, - "name": self.name, - "manufacturer": "https://environment.data.gov.uk/", - "model": self.parameter_name, - "entry_type": "service", - } + return DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, "measure-id", self.station_id)}, + manufacturer="https://environment.data.gov.uk/", + model=self.parameter_name, + name=self.name, + ) @property def available(self) -> bool: diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index b81a5e6bef6..94c4ade7398 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -1,8 +1,11 @@ """Support for Ecobee binary sensors.""" +from __future__ import annotations + from homeassistant.components.binary_sensor import ( DEVICE_CLASS_OCCUPANCY, BinarySensorEntity, ) +from homeassistant.helpers.entity import DeviceInfo from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER @@ -49,7 +52,7 @@ class EcobeeBinarySensor(BinarySensorEntity): return f"{thermostat['identifier']}-{sensor['id']}-{self.device_class}" @property - def device_info(self): + def device_info(self) -> DeviceInfo | None: """Return device information for this sensor.""" identifier = None model = None @@ -72,12 +75,12 @@ class EcobeeBinarySensor(BinarySensorEntity): break if identifier is not None: - return { - "identifiers": {(DOMAIN, identifier)}, - "name": self.sensor_name, - "manufacturer": MANUFACTURER, - "model": model, - } + return DeviceInfo( + identifiers={(DOMAIN, identifier)}, + manufacturer=MANUFACTURER, + model=model, + name=self.sensor_name, + ) return None @property diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 0e7a5e52fa7..bc5f74c5022 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -38,6 +38,7 @@ from homeassistant.const import ( ) from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.util.temperature import convert from .const import _LOGGER, DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER @@ -366,20 +367,21 @@ class Thermostat(ClimateEntity): return self.thermostat["identifier"] @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device information for this ecobee thermostat.""" + model: str | None try: model = f"{ECOBEE_MODEL_TO_NAME[self.thermostat['modelNumber']]} Thermostat" except KeyError: # Ecobee model is not in our list model = None - return { - "identifiers": {(DOMAIN, self.thermostat["identifier"])}, - "name": self.name, - "manufacturer": MANUFACTURER, - "model": model, - } + return DeviceInfo( + identifiers={(DOMAIN, self.thermostat["identifier"])}, + manufacturer=MANUFACTURER, + model=model, + name=self.name, + ) @property def temperature_unit(self): diff --git a/homeassistant/components/ecobee/humidifier.py b/homeassistant/components/ecobee/humidifier.py index 984609c2f22..b660a75363f 100644 --- a/homeassistant/components/ecobee/humidifier.py +++ b/homeassistant/components/ecobee/humidifier.py @@ -1,4 +1,6 @@ """Support for using humidifier with ecobee thermostats.""" +from __future__ import annotations + from datetime import timedelta from homeassistant.components.humidifier import HumidifierEntity @@ -9,6 +11,7 @@ from homeassistant.components.humidifier.const import ( MODE_AUTO, SUPPORT_MODES, ) +from homeassistant.helpers.entity import DeviceInfo from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER @@ -54,20 +57,21 @@ class EcobeeHumidifier(HumidifierEntity): return f"{self.thermostat['identifier']}" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device information for the ecobee humidifier.""" + model: str | None try: model = f"{ECOBEE_MODEL_TO_NAME[self.thermostat['modelNumber']]} Thermostat" except KeyError: # Ecobee model is not in our list model = None - return { - "identifiers": {(DOMAIN, self.thermostat["identifier"])}, - "name": self.name, - "manufacturer": MANUFACTURER, - "model": model, - } + return DeviceInfo( + identifiers={(DOMAIN, self.thermostat["identifier"])}, + manufacturer=MANUFACTURER, + model=model, + name=self.name, + ) @property def available(self): diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 47e7af66e57..cfdf861e7bc 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -14,6 +14,7 @@ from homeassistant.const import ( PERCENTAGE, TEMP_FAHRENHEIT, ) +from homeassistant.helpers.entity import DeviceInfo from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER @@ -76,7 +77,7 @@ class EcobeeSensor(SensorEntity): return f"{thermostat['identifier']}-{sensor['id']}-{self.device_class}" @property - def device_info(self): + def device_info(self) -> DeviceInfo | None: """Return device information for this sensor.""" identifier = None model = None @@ -99,12 +100,12 @@ class EcobeeSensor(SensorEntity): break if identifier is not None and model is not None: - return { - "identifiers": {(DOMAIN, identifier)}, - "name": self.sensor_name, - "manufacturer": MANUFACTURER, - "model": model, - } + return DeviceInfo( + identifiers={(DOMAIN, identifier)}, + manufacturer=MANUFACTURER, + model=model, + name=self.sensor_name, + ) return None @property diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index 8e3de2be90a..aa73bd01c53 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -1,4 +1,6 @@ """Support for displaying weather info from Ecobee API.""" +from __future__ import annotations + from datetime import timedelta from pyecobee.const import ECOBEE_STATE_UNKNOWN @@ -13,6 +15,7 @@ from homeassistant.components.weather import ( WeatherEntity, ) from homeassistant.const import PRESSURE_HPA, PRESSURE_INHG, TEMP_FAHRENHEIT +from homeassistant.helpers.entity import DeviceInfo from homeassistant.util import dt as dt_util from homeassistant.util.pressure import convert as pressure_convert @@ -65,21 +68,22 @@ class EcobeeWeather(WeatherEntity): return self.data.ecobee.get_thermostat(self._index)["identifier"] @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device information for the ecobee weather platform.""" thermostat = self.data.ecobee.get_thermostat(self._index) + model: str | None try: model = f"{ECOBEE_MODEL_TO_NAME[thermostat['modelNumber']]} Thermostat" except KeyError: # Ecobee model is not in our list model = None - return { - "identifiers": {(DOMAIN, thermostat["identifier"])}, - "name": self.name, - "manufacturer": MANUFACTURER, - "model": model, - } + return DeviceInfo( + identifiers={(DOMAIN, thermostat["identifier"])}, + manufacturer=MANUFACTURER, + model=model, + name=self.name, + ) @property def condition(self): diff --git a/homeassistant/components/econet/__init__.py b/homeassistant/components/econet/__init__.py index 5a20337e454..8e39a6a6267 100644 --- a/homeassistant/components/econet/__init__.py +++ b/homeassistant/components/econet/__init__.py @@ -16,7 +16,7 @@ from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, TEMP_FAHRENHEIT from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import dispatcher_send -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.event import async_track_time_interval from .const import API_CLIENT, DOMAIN, EQUIPMENT @@ -128,13 +128,13 @@ class EcoNetEntity(Entity): return self._econet.connected @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device registry information for this entity.""" - return { - "identifiers": {(DOMAIN, self._econet.device_id)}, - "manufacturer": "Rheem", - "name": self._econet.device_name, - } + return DeviceInfo( + identifiers={(DOMAIN, self._econet.device_id)}, + manufacturer="Rheem", + name=self._econet.device_name, + ) @property def name(self): diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py index 7f7e432c5f0..8ed443b65e1 100644 --- a/homeassistant/components/elgato/light.py +++ b/homeassistant/components/elgato/light.py @@ -16,13 +16,6 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, - ATTR_SW_VERSION, -) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import ( @@ -187,13 +180,13 @@ class ElgatoLight(LightEntity): @property def device_info(self) -> DeviceInfo: """Return device information about this Elgato Light.""" - return { - ATTR_IDENTIFIERS: {(DOMAIN, self._info.serial_number)}, - ATTR_NAME: self._info.product_name, - ATTR_MANUFACTURER: "Elgato", - ATTR_MODEL: self._info.product_name, - ATTR_SW_VERSION: f"{self._info.firmware_version} ({self._info.firmware_build_number})", - } + return DeviceInfo( + identifiers={(DOMAIN, self._info.serial_number)}, + manufacturer="Elgato", + model=self._info.product_name, + name=self._info.product_name, + sw_version=f"{self._info.firmware_version} ({self._info.firmware_build_number})", + ) async def async_identify(self) -> None: """Identify the light, will make it blink.""" diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 775ac466445..3b59fffe553 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -463,15 +463,15 @@ class ElkAttachedEntity(ElkEntity): """An elk entity that is attached to the elk system.""" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Device info for the underlying ElkM1 system.""" device_name = "ElkM1" if self._prefix: device_name += f" {self._prefix}" - return { - "name": device_name, - "identifiers": {(DOMAIN, f"{self._prefix}_system")}, - "sw_version": self._elk.panel.elkm1_version, - "manufacturer": "ELK Products, Inc.", - "model": "M1", - } + return DeviceInfo( + identifiers={(DOMAIN, f"{self._prefix}_system")}, + manufacturer="ELK Products, Inc.", + model="M1", + name=device_name, + sw_version=self._elk.panel.elkm1_version, + ) diff --git a/homeassistant/components/emonitor/sensor.py b/homeassistant/components/emonitor/sensor.py index 1d699b42473..5d4f1983ac1 100644 --- a/homeassistant/components/emonitor/sensor.py +++ b/homeassistant/components/emonitor/sensor.py @@ -94,9 +94,9 @@ class EmonitorPowerSensor(CoordinatorEntity, SensorEntity): @property def device_info(self) -> DeviceInfo: """Return info about the emonitor device.""" - return { - "name": name_short_mac(self.mac_address[-6:]), - "connections": {(dr.CONNECTION_NETWORK_MAC, self.mac_address)}, - "manufacturer": "Powerhouse Dynamics, Inc.", - "sw_version": self.coordinator.data.hardware.firmware_version, - } + return DeviceInfo( + connections={(dr.CONNECTION_NETWORK_MAC, self.mac_address)}, + manufacturer="Powerhouse Dynamics, Inc.", + name=name_short_mac(self.mac_address[-6:]), + sw_version=self.coordinator.data.hardware.firmware_version, + ) diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 9bf4073847e..eda3c229255 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -15,6 +15,7 @@ from homeassistant.const import ( CONF_USERNAME, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import COORDINATOR, DOMAIN, NAME, SENSORS @@ -169,13 +170,13 @@ class Envoy(CoordinatorEntity, SensorEntity): return None @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" if not self._device_serial_number: return None - return { - "identifiers": {(DOMAIN, str(self._device_serial_number))}, - "name": self._device_name, - "model": "Envoy", - "manufacturer": "Enphase", - } + return DeviceInfo( + identifiers={(DOMAIN, str(self._device_serial_number))}, + manufacturer="Enphase", + model="Envoy", + name=self._device_name, + ) diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index 1fd0b7f6e70..5223a9663d0 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -1,4 +1,6 @@ """Support for Epson projector.""" +from __future__ import annotations + import logging from epson_projector.const import ( @@ -39,6 +41,7 @@ from homeassistant.components.media_player.const import ( from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry from .const import ATTR_CMODE, DOMAIN, SERVICE_SELECT_CMODE @@ -137,17 +140,17 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity): self._state = STATE_OFF @property - def device_info(self): + def device_info(self) -> DeviceInfo | None: """Get attributes about the device.""" if not self._unique_id: return None - return { - "identifiers": {(DOMAIN, self._unique_id)}, - "manufacturer": "Epson", - "name": "Epson projector", - "model": "Epson", - "via_hub": (DOMAIN, self._unique_id), - } + return DeviceInfo( + identifiers={(DOMAIN, self._unique_id)}, + manufacturer="Epson", + model="Epson", + name="Epson projector", + via_device=(DOMAIN, self._unique_id), + ) @property def name(self): diff --git a/homeassistant/components/ezviz/entity.py b/homeassistant/components/ezviz/entity.py index e7aa7d5039a..288c4a5d9eb 100644 --- a/homeassistant/components/ezviz/entity.py +++ b/homeassistant/components/ezviz/entity.py @@ -22,13 +22,13 @@ class EzvizEntity(CoordinatorEntity, Entity): super().__init__(coordinator) self._serial = serial self._camera_name = self.data["name"] - self._attr_device_info: DeviceInfo = { - "identifiers": {(DOMAIN, serial)}, - "name": self.data["name"], - "model": self.data["device_sub_category"], - "manufacturer": MANUFACTURER, - "sw_version": self.data["version"], - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, serial)}, + manufacturer=MANUFACTURER, + model=self.data["device_sub_category"], + name=self.data["name"], + sw_version=self.data["version"], + ) @property def data(self) -> dict[str, Any]: From 63646a19ccf8a6c0557317811e912a609b7b312c Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 23 Oct 2021 06:01:21 -0400 Subject: [PATCH 0727/1038] Use DeviceInfo Class F-G (#58255) --- homeassistant/components/firmata/entity.py | 14 +++++------ .../components/fjaraskupan/__init__.py | 10 ++++---- homeassistant/components/flipr/__init__.py | 12 ++++----- homeassistant/components/flo/entity.py | 16 ++++++------ homeassistant/components/flume/sensor.py | 13 +++++----- homeassistant/components/flux_led/entity.py | 21 ++++++---------- .../components/forecast_solar/sensor.py | 23 +++++++---------- .../components/freebox/device_tracker.py | 12 ++++----- homeassistant/components/freebox/router.py | 14 +++++------ homeassistant/components/freebox/sensor.py | 2 +- .../components/freedompro/binary_sensor.py | 13 +++++----- .../components/freedompro/climate.py | 13 +++++----- homeassistant/components/freedompro/cover.py | 13 +++++----- homeassistant/components/freedompro/fan.py | 13 +++++----- homeassistant/components/freedompro/light.py | 13 +++++----- homeassistant/components/freedompro/lock.py | 13 +++++----- homeassistant/components/freedompro/sensor.py | 13 +++++----- homeassistant/components/freedompro/switch.py | 13 +++++----- homeassistant/components/fritz/common.py | 9 +++---- homeassistant/components/fritzbox/__init__.py | 14 +++++------ .../components/fritzbox_callmonitor/sensor.py | 17 +++++++------ .../components/geofency/device_tracker.py | 5 ++-- homeassistant/components/gios/sensor.py | 13 +++++----- homeassistant/components/goalzero/__init__.py | 25 ++++++------------- homeassistant/components/gogogate2/common.py | 23 ++++++++--------- .../components/google_travel_time/sensor.py | 13 +++++----- .../components/gpslogger/device_tracker.py | 5 ++-- homeassistant/components/gree/climate.py | 15 +++++------ homeassistant/components/gree/entity.py | 15 +++++------ .../components/growatt_server/sensor.py | 14 +++++------ homeassistant/components/guardian/__init__.py | 12 ++++----- 31 files changed, 207 insertions(+), 214 deletions(-) diff --git a/homeassistant/components/firmata/entity.py b/homeassistant/components/firmata/entity.py index e9f9f3619fe..0f248e0b9d7 100644 --- a/homeassistant/components/firmata/entity.py +++ b/homeassistant/components/firmata/entity.py @@ -19,13 +19,13 @@ class FirmataEntity: @property def device_info(self) -> DeviceInfo: """Return device info.""" - return { - "connections": {}, - "identifiers": {(DOMAIN, self._api.board.name)}, - "manufacturer": FIRMATA_MANUFACTURER, - "name": self._api.board.name, - "sw_version": self._api.board.firmware_version, - } + return DeviceInfo( + connections={}, + identifiers={(DOMAIN, self._api.board.name)}, + manufacturer=FIRMATA_MANUFACTURER, + name=self._api.board.name, + sw_version=self._api.board.firmware_version, + ) class FirmataPinEntity(FirmataEntity): diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index b6eda82ea10..56a67a14a02 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -89,11 +89,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) coordinator.async_set_updated_data(device.state) - device_info: DeviceInfo = { - "identifiers": {(DOMAIN, ble_device.address)}, - "manufacturer": "Fjäråskupan", - "name": "Fjäråskupan", - } + device_info = DeviceInfo( + identifiers={(DOMAIN, ble_device.address)}, + manufacturer="Fjäråskupan", + name="Fjäråskupan", + ) device_state = DeviceState(device, coordinator, device_info) state.devices[ble_device.address] = device_state async_dispatcher_send( diff --git a/homeassistant/components/flipr/__init__.py b/homeassistant/components/flipr/__init__.py index fd7c3f5c02a..9280c77f95c 100644 --- a/homeassistant/components/flipr/__init__.py +++ b/homeassistant/components/flipr/__init__.py @@ -7,7 +7,7 @@ from flipr_api import FliprAPIRestClient from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -88,10 +88,10 @@ class FliprEntity(CoordinatorEntity): flipr_id = coordinator.config_entry.data[CONF_FLIPR_ID] self._attr_unique_id = f"{flipr_id}-{description.key}" - self._attr_device_info = { - "identifiers": {(DOMAIN, flipr_id)}, - "name": NAME, - "manufacturer": MANUFACTURER, - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, flipr_id)}, + manufacturer=MANUFACTURER, + name=NAME, + ) self._attr_name = f"Flipr {flipr_id} {description.name}" diff --git a/homeassistant/components/flo/entity.py b/homeassistant/components/flo/entity.py index 9f0e8029888..280f19dc57e 100644 --- a/homeassistant/components/flo/entity.py +++ b/homeassistant/components/flo/entity.py @@ -33,14 +33,14 @@ class FloEntity(Entity): @property def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" - return { - "identifiers": {(FLO_DOMAIN, self._device.id)}, - "connections": {(CONNECTION_NETWORK_MAC, self._device.mac_address)}, - "manufacturer": self._device.manufacturer, - "model": self._device.model, - "name": self._device.device_name, - "sw_version": self._device.firmware_version, - } + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, self._device.mac_address)}, + identifiers={(FLO_DOMAIN, self._device.id)}, + manufacturer=self._device.manufacturer, + model=self._device.model, + name=self._device.device_name, + sw_version=self._device.firmware_version, + ) @property def available(self) -> bool: diff --git a/homeassistant/components/flume/sensor.py b/homeassistant/components/flume/sensor.py index ff4610ca788..2ff7712cfd5 100644 --- a/homeassistant/components/flume/sensor.py +++ b/homeassistant/components/flume/sensor.py @@ -20,6 +20,7 @@ from homeassistant.const import ( CONF_USERNAME, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -132,12 +133,12 @@ class FlumeSensor(CoordinatorEntity, SensorEntity): self._attr_name = f"{name} {description.name}" self._attr_unique_id = f"{description.key}_{device_id}" - self._attr_device_info = { - "name": self.name, - "identifiers": {(DOMAIN, device_id)}, - "manufacturer": "Flume, Inc.", - "model": "Flume Smart Water Monitor", - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device_id)}, + manufacturer="Flume, Inc.", + model="Flume Smart Water Monitor", + name=self.name, + ) @property def native_value(self): diff --git a/homeassistant/components/flux_led/entity.py b/homeassistant/components/flux_led/entity.py index 0918d6af62a..4183ccc14cd 100644 --- a/homeassistant/components/flux_led/entity.py +++ b/homeassistant/components/flux_led/entity.py @@ -6,15 +6,10 @@ from typing import Any, cast from flux_led.aiodevice import AIOWifiLedBulb -from homeassistant.const import ( - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, - ATTR_SW_VERSION, -) from homeassistant.core import callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import FluxLedUpdateCoordinator @@ -39,13 +34,13 @@ class FluxEntity(CoordinatorEntity): self._attr_name = name self._attr_unique_id = unique_id if self.unique_id: - self._attr_device_info = { - "connections": {(dr.CONNECTION_NETWORK_MAC, self.unique_id)}, - ATTR_MODEL: self._device.model, - ATTR_NAME: self.name, - ATTR_SW_VERSION: str(self._device.version_num), - ATTR_MANUFACTURER: "FluxLED/Magic Home", - } + self._attr_device_info = DeviceInfo( + connections={(dr.CONNECTION_NETWORK_MAC, self.unique_id)}, + manufacturer="FluxLED/Magic Home", + model=self._device.model, + name=self.name, + sw_version=str(self._device.version_num), + ) @property def is_on(self) -> bool: diff --git a/homeassistant/components/forecast_solar/sensor.py b/homeassistant/components/forecast_solar/sensor.py index 2ad86186652..cd672311c52 100644 --- a/homeassistant/components/forecast_solar/sensor.py +++ b/homeassistant/components/forecast_solar/sensor.py @@ -5,13 +5,8 @@ from datetime import datetime from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, -) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import ( @@ -19,7 +14,7 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .const import ATTR_ENTRY_TYPE, DOMAIN, ENTRY_TYPE_SERVICE, SENSORS +from .const import DOMAIN, ENTRY_TYPE_SERVICE, SENSORS from .models import ForecastSolarSensorEntityDescription @@ -57,13 +52,13 @@ class ForecastSolarSensorEntity(CoordinatorEntity, SensorEntity): self.entity_id = f"{SENSOR_DOMAIN}.{entity_description.key}" self._attr_unique_id = f"{entry_id}_{entity_description.key}" - self._attr_device_info = { - ATTR_IDENTIFIERS: {(DOMAIN, entry_id)}, - ATTR_NAME: "Solar Production Forecast", - ATTR_MANUFACTURER: "Forecast.Solar", - ATTR_MODEL: coordinator.data.account_type.value, - ATTR_ENTRY_TYPE: ENTRY_TYPE_SERVICE, - } + self._attr_device_info = DeviceInfo( + entry_type=ENTRY_TYPE_SERVICE, + identifiers={(DOMAIN, entry_id)}, + manufacturer="Forecast.Solar", + model=coordinator.data.account_type.value, + name="Solar Production Forecast", + ) @property def native_value(self) -> StateType: diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index 2ad262dd2bd..38a781c8c12 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -114,12 +114,12 @@ class FreeboxDevice(ScannerEntity): @property def device_info(self) -> DeviceInfo: """Return the device information.""" - return { - "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "manufacturer": self._manufacturer, - } + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, self._mac)}, + identifiers={(DOMAIN, self.unique_id)}, + manufacturer=self._manufacturer, + name=self.name, + ) @property def should_poll(self) -> bool: diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 0673d550d76..2eef38d7d1e 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -183,13 +183,13 @@ class FreeboxRouter: @property def device_info(self) -> DeviceInfo: """Return the device information.""" - return { - "connections": {(CONNECTION_NETWORK_MAC, self.mac)}, - "identifiers": {(DOMAIN, self.mac)}, - "name": self.name, - "manufacturer": "Freebox SAS", - "sw_version": self._sw_v, - } + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, self.mac)}, + identifiers={(DOMAIN, self.mac)}, + manufacturer="Freebox SAS", + name=self.name, + sw_version=self._sw_v, + ) @property def signal_device_new(self) -> str: diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 6b56300d743..814c2ea402f 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -165,8 +165,8 @@ class FreeboxDiskSensor(FreeboxSensor): """Return the device information.""" return DeviceInfo( identifiers={(DOMAIN, self._disk["id"])}, - name=f"Disk {self._disk['id']}", model=self._disk["model"], + name=f"Disk {self._disk['id']}", sw_version=self._disk["firmware"], via_device=( DOMAIN, diff --git a/homeassistant/components/freedompro/binary_sensor.py b/homeassistant/components/freedompro/binary_sensor.py index 133f64019c2..ac70824be4c 100644 --- a/homeassistant/components/freedompro/binary_sensor.py +++ b/homeassistant/components/freedompro/binary_sensor.py @@ -7,6 +7,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.core import callback +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -47,14 +48,14 @@ class Device(CoordinatorEntity, BinarySensorEntity): self._attr_name = device["name"] self._attr_unique_id = device["uid"] self._type = device["type"] - self._attr_device_info = { - "name": self.name, - "identifiers": { + self._attr_device_info = DeviceInfo( + identifiers={ (DOMAIN, self.unique_id), }, - "model": device["type"], - "manufacturer": "Freedompro", - } + manufacturer="Freedompro", + model=device["type"], + name=self.name, + ) self._attr_device_class = DEVICE_CLASS_MAP[device["type"]] @callback diff --git a/homeassistant/components/freedompro/climate.py b/homeassistant/components/freedompro/climate.py index e37ae9dea1b..1707ee4a884 100644 --- a/homeassistant/components/freedompro/climate.py +++ b/homeassistant/components/freedompro/climate.py @@ -15,6 +15,7 @@ from homeassistant.components.climate.const import ( from homeassistant.const import ATTR_TEMPERATURE, CONF_API_KEY, TEMP_CELSIUS from homeassistant.core import callback from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -59,14 +60,14 @@ class Device(CoordinatorEntity, ClimateEntity): self._attr_name = device["name"] self._attr_unique_id = device["uid"] self._characteristics = device["characteristics"] - self._attr_device_info = { - "name": self.name, - "identifiers": { + self._attr_device_info = DeviceInfo( + identifiers={ (DOMAIN, self.unique_id), }, - "model": device["type"], - "manufacturer": "Freedompro", - } + manufacturer="Freedompro", + model=device["type"], + name=self.name, + ) self._attr_supported_features = SUPPORT_TARGET_TEMPERATURE self._attr_current_temperature = 0 self._attr_target_temperature = 0 diff --git a/homeassistant/components/freedompro/cover.py b/homeassistant/components/freedompro/cover.py index 439887c9626..fd6c747da46 100644 --- a/homeassistant/components/freedompro/cover.py +++ b/homeassistant/components/freedompro/cover.py @@ -18,6 +18,7 @@ from homeassistant.components.cover import ( from homeassistant.const import CONF_API_KEY from homeassistant.core import callback from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -54,14 +55,14 @@ class Device(CoordinatorEntity, CoverEntity): self._api_key = api_key self._attr_name = device["name"] self._attr_unique_id = device["uid"] - self._attr_device_info = { - "name": self.name, - "identifiers": { + self._attr_device_info = DeviceInfo( + identifiers={ (DOMAIN, self.unique_id), }, - "model": device["type"], - "manufacturer": "Freedompro", - } + manufacturer="Freedompro", + model=device["type"], + name=self.name, + ) self._attr_current_cover_position = 0 self._attr_is_closed = True self._attr_supported_features = ( diff --git a/homeassistant/components/freedompro/fan.py b/homeassistant/components/freedompro/fan.py index 55955042804..52c2de85ca6 100644 --- a/homeassistant/components/freedompro/fan.py +++ b/homeassistant/components/freedompro/fan.py @@ -7,6 +7,7 @@ from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity from homeassistant.const import CONF_API_KEY from homeassistant.core import callback from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -34,14 +35,14 @@ class FreedomproFan(CoordinatorEntity, FanEntity): self._attr_name = device["name"] self._attr_unique_id = device["uid"] self._characteristics = device["characteristics"] - self._attr_device_info = { - "name": self.name, - "identifiers": { + self._attr_device_info = DeviceInfo( + identifiers={ (DOMAIN, self.unique_id), }, - "model": device["type"], - "manufacturer": "Freedompro", - } + manufacturer="Freedompro", + model=device["type"], + name=self.name, + ) self._attr_is_on = False self._attr_percentage = 0 diff --git a/homeassistant/components/freedompro/light.py b/homeassistant/components/freedompro/light.py index 0b944a682d4..5610a561fda 100644 --- a/homeassistant/components/freedompro/light.py +++ b/homeassistant/components/freedompro/light.py @@ -14,6 +14,7 @@ from homeassistant.components.light import ( from homeassistant.const import CONF_API_KEY from homeassistant.core import callback from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -40,14 +41,14 @@ class Device(CoordinatorEntity, LightEntity): self._api_key = api_key self._attr_name = device["name"] self._attr_unique_id = device["uid"] - self._attr_device_info = { - "name": self.name, - "identifiers": { + self._attr_device_info = DeviceInfo( + identifiers={ (DOMAIN, self.unique_id), }, - "model": device["type"], - "manufacturer": "Freedompro", - } + manufacturer="Freedompro", + model=device["type"], + name=self.name, + ) self._attr_is_on = False self._attr_brightness = 0 color_mode = COLOR_MODE_ONOFF diff --git a/homeassistant/components/freedompro/lock.py b/homeassistant/components/freedompro/lock.py index f3a689016f6..57486f58d79 100644 --- a/homeassistant/components/freedompro/lock.py +++ b/homeassistant/components/freedompro/lock.py @@ -7,6 +7,7 @@ from homeassistant.components.lock import LockEntity from homeassistant.const import CONF_API_KEY from homeassistant.core import callback from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -36,14 +37,14 @@ class Device(CoordinatorEntity, LockEntity): self._attr_unique_id = device["uid"] self._type = device["type"] self._characteristics = device["characteristics"] - self._attr_device_info = { - "name": self.name, - "identifiers": { + self._attr_device_info = DeviceInfo( + identifiers={ (DOMAIN, self.unique_id), }, - "model": self._type, - "manufacturer": "Freedompro", - } + manufacturer="Freedompro", + model=self._type, + name=self.name, + ) @callback def _handle_coordinator_update(self) -> None: diff --git a/homeassistant/components/freedompro/sensor.py b/homeassistant/components/freedompro/sensor.py index e5322924864..74b54474dbd 100644 --- a/homeassistant/components/freedompro/sensor.py +++ b/homeassistant/components/freedompro/sensor.py @@ -8,6 +8,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import LIGHT_LUX, PERCENTAGE, TEMP_CELSIUS from homeassistant.core import callback +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -54,14 +55,14 @@ class Device(CoordinatorEntity, SensorEntity): self._attr_name = device["name"] self._attr_unique_id = device["uid"] self._type = device["type"] - self._attr_device_info = { - "name": self.name, - "identifiers": { + self._attr_device_info = DeviceInfo( + identifiers={ (DOMAIN, self.unique_id), }, - "model": device["type"], - "manufacturer": "Freedompro", - } + manufacturer="Freedompro", + model=device["type"], + name=self.name, + ) self._attr_device_class = DEVICE_CLASS_MAP[device["type"]] self._attr_state_class = STATE_CLASS_MAP[device["type"]] self._attr_native_unit_of_measurement = UNIT_MAP[device["type"]] diff --git a/homeassistant/components/freedompro/switch.py b/homeassistant/components/freedompro/switch.py index c4c6b8ec353..c44af65ba32 100644 --- a/homeassistant/components/freedompro/switch.py +++ b/homeassistant/components/freedompro/switch.py @@ -7,6 +7,7 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.const import CONF_API_KEY from homeassistant.core import callback from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -33,14 +34,14 @@ class Device(CoordinatorEntity, SwitchEntity): self._api_key = api_key self._attr_name = device["name"] self._attr_unique_id = device["uid"] - self._attr_device_info = { - "name": self.name, - "identifiers": { + self._attr_device_info = DeviceInfo( + identifiers={ (DOMAIN, self.unique_id), }, - "model": device["type"], - "manufacturer": "Freedompro", - } + manufacturer="Freedompro", + model=device["type"], + name=self.name, + ) self._attr_is_on = False @callback diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 6503a5e84ba..58c3357b33d 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -353,10 +353,10 @@ class FritzDeviceBase(Entity): """Return the device information.""" return DeviceInfo( connections={(CONNECTION_NETWORK_MAC, self._mac)}, - identifiers={(DOMAIN, self._mac)}, - default_name=self.name, default_manufacturer="AVM", default_model="FRITZ!Box Tracked device", + default_name=self.name, + identifiers={(DOMAIN, self._mac)}, via_device=( DOMAIN, self._router.unique_id, @@ -479,13 +479,12 @@ class FritzBoxBaseEntity: @property def device_info(self) -> DeviceInfo: """Return the device information.""" - return DeviceInfo( + configuration_url=f"http://{self._fritzbox_tools.host}", connections={(CONNECTION_NETWORK_MAC, self.mac_address)}, identifiers={(DOMAIN, self._fritzbox_tools.unique_id)}, - name=self._device_name, manufacturer="AVM", model=self._fritzbox_tools.model, + name=self._device_name, sw_version=self._fritzbox_tools.current_firmware, - configuration_url=f"http://{self._fritzbox_tools.host}", ) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 36b51a630ea..0ddd0b8d417 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -173,13 +173,13 @@ class FritzBoxEntity(CoordinatorEntity): @property def device_info(self) -> DeviceInfo: """Return device specific attributes.""" - return { - "name": self.device.name, - "identifiers": {(DOMAIN, self.ain)}, - "manufacturer": self.device.manufacturer, - "model": self.device.productname, - "sw_version": self.device.fw_version, - } + return DeviceInfo( + identifiers={(DOMAIN, self.ain)}, + manufacturer=self.device.manufacturer, + model=self.device.productname, + name=self.device.name, + sw_version=self.device.fw_version, + ) @property def extra_state_attributes(self) -> FritzExtraAttributes: diff --git a/homeassistant/components/fritzbox_callmonitor/sensor.py b/homeassistant/components/fritzbox_callmonitor/sensor.py index 31e04077656..171e6966b28 100644 --- a/homeassistant/components/fritzbox_callmonitor/sensor.py +++ b/homeassistant/components/fritzbox_callmonitor/sensor.py @@ -19,6 +19,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from .const import ( ATTR_PREFIXES, @@ -175,15 +176,15 @@ class FritzBoxCallSensor(SensorEntity): return self._attributes @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device specific attributes.""" - return { - "name": self._fritzbox_phonebook.fph.modelname, - "identifiers": {(DOMAIN, self._unique_id)}, - "manufacturer": MANUFACTURER, - "model": self._fritzbox_phonebook.fph.modelname, - "sw_version": self._fritzbox_phonebook.fph.fc.system_version, - } + return DeviceInfo( + identifiers={(DOMAIN, self._unique_id)}, + manufacturer=MANUFACTURER, + model=self._fritzbox_phonebook.fph.modelname, + name=self._fritzbox_phonebook.fph.modelname, + sw_version=self._fritzbox_phonebook.fph.fc.system_version, + ) @property def unique_id(self): diff --git a/homeassistant/components/geofency/device_tracker.py b/homeassistant/components/geofency/device_tracker.py index 5a58e73d44a..b2d26dcb2a5 100644 --- a/homeassistant/components/geofency/device_tracker.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -5,6 +5,7 @@ from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE from homeassistant.core import callback from homeassistant.helpers import device_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.restore_state import RestoreEntity from . import DOMAIN as GF_DOMAIN, TRACKER_UPDATE @@ -86,9 +87,9 @@ class GeofencyEntity(TrackerEntity, RestoreEntity): return self._unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" - return {"name": self._name, "identifiers": {(GF_DOMAIN, self._unique_id)}} + return DeviceInfo(identifiers={(GF_DOMAIN, self._unique_id)}, name=self._name) @property def source_type(self): diff --git a/homeassistant/components/gios/sensor.py b/homeassistant/components/gios/sensor.py index 9ba5e5410b0..5dd48656d12 100644 --- a/homeassistant/components/gios/sensor.py +++ b/homeassistant/components/gios/sensor.py @@ -8,6 +8,7 @@ from homeassistant.components.sensor import DOMAIN as PLATFORM, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, ATTR_NAME, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.helpers.typing import StateType @@ -80,12 +81,12 @@ class GiosSensor(CoordinatorEntity, SensorEntity): ) -> None: """Initialize.""" super().__init__(coordinator) - self._attr_device_info = { - "identifiers": {(DOMAIN, str(coordinator.gios.station_id))}, - "name": DEFAULT_NAME, - "manufacturer": MANUFACTURER, - "entry_type": "service", - } + self._attr_device_info = DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, str(coordinator.gios.station_id))}, + manufacturer=MANUFACTURER, + name=DEFAULT_NAME, + ) self._attr_name = f"{name} {description.name}" self._attr_unique_id = f"{coordinator.gios.station_id}-{description.key}" self._attrs: dict[str, Any] = { diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py index 08ad70c0a79..774f1fd0e21 100644 --- a/homeassistant/components/goalzero/__init__.py +++ b/homeassistant/components/goalzero/__init__.py @@ -9,16 +9,7 @@ from homeassistant.components.binary_sensor import DOMAIN as DOMAIN_BINARY_SENSO from homeassistant.components.sensor import DOMAIN as DOMAIN_SENSOR from homeassistant.components.switch import DOMAIN as DOMAIN_SWITCH from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ATTRIBUTION, - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, - ATTR_SW_VERSION, - CONF_HOST, - CONF_NAME, -) +from homeassistant.const import ATTR_ATTRIBUTION, ATTR_MODEL, CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -109,10 +100,10 @@ class YetiEntity(CoordinatorEntity): @property def device_info(self) -> DeviceInfo: """Return the device information of the entity.""" - return { - ATTR_IDENTIFIERS: {(DOMAIN, self._server_unique_id)}, - ATTR_MANUFACTURER: "Goal Zero", - ATTR_NAME: self._name, - ATTR_MODEL: self.api.sysdata[ATTR_MODEL], - ATTR_SW_VERSION: self.api.data["firmwareVersion"], - } + return DeviceInfo( + identifiers={(DOMAIN, self._server_unique_id)}, + manufacturer="Goal Zero", + model=self.api.sysdata[ATTR_MODEL], + name=self._name, + sw_version=self.api.data["firmwareVersion"], + ) diff --git a/homeassistant/components/gogogate2/common.py b/homeassistant/components/gogogate2/common.py index 776b90e1c5e..42217910b81 100644 --- a/homeassistant/components/gogogate2/common.py +++ b/homeassistant/components/gogogate2/common.py @@ -93,21 +93,20 @@ class GoGoGate2Entity(CoordinatorEntity): return self._door @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Device info for the controller.""" data = self.coordinator.data - info: DeviceInfo = { - "identifiers": {(DOMAIN, self._config_entry.unique_id)}, - "name": self._config_entry.title, - "manufacturer": MANUFACTURER, - "model": data.model, - "sw_version": data.firmwareversion, - } + url = None if data.model.startswith("ismartgate"): - info[ - "configuration_url" - ] = f"https://{self._config_entry.unique_id}.isgaccess.com" - return info + url = f"https://{self._config_entry.unique_id}.isgaccess.com" + return DeviceInfo( + configuration_url=url, + identifiers={(DOMAIN, str(self._config_entry.unique_id))}, + name=self._config_entry.title, + manufacturer=MANUFACTURER, + model=data.model, + sw_version=data.firmwareversion, + ) def get_data_update_coordinator( diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py index c8cb9d54510..1b999edc8b7 100644 --- a/homeassistant/components/google_travel_time/sensor.py +++ b/homeassistant/components/google_travel_time/sensor.py @@ -22,6 +22,7 @@ from homeassistant.const import ( ) from homeassistant.core import CoreState, HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util @@ -209,13 +210,13 @@ class GoogleTravelTimeSensor(SensorEntity): return None @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device specific attributes.""" - return { - "name": DOMAIN, - "identifiers": {(DOMAIN, self._api_key)}, - "entry_type": "service", - } + return DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, self._api_key)}, + name=DOMAIN, + ) @property def unique_id(self) -> str: diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py index 2493054473a..8b0965cc434 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -11,6 +11,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.restore_state import RestoreEntity from . import DOMAIN as GPL_DOMAIN, TRACKER_UPDATE @@ -111,9 +112,9 @@ class GPSLoggerEntity(TrackerEntity, RestoreEntity): return self._unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" - return {"name": self._name, "identifiers": {(GPL_DOMAIN, self._unique_id)}} + return DeviceInfo(identifiers={(GPL_DOMAIN, self._unique_id)}, name=self._name) @property def source_type(self): diff --git a/homeassistant/components/gree/climate.py b/homeassistant/components/gree/climate.py index 73ea66e5895..dbf8214e29a 100644 --- a/homeassistant/components/gree/climate.py +++ b/homeassistant/components/gree/climate.py @@ -50,6 +50,7 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -137,14 +138,14 @@ class GreeClimateEntity(CoordinatorEntity, ClimateEntity): return self._mac @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device specific attributes.""" - return { - "name": self._name, - "identifiers": {(DOMAIN, self._mac)}, - "manufacturer": "Gree", - "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, - } + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, self._mac)}, + identifiers={(DOMAIN, self._mac)}, + manufacturer="Gree", + name=self._name, + ) @property def temperature_unit(self) -> str: diff --git a/homeassistant/components/gree/entity.py b/homeassistant/components/gree/entity.py index 0753a780f4b..7407a90b4d0 100644 --- a/homeassistant/components/gree/entity.py +++ b/homeassistant/components/gree/entity.py @@ -1,5 +1,6 @@ """Entity object for shared properties of Gree entities.""" from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .bridge import DeviceDataUpdateCoordinator @@ -27,11 +28,11 @@ class GreeEntity(CoordinatorEntity): return f"{self._mac}_{self._desc}" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return info about the device.""" - return { - "identifiers": {(DOMAIN, self._mac)}, - "name": self._name, - "manufacturer": "Gree", - "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, - } + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, self._mac)}, + identifiers={(DOMAIN, self._mac)}, + manufacturer="Gree", + name=self._name, + ) diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 599efcb6f42..af9223c2e1d 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -15,9 +15,6 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_NAME, CONF_NAME, CONF_PASSWORD, CONF_URL, @@ -38,6 +35,7 @@ from homeassistant.const import ( POWER_WATT, TEMP_CELSIUS, ) +from homeassistant.helpers.entity import DeviceInfo from homeassistant.util import Throttle, dt from .const import ( @@ -979,11 +977,11 @@ class GrowattInverter(SensorEntity): self._attr_unique_id = unique_id self._attr_icon = "mdi:solar-power" - self._attr_device_info = { - ATTR_IDENTIFIERS: {(DOMAIN, probe.device_id)}, - ATTR_NAME: name, - ATTR_MANUFACTURER: "Growatt", - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, probe.device_id)}, + manufacturer="Growatt", + name=name, + ) @property def native_value(self): diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 4284d793764..3610d3d3d80 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -203,7 +203,7 @@ class GuardianEntity(CoordinatorEntity): self, entry: ConfigEntry, description: EntityDescription ) -> None: """Initialize.""" - self._attr_device_info = {"manufacturer": "Elexa"} + self._attr_device_info = DeviceInfo(manufacturer="Elexa") self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: "Data provided by Elexa"} self._entry = entry self.entity_description = description @@ -258,11 +258,11 @@ class ValveControllerEntity(GuardianEntity): """Initialize.""" super().__init__(entry, description) - self._attr_device_info = { - "identifiers": {(DOMAIN, entry.data[CONF_UID])}, - "name": f"Guardian Valve Controller {entry.data[CONF_UID]}", - "model": coordinators[API_SYSTEM_DIAGNOSTICS].data["firmware"], - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, entry.data[CONF_UID])}, + model=coordinators[API_SYSTEM_DIAGNOSTICS].data["firmware"], + name=f"Guardian Valve Controller {entry.data[CONF_UID]}", + ) self._attr_name = f"Guardian {entry.data[CONF_UID]}: {description.name}" self._attr_unique_id = f"{entry.data[CONF_UID]}_{description.key}" self.coordinators = coordinators From 513c90123e6fda1749a01aca15d5f30bc12d2efe Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 23 Oct 2021 12:03:14 +0200 Subject: [PATCH 0728/1038] Use DeviceInfo on components with suggested_area (#58225) Co-authored-by: epenet --- .../hunterdouglas_powerview/entity.py | 20 ++++++++-------- homeassistant/components/nuheat/climate.py | 17 +++++++------- homeassistant/components/roku/entity.py | 23 +++++++------------ homeassistant/components/tado/entity.py | 16 ++++++------- 4 files changed, 35 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/entity.py b/homeassistant/components/hunterdouglas_powerview/entity.py index a5a4fe852fa..810978ce9b3 100644 --- a/homeassistant/components/hunterdouglas_powerview/entity.py +++ b/homeassistant/components/hunterdouglas_powerview/entity.py @@ -38,21 +38,21 @@ class HDEntity(CoordinatorEntity): return self._unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" firmware = self._device_info[DEVICE_FIRMWARE] sw_version = f"{firmware[FIRMWARE_REVISION]}.{firmware[FIRMWARE_SUB_REVISION]}.{firmware[FIRMWARE_BUILD]}" - return { - "identifiers": {(DOMAIN, self._device_info[DEVICE_SERIAL_NUMBER])}, - "connections": { + return DeviceInfo( + identifiers={(DOMAIN, self._device_info[DEVICE_SERIAL_NUMBER])}, + connections={ (dr.CONNECTION_NETWORK_MAC, self._device_info[DEVICE_MAC_ADDRESS]) }, - "name": self._device_info[DEVICE_NAME], - "suggested_area": self._room_name, - "model": self._device_info[DEVICE_MODEL], - "sw_version": sw_version, - "manufacturer": MANUFACTURER, - } + name=self._device_info[DEVICE_NAME], + suggested_area=self._room_name, + model=self._device_info[DEVICE_MODEL], + sw_version=sw_version, + manufacturer=MANUFACTURER, + ) class ShadeEntity(HDEntity): diff --git a/homeassistant/components/nuheat/climate.py b/homeassistant/components/nuheat/climate.py index 35000dd21fa..0148e97ab4a 100644 --- a/homeassistant/components/nuheat/climate.py +++ b/homeassistant/components/nuheat/climate.py @@ -24,6 +24,7 @@ from homeassistant.components.climate.const import ( from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import callback from homeassistant.helpers import event as event_helper +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -284,12 +285,12 @@ class NuHeatThermostat(CoordinatorEntity, ClimateEntity): self.async_write_ha_state() @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN, self._thermostat.serial_number)}, - "name": self._thermostat.room, - "model": "nVent Signature", - "manufacturer": MANUFACTURER, - "suggested_area": self._thermostat.room, - } + return DeviceInfo( + identifiers={(DOMAIN, self._thermostat.serial_number)}, + name=self._thermostat.room, + model="nVent Signature", + manufacturer=MANUFACTURER, + suggested_area=self._thermostat.room, + ) diff --git a/homeassistant/components/roku/entity.py b/homeassistant/components/roku/entity.py index 5dc58d4b387..55e7c3a1ab3 100644 --- a/homeassistant/components/roku/entity.py +++ b/homeassistant/components/roku/entity.py @@ -1,13 +1,6 @@ """Base Entity for Roku.""" from __future__ import annotations -from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, - ATTR_SW_VERSION, -) from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -33,11 +26,11 @@ class RokuEntity(CoordinatorEntity): if self._device_id is None: return None - return { - ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)}, - ATTR_NAME: self.coordinator.data.info.name, - ATTR_MANUFACTURER: self.coordinator.data.info.brand, - ATTR_MODEL: self.coordinator.data.info.model_name, - ATTR_SW_VERSION: self.coordinator.data.info.version, - "suggested_area": self.coordinator.data.info.device_location, - } + return DeviceInfo( + identifiers={(DOMAIN, self._device_id)}, + name=self.coordinator.data.info.name, + manufacturer=self.coordinator.data.info.brand, + model=self.coordinator.data.info.model_name, + sw_version=self.coordinator.data.info.version, + suggested_area=self.coordinator.data.info.device_location, + ) diff --git a/homeassistant/components/tado/entity.py b/homeassistant/components/tado/entity.py index c10d0b50ab9..95c0643191b 100644 --- a/homeassistant/components/tado/entity.py +++ b/homeassistant/components/tado/entity.py @@ -63,15 +63,15 @@ class TadoZoneEntity(Entity): self.zone_id = zone_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN, self._device_zone_id)}, - "name": self.zone_name, - "manufacturer": DEFAULT_NAME, - "model": TADO_ZONE, - "suggested_area": self.zone_name, - } + return DeviceInfo( + identifiers={(DOMAIN, self._device_zone_id)}, + name=self.zone_name, + manufacturer=DEFAULT_NAME, + model=TADO_ZONE, + suggested_area=self.zone_name, + ) @property def should_poll(self): From e38754a83686b5fa6b3537f7b04353206a766ae3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 23 Oct 2021 12:14:15 +0200 Subject: [PATCH 0729/1038] Address Watson TTS review comment (#58277) --- homeassistant/components/watson_tts/tts.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py index b02235d4d45..93e6b98fbd6 100644 --- a/homeassistant/components/watson_tts/tts.py +++ b/homeassistant/components/watson_tts/tts.py @@ -191,9 +191,7 @@ class WatsonTTSProvider(Provider): def get_tts_audio(self, message, language=None, options=None): """Request TTS file from Watson TTS.""" response = self.service.synthesize( - text=message, - accept=self.output_format, - voice=options.get(CONF_VOICE, self.default_voice), + text=message, accept=self.output_format, voice=options[CONF_VOICE] ).get_result() return (CONTENT_TYPE_EXTENSIONS[self.output_format], response.content) From 2a7192167d4f06006ac72382e9cdd989bb20ab14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 23 Oct 2021 13:19:56 +0200 Subject: [PATCH 0730/1038] Use regex instead of partition to section package definition (#58278) --- script/hassfest/requirements.py | 13 +++++++++---- tests/hassfest/test_requirements.py | 7 +++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index 4d111265b1e..26cb834e4e2 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -22,7 +22,9 @@ from .model import Config, Integration IGNORE_PACKAGES = { commented.lower().replace("_", "-") for commented in COMMENT_REQUIREMENTS } -PACKAGE_REGEX = re.compile(r"^(?:--.+\s)?([-_\.\w\d]+).*==.+$") +PACKAGE_REGEX = re.compile( + r"^(?:--.+\s)?([-_\.\w\d\[\]]+)(==|>=|<=|~=|!=|<|>|===)*(.*)$" +) PIP_REGEX = re.compile(r"^(--.+\s)?([-_\.\w\d]+.*(?:==|>=|<=|~=|!=|<|>|===)?.*$)") SUPPORTED_PYTHON_TUPLES = [ REQUIRED_PYTHON_VER[:2], @@ -84,16 +86,19 @@ def validate_requirements_format(integration: Integration) -> bool: ) continue - pkg, sep, version = req.partition("==") + pkg, sep, version = PACKAGE_REGEX.match(req).groups() - if not sep and integration.core: + if integration.core and sep != "==": integration.add_error( "requirements", f'Requirement {req} need to be pinned "==".', ) continue - if AwesomeVersion(version).strategy == AwesomeVersionStrategy.UNKNOWN: + if ( + version + and AwesomeVersion(version).strategy == AwesomeVersionStrategy.UNKNOWN + ): integration.add_error( "requirements", f"Unable to parse package version ({version}) for {pkg}.", diff --git a/tests/hassfest/test_requirements.py b/tests/hassfest/test_requirements.py index c65716d5d92..079e77f909b 100644 --- a/tests/hassfest/test_requirements.py +++ b/tests/hassfest/test_requirements.py @@ -45,7 +45,7 @@ def test_validate_requirements_format_wrongly_pinned(integration: Integration): def test_validate_requirements_format_ignore_pin_for_custom(integration: Integration): """Test requirement ignore pinning for custom.""" - integration.manifest["requirements"] = ["test_package>=1"] + integration.manifest["requirements"] = ["test_package>=1", "test_package"] integration.path = Path("") assert validate_requirements_format(integration) assert len(integration.errors) == 0 @@ -63,6 +63,9 @@ def test_validate_requirements_format_invalid_version(integration: Integration): def test_validate_requirements_format_successful(integration: Integration): """Test requirement with successful result.""" - integration.manifest["requirements"] = ["test_package==1.2.3"] + integration.manifest["requirements"] = [ + "test_package==1.2.3", + "test_package[async]==1.2.3", + ] assert validate_requirements_format(integration) assert len(integration.errors) == 0 From 23e362faf387c1535be0abab81b30d8e4631df4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 23 Oct 2021 14:00:18 +0200 Subject: [PATCH 0731/1038] Bump awesomeversion from 21.8.1 to 21.10.1 (#58258) --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 82a19e54b6f..63472852399 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -7,7 +7,7 @@ astral==2.2 async-upnp-client==0.22.9 async_timeout==3.0.1 attrs==21.2.0 -awesomeversion==21.8.1 +awesomeversion==21.10.1 backports.zoneinfo;python_version<"3.9" bcrypt==3.1.7 certifi>=2021.5.30 diff --git a/requirements.txt b/requirements.txt index abe90479f01..84f3e342435 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ aiohttp==3.7.4.post0 astral==2.2 async_timeout==3.0.1 attrs==21.2.0 -awesomeversion==21.8.1 +awesomeversion==21.10.1 backports.zoneinfo;python_version<"3.9" bcrypt==3.1.7 certifi>=2021.5.30 diff --git a/setup.py b/setup.py index 38b05a6f0f3..2834a0b973d 100755 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ REQUIRES = [ "astral==2.2", "async_timeout==3.0.1", "attrs==21.2.0", - "awesomeversion==21.8.1", + "awesomeversion==21.10.1", 'backports.zoneinfo;python_version<"3.9"', "bcrypt==3.1.7", "certifi>=2021.5.30", From a4641a91ff12a70fae09ee07ecc0c83ec1148bff Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 23 Oct 2021 15:00:53 +0200 Subject: [PATCH 0732/1038] Ensure all devices show up in Tuya (#58280) --- homeassistant/components/tuya/__init__.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index d1e661fbe42..bcc5c9dc79c 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -16,7 +16,7 @@ from tuya_iot import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import device_registry +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import dispatcher_send from .const import ( @@ -116,7 +116,15 @@ async def _init_tuya_sdk(hass: HomeAssistant, entry: ConfigEntry) -> bool: await cleanup_device_registry(hass, device_manager) # Register known device IDs + device_registry = dr.async_get(hass) for device in device_manager.device_map.values(): + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, device.id)}, + manufacturer="Tuya", + name=device.name, + model=f"{device.product_name} (unsupported)", + ) device_ids.add(device.id) hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -127,11 +135,11 @@ async def cleanup_device_registry( hass: HomeAssistant, device_manager: TuyaDeviceManager ) -> None: """Remove deleted device registry entry if there are no remaining entities.""" - device_registry_object = device_registry.async_get(hass) - for dev_id, device_entry in list(device_registry_object.devices.items()): + device_registry = dr.async_get(hass) + for dev_id, device_entry in list(device_registry.devices.items()): for item in device_entry.identifiers: if DOMAIN == item[0] and item[1] not in device_manager.device_map: - device_registry_object.async_remove_device(dev_id) + device_registry.async_remove_device(dev_id) break @@ -198,10 +206,10 @@ class DeviceListener(TuyaDeviceListener): def async_remove_device(self, device_id: str) -> None: """Remove device from Home Assistant.""" _LOGGER.debug("Remove device: %s", device_id) - device_registry_object = device_registry.async_get(self.hass) - device_entry = device_registry_object.async_get_device( + device_registry = dr.async_get(self.hass) + device_entry = device_registry.async_get_device( identifiers={(DOMAIN, device_id)} ) if device_entry is not None: - device_registry_object.async_remove_device(device_entry.id) + device_registry.async_remove_device(device_entry.id) self.device_ids.discard(device_id) From cbf236b2f6164adb32333765e00338a765208303 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 23 Oct 2021 09:15:43 -0400 Subject: [PATCH 0733/1038] Fix modem callerid callback (#58275) * fix async_on_hass_stop * fix --- homeassistant/components/modem_callerid/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/modem_callerid/sensor.py b/homeassistant/components/modem_callerid/sensor.py index 372f9700201..a1df80f6bcb 100644 --- a/homeassistant/components/modem_callerid/sensor.py +++ b/homeassistant/components/modem_callerid/sensor.py @@ -12,7 +12,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, STATE_IDLE, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.typing import DiscoveryInfoType @@ -63,7 +63,7 @@ async def async_setup_entry( ] ) - async def _async_on_hass_stop() -> None: + async def _async_on_hass_stop(event: Event) -> None: """HA is shutting down, close modem port.""" if hass.data[DOMAIN][entry.entry_id][DATA_KEY_API]: await hass.data[DOMAIN][entry.entry_id][DATA_KEY_API].close() From f176dc512c2a9fa39701e3850b1c2080c64a71ca Mon Sep 17 00:00:00 2001 From: jrester <31157644+jrester@users.noreply.github.com> Date: Sat, 23 Oct 2021 15:35:23 +0200 Subject: [PATCH 0734/1038] Update tesla_powerwall to 0.3.12 (#58284) --- homeassistant/components/powerwall/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/powerwall/manifest.json b/homeassistant/components/powerwall/manifest.json index 802d1fdf5e3..5dcccadb681 100644 --- a/homeassistant/components/powerwall/manifest.json +++ b/homeassistant/components/powerwall/manifest.json @@ -3,7 +3,7 @@ "name": "Tesla Powerwall", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/powerwall", - "requirements": ["tesla-powerwall==0.3.11"], + "requirements": ["tesla-powerwall==0.3.12"], "codeowners": ["@bdraco", "@jrester"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 90e7bbd3208..04949830118 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2290,7 +2290,7 @@ temperusb==1.5.3 # tensorflow==2.3.0 # homeassistant.components.powerwall -tesla-powerwall==0.3.11 +tesla-powerwall==0.3.12 # homeassistant.components.tensorflow # tf-models-official==2.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6ea83e4a02c..c54b338ac58 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1321,7 +1321,7 @@ systembridge==2.1.3 tellduslive==0.10.11 # homeassistant.components.powerwall -tesla-powerwall==0.3.11 +tesla-powerwall==0.3.12 # homeassistant.components.toon toonapi==0.2.1 From 4a8e9df026095bb25b5d0d0b05754f96bd31b6ce Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 23 Oct 2021 09:35:33 -0400 Subject: [PATCH 0735/1038] Use DeviceInfo Class H (#58276) --- homeassistant/components/harmony/data.py | 17 +++++----- homeassistant/components/hassio/entity.py | 6 ++-- homeassistant/components/heos/media_player.py | 14 ++++----- .../components/hive/binary_sensor.py | 4 +-- homeassistant/components/hive/climate.py | 4 +-- homeassistant/components/hive/light.py | 4 +-- homeassistant/components/hive/sensor.py | 4 +-- homeassistant/components/hive/switch.py | 4 +-- homeassistant/components/hive/water_heater.py | 4 +-- .../components/home_connect/entity.py | 16 +++++----- .../components/home_plus_control/switch.py | 17 +++++----- .../components/homekit_controller/__init__.py | 2 +- .../homematicip_cloud/alarm_control_panel.py | 2 +- .../homematicip_cloud/binary_sensor.py | 6 ++-- .../components/homematicip_cloud/climate.py | 2 +- .../homematicip_cloud/generic_entity.py | 2 +- .../components/huawei_lte/__init__.py | 8 ++--- homeassistant/components/hue/light.py | 31 ++++++++++--------- homeassistant/components/hue/sensor_device.py | 2 +- .../hunterdouglas_powerview/entity.py | 6 ++-- .../hvv_departures/binary_sensor.py | 11 ++++--- .../components/hvv_departures/sensor.py | 11 ++++--- homeassistant/components/hyperion/camera.py | 12 +++---- homeassistant/components/hyperion/light.py | 12 +++---- homeassistant/components/hyperion/switch.py | 12 +++---- 25 files changed, 110 insertions(+), 103 deletions(-) diff --git a/homeassistant/components/harmony/data.py b/homeassistant/components/harmony/data.py index 78377265c07..706e06e881e 100644 --- a/homeassistant/components/harmony/data.py +++ b/homeassistant/components/harmony/data.py @@ -10,6 +10,7 @@ import aioharmony.exceptions as aioexc from aioharmony.harmonyapi import HarmonyAPI as HarmonyClient from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.entity import DeviceInfo from .const import ACTIVITY_POWER_OFF from .subscriber import HarmonySubscriberMixin @@ -82,20 +83,20 @@ class HarmonyData(HarmonySubscriberMixin): """Return the current activity tuple.""" return self._client.current_activity - def device_info(self, domain: str): + def device_info(self, domain: str) -> DeviceInfo: """Return hub device info.""" model = "Harmony Hub" if "ethernetStatus" in self._client.hub_config.info: model = "Harmony Hub Pro 2400" - return { - "identifiers": {(domain, self.unique_id)}, - "manufacturer": "Logitech", - "sw_version": self._client.hub_config.info.get( + return DeviceInfo( + identifiers={(domain, self.unique_id)}, + manufacturer="Logitech", + model=model, + name=self.name, + sw_version=self._client.hub_config.info.get( "hubSwVersion", self._client.fw_version ), - "name": self.name, - "model": model, - } + ) async def connect(self) -> bool: """Connect to the Harmony Hub.""" diff --git a/homeassistant/components/hassio/entity.py b/homeassistant/components/hassio/entity.py index 4a342e9965f..1fd2926fe56 100644 --- a/homeassistant/components/hassio/entity.py +++ b/homeassistant/components/hassio/entity.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any from homeassistant.const import ATTR_NAME -from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import DOMAIN, HassioDataUpdateCoordinator @@ -26,7 +26,7 @@ class HassioAddonEntity(CoordinatorEntity): self._addon_slug = addon[ATTR_SLUG] self._attr_name = f"{addon[ATTR_NAME]}: {entity_description.name}" self._attr_unique_id = f"{addon[ATTR_SLUG]}_{entity_description.key}" - self._attr_device_info = {"identifiers": {(DOMAIN, addon[ATTR_SLUG])}} + self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, addon[ATTR_SLUG])}) class HassioOSEntity(CoordinatorEntity): @@ -42,4 +42,4 @@ class HassioOSEntity(CoordinatorEntity): self.entity_description = entity_description self._attr_name = f"Home Assistant Operating System: {entity_description.name}" self._attr_unique_id = f"home_assistant_os_{entity_description.key}" - self._attr_device_info = {"identifiers": {(DOMAIN, "OS")}} + self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, "OS")}) diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 46a751983e9..a562d8e3d7a 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -255,13 +255,13 @@ class HeosMediaPlayer(MediaPlayerEntity): @property def device_info(self) -> DeviceInfo: """Get attributes about the device.""" - return { - "identifiers": {(HEOS_DOMAIN, self._player.player_id)}, - "name": self._player.name, - "model": self._player.model, - "manufacturer": "HEOS", - "sw_version": self._player.version, - } + return DeviceInfo( + identifiers={(HEOS_DOMAIN, self._player.player_id)}, + manufacturer="HEOS", + model=self._player.model, + name=self._player.name, + sw_version=self._player.version, + ) @property def extra_state_attributes(self) -> dict: diff --git a/homeassistant/components/hive/binary_sensor.py b/homeassistant/components/hive/binary_sensor.py index 71a2e787aec..cd1bef406d2 100644 --- a/homeassistant/components/hive/binary_sensor.py +++ b/homeassistant/components/hive/binary_sensor.py @@ -51,9 +51,9 @@ class HiveBinarySensorEntity(HiveEntity, BinarySensorEntity): """Return device information.""" return DeviceInfo( identifiers={(DOMAIN, self.device["device_id"])}, - name=self.device["device_name"], - model=self.device["deviceData"]["model"], manufacturer=self.device["deviceData"]["manufacturer"], + model=self.device["deviceData"]["model"], + name=self.device["device_name"], sw_version=self.device["deviceData"]["version"], via_device=(DOMAIN, self.device["parentDevice"]), ) diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index 930a6698518..0b038bbde0d 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -122,9 +122,9 @@ class HiveClimateEntity(HiveEntity, ClimateEntity): """Return device information.""" return DeviceInfo( identifiers={(DOMAIN, self.device["device_id"])}, - name=self.device["device_name"], - model=self.device["deviceData"]["model"], manufacturer=self.device["deviceData"]["manufacturer"], + model=self.device["deviceData"]["model"], + name=self.device["device_name"], sw_version=self.device["deviceData"]["version"], via_device=(DOMAIN, self.device["parentDevice"]), ) diff --git a/homeassistant/components/hive/light.py b/homeassistant/components/hive/light.py index 37158da7b58..f9d235d625f 100644 --- a/homeassistant/components/hive/light.py +++ b/homeassistant/components/hive/light.py @@ -45,9 +45,9 @@ class HiveDeviceLight(HiveEntity, LightEntity): """Return device information.""" return DeviceInfo( identifiers={(DOMAIN, self.device["device_id"])}, - name=self.device["device_name"], - model=self.device["deviceData"]["model"], manufacturer=self.device["deviceData"]["manufacturer"], + model=self.device["deviceData"]["model"], + name=self.device["device_name"], sw_version=self.device["deviceData"]["version"], via_device=(DOMAIN, self.device["parentDevice"]), ) diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index abb45d04dec..bca144cd59c 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -40,9 +40,9 @@ class HiveSensorEntity(HiveEntity, SensorEntity): """Return device information.""" return DeviceInfo( identifiers={(DOMAIN, self.device["device_id"])}, - name=self.device["device_name"], - model=self.device["deviceData"]["model"], manufacturer=self.device["deviceData"]["manufacturer"], + model=self.device["deviceData"]["model"], + name=self.device["device_name"], sw_version=self.device["deviceData"]["version"], via_device=(DOMAIN, self.device["parentDevice"]), ) diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py index 9c009e1627c..adfcfa442ee 100644 --- a/homeassistant/components/hive/switch.py +++ b/homeassistant/components/hive/switch.py @@ -39,9 +39,9 @@ class HiveDevicePlug(HiveEntity, SwitchEntity): if self.device["hiveType"] == "activeplug": return DeviceInfo( identifiers={(DOMAIN, self.device["device_id"])}, - name=self.device["device_name"], - model=self.device["deviceData"]["model"], manufacturer=self.device["deviceData"]["manufacturer"], + model=self.device["deviceData"]["model"], + name=self.device["device_name"], sw_version=self.device["deviceData"]["version"], via_device=(DOMAIN, self.device["parentDevice"]), ) diff --git a/homeassistant/components/hive/water_heater.py b/homeassistant/components/hive/water_heater.py index 8cefda074b6..096fc468ecb 100644 --- a/homeassistant/components/hive/water_heater.py +++ b/homeassistant/components/hive/water_heater.py @@ -83,9 +83,9 @@ class HiveWaterHeater(HiveEntity, WaterHeaterEntity): """Return device information.""" return DeviceInfo( identifiers={(DOMAIN, self.device["device_id"])}, - name=self.device["device_name"], - model=self.device["deviceData"]["model"], manufacturer=self.device["deviceData"]["manufacturer"], + model=self.device["deviceData"]["model"], + name=self.device["device_name"], sw_version=self.device["deviceData"]["version"], via_device=(DOMAIN, self.device["parentDevice"]), ) diff --git a/homeassistant/components/home_connect/entity.py b/homeassistant/components/home_connect/entity.py index 12f86059023..b27988f997d 100644 --- a/homeassistant/components/home_connect/entity.py +++ b/homeassistant/components/home_connect/entity.py @@ -4,7 +4,7 @@ import logging from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .api import HomeConnectDevice from .const import DOMAIN, SIGNAL_UPDATE_ENTITIES @@ -51,14 +51,14 @@ class HomeConnectEntity(Entity): return f"{self.device.appliance.haId}-{self.desc}" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return info about the device.""" - return { - "identifiers": {(DOMAIN, self.device.appliance.haId)}, - "name": self.device.appliance.name, - "manufacturer": self.device.appliance.brand, - "model": self.device.appliance.vib, - } + return DeviceInfo( + identifiers={(DOMAIN, self.device.appliance.haId)}, + manufacturer=self.device.appliance.brand, + model=self.device.appliance.vib, + name=self.device.appliance.name, + ) @callback def async_entity_update(self): diff --git a/homeassistant/components/home_plus_control/switch.py b/homeassistant/components/home_plus_control/switch.py index d4167ae1f9e..809a246e631 100644 --- a/homeassistant/components/home_plus_control/switch.py +++ b/homeassistant/components/home_plus_control/switch.py @@ -8,6 +8,7 @@ from homeassistant.components.switch import ( ) from homeassistant.core import callback from homeassistant.helpers import dispatcher +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DISPATCHER_REMOVERS, DOMAIN, HW_TYPE, SIGNAL_ADD_ENTITIES @@ -79,18 +80,18 @@ class HomeControlSwitchEntity(CoordinatorEntity, SwitchEntity): return self.idx @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Device information.""" - return { - "identifiers": { + return DeviceInfo( + identifiers={ # Unique identifiers within the domain (DOMAIN, self.unique_id) }, - "name": self.name, - "manufacturer": "Legrand", - "model": HW_TYPE.get(self.module.hw_type), - "sw_version": self.module.fw, - } + manufacturer="Legrand", + model=HW_TYPE.get(self.module.hw_type), + name=self.name, + sw_version=self.module.fw, + ) @property def device_class(self): diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 92853faedad..d80b849bf80 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -152,9 +152,9 @@ class HomeKitEntity(Entity): device_info = DeviceInfo( identifiers={(DOMAIN, "serial-number", accessory_serial)}, - name=info.value(CharacteristicsTypes.NAME), manufacturer=info.value(CharacteristicsTypes.MANUFACTURER, ""), model=info.value(CharacteristicsTypes.MODEL, ""), + name=info.value(CharacteristicsTypes.NAME), sw_version=info.value(CharacteristicsTypes.FIRMWARE_REVISION, ""), ) diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index 6249b004d23..d2dee3c3744 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -49,9 +49,9 @@ class HomematicipAlarmControlPanelEntity(AlarmControlPanelEntity): """Return device specific attributes.""" return DeviceInfo( identifiers={(HMIPC_DOMAIN, f"ACP {self._home.id}")}, - name=self.name, manufacturer="eQ-3", model=CONST_ALARM_CONTROL_PANEL_NAME, + name=self.name, via_device=(HMIPC_DOMAIN, self._home.id), ) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 3cf8fc72c56..b1261258bf4 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -172,12 +172,12 @@ class HomematicipCloudConnectionSensor(HomematicipGenericEntity, BinarySensorEnt def device_info(self) -> DeviceInfo: """Return device specific attributes.""" # Adds a sensor to the existing HAP device - return { - "identifiers": { + return DeviceInfo( + identifiers={ # Serial numbers of Homematic IP device (HMIPC_DOMAIN, self._home.id) } - } + ) @property def icon(self) -> str: diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index df1c3d11f31..ed91559e489 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -78,9 +78,9 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity): """Return device specific attributes.""" return DeviceInfo( identifiers={(HMIPC_DOMAIN, self._device.id)}, - name=self._device.label, manufacturer="eQ-3", model=self._device.modelType, + name=self._device.label, via_device=(HMIPC_DOMAIN, self._device.homeId), ) diff --git a/homeassistant/components/homematicip_cloud/generic_entity.py b/homeassistant/components/homematicip_cloud/generic_entity.py index 2de3ded42db..ecf0549d8b8 100644 --- a/homeassistant/components/homematicip_cloud/generic_entity.py +++ b/homeassistant/components/homematicip_cloud/generic_entity.py @@ -101,9 +101,9 @@ class HomematicipGenericEntity(Entity): # Serial numbers of Homematic IP device (HMIPC_DOMAIN, self._device.id) }, - name=self._device.label, manufacturer=self._device.oem, model=self._device.modelType, + name=self._device.label, sw_version=self._device.firmwareVersion, # Link to the homematic ip access point. via_device=(HMIPC_DOMAIN, self._device.homeId), diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 9ee5927695d..b1e95bcd07a 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -651,10 +651,10 @@ class HuaweiLteBaseEntity(Entity): @property def device_info(self) -> DeviceInfo: """Get info for matching with parent router.""" - return { - "identifiers": self.router.device_identifiers, - "connections": self.router.device_connections, - } + return DeviceInfo( + connections=self.router.device_connections, + identifiers=self.router.device_identifiers, + ) async def async_update(self) -> None: """Update state.""" diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index cc3144b99ca..13a3a70ae53 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -1,4 +1,6 @@ """Support for the Philips Hue lights.""" +from __future__ import annotations + from datetime import timedelta from functools import partial import logging @@ -29,6 +31,7 @@ from homeassistant.components.light import ( from homeassistant.core import callback from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -431,7 +434,7 @@ class HueLight(CoordinatorEntity, LightEntity): return [EFFECT_COLORLOOP, EFFECT_RANDOM] @property - def device_info(self): + def device_info(self) -> DeviceInfo | None: """Return the device info.""" if self.light.type in ( GROUP_TYPE_LIGHT_GROUP, @@ -441,22 +444,22 @@ class HueLight(CoordinatorEntity, LightEntity): ): return None - info = { - "identifiers": {(HUE_DOMAIN, self.device_id)}, - "name": self.name, - "manufacturer": self.light.manufacturername, + suggested_area = None + if self.light.id in self._rooms: + suggested_area = self._rooms[self.light.id] + + return DeviceInfo( + identifiers={(HUE_DOMAIN, self.device_id)}, + manufacturer=self.light.manufacturername, # productname added in Hue Bridge API 1.24 # (published 03/05/2018) - "model": self.light.productname or self.light.modelid, + model=self.light.productname or self.light.modelid, + name=self.name, # Not yet exposed as properties in aiohue - "sw_version": self.light.raw["swversion"], - "via_device": (HUE_DOMAIN, self.bridge.api.config.bridgeid), - } - - if self.light.id in self._rooms: - info["suggested_area"] = self._rooms[self.light.id] - - return info + suggested_area=suggested_area, + sw_version=self.light.raw["swversion"], + via_device=(HUE_DOMAIN, self.bridge.api.config.bridgeid), + ) async def async_added_to_hass(self) -> None: """Handle entity being added to Home Assistant.""" diff --git a/homeassistant/components/hue/sensor_device.py b/homeassistant/components/hue/sensor_device.py index 4efb89a913f..92c586ff8e0 100644 --- a/homeassistant/components/hue/sensor_device.py +++ b/homeassistant/components/hue/sensor_device.py @@ -47,9 +47,9 @@ class GenericHueDevice(entity.Entity): """ return entity.DeviceInfo( identifiers={(HUE_DOMAIN, self.device_id)}, - name=self.primary_sensor.name, manufacturer=self.primary_sensor.manufacturername, model=(self.primary_sensor.productname or self.primary_sensor.modelid), + name=self.primary_sensor.name, sw_version=self.primary_sensor.swversion, via_device=(HUE_DOMAIN, self.bridge.api.config.bridgeid), ) diff --git a/homeassistant/components/hunterdouglas_powerview/entity.py b/homeassistant/components/hunterdouglas_powerview/entity.py index 810978ce9b3..50894d59f8b 100644 --- a/homeassistant/components/hunterdouglas_powerview/entity.py +++ b/homeassistant/components/hunterdouglas_powerview/entity.py @@ -43,15 +43,15 @@ class HDEntity(CoordinatorEntity): firmware = self._device_info[DEVICE_FIRMWARE] sw_version = f"{firmware[FIRMWARE_REVISION]}.{firmware[FIRMWARE_SUB_REVISION]}.{firmware[FIRMWARE_BUILD]}" return DeviceInfo( - identifiers={(DOMAIN, self._device_info[DEVICE_SERIAL_NUMBER])}, connections={ (dr.CONNECTION_NETWORK_MAC, self._device_info[DEVICE_MAC_ADDRESS]) }, + identifiers={(DOMAIN, self._device_info[DEVICE_SERIAL_NUMBER])}, + manufacturer=MANUFACTURER, + model=self._device_info[DEVICE_MODEL], name=self._device_info[DEVICE_NAME], suggested_area=self._room_name, - model=self._device_info[DEVICE_MODEL], sw_version=sw_version, - manufacturer=MANUFACTURER, ) diff --git a/homeassistant/components/hvv_departures/binary_sensor.py b/homeassistant/components/hvv_departures/binary_sensor.py index 45ac0e45ad9..a3494a2b6d8 100644 --- a/homeassistant/components/hvv_departures/binary_sensor.py +++ b/homeassistant/components/hvv_departures/binary_sensor.py @@ -11,6 +11,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -145,8 +146,8 @@ class HvvDepartureBinarySensor(CoordinatorEntity, BinarySensorEntity): @property def device_info(self): """Return the device info for this sensor.""" - return { - "identifiers": { + return DeviceInfo( + identifiers={ ( DOMAIN, self.config_entry.entry_id, @@ -154,9 +155,9 @@ class HvvDepartureBinarySensor(CoordinatorEntity, BinarySensorEntity): self.config_entry.data[CONF_STATION]["type"], ) }, - "name": f"Departures at {self.config_entry.data[CONF_STATION]['name']}", - "manufacturer": MANUFACTURER, - } + manufacturer=MANUFACTURER, + name=f"Departures at {self.config_entry.data[CONF_STATION]['name']}", + ) @property def name(self): diff --git a/homeassistant/components/hvv_departures/sensor.py b/homeassistant/components/hvv_departures/sensor.py index 8a188f7dde8..da52fd878d8 100644 --- a/homeassistant/components/hvv_departures/sensor.py +++ b/homeassistant/components/hvv_departures/sensor.py @@ -8,6 +8,7 @@ from pygti.exceptions import InvalidAuth from homeassistant.components.sensor import SensorEntity from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ID, DEVICE_CLASS_TIMESTAMP from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.entity import DeviceInfo from homeassistant.util import Throttle from homeassistant.util.dt import get_time_zone, utcnow @@ -158,8 +159,8 @@ class HVVDepartureSensor(SensorEntity): @property def device_info(self): """Return the device info for this sensor.""" - return { - "identifiers": { + return DeviceInfo( + identifiers={ ( DOMAIN, self.config_entry.entry_id, @@ -167,9 +168,9 @@ class HVVDepartureSensor(SensorEntity): self.config_entry.data[CONF_STATION]["type"], ) }, - "name": self._name, - "manufacturer": MANUFACTURER, - } + manufacturer=MANUFACTURER, + name=self._name, + ) @property def name(self): diff --git a/homeassistant/components/hyperion/camera.py b/homeassistant/components/hyperion/camera.py index d172d00c021..c1763c3c21c 100644 --- a/homeassistant/components/hyperion/camera.py +++ b/homeassistant/components/hyperion/camera.py @@ -249,12 +249,12 @@ class HyperionCamera(Camera): @property def device_info(self) -> DeviceInfo: """Return device information.""" - return { - "identifiers": {(DOMAIN, self._device_id)}, - "name": self._instance_name, - "manufacturer": HYPERION_MANUFACTURER_NAME, - "model": HYPERION_MODEL_NAME, - } + return DeviceInfo( + identifiers={(DOMAIN, self._device_id)}, + manufacturer=HYPERION_MANUFACTURER_NAME, + model=HYPERION_MODEL_NAME, + name=self._instance_name, + ) CAMERA_TYPES = { diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py index d27e96e85de..0c0070d0c23 100644 --- a/homeassistant/components/hyperion/light.py +++ b/homeassistant/components/hyperion/light.py @@ -240,12 +240,12 @@ class HyperionBaseLight(LightEntity): @property def device_info(self) -> DeviceInfo: """Return device information.""" - return { - "identifiers": {(DOMAIN, self._device_id)}, - "name": self._instance_name, - "manufacturer": HYPERION_MANUFACTURER_NAME, - "model": HYPERION_MODEL_NAME, - } + return DeviceInfo( + identifiers={(DOMAIN, self._device_id)}, + manufacturer=HYPERION_MANUFACTURER_NAME, + model=HYPERION_MODEL_NAME, + name=self._instance_name, + ) def _get_option(self, key: str) -> Any: """Get a value from the provided options.""" diff --git a/homeassistant/components/hyperion/switch.py b/homeassistant/components/hyperion/switch.py index d36fcd79836..1c884cca908 100644 --- a/homeassistant/components/hyperion/switch.py +++ b/homeassistant/components/hyperion/switch.py @@ -185,12 +185,12 @@ class HyperionComponentSwitch(SwitchEntity): @property def device_info(self) -> DeviceInfo: """Return device information.""" - return { - "identifiers": {(DOMAIN, self._device_id)}, - "name": self._instance_name, - "manufacturer": HYPERION_MANUFACTURER_NAME, - "model": HYPERION_MODEL_NAME, - } + return DeviceInfo( + identifiers={(DOMAIN, self._device_id)}, + manufacturer=HYPERION_MANUFACTURER_NAME, + model=HYPERION_MODEL_NAME, + name=self._instance_name, + ) async def _async_send_set_component(self, value: bool) -> None: """Send a component control request.""" From 581d9ec281dcf36a31dd1083b2c3e6fda04652c7 Mon Sep 17 00:00:00 2001 From: ANMalko Date: Sat, 23 Oct 2021 17:17:25 +0300 Subject: [PATCH 0736/1038] Update aiolookin to 0.0.3 version (#58249) --- homeassistant/components/lookin/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lookin/manifest.json b/homeassistant/components/lookin/manifest.json index 2307c89f3aa..046b0e482a1 100644 --- a/homeassistant/components/lookin/manifest.json +++ b/homeassistant/components/lookin/manifest.json @@ -3,7 +3,7 @@ "name": "LOOKin", "documentation": "https://www.home-assistant.io/integrations/lookin/", "codeowners": ["@ANMalko"], - "requirements": ["aiolookin==0.0.2"], + "requirements": ["aiolookin==0.0.3"], "zeroconf": ["_lookin._tcp.local."], "config_flow": true, "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 04949830118..74435306616 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -207,7 +207,7 @@ aiolifx_effects==0.2.2 aiolip==1.1.6 # homeassistant.components.lookin -aiolookin==0.0.2 +aiolookin==0.0.3 # homeassistant.components.lyric aiolyric==1.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c54b338ac58..9bc6da885d6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -137,7 +137,7 @@ aiokafka==0.6.0 aiolip==1.1.6 # homeassistant.components.lookin -aiolookin==0.0.2 +aiolookin==0.0.3 # homeassistant.components.lyric aiolyric==1.0.7 From 0a272669ed22d01a50e66e87c1fb1412a3f84c01 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 23 Oct 2021 18:15:12 +0200 Subject: [PATCH 0737/1038] Add CO Detector (cobj) device support to Tuya (#58292) --- homeassistant/components/tuya/binary_sensor.py | 15 +++++++++++++++ homeassistant/components/tuya/const.py | 9 ++++++--- homeassistant/components/tuya/sensor.py | 12 ++++++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index ad026d2cef0..202362da0d9 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -62,6 +62,21 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { ), TAMPER_BINARY_SENSOR, ), + # CO Detector + # https://developer.tuya.com/en/docs/iot/categorycobj?id=Kaiuz3u1j6q1v + "cobj": ( + TuyaBinarySensorEntityDescription( + key=DPCode.CO_STATE, + device_class=DEVICE_CLASS_SAFETY, + on_value="1", + ), + TuyaBinarySensorEntityDescription( + key=DPCode.CO_STATUS, + device_class=DEVICE_CLASS_SAFETY, + on_value="alarm", + ), + TAMPER_BINARY_SENSOR, + ), # Human Presence Sensor # https://developer.tuya.com/en/docs/iot/categoryhps?id=Kaiuz42yhn1hs "hps": ( diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 276debe6b96..eb2f10429a0 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -150,10 +150,10 @@ class DPCode(str, Enum): CH2O_STATE = "ch2o_state" CH2O_VALUE = "ch2o_value" CHILD_LOCK = "child_lock" # Child lock + CO_STATE = "co_state" + CO_STATUS = "co_status" + CO_VALUE = "co_value" CO2_STATE = "co2_state" - SMOKE_SENSOR_STATUS = "smoke_sensor_status" - SMOKE_SENSOR_STATE = "smoke_sensor_state" - SMOKE_SENSOR_VALUE = "smoke_sensor_value" CO2_VALUE = "co2_value" # CO2 concentration COLOR_DATA_V2 = "color_data_v2" COLOUR_DATA = "colour_data" # Colored light mode @@ -211,6 +211,9 @@ class DPCode(str, Enum): SHAKE = "shake" # Oscillating SHOCK_STATE = "shock_state" # Vibration status SITUATION_SET = "situation_set" + SMOKE_SENSOR_STATE = "smoke_sensor_state" + SMOKE_SENSOR_STATUS = "smoke_sensor_status" + SMOKE_SENSOR_VALUE = "smoke_sensor_value" SOS = "sos" # Emergency State SOS_STATE = "sos_state" # Emergency mode SPEED = "speed" # Speed level diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 6624635fe18..bea3b52a26e 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -14,6 +14,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + DEVICE_CLASS_CO, DEVICE_CLASS_CO2, DEVICE_CLASS_CURRENT, DEVICE_CLASS_HUMIDITY, @@ -94,6 +95,17 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { ), *BATTERY_SENSORS, ), + # CO Detector + # https://developer.tuya.com/en/docs/iot/categorycobj?id=Kaiuz3u1j6q1v + "cobj": ( + SensorEntityDescription( + key=DPCode.CO_VALUE, + name="Carbon Monoxide", + device_class=DEVICE_CLASS_CO, + state_class=STATE_CLASS_MEASUREMENT, + ), + *BATTERY_SENSORS, + ), # Switch # https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s "kg": ( From 3cdfb84b79e7e5b8b1ebb1b7da61f1dca180bc03 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 23 Oct 2021 18:15:52 +0200 Subject: [PATCH 0738/1038] Add Gas Detector (rqbj) device support to Tuya (#58293) --- homeassistant/components/tuya/binary_sensor.py | 16 ++++++++++++++++ homeassistant/components/tuya/const.py | 3 +++ homeassistant/components/tuya/sensor.py | 10 ++++++++++ 3 files changed, 29 insertions(+) diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index 202362da0d9..33a0eef3572 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -7,6 +7,7 @@ from tuya_iot import TuyaDevice, TuyaDeviceManager from homeassistant.components.binary_sensor import ( DEVICE_CLASS_DOOR, + DEVICE_CLASS_GAS, DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, DEVICE_CLASS_SAFETY, @@ -126,6 +127,21 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { ), TAMPER_BINARY_SENSOR, ), + # Gas Detector + # https://developer.tuya.com/en/docs/iot/categoryrqbj?id=Kaiuz3d162ubw + "rqbj": ( + TuyaBinarySensorEntityDescription( + key=DPCode.GAS_SENSOR_STATUS, + device_class=DEVICE_CLASS_GAS, + on_value="alarm", + ), + TuyaBinarySensorEntityDescription( + key=DPCode.GAS_SENSOR_STATE, + device_class=DEVICE_CLASS_GAS, + on_value="1", + ), + TAMPER_BINARY_SENSOR, + ), # Water Detector # https://developer.tuya.com/en/docs/iot/categorysj?id=Kaiuz3iub2sli "sj": ( diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index eb2f10429a0..0f42bc9265c 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -177,6 +177,9 @@ class DPCode(str, Enum): FAN_SPEED_PERCENT = "fan_speed_percent" # Stepless speed FAR_DETECTION = "far_detection" FILTER_RESET = "filter_reset" # Filter (cartridge) reset + GAS_SENSOR_STATE = "gas_sensor_state" + GAS_SENSOR_STATUS = "gas_sensor_status" + GAS_SENSOR_VALUE = "gas_sensor_value" HUMIDITY_CURRENT = "humidity_current" # Current humidity HUMIDITY_SET = "humidity_set" # Humidity setting HUMIDITY_VALUE = "humidity_value" # Humidity diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index bea3b52a26e..eddbd650688 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -211,6 +211,16 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { # PIR Detector # https://developer.tuya.com/en/docs/iot/categorypir?id=Kaiuz3ss11b80 "pir": BATTERY_SENSORS, + # Gas Detector + # https://developer.tuya.com/en/docs/iot/categoryrqbj?id=Kaiuz3d162ubw + "rqbj": ( + SensorEntityDescription( + key=DPCode.GAS_SENSOR_VALUE, + icon="mdi:gas-cylinder", + device_class=STATE_CLASS_MEASUREMENT, + ), + *BATTERY_SENSORS, + ), # Water Detector # https://developer.tuya.com/en/docs/iot/categorysj?id=Kaiuz3iub2sli "sj": BATTERY_SENSORS, From 7d0fc8ca982b570452b3418a4e7db69f965e4b37 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 23 Oct 2021 18:16:28 +0200 Subject: [PATCH 0739/1038] Add Pressure Sensor (ylcg) device support to Tuya (#58294) --- homeassistant/components/tuya/binary_sensor.py | 9 +++++++++ homeassistant/components/tuya/const.py | 2 ++ homeassistant/components/tuya/sensor.py | 11 +++++++++++ 3 files changed, 22 insertions(+) diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index 33a0eef3572..a8627d17eae 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -161,6 +161,15 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { ), TAMPER_BINARY_SENSOR, ), + # Pressure Sensor + # https://developer.tuya.com/en/docs/iot/categoryylcg?id=Kaiuz3kc2e4gm + "ylcg": ( + TuyaBinarySensorEntityDescription( + key=DPCode.PRESSURE_STATE, + on_value="alarm", + ), + TAMPER_BINARY_SENSOR, + ), # Smoke Detector # https://developer.tuya.com/en/docs/iot/categoryywbj?id=Kaiuz3f6sf952 "ywbj": ( diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 0f42bc9265c..99f1757c4af 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -206,6 +206,8 @@ class DPCode(str, Enum): POWDER_SET = "powder_set" # Powder POWER_GO = "power_go" PRESENCE_STATE = "presence_state" + PRESSURE_STATE = "pressure_state" + PRESSURE_VALUE = "pressure_value" PUMP_RESET = "pump_reset" # Water pump reset RECORD_SWITCH = "record_switch" # Recording switch RELAY_STATUS = "relay_status" diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index eddbd650688..19bb2ff6a90 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -21,6 +21,7 @@ from homeassistant.const import ( DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_PM25, DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, DEVICE_CLASS_VOLTAGE, @@ -227,6 +228,16 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { # Emergency Button # https://developer.tuya.com/en/docs/iot/categorysos?id=Kaiuz3oi6agjy "sos": BATTERY_SENSORS, + # Pressure Sensor + # https://developer.tuya.com/en/docs/iot/categoryylcg?id=Kaiuz3kc2e4gm + "ylcg": ( + SensorEntityDescription( + key=DPCode.PRESSURE_VALUE, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + *BATTERY_SENSORS, + ), # Smoke Detector # https://developer.tuya.com/en/docs/iot/categoryywbj?id=Kaiuz3f6sf952 "ywbj": ( From 43abf38d929cc364d231a64fcc3a1220d819abf7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 23 Oct 2021 20:06:46 +0200 Subject: [PATCH 0740/1038] Complete Heater (qn) device support to Tuya (#58296) --- homeassistant/components/tuya/const.py | 1 + homeassistant/components/tuya/sensor.py | 10 ++++++++++ homeassistant/components/tuya/switch.py | 16 ++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 99f1757c4af..364febe5703 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -271,6 +271,7 @@ class DPCode(str, Enum): WATERSENSOR_STATE = "watersensor_state" WET = "wet" # Humidification WORK_MODE = "work_mode" # Working mode + WORK_POWER = "work_power" @dataclass diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 19bb2ff6a90..2e0b1562ff1 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -212,6 +212,16 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { # PIR Detector # https://developer.tuya.com/en/docs/iot/categorypir?id=Kaiuz3ss11b80 "pir": BATTERY_SENSORS, + # Heater + # https://developer.tuya.com/en/docs/iot/categoryqn?id=Kaiuz18kih0sm + "qn": ( + SensorEntityDescription( + key=DPCode.WORK_POWER, + name="Power", + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + ), # Gas Detector # https://developer.tuya.com/en/docs/iot/categoryrqbj?id=Kaiuz3d162ubw "rqbj": ( diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 2201ce30539..8f5b2715731 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -247,6 +247,22 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { device_class=DEVICE_CLASS_OUTLET, ), ), + # Heater + # https://developer.tuya.com/en/docs/iot/categoryqn?id=Kaiuz18kih0sm + "qn": ( + SwitchEntityDescription( + key=DPCode.ANION, + name="Ionizer", + icon="mdi:minus-circle-outline", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + SwitchEntityDescription( + key=DPCode.LOCK, + name="Child Lock", + icon="mdi:account-lock", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), # Siren Alarm # https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu "sgbj": ( From 583ae3c953261756382dc0268e70a8be7e553a03 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 23 Oct 2021 12:18:34 -0600 Subject: [PATCH 0741/1038] Make sure Flu Near You data storage conforms to standards (#57808) --- .../components/flunearyou/__init__.py | 20 +++++++------------ homeassistant/components/flunearyou/const.py | 2 -- homeassistant/components/flunearyou/sensor.py | 4 ++-- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/flunearyou/__init__.py b/homeassistant/components/flunearyou/__init__.py index 22de54180a6..86a86e440c9 100644 --- a/homeassistant/components/flunearyou/__init__.py +++ b/homeassistant/components/flunearyou/__init__.py @@ -15,13 +15,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import ( - CATEGORY_CDC_REPORT, - CATEGORY_USER_REPORT, - DATA_COORDINATOR, - DOMAIN, - LOGGER, -) +from .const import CATEGORY_CDC_REPORT, CATEGORY_USER_REPORT, DOMAIN, LOGGER DEFAULT_UPDATE_INTERVAL = timedelta(minutes=30) @@ -32,8 +26,7 @@ PLATFORMS = ["sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Flu Near You as config entry.""" - hass.data.setdefault(DOMAIN, {DATA_COORDINATOR: {}}) - hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = {} + hass.data.setdefault(DOMAIN, {}) websession = aiohttp_client.async_get_clientsession(hass) client = Client(session=websession) @@ -57,11 +50,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return data + coordinators = {} data_init_tasks = [] + for api_category in (CATEGORY_CDC_REPORT, CATEGORY_USER_REPORT): - coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ - api_category - ] = DataUpdateCoordinator( + coordinator = coordinators[api_category] = DataUpdateCoordinator( hass, LOGGER, name=f"{api_category} ({latitude}, {longitude})", @@ -71,6 +64,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data_init_tasks.append(coordinator.async_refresh()) await asyncio.gather(*data_init_tasks) + hass.data[DOMAIN][entry.entry_id] = coordinators hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -81,6 +75,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload an Flu Near You config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/flunearyou/const.py b/homeassistant/components/flunearyou/const.py index 96df29aa300..dc9ac629d92 100644 --- a/homeassistant/components/flunearyou/const.py +++ b/homeassistant/components/flunearyou/const.py @@ -4,7 +4,5 @@ import logging DOMAIN = "flunearyou" LOGGER = logging.getLogger(__package__) -DATA_COORDINATOR = "coordinator" - CATEGORY_CDC_REPORT = "cdc_report" CATEGORY_USER_REPORT = "user_report" diff --git a/homeassistant/components/flunearyou/sensor.py b/homeassistant/components/flunearyou/sensor.py index 1a7aba5966b..a30c2423253 100644 --- a/homeassistant/components/flunearyou/sensor.py +++ b/homeassistant/components/flunearyou/sensor.py @@ -24,7 +24,7 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .const import CATEGORY_CDC_REPORT, CATEGORY_USER_REPORT, DATA_COORDINATOR, DOMAIN +from .const import CATEGORY_CDC_REPORT, CATEGORY_USER_REPORT, DOMAIN ATTR_CITY = "city" ATTR_REPORTED_DATE = "reported_date" @@ -122,7 +122,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Flu Near You sensors based on a config entry.""" - coordinators = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] + coordinators = hass.data[DOMAIN][entry.entry_id] sensors: list[CdcSensor | UserSensor] = [ CdcSensor(coordinators[CATEGORY_CDC_REPORT], entry, description) From b52c5c82b18e016f3420cd7cf2f4d0c6f1b8b36b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 23 Oct 2021 21:34:53 +0300 Subject: [PATCH 0742/1038] Use http.HTTPStatus in components/[gh]* (#58246) --- .../components/google_assistant/helpers.py | 3 +- .../components/google_assistant/http.py | 9 ++- homeassistant/components/habitica/sensor.py | 7 ++- .../components/hangouts/hangouts_bot.py | 4 +- homeassistant/components/hassio/handler.py | 5 +- .../components/haveibeenpwned/sensor.py | 13 ++-- .../components/hitron_coda/device_tracker.py | 13 ++-- homeassistant/components/http/ban.py | 6 +- homeassistant/components/http/view.py | 4 +- homeassistant/components/hue/bridge.py | 4 +- .../garages_amsterdam/test_config_flow.py | 6 +- tests/components/generic/test_camera.py | 41 ++++++------ tests/components/geofency/test_init.py | 28 ++++----- .../google_assistant/test_google_assistant.py | 14 +++-- .../google_assistant/test_helpers.py | 9 +-- .../components/google_assistant/test_http.py | 9 +-- .../components/google_assistant/test_init.py | 6 +- tests/components/google_wifi/test_sensor.py | 7 ++- tests/components/gpslogger/test_init.py | 26 ++++---- tests/components/habitica/test_init.py | 4 +- tests/components/hassio/test_addon_panel.py | 5 +- tests/components/hassio/test_auth.py | 23 +++---- tests/components/hassio/test_discovery.py | 3 +- tests/components/hassio/test_http.py | 25 ++++---- tests/components/hassio/test_ingress.py | 16 ++--- tests/components/history/test_init.py | 23 +++---- .../home_connect/test_config_flow.py | 3 +- .../home_plus_control/test_config_flow.py | 5 +- tests/components/html5/test_notify.py | 32 +++++----- tests/components/http/test_auth.py | 63 +++++++++++-------- tests/components/http/test_ban.py | 25 ++++---- tests/components/http/test_cors.py | 15 ++--- tests/components/http/test_data_validator.py | 13 ++-- tests/components/http/test_forwarded.py | 35 ++++++----- tests/components/http/test_init.py | 3 +- tests/components/http/test_request_context.py | 3 +- tests/components/http/test_security_filter.py | 6 +- tests/components/http/test_view.py | 3 +- 38 files changed, 272 insertions(+), 247 deletions(-) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 67f4e0c60df..14667dbb303 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -4,6 +4,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from asyncio import gather from collections.abc import Mapping +from http import HTTPStatus import logging import pprint @@ -203,7 +204,7 @@ class AbstractConfig(ABC): # Remove any pending sync self._google_sync_unsub.pop(agent_user_id, lambda: None)() status = await self._async_request_sync_devices(agent_user_id) - if status == 404: + if status == HTTPStatus.NOT_FOUND: await self.async_disconnect_agent_user(agent_user_id) return status diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index d5489aad05a..ba7dc2597bc 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -1,6 +1,7 @@ """Support for Google Actions Smart Home Control.""" import asyncio from datetime import timedelta +from http import HTTPStatus import logging from uuid import uuid4 @@ -14,8 +15,6 @@ from homeassistant.const import ( CLOUD_NEVER_EXPOSED_ENTITIES, ENTITY_CATEGORY_CONFIG, ENTITY_CATEGORY_DIAGNOSTIC, - HTTP_INTERNAL_SERVER_ERROR, - HTTP_UNAUTHORIZED, ) from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -157,7 +156,7 @@ class GoogleConfig(AbstractConfig): ) _LOGGER.error("No configuration for request_sync available") - return HTTP_INTERNAL_SERVER_ERROR + return HTTPStatus.INTERNAL_SERVER_ERROR async def _async_update_token(self, force=False): if CONF_SERVICE_ACCOUNT not in self._config: @@ -198,7 +197,7 @@ class GoogleConfig(AbstractConfig): try: return await _call() except ClientResponseError as error: - if error.status == HTTP_UNAUTHORIZED: + if error.status == HTTPStatus.UNAUTHORIZED: _LOGGER.warning( "Request for %s unauthorized, renewing token and retrying", url ) @@ -210,7 +209,7 @@ class GoogleConfig(AbstractConfig): return error.status except (asyncio.TimeoutError, ClientError): _LOGGER.error("Could not contact %s", url) - return HTTP_INTERNAL_SERVER_ERROR + return HTTPStatus.INTERNAL_SERVER_ERROR async def async_report_state(self, message, agent_user_id: str): """Send a state report to Google.""" diff --git a/homeassistant/components/habitica/sensor.py b/homeassistant/components/habitica/sensor.py index 64494fb9694..cd488819eda 100644 --- a/homeassistant/components/habitica/sensor.py +++ b/homeassistant/components/habitica/sensor.py @@ -1,12 +1,13 @@ """Support for Habitica sensors.""" from collections import namedtuple from datetime import timedelta +from http import HTTPStatus import logging from aiohttp import ClientResponseError from homeassistant.components.sensor import SensorEntity -from homeassistant.const import CONF_NAME, HTTP_TOO_MANY_REQUESTS +from homeassistant.const import CONF_NAME from homeassistant.util import Throttle from .const import DOMAIN @@ -94,7 +95,7 @@ class HabitipyData: try: self.data = await self.api.user.get() except ClientResponseError as error: - if error.status == HTTP_TOO_MANY_REQUESTS: + if error.status == HTTPStatus.TOO_MANY_REQUESTS: _LOGGER.warning( "Sensor data update for %s has too many API requests;" " Skipping the update", @@ -111,7 +112,7 @@ class HabitipyData: try: self.tasks[task_type] = await self.api.tasks.user.get(type=task_type) except ClientResponseError as error: - if error.status == HTTP_TOO_MANY_REQUESTS: + if error.status == HTTPStatus.TOO_MANY_REQUESTS: _LOGGER.warning( "Sensor data update for %s has too many API requests;" " Skipping the update", diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index 24be9fff779..16872079be3 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -1,6 +1,7 @@ """The Hangouts Bot.""" import asyncio from contextlib import suppress +from http import HTTPStatus import io import logging @@ -8,7 +9,6 @@ import aiohttp import hangups from hangups import ChatMessageEvent, ChatMessageSegment, Client, get_auth, hangouts_pb2 -from homeassistant.const import HTTP_OK from homeassistant.core import callback from homeassistant.helpers import dispatcher, intent from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -273,7 +273,7 @@ class HangoutsBot: try: websession = async_get_clientsession(self.hass) async with websession.get(uri, timeout=5) as response: - if response.status != HTTP_OK: + if response.status != HTTPStatus.OK: _LOGGER.error( "Fetch image failed, %s, %s", response.status, response ) diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 7d4b5da8f5f..4a0312bcecb 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -1,5 +1,6 @@ """Handler for Hass.io.""" import asyncio +from http import HTTPStatus import logging import os @@ -10,7 +11,7 @@ from homeassistant.components.http import ( CONF_SERVER_PORT, CONF_SSL_CERTIFICATE, ) -from homeassistant.const import HTTP_BAD_REQUEST, HTTP_OK, SERVER_PORT +from homeassistant.const import SERVER_PORT from .const import X_HASSIO @@ -225,7 +226,7 @@ class HassIO: timeout=aiohttp.ClientTimeout(total=timeout), ) - if request.status not in (HTTP_OK, HTTP_BAD_REQUEST): + if request.status not in (HTTPStatus.OK, HTTPStatus.BAD_REQUEST): _LOGGER.error("%s return code %d", command, request.status) raise HassioAPIError() diff --git a/homeassistant/components/haveibeenpwned/sensor.py b/homeassistant/components/haveibeenpwned/sensor.py index 738837989b9..cdcc526c8d8 100644 --- a/homeassistant/components/haveibeenpwned/sensor.py +++ b/homeassistant/components/haveibeenpwned/sensor.py @@ -1,5 +1,6 @@ """Support for haveibeenpwned (email breaches) sensor.""" from datetime import timedelta +from http import HTTPStatus import logging from aiohttp.hdrs import USER_AGENT @@ -7,13 +8,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_EMAIL, - HTTP_NOT_FOUND, - HTTP_OK, -) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_EMAIL import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_point_in_time from homeassistant.util import Throttle @@ -163,7 +158,7 @@ class HaveIBeenPwnedData: _LOGGER.error("Failed fetching data for %s", self._email) return - if req.status_code == HTTP_OK: + if req.status_code == HTTPStatus.OK: self.data[self._email] = sorted( req.json(), key=lambda k: k["AddedDate"], reverse=True ) @@ -172,7 +167,7 @@ class HaveIBeenPwnedData: # the forced updates try this current email again self.set_next_email() - elif req.status_code == HTTP_NOT_FOUND: + elif req.status_code == HTTPStatus.NOT_FOUND: self.data[self._email] = [] # only goto next email if we had data so that diff --git a/homeassistant/components/hitron_coda/device_tracker.py b/homeassistant/components/hitron_coda/device_tracker.py index 666f6796d4c..ac362f173e4 100644 --- a/homeassistant/components/hitron_coda/device_tracker.py +++ b/homeassistant/components/hitron_coda/device_tracker.py @@ -1,5 +1,6 @@ """Support for the Hitron CODA-4582U, provided by Rogers.""" from collections import namedtuple +from http import HTTPStatus import logging import requests @@ -10,13 +11,7 @@ from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_TYPE, - CONF_USERNAME, - HTTP_OK, -) +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_TYPE, CONF_USERNAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -88,7 +83,7 @@ class HitronCODADeviceScanner(DeviceScanner): except requests.exceptions.Timeout: _LOGGER.error("Connection to the router timed out at URL %s", self._url) return False - if res.status_code != HTTP_OK: + if res.status_code != HTTPStatus.OK: _LOGGER.error("Connection failed with http code %s", res.status_code) return False try: @@ -113,7 +108,7 @@ class HitronCODADeviceScanner(DeviceScanner): except requests.exceptions.Timeout: _LOGGER.error("Connection to the router timed out at URL %s", self._url) return False - if res.status_code != HTTP_OK: + if res.status_code != HTTPStatus.OK: _LOGGER.error("Connection failed with http code %s", res.status_code) return False try: diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 6e4f5c0a661..a1d50dbdcb5 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -5,6 +5,7 @@ from collections import defaultdict from collections.abc import Awaitable, Callable from contextlib import suppress from datetime import datetime +from http import HTTPStatus from ipaddress import ip_address import logging from socket import gethostbyaddr, herror @@ -15,7 +16,6 @@ from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized import voluptuous as vol from homeassistant.config import load_yaml_config_file -from homeassistant.const import HTTP_BAD_REQUEST from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv @@ -89,9 +89,9 @@ def log_invalid_auth( async def handle_req( view: HomeAssistantView, request: Request, *args: Any, **kwargs: Any ) -> StreamResponse: - """Try to log failed login attempts if response status >= 400.""" + """Try to log failed login attempts if response status >= BAD_REQUEST.""" resp = await func(view, request, *args, **kwargs) - if resp.status >= HTTP_BAD_REQUEST: + if resp.status >= HTTPStatus.BAD_REQUEST: await process_wrong_login(request) return resp diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 949813ca4ad..bf8dc4b432b 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -19,7 +19,7 @@ from aiohttp.web_urldispatcher import AbstractRoute import voluptuous as vol from homeassistant import exceptions -from homeassistant.const import CONTENT_TYPE_JSON, HTTP_OK +from homeassistant.const import CONTENT_TYPE_JSON from homeassistant.core import Context, is_callback from homeassistant.helpers.json import JSONEncoder @@ -144,7 +144,7 @@ def request_handler_factory( # The method handler returned a ready-made Response, how nice of it return result - status_code = HTTP_OK + status_code = HTTPStatus.OK if isinstance(result, tuple): result, status_code = result diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 1f19138b28f..19ab2128d62 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from functools import partial +from http import HTTPStatus import logging from aiohttp import client_exceptions @@ -11,7 +12,6 @@ import async_timeout import slugify as unicode_slug from homeassistant import core -from homeassistant.const import HTTP_INTERNAL_SERVER_ERROR from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client @@ -148,7 +148,7 @@ class HueBridge: # We only retry if it's a server error. So raise on all 4XX errors. if ( isinstance(err, client_exceptions.ClientResponseError) - and err.status < HTTP_INTERNAL_SERVER_ERROR + and err.status < HTTPStatus.INTERNAL_SERVER_ERROR ): raise diff --git a/tests/components/garages_amsterdam/test_config_flow.py b/tests/components/garages_amsterdam/test_config_flow.py index 4ff064ca9d4..a9f5f2c58ad 100644 --- a/tests/components/garages_amsterdam/test_config_flow.py +++ b/tests/components/garages_amsterdam/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Garages Amsterdam config flow.""" +from http import HTTPStatus from unittest.mock import patch from aiohttp import ClientResponseError @@ -44,7 +45,10 @@ async def test_full_flow(hass: HomeAssistant) -> None: "side_effect,reason", [ (RuntimeError, "unknown"), - (ClientResponseError(None, None, status=500), "cannot_connect"), + ( + ClientResponseError(None, None, status=HTTPStatus.INTERNAL_SERVER_ERROR), + "cannot_connect", + ), ], ) async def test_error_handling( diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index 8642a6a7fac..36a9304cdc1 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -1,5 +1,6 @@ """The tests for generic camera component.""" import asyncio +from http import HTTPStatus from os import path from unittest.mock import patch @@ -9,11 +10,7 @@ import respx from homeassistant import config as hass_config from homeassistant.components.generic import DOMAIN from homeassistant.components.websocket_api.const import TYPE_RESULT -from homeassistant.const import ( - HTTP_INTERNAL_SERVER_ERROR, - HTTP_NOT_FOUND, - SERVICE_RELOAD, -) +from homeassistant.const import SERVICE_RELOAD from homeassistant.setup import async_setup_component @@ -41,7 +38,7 @@ async def test_fetching_url(hass, hass_client): resp = await client.get("/api/camera_proxy/camera.config_test") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert respx.calls.call_count == 1 body = await resp.text() assert body == "hello world" @@ -75,7 +72,7 @@ async def test_fetching_without_verify_ssl(hass, hass_client): resp = await client.get("/api/camera_proxy/camera.config_test") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK @respx.mock @@ -103,7 +100,7 @@ async def test_fetching_url_with_verify_ssl(hass, hass_client): resp = await client.get("/api/camera_proxy/camera.config_test") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK @respx.mock @@ -112,7 +109,7 @@ async def test_limit_refetch(hass, hass_client): respx.get("http://example.com/5a").respond(text="hello world") respx.get("http://example.com/10a").respond(text="hello world") respx.get("http://example.com/15a").respond(text="hello planet") - respx.get("http://example.com/20a").respond(status_code=HTTP_NOT_FOUND) + respx.get("http://example.com/20a").respond(status_code=HTTPStatus.NOT_FOUND) await async_setup_component( hass, @@ -137,19 +134,19 @@ async def test_limit_refetch(hass, hass_client): with patch("async_timeout.timeout", side_effect=asyncio.TimeoutError()): resp = await client.get("/api/camera_proxy/camera.config_test") assert respx.calls.call_count == 0 - assert resp.status == HTTP_INTERNAL_SERVER_ERROR + assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR hass.states.async_set("sensor.temp", "10") resp = await client.get("/api/camera_proxy/camera.config_test") assert respx.calls.call_count == 1 - assert resp.status == 200 + assert resp.status == HTTPStatus.OK body = await resp.text() assert body == "hello world" resp = await client.get("/api/camera_proxy/camera.config_test") assert respx.calls.call_count == 1 - assert resp.status == 200 + assert resp.status == HTTPStatus.OK body = await resp.text() assert body == "hello world" @@ -158,7 +155,7 @@ async def test_limit_refetch(hass, hass_client): # Url change = fetch new image resp = await client.get("/api/camera_proxy/camera.config_test") assert respx.calls.call_count == 2 - assert resp.status == 200 + assert resp.status == HTTPStatus.OK body = await resp.text() assert body == "hello planet" @@ -166,7 +163,7 @@ async def test_limit_refetch(hass, hass_client): hass.states.async_remove("sensor.temp") resp = await client.get("/api/camera_proxy/camera.config_test") assert respx.calls.call_count == 2 - assert resp.status == 200 + assert resp.status == HTTPStatus.OK body = await resp.text() assert body == "hello planet" @@ -340,14 +337,14 @@ async def test_camera_content_type(hass, hass_client): resp_1 = await client.get("/api/camera_proxy/camera.config_test_svg") assert respx.calls.call_count == 1 - assert resp_1.status == 200 + assert resp_1.status == HTTPStatus.OK assert resp_1.content_type == "image/svg+xml" body = await resp_1.text() assert body == svg_image resp_2 = await client.get("/api/camera_proxy/camera.config_test_jpg") assert respx.calls.call_count == 2 - assert resp_2.status == 200 + assert resp_2.status == HTTPStatus.OK assert resp_2.content_type == "image/jpeg" body = await resp_2.text() assert body == svg_image @@ -377,7 +374,7 @@ async def test_reloading(hass, hass_client): resp = await client.get("/api/camera_proxy/camera.config_test") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert respx.calls.call_count == 1 body = await resp.text() assert body == "hello world" @@ -400,11 +397,11 @@ async def test_reloading(hass, hass_client): resp = await client.get("/api/camera_proxy/camera.config_test") - assert resp.status == 404 + assert resp.status == HTTPStatus.NOT_FOUND resp = await client.get("/api/camera_proxy/camera.reload") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert respx.calls.call_count == 2 body = await resp.text() assert body == "hello world" @@ -435,7 +432,7 @@ async def test_timeout_cancelled(hass, hass_client): resp = await client.get("/api/camera_proxy/camera.config_test") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert respx.calls.call_count == 1 assert await resp.text() == "hello world" @@ -447,7 +444,7 @@ async def test_timeout_cancelled(hass, hass_client): ): resp = await client.get("/api/camera_proxy/camera.config_test") assert respx.calls.call_count == 1 - assert resp.status == 500 + assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR respx.get("http://example.com").side_effect = [ httpx.RequestError, @@ -457,7 +454,7 @@ async def test_timeout_cancelled(hass, hass_client): for total_calls in range(2, 4): resp = await client.get("/api/camera_proxy/camera.config_test") assert respx.calls.call_count == total_calls - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert await resp.text() == "hello world" diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index 44d4e954d28..013450212ed 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -1,4 +1,6 @@ """The tests for the Geofency device tracker platform.""" +from http import HTTPStatus + # pylint: disable=redefined-outer-name from unittest.mock import patch @@ -11,8 +13,6 @@ from homeassistant.config import async_process_ha_core_config from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, - HTTP_OK, - HTTP_UNPROCESSABLE_ENTITY, STATE_HOME, STATE_NOT_HOME, ) @@ -172,7 +172,7 @@ async def test_data_validation(geofency_client, webhook_id): # No data req = await geofency_client.post(url) - assert req.status == HTTP_UNPROCESSABLE_ENTITY + assert req.status == HTTPStatus.UNPROCESSABLE_ENTITY missing_attributes = ["address", "device", "entry", "latitude", "longitude", "name"] @@ -181,7 +181,7 @@ async def test_data_validation(geofency_client, webhook_id): copy = GPS_ENTER_HOME.copy() del copy[attribute] req = await geofency_client.post(url, data=copy) - assert req.status == HTTP_UNPROCESSABLE_ENTITY + assert req.status == HTTPStatus.UNPROCESSABLE_ENTITY async def test_gps_enter_and_exit_home(hass, geofency_client, webhook_id): @@ -191,7 +191,7 @@ async def test_gps_enter_and_exit_home(hass, geofency_client, webhook_id): # Enter the Home zone req = await geofency_client.post(url, data=GPS_ENTER_HOME) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK device_name = slugify(GPS_ENTER_HOME["device"]) state_name = hass.states.get(f"device_tracker.{device_name}").state assert state_name == STATE_HOME @@ -199,7 +199,7 @@ async def test_gps_enter_and_exit_home(hass, geofency_client, webhook_id): # Exit the Home zone req = await geofency_client.post(url, data=GPS_EXIT_HOME) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK device_name = slugify(GPS_EXIT_HOME["device"]) state_name = hass.states.get(f"device_tracker.{device_name}").state assert state_name == STATE_NOT_HOME @@ -211,7 +211,7 @@ async def test_gps_enter_and_exit_home(hass, geofency_client, webhook_id): req = await geofency_client.post(url, data=data) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK device_name = slugify(GPS_EXIT_HOME["device"]) current_latitude = hass.states.get(f"device_tracker.{device_name}").attributes[ "latitude" @@ -236,7 +236,7 @@ async def test_beacon_enter_and_exit_home(hass, geofency_client, webhook_id): # Enter the Home zone req = await geofency_client.post(url, data=BEACON_ENTER_HOME) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK device_name = slugify(f"beacon_{BEACON_ENTER_HOME['name']}") state_name = hass.states.get(f"device_tracker.{device_name}").state assert state_name == STATE_HOME @@ -244,7 +244,7 @@ async def test_beacon_enter_and_exit_home(hass, geofency_client, webhook_id): # Exit the Home zone req = await geofency_client.post(url, data=BEACON_EXIT_HOME) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK device_name = slugify(f"beacon_{BEACON_ENTER_HOME['name']}") state_name = hass.states.get(f"device_tracker.{device_name}").state assert state_name == STATE_NOT_HOME @@ -257,7 +257,7 @@ async def test_beacon_enter_and_exit_car(hass, geofency_client, webhook_id): # Enter the Car away from Home zone req = await geofency_client.post(url, data=BEACON_ENTER_CAR) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK device_name = slugify(f"beacon_{BEACON_ENTER_CAR['name']}") state_name = hass.states.get(f"device_tracker.{device_name}").state assert state_name == STATE_NOT_HOME @@ -265,7 +265,7 @@ async def test_beacon_enter_and_exit_car(hass, geofency_client, webhook_id): # Exit the Car away from Home zone req = await geofency_client.post(url, data=BEACON_EXIT_CAR) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK device_name = slugify(f"beacon_{BEACON_ENTER_CAR['name']}") state_name = hass.states.get(f"device_tracker.{device_name}").state assert state_name == STATE_NOT_HOME @@ -276,7 +276,7 @@ async def test_beacon_enter_and_exit_car(hass, geofency_client, webhook_id): data["longitude"] = HOME_LONGITUDE req = await geofency_client.post(url, data=data) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK device_name = slugify(f"beacon_{data['name']}") state_name = hass.states.get(f"device_tracker.{device_name}").state assert state_name == STATE_HOME @@ -284,7 +284,7 @@ async def test_beacon_enter_and_exit_car(hass, geofency_client, webhook_id): # Exit the Car in the Home zone req = await geofency_client.post(url, data=data) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK device_name = slugify(f"beacon_{data['name']}") state_name = hass.states.get(f"device_tracker.{device_name}").state assert state_name == STATE_HOME @@ -297,7 +297,7 @@ async def test_load_unload_entry(hass, geofency_client, webhook_id): # Enter the Home zone req = await geofency_client.post(url, data=GPS_ENTER_HOME) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK device_name = slugify(GPS_ENTER_HOME["device"]) state_1 = hass.states.get(f"device_tracker.{device_name}") assert state_1.state == STATE_HOME diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index c17a05ddb3d..ded7429bdee 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -1,4 +1,6 @@ """The tests for the Google Assistant component.""" +from http import HTTPStatus + # pylint: disable=protected-access import json @@ -154,7 +156,7 @@ async def test_sync_request(hass_fixture, assistant_client, auth_header): data=json.dumps(data), headers=auth_header, ) - assert result.status == 200 + assert result.status == HTTPStatus.OK body = await result.json() assert body.get("requestId") == reqid devices = body["payload"]["devices"] @@ -198,7 +200,7 @@ async def test_query_request(hass_fixture, assistant_client, auth_header): data=json.dumps(data), headers=auth_header, ) - assert result.status == 200 + assert result.status == HTTPStatus.OK body = await result.json() assert body.get("requestId") == reqid devices = body["payload"]["devices"] @@ -238,7 +240,7 @@ async def test_query_climate_request(hass_fixture, assistant_client, auth_header data=json.dumps(data), headers=auth_header, ) - assert result.status == 200 + assert result.status == HTTPStatus.OK body = await result.json() assert body.get("requestId") == reqid devices = body["payload"]["devices"] @@ -297,7 +299,7 @@ async def test_query_climate_request_f(hass_fixture, assistant_client, auth_head data=json.dumps(data), headers=auth_header, ) - assert result.status == 200 + assert result.status == HTTPStatus.OK body = await result.json() assert body.get("requestId") == reqid devices = body["payload"]["devices"] @@ -350,7 +352,7 @@ async def test_query_humidifier_request(hass_fixture, assistant_client, auth_hea data=json.dumps(data), headers=auth_header, ) - assert result.status == 200 + assert result.status == HTTPStatus.OK body = await result.json() assert body.get("requestId") == reqid devices = body["payload"]["devices"] @@ -464,7 +466,7 @@ async def test_execute_request(hass_fixture, assistant_client, auth_header): data=json.dumps(data), headers=auth_header, ) - assert result.status == 200 + assert result.status == HTTPStatus.OK body = await result.json() assert body.get("requestId") == reqid commands = body["payload"]["commands"] diff --git a/tests/components/google_assistant/test_helpers.py b/tests/components/google_assistant/test_helpers.py index e86156fa614..9e54e6cff3f 100644 --- a/tests/components/google_assistant/test_helpers.py +++ b/tests/components/google_assistant/test_helpers.py @@ -1,5 +1,6 @@ """Test Google Assistant helpers.""" from datetime import timedelta +from http import HTTPStatus from unittest.mock import Mock, call, patch import pytest @@ -112,7 +113,7 @@ async def test_config_local_sdk(hass, hass_client): "requestId": "mock-req-id", }, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result["requestId"] == "mock-req-id" @@ -126,7 +127,7 @@ async def test_config_local_sdk(hass, hass_client): # Webhook is no longer active resp = await client.post("/api/webhook/mock-webhook-id") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert await resp.read() == b"" @@ -148,7 +149,7 @@ async def test_config_local_sdk_if_disabled(hass, hass_client): resp = await client.post( "/api/webhook/mock-webhook-id", json={"requestId": "mock-req-id"} ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK result = await resp.json() assert result == { "payload": {"errorCode": "deviceTurnedOff"}, @@ -159,7 +160,7 @@ async def test_config_local_sdk_if_disabled(hass, hass_client): # Webhook is no longer active resp = await client.post("/api/webhook/mock-webhook-id") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert await resp.read() == b"" diff --git a/tests/components/google_assistant/test_http.py b/tests/components/google_assistant/test_http.py index 013fa3c1d0c..1d62d034703 100644 --- a/tests/components/google_assistant/test_http.py +++ b/tests/components/google_assistant/test_http.py @@ -1,5 +1,6 @@ """Test Google http services.""" from datetime import datetime, timedelta, timezone +from http import HTTPStatus from unittest.mock import ANY, patch from homeassistant.components.google_assistant import GOOGLE_ASSISTANT_SCHEMA @@ -49,7 +50,7 @@ async def test_get_access_token(hass, aioclient_mock): aioclient_mock.post( HOMEGRAPH_TOKEN_URL, - status=200, + status=HTTPStatus.OK, json={"access_token": "1234", "expires_in": 3600}, ) @@ -106,10 +107,10 @@ async def test_call_homegraph_api(hass, aioclient_mock, hass_storage): ) as mock_get_token: mock_get_token.return_value = MOCK_TOKEN - aioclient_mock.post(MOCK_URL, status=200, json={}) + aioclient_mock.post(MOCK_URL, status=HTTPStatus.OK, json={}) res = await config.async_call_homegraph_api(MOCK_URL, MOCK_JSON) - assert res == 200 + assert res == HTTPStatus.OK assert mock_get_token.call_count == 1 assert aioclient_mock.call_count == 1 @@ -129,7 +130,7 @@ async def test_call_homegraph_api_retry(hass, aioclient_mock, hass_storage): ) as mock_get_token: mock_get_token.return_value = MOCK_TOKEN - aioclient_mock.post(MOCK_URL, status=401, json={}) + aioclient_mock.post(MOCK_URL, status=HTTPStatus.UNAUTHORIZED, json={}) await config.async_call_homegraph_api(MOCK_URL, MOCK_JSON) diff --git a/tests/components/google_assistant/test_init.py b/tests/components/google_assistant/test_init.py index e663df19d88..69198b99aaa 100644 --- a/tests/components/google_assistant/test_init.py +++ b/tests/components/google_assistant/test_init.py @@ -1,4 +1,6 @@ """The tests for google-assistant init.""" +from http import HTTPStatus + from homeassistant.components import google_assistant as ga from homeassistant.core import Context from homeassistant.setup import async_setup_component @@ -10,11 +12,11 @@ async def test_request_sync_service(aioclient_mock, hass): """Test that it posts to the request_sync url.""" aioclient_mock.post( ga.const.HOMEGRAPH_TOKEN_URL, - status=200, + status=HTTPStatus.OK, json={"access_token": "1234", "expires_in": 3600}, ) - aioclient_mock.post(ga.const.REQUEST_SYNC_BASE_URL, status=200) + aioclient_mock.post(ga.const.REQUEST_SYNC_BASE_URL, status=HTTPStatus.OK) await async_setup_component( hass, diff --git a/tests/components/google_wifi/test_sensor.py b/tests/components/google_wifi/test_sensor.py index 59c95d9883b..add8ec04cbe 100644 --- a/tests/components/google_wifi/test_sensor.py +++ b/tests/components/google_wifi/test_sensor.py @@ -1,5 +1,6 @@ """The tests for the Google Wifi platform.""" from datetime import datetime, timedelta +from http import HTTPStatus from unittest.mock import Mock, patch import homeassistant.components.google_wifi.sensor as google_wifi @@ -33,7 +34,7 @@ MOCK_DATA_MISSING = '{"software": {},' '"system": {},' '"wan": {}}' async def test_setup_minimum(hass, requests_mock): """Test setup with minimum configuration.""" resource = f"http://{google_wifi.DEFAULT_HOST}{google_wifi.ENDPOINT}" - requests_mock.get(resource, status_code=200) + requests_mock.get(resource, status_code=HTTPStatus.OK) assert await async_setup_component( hass, "sensor", @@ -45,7 +46,7 @@ async def test_setup_minimum(hass, requests_mock): async def test_setup_get(hass, requests_mock): """Test setup with full configuration.""" resource = f"http://localhost{google_wifi.ENDPOINT}" - requests_mock.get(resource, status_code=200) + requests_mock.get(resource, status_code=HTTPStatus.OK) assert await async_setup_component( hass, "sensor", @@ -74,7 +75,7 @@ def setup_api(hass, data, requests_mock): now = datetime(1970, month=1, day=1) sensor_dict = {} with patch("homeassistant.util.dt.now", return_value=now): - requests_mock.get(resource, text=data, status_code=200) + requests_mock.get(resource, text=data, status_code=HTTPStatus.OK) conditions = google_wifi.SENSOR_KEYS api = google_wifi.GoogleWifiAPI("localhost", conditions) for desc in google_wifi.SENSOR_TYPES: diff --git a/tests/components/gpslogger/test_init.py b/tests/components/gpslogger/test_init.py index dc9a3720709..a885699ca05 100644 --- a/tests/components/gpslogger/test_init.py +++ b/tests/components/gpslogger/test_init.py @@ -1,4 +1,5 @@ """The tests the for GPSLogger device tracker platform.""" +from http import HTTPStatus from unittest.mock import patch import pytest @@ -8,12 +9,7 @@ from homeassistant.components import gpslogger, zone from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.gpslogger import DOMAIN, TRACKER_UPDATE from homeassistant.config import async_process_ha_core_config -from homeassistant.const import ( - HTTP_OK, - HTTP_UNPROCESSABLE_ENTITY, - STATE_HOME, - STATE_NOT_HOME, -) +from homeassistant.const import STATE_HOME, STATE_NOT_HOME from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component @@ -87,21 +83,21 @@ async def test_missing_data(hass, gpslogger_client, webhook_id): # No data req = await gpslogger_client.post(url) await hass.async_block_till_done() - assert req.status == HTTP_UNPROCESSABLE_ENTITY + assert req.status == HTTPStatus.UNPROCESSABLE_ENTITY # No latitude copy = data.copy() del copy["latitude"] req = await gpslogger_client.post(url, data=copy) await hass.async_block_till_done() - assert req.status == HTTP_UNPROCESSABLE_ENTITY + assert req.status == HTTPStatus.UNPROCESSABLE_ENTITY # No device copy = data.copy() del copy["device"] req = await gpslogger_client.post(url, data=copy) await hass.async_block_till_done() - assert req.status == HTTP_UNPROCESSABLE_ENTITY + assert req.status == HTTPStatus.UNPROCESSABLE_ENTITY async def test_enter_and_exit(hass, gpslogger_client, webhook_id): @@ -113,14 +109,14 @@ async def test_enter_and_exit(hass, gpslogger_client, webhook_id): # Enter the Home req = await gpslogger_client.post(url, data=data) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK state_name = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data['device']}").state assert state_name == STATE_HOME # Enter Home again req = await gpslogger_client.post(url, data=data) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK state_name = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data['device']}").state assert state_name == STATE_HOME @@ -130,7 +126,7 @@ async def test_enter_and_exit(hass, gpslogger_client, webhook_id): # Enter Somewhere else req = await gpslogger_client.post(url, data=data) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK state_name = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data['device']}").state assert state_name == STATE_NOT_HOME @@ -160,7 +156,7 @@ async def test_enter_with_attrs(hass, gpslogger_client, webhook_id): req = await gpslogger_client.post(url, data=data) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK state = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data['device']}") assert state.state == STATE_NOT_HOME assert state.attributes["gps_accuracy"] == 10.5 @@ -186,7 +182,7 @@ async def test_enter_with_attrs(hass, gpslogger_client, webhook_id): req = await gpslogger_client.post(url, data=data) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK state = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data['device']}") assert state.state == STATE_HOME assert state.attributes["gps_accuracy"] == 123 @@ -209,7 +205,7 @@ async def test_load_unload_entry(hass, gpslogger_client, webhook_id): # Enter the Home req = await gpslogger_client.post(url, data=data) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK state_name = hass.states.get(f"{DEVICE_TRACKER_DOMAIN}.{data['device']}").state assert state_name == STATE_HOME assert len(hass.data[DATA_DISPATCHER][TRACKER_UPDATE]) == 1 diff --git a/tests/components/habitica/test_init.py b/tests/components/habitica/test_init.py index 97d4fb092fc..564379aa9d1 100644 --- a/tests/components/habitica/test_init.py +++ b/tests/components/habitica/test_init.py @@ -1,4 +1,6 @@ """Test the habitica module.""" +from http import HTTPStatus + import pytest from homeassistant.components.habitica.const import ( @@ -82,7 +84,7 @@ def common_requests(aioclient_mock): aioclient_mock.post( "https://habitica.com/api/v3/tasks/user", - status=201, + status=HTTPStatus.CREATED, json={"data": TEST_API_CALL_ARGS}, ) diff --git a/tests/components/hassio/test_addon_panel.py b/tests/components/hassio/test_addon_panel.py index 3f6db4dc430..c69ceb2cb5c 100644 --- a/tests/components/hassio/test_addon_panel.py +++ b/tests/components/hassio/test_addon_panel.py @@ -1,4 +1,5 @@ """Test add-on panel.""" +from http import HTTPStatus from unittest.mock import patch import pytest @@ -104,10 +105,10 @@ async def test_hassio_addon_panel_api(hass, aioclient_mock, hassio_env, hass_cli hass_client = await hass_client() resp = await hass_client.post("/api/hassio_push/panel/test2") - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST resp = await hass_client.post("/api/hassio_push/panel/test1") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert mock_panel.call_count == 2 mock_panel.assert_called_with( diff --git a/tests/components/hassio/test_auth.py b/tests/components/hassio/test_auth.py index a533d468069..e5f1f848fcb 100644 --- a/tests/components/hassio/test_auth.py +++ b/tests/components/hassio/test_auth.py @@ -1,5 +1,6 @@ """The tests for the hassio component.""" +from http import HTTPStatus from unittest.mock import Mock, patch from homeassistant.auth.providers.homeassistant import InvalidAuth @@ -17,7 +18,7 @@ async def test_auth_success(hass, hassio_client_supervisor): ) # Check we got right response - assert resp.status == 200 + assert resp.status == HTTPStatus.OK mock_login.assert_called_with("test", "123456") @@ -33,7 +34,7 @@ async def test_auth_fails_no_supervisor(hass, hassio_client): ) # Check we got right response - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED assert not mock_login.called @@ -49,7 +50,7 @@ async def test_auth_fails_no_auth(hass, hassio_noauth_client): ) # Check we got right response - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED assert not mock_login.called @@ -66,7 +67,7 @@ async def test_login_error(hass, hassio_client_supervisor): ) # Check we got right response - assert resp.status == 404 + assert resp.status == HTTPStatus.NOT_FOUND mock_login.assert_called_with("test", "123456") @@ -80,7 +81,7 @@ async def test_login_no_data(hass, hassio_client_supervisor): resp = await hassio_client_supervisor.post("/api/hassio_auth") # Check we got right response - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST assert not mock_login.called @@ -96,7 +97,7 @@ async def test_login_no_username(hass, hassio_client_supervisor): ) # Check we got right response - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST assert not mock_login.called @@ -117,7 +118,7 @@ async def test_login_success_extra(hass, hassio_client_supervisor): ) # Check we got right response - assert resp.status == 200 + assert resp.status == HTTPStatus.OK mock_login.assert_called_with("test", "123456") @@ -133,7 +134,7 @@ async def test_password_success(hass, hassio_client_supervisor): ) # Check we got right response - assert resp.status == 200 + assert resp.status == HTTPStatus.OK mock_change.assert_called_with("test", "123456") @@ -145,7 +146,7 @@ async def test_password_fails_no_supervisor(hass, hassio_client): ) # Check we got right response - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED async def test_password_fails_no_auth(hass, hassio_noauth_client): @@ -156,7 +157,7 @@ async def test_password_fails_no_auth(hass, hassio_noauth_client): ) # Check we got right response - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED async def test_password_no_user(hass, hassio_client_supervisor): @@ -167,4 +168,4 @@ async def test_password_no_user(hass, hassio_client_supervisor): ) # Check we got right response - assert resp.status == 404 + assert resp.status == HTTPStatus.NOT_FOUND diff --git a/tests/components/hassio/test_discovery.py b/tests/components/hassio/test_discovery.py index c23ee40de6e..fc99b06619f 100644 --- a/tests/components/hassio/test_discovery.py +++ b/tests/components/hassio/test_discovery.py @@ -1,4 +1,5 @@ """Test config flow.""" +from http import HTTPStatus from unittest.mock import Mock, patch from homeassistant.components.hassio.handler import HassioAPIError @@ -154,7 +155,7 @@ async def test_hassio_discovery_webhook(hass, aioclient_mock, hassio_client): ) await hass.async_block_till_done() - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert aioclient_mock.call_count == 2 assert mock_mqtt.called mock_mqtt.assert_called_with( diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index 16121393170..7947f7cccae 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -1,5 +1,6 @@ """The tests for the hassio component.""" import asyncio +from http import HTTPStatus from aiohttp import StreamReader import pytest @@ -14,7 +15,7 @@ async def test_forward_request(hassio_client, aioclient_mock): resp = await hassio_client.post("/api/hassio/beer") # Check we got right response - assert resp.status == 200 + assert resp.status == HTTPStatus.OK body = await resp.text() assert body == "response" @@ -30,7 +31,7 @@ async def test_auth_required_forward_request(hassio_noauth_client, build_type): resp = await hassio_noauth_client.post(f"/api/hassio/{build_type}") # Check we got right response - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED @pytest.mark.parametrize( @@ -53,7 +54,7 @@ async def test_forward_request_no_auth_for_panel( resp = await hassio_client.get(f"/api/hassio/{build_type}") # Check we got right response - assert resp.status == 200 + assert resp.status == HTTPStatus.OK body = await resp.text() assert body == "response" @@ -68,7 +69,7 @@ async def test_forward_request_no_auth_for_logo(hassio_client, aioclient_mock): resp = await hassio_client.get("/api/hassio/addons/bl_b392/logo") # Check we got right response - assert resp.status == 200 + assert resp.status == HTTPStatus.OK body = await resp.text() assert body == "response" @@ -83,7 +84,7 @@ async def test_forward_request_no_auth_for_icon(hassio_client, aioclient_mock): resp = await hassio_client.get("/api/hassio/addons/bl_b392/icon") # Check we got right response - assert resp.status == 200 + assert resp.status == HTTPStatus.OK body = await resp.text() assert body == "response" @@ -98,7 +99,7 @@ async def test_forward_log_request(hassio_client, aioclient_mock): resp = await hassio_client.get("/api/hassio/beer/logs") # Check we got right response - assert resp.status == 200 + assert resp.status == HTTPStatus.OK body = await resp.text() assert body == "\033[32mresponse\033[0m" @@ -111,7 +112,7 @@ async def test_bad_gateway_when_cannot_find_supervisor(hassio_client, aioclient_ aioclient_mock.get("http://127.0.0.1/addons/test/info", exc=asyncio.TimeoutError) resp = await hassio_client.get("/api/hassio/addons/test/info") - assert resp.status == 502 + assert resp.status == HTTPStatus.BAD_GATEWAY async def test_forwarding_user_info(hassio_client, hass_admin_user, aioclient_mock): @@ -121,7 +122,7 @@ async def test_forwarding_user_info(hassio_client, hass_admin_user, aioclient_mo resp = await hassio_client.get("/api/hassio/hello") # Check we got right response - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert len(aioclient_mock.mock_calls) == 1 @@ -140,7 +141,7 @@ async def test_backup_upload_headers(hassio_client, aioclient_mock, caplog): ) # Check we got right response - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert len(aioclient_mock.mock_calls) == 1 @@ -162,7 +163,7 @@ async def test_backup_download_headers(hassio_client, aioclient_mock): resp = await hassio_client.get("/api/hassio/backups/slug/download") # Check we got right response - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert len(aioclient_mock.mock_calls) == 1 @@ -196,8 +197,8 @@ async def test_entrypoint_cache_control(hassio_client, aioclient_mock): resp2 = await hassio_client.get("/api/hassio/app/entrypoint.fdhkusd8y43r.js") # Check we got right response - assert resp1.status == 200 - assert resp2.status == 200 + assert resp1.status == HTTPStatus.OK + assert resp2.status == HTTPStatus.OK assert len(aioclient_mock.mock_calls) == 2 assert resp1.headers["Cache-Control"] == "no-store, max-age=0" diff --git a/tests/components/hassio/test_ingress.py b/tests/components/hassio/test_ingress.py index 8f7d97213e0..60fea96d4ea 100644 --- a/tests/components/hassio/test_ingress.py +++ b/tests/components/hassio/test_ingress.py @@ -1,5 +1,5 @@ """The tests for the hassio component.""" - +from http import HTTPStatus from unittest.mock import MagicMock, patch from aiohttp.hdrs import X_FORWARDED_FOR, X_FORWARDED_HOST, X_FORWARDED_PROTO @@ -29,7 +29,7 @@ async def test_ingress_request_get(hassio_client, build_type, aioclient_mock): ) # Check we got right response - assert resp.status == 200 + assert resp.status == HTTPStatus.OK body = await resp.text() assert body == "test" @@ -69,7 +69,7 @@ async def test_ingress_request_post(hassio_client, build_type, aioclient_mock): ) # Check we got right response - assert resp.status == 200 + assert resp.status == HTTPStatus.OK body = await resp.text() assert body == "test" @@ -109,7 +109,7 @@ async def test_ingress_request_put(hassio_client, build_type, aioclient_mock): ) # Check we got right response - assert resp.status == 200 + assert resp.status == HTTPStatus.OK body = await resp.text() assert body == "test" @@ -149,7 +149,7 @@ async def test_ingress_request_delete(hassio_client, build_type, aioclient_mock) ) # Check we got right response - assert resp.status == 200 + assert resp.status == HTTPStatus.OK body = await resp.text() assert body == "test" @@ -189,7 +189,7 @@ async def test_ingress_request_patch(hassio_client, build_type, aioclient_mock): ) # Check we got right response - assert resp.status == 200 + assert resp.status == HTTPStatus.OK body = await resp.text() assert body == "test" @@ -229,7 +229,7 @@ async def test_ingress_request_options(hassio_client, build_type, aioclient_mock ) # Check we got right response - assert resp.status == 200 + assert resp.status == HTTPStatus.OK body = await resp.text() assert body == "test" @@ -302,4 +302,4 @@ async def test_ingress_missing_peername(hassio_client, aioclient_mock, caplog): assert "Can't set forward_for header, missing peername" in caplog.text # Check we got right response - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 35075d79241..422bccf100d 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -1,6 +1,7 @@ """The tests the History component.""" # pylint: disable=protected-access,invalid-name from datetime import timedelta +from http import HTTPStatus import json from unittest.mock import patch, sentinel @@ -586,7 +587,7 @@ async def test_fetch_period_api(hass, hass_client): await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) client = await hass_client() response = await client.get(f"/api/history/period/{dt_util.utcnow().isoformat()}") - assert response.status == 200 + assert response.status == HTTPStatus.OK async def test_fetch_period_api_with_use_include_order(hass, hass_client): @@ -598,7 +599,7 @@ async def test_fetch_period_api_with_use_include_order(hass, hass_client): await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) client = await hass_client() response = await client.get(f"/api/history/period/{dt_util.utcnow().isoformat()}") - assert response.status == 200 + assert response.status == HTTPStatus.OK async def test_fetch_period_api_with_minimal_response(hass, hass_client): @@ -610,7 +611,7 @@ async def test_fetch_period_api_with_minimal_response(hass, hass_client): response = await client.get( f"/api/history/period/{dt_util.utcnow().isoformat()}?minimal_response" ) - assert response.status == 200 + assert response.status == HTTPStatus.OK async def test_fetch_period_api_with_no_timestamp(hass, hass_client): @@ -620,7 +621,7 @@ async def test_fetch_period_api_with_no_timestamp(hass, hass_client): await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) client = await hass_client() response = await client.get("/api/history/period") - assert response.status == 200 + assert response.status == HTTPStatus.OK async def test_fetch_period_api_with_include_order(hass, hass_client): @@ -642,7 +643,7 @@ async def test_fetch_period_api_with_include_order(hass, hass_client): f"/api/history/period/{dt_util.utcnow().isoformat()}", params={"filter_entity_id": "non.existing,something.else"}, ) - assert response.status == 200 + assert response.status == HTTPStatus.OK async def test_fetch_period_api_with_entity_glob_include(hass, hass_client): @@ -672,7 +673,7 @@ async def test_fetch_period_api_with_entity_glob_include(hass, hass_client): response = await client.get( f"/api/history/period/{dt_util.utcnow().isoformat()}", ) - assert response.status == 200 + assert response.status == HTTPStatus.OK response_json = await response.json() assert response_json[0][0]["entity_id"] == "light.kitchen" @@ -710,7 +711,7 @@ async def test_fetch_period_api_with_entity_glob_exclude(hass, hass_client): response = await client.get( f"/api/history/period/{dt_util.utcnow().isoformat()}", ) - assert response.status == 200 + assert response.status == HTTPStatus.OK response_json = await response.json() assert len(response_json) == 2 assert response_json[0][0]["entity_id"] == "light.cow" @@ -754,7 +755,7 @@ async def test_fetch_period_api_with_entity_glob_include_and_exclude(hass, hass_ response = await client.get( f"/api/history/period/{dt_util.utcnow().isoformat()}", ) - assert response.status == 200 + assert response.status == HTTPStatus.OK response_json = await response.json() assert len(response_json) == 3 assert response_json[0][0]["entity_id"] == "light.match" @@ -785,7 +786,7 @@ async def test_entity_ids_limit_via_api(hass, hass_client): response = await client.get( f"/api/history/period/{dt_util.utcnow().isoformat()}?filter_entity_id=light.kitchen,light.cow", ) - assert response.status == 200 + assert response.status == HTTPStatus.OK response_json = await response.json() assert len(response_json) == 2 assert response_json[0][0]["entity_id"] == "light.kitchen" @@ -815,7 +816,7 @@ async def test_entity_ids_limit_via_api_with_skip_initial_state(hass, hass_clien response = await client.get( f"/api/history/period/{dt_util.utcnow().isoformat()}?filter_entity_id=light.kitchen,light.cow&skip_initial_state", ) - assert response.status == 200 + assert response.status == HTTPStatus.OK response_json = await response.json() assert len(response_json) == 0 @@ -823,7 +824,7 @@ async def test_entity_ids_limit_via_api_with_skip_initial_state(hass, hass_clien response = await client.get( f"/api/history/period/{when.isoformat()}?filter_entity_id=light.kitchen,light.cow&skip_initial_state", ) - assert response.status == 200 + assert response.status == HTTPStatus.OK response_json = await response.json() assert len(response_json) == 2 assert response_json[0][0]["entity_id"] == "light.kitchen" diff --git a/tests/components/home_connect/test_config_flow.py b/tests/components/home_connect/test_config_flow.py index 1f4120115ea..fa7d3fee8f0 100644 --- a/tests/components/home_connect/test_config_flow.py +++ b/tests/components/home_connect/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Home Connect config flow.""" +from http import HTTPStatus from unittest.mock import patch from homeassistant import config_entries, data_entry_flow, setup @@ -50,7 +51,7 @@ async def test_full_flow( client = await hass_client_no_auth() resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert resp.headers["content-type"] == "text/html; charset=utf-8" aioclient_mock.post( diff --git a/tests/components/home_plus_control/test_config_flow.py b/tests/components/home_plus_control/test_config_flow.py index 5eb4115f031..cdf1f85f187 100644 --- a/tests/components/home_plus_control/test_config_flow.py +++ b/tests/components/home_plus_control/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Legrand Home+ Control config flow.""" +from http import HTTPStatus from unittest.mock import patch from homeassistant import config_entries, data_entry_flow, setup @@ -56,7 +57,7 @@ async def test_full_flow( client = await hass_client_no_auth() resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert resp.headers["content-type"] == "text/html; charset=utf-8" aioclient_mock.post( @@ -174,7 +175,7 @@ async def test_abort_if_invalid_token( client = await hass_client_no_auth() resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert resp.headers["content-type"] == "text/html; charset=utf-8" aioclient_mock.post( diff --git a/tests/components/html5/test_notify.py b/tests/components/html5/test_notify.py index 4a62eb76c27..116b4437d61 100644 --- a/tests/components/html5/test_notify.py +++ b/tests/components/html5/test_notify.py @@ -1,11 +1,11 @@ """Test HTML5 notify platform.""" +from http import HTTPStatus import json from unittest.mock import MagicMock, mock_open, patch from aiohttp.hdrs import AUTHORIZATION import homeassistant.components.html5.notify as html5 -from homeassistant.const import HTTP_INTERNAL_SERVER_ERROR from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component @@ -294,7 +294,7 @@ async def test_registering_new_device_view(hass, hass_client): with patch("homeassistant.components.html5.notify.save_json") as mock_save: resp = await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_1)) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert len(mock_save.mock_calls) == 1 assert mock_save.mock_calls[0][1][1] == {"unnamed device": SUBSCRIPTION_1} @@ -309,7 +309,7 @@ async def test_registering_new_device_view_with_name(hass, hass_client): with patch("homeassistant.components.html5.notify.save_json") as mock_save: resp = await client.post(REGISTER_URL, data=json.dumps(SUB_WITH_NAME)) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert len(mock_save.mock_calls) == 1 assert mock_save.mock_calls[0][1][1] == {"test device": SUBSCRIPTION_1} @@ -321,7 +321,7 @@ async def test_registering_new_device_expiration_view(hass, hass_client): with patch("homeassistant.components.html5.notify.save_json") as mock_save: resp = await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_4)) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert mock_save.mock_calls[0][1][1] == {"unnamed device": SUBSCRIPTION_4} @@ -336,7 +336,7 @@ async def test_registering_new_device_fails_view(hass, hass_client): ): resp = await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_4)) - assert resp.status == HTTP_INTERNAL_SERVER_ERROR + assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR assert registrations == {} @@ -349,7 +349,7 @@ async def test_registering_existing_device_view(hass, hass_client): await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_1)) resp = await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_4)) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert mock_save.mock_calls[0][1][1] == {"unnamed device": SUBSCRIPTION_4} assert registrations == {"unnamed device": SUBSCRIPTION_4} @@ -366,7 +366,7 @@ async def test_registering_existing_device_view_with_name(hass, hass_client): await client.post(REGISTER_URL, data=json.dumps(SUB_WITH_NAME)) resp = await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_4)) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert mock_save.mock_calls[0][1][1] == {"test device": SUBSCRIPTION_4} assert registrations == {"test device": SUBSCRIPTION_4} @@ -381,7 +381,7 @@ async def test_registering_existing_device_fails_view(hass, hass_client): mock_save.side_effect = HomeAssistantError resp = await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_4)) - assert resp.status == HTTP_INTERNAL_SERVER_ERROR + assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR assert registrations == {"unnamed device": SUBSCRIPTION_1} @@ -393,17 +393,17 @@ async def test_registering_new_device_validation(hass, hass_client): REGISTER_URL, data=json.dumps({"browser": "invalid browser", "subscription": "sub info"}), ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST resp = await client.post(REGISTER_URL, data=json.dumps({"browser": "chrome"})) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST with patch("homeassistant.components.html5.notify.save_json", return_value=False): resp = await client.post( REGISTER_URL, data=json.dumps({"browser": "chrome", "subscription": "sub info"}), ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST async def test_unregistering_device_view(hass, hass_client): @@ -417,7 +417,7 @@ async def test_unregistering_device_view(hass, hass_client): data=json.dumps({"subscription": SUBSCRIPTION_1["subscription"]}), ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert len(mock_save.mock_calls) == 1 assert registrations == {"other device": SUBSCRIPTION_2} @@ -433,7 +433,7 @@ async def test_unregister_device_view_handle_unknown_subscription(hass, hass_cli data=json.dumps({"subscription": SUBSCRIPTION_3["subscription"]}), ) - assert resp.status == 200, resp.response + assert resp.status == HTTPStatus.OK, resp.response assert registrations == {} assert len(mock_save.mock_calls) == 0 @@ -452,7 +452,7 @@ async def test_unregistering_device_view_handles_save_error(hass, hass_client): data=json.dumps({"subscription": SUBSCRIPTION_1["subscription"]}), ) - assert resp.status == HTTP_INTERNAL_SERVER_ERROR, resp.response + assert resp.status == HTTPStatus.INTERNAL_SERVER_ERROR, resp.response assert registrations == { "some device": SUBSCRIPTION_1, "other device": SUBSCRIPTION_2, @@ -469,7 +469,7 @@ async def test_callback_view_no_jwt(hass, hass_client): ), ) - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED async def test_callback_view_with_jwt(hass, hass_client): @@ -504,7 +504,7 @@ async def test_callback_view_with_jwt(hass, hass_client): PUBLISH_URL, json={"type": "push"}, headers={AUTHORIZATION: bearer_token} ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK body = await resp.json() assert body == {"event": "push", "status": "ok"} diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index 71d848d12ab..8e2703cd51b 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -1,5 +1,6 @@ """The tests for the Home Assistant HTTP component.""" from datetime import timedelta +from http import HTTPStatus from ipaddress import ip_network from unittest.mock import patch @@ -94,10 +95,10 @@ async def test_cant_access_with_password_in_header( client = await aiohttp_client(app) req = await client.get("/", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}) - assert req.status == 401 + assert req.status == HTTPStatus.UNAUTHORIZED req = await client.get("/", headers={HTTP_HEADER_HA_AUTH: "wrong-pass"}) - assert req.status == 401 + assert req.status == HTTPStatus.UNAUTHORIZED async def test_cant_access_with_password_in_query( @@ -108,13 +109,13 @@ async def test_cant_access_with_password_in_query( client = await aiohttp_client(app) resp = await client.get("/", params={"api_password": API_PASSWORD}) - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED resp = await client.get("/") - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED resp = await client.get("/", params={"api_password": "wrong-password"}) - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED async def test_basic_auth_does_not_work(app, aiohttp_client, hass, legacy_auth): @@ -123,16 +124,16 @@ async def test_basic_auth_does_not_work(app, aiohttp_client, hass, legacy_auth): client = await aiohttp_client(app) req = await client.get("/", auth=BasicAuth("homeassistant", API_PASSWORD)) - assert req.status == 401 + assert req.status == HTTPStatus.UNAUTHORIZED req = await client.get("/", auth=BasicAuth("wrong_username", API_PASSWORD)) - assert req.status == 401 + assert req.status == HTTPStatus.UNAUTHORIZED req = await client.get("/", auth=BasicAuth("homeassistant", "wrong password")) - assert req.status == 401 + assert req.status == HTTPStatus.UNAUTHORIZED req = await client.get("/", headers={"authorization": "NotBasic abcdefg"}) - assert req.status == 401 + assert req.status == HTTPStatus.UNAUTHORIZED async def test_cannot_access_with_trusted_ip( @@ -147,12 +148,16 @@ async def test_cannot_access_with_trusted_ip( for remote_addr in UNTRUSTED_ADDRESSES: set_mock_ip(remote_addr) resp = await client.get("/") - assert resp.status == 401, f"{remote_addr} shouldn't be trusted" + assert ( + resp.status == HTTPStatus.UNAUTHORIZED + ), f"{remote_addr} shouldn't be trusted" for remote_addr in TRUSTED_ADDRESSES: set_mock_ip(remote_addr) resp = await client.get("/") - assert resp.status == 401, f"{remote_addr} shouldn't be trusted" + assert ( + resp.status == HTTPStatus.UNAUTHORIZED + ), f"{remote_addr} shouldn't be trusted" async def test_auth_active_access_with_access_token_in_header( @@ -165,27 +170,27 @@ async def test_auth_active_access_with_access_token_in_header( refresh_token = await hass.auth.async_validate_access_token(hass_access_token) req = await client.get("/", headers={"Authorization": f"Bearer {token}"}) - assert req.status == 200 + assert req.status == HTTPStatus.OK assert await req.json() == {"user_id": refresh_token.user.id} req = await client.get("/", headers={"AUTHORIZATION": f"Bearer {token}"}) - assert req.status == 200 + assert req.status == HTTPStatus.OK assert await req.json() == {"user_id": refresh_token.user.id} req = await client.get("/", headers={"authorization": f"Bearer {token}"}) - assert req.status == 200 + assert req.status == HTTPStatus.OK assert await req.json() == {"user_id": refresh_token.user.id} req = await client.get("/", headers={"Authorization": token}) - assert req.status == 401 + assert req.status == HTTPStatus.UNAUTHORIZED req = await client.get("/", headers={"Authorization": f"BEARER {token}"}) - assert req.status == 401 + assert req.status == HTTPStatus.UNAUTHORIZED refresh_token = await hass.auth.async_validate_access_token(hass_access_token) refresh_token.user.is_active = False req = await client.get("/", headers={"Authorization": f"Bearer {token}"}) - assert req.status == 401 + assert req.status == HTTPStatus.UNAUTHORIZED async def test_auth_active_access_with_trusted_ip( @@ -200,12 +205,16 @@ async def test_auth_active_access_with_trusted_ip( for remote_addr in UNTRUSTED_ADDRESSES: set_mock_ip(remote_addr) resp = await client.get("/") - assert resp.status == 401, f"{remote_addr} shouldn't be trusted" + assert ( + resp.status == HTTPStatus.UNAUTHORIZED + ), f"{remote_addr} shouldn't be trusted" for remote_addr in TRUSTED_ADDRESSES: set_mock_ip(remote_addr) resp = await client.get("/") - assert resp.status == 401, f"{remote_addr} shouldn't be trusted" + assert ( + resp.status == HTTPStatus.UNAUTHORIZED + ), f"{remote_addr} shouldn't be trusted" async def test_auth_legacy_support_api_password_cannot_access( @@ -216,13 +225,13 @@ async def test_auth_legacy_support_api_password_cannot_access( client = await aiohttp_client(app) req = await client.get("/", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}) - assert req.status == 401 + assert req.status == HTTPStatus.UNAUTHORIZED resp = await client.get("/", params={"api_password": API_PASSWORD}) - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED req = await client.get("/", auth=BasicAuth("homeassistant", API_PASSWORD)) - assert req.status == 401 + assert req.status == HTTPStatus.UNAUTHORIZED async def test_auth_access_signed_path(hass, app, aiohttp_client, hass_access_token): @@ -237,17 +246,17 @@ async def test_auth_access_signed_path(hass, app, aiohttp_client, hass_access_to signed_path = async_sign_path(hass, refresh_token.id, "/", timedelta(seconds=5)) req = await client.get(signed_path) - assert req.status == 200 + assert req.status == HTTPStatus.OK data = await req.json() assert data["user_id"] == refresh_token.user.id # Use signature on other path req = await client.get("/another_path?{}".format(signed_path.split("?")[1])) - assert req.status == 401 + assert req.status == HTTPStatus.UNAUTHORIZED # We only allow GET req = await client.post(signed_path) - assert req.status == 401 + assert req.status == HTTPStatus.UNAUTHORIZED # Never valid as expired in the past. expired_signed_path = async_sign_path( @@ -255,9 +264,9 @@ async def test_auth_access_signed_path(hass, app, aiohttp_client, hass_access_to ) req = await client.get(expired_signed_path) - assert req.status == 401 + assert req.status == HTTPStatus.UNAUTHORIZED # refresh token gone should also invalidate signature await hass.auth.async_remove_refresh_token(refresh_token) req = await client.get(signed_path) - assert req.status == 401 + assert req.status == HTTPStatus.UNAUTHORIZED diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index ed3c2ad23af..fbd545e0506 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -1,4 +1,6 @@ """The tests for the Home Assistant HTTP component.""" +from http import HTTPStatus + # pylint: disable=protected-access from ipaddress import ip_address import os @@ -19,7 +21,6 @@ from homeassistant.components.http.ban import ( setup_bans, ) from homeassistant.components.http.view import request_handler_factory -from homeassistant.const import HTTP_FORBIDDEN from homeassistant.setup import async_setup_component from . import mock_real_ip @@ -65,14 +66,16 @@ async def test_access_from_banned_ip(hass, aiohttp_client): for remote_addr in BANNED_IPS: set_real_ip(remote_addr) resp = await client.get("/") - assert resp.status == HTTP_FORBIDDEN + assert resp.status == HTTPStatus.FORBIDDEN @pytest.mark.parametrize( "remote_addr, bans, status", list( zip( - BANNED_IPS_WITH_SUPERVISOR, [1, 1, 0], [HTTP_FORBIDDEN, HTTP_FORBIDDEN, 401] + BANNED_IPS_WITH_SUPERVISOR, + [1, 1, 0], + [HTTPStatus.FORBIDDEN, HTTPStatus.FORBIDDEN, HTTPStatus.UNAUTHORIZED], ) ), ) @@ -104,7 +107,7 @@ async def test_access_from_supervisor_ip( "homeassistant.components.http.ban.open", m_open, create=True ): resp = await client.get("/") - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED assert len(app[KEY_BANNED_IPS]) == bans assert m_open.call_count == bans @@ -155,19 +158,19 @@ async def test_ip_bans_file_creation(hass, aiohttp_client): with patch("homeassistant.components.http.ban.open", m_open, create=True): resp = await client.get("/") - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED assert len(app[KEY_BANNED_IPS]) == len(BANNED_IPS) assert m_open.call_count == 0 resp = await client.get("/") - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED assert len(app[KEY_BANNED_IPS]) == len(BANNED_IPS) + 1 m_open.assert_called_once_with( hass.config.path(IP_BANS_FILE), "a", encoding="utf8" ) resp = await client.get("/") - assert resp.status == HTTP_FORBIDDEN + assert resp.status == HTTPStatus.FORBIDDEN assert m_open.call_count == 1 assert ( @@ -216,19 +219,19 @@ async def test_failed_login_attempts_counter(hass, aiohttp_client): client = await aiohttp_client(app) resp = await client.get("/auth_false") - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED assert app[KEY_FAILED_LOGIN_ATTEMPTS][remote_ip] == 1 resp = await client.get("/auth_false") - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED assert app[KEY_FAILED_LOGIN_ATTEMPTS][remote_ip] == 2 resp = await client.get("/") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert app[KEY_FAILED_LOGIN_ATTEMPTS][remote_ip] == 2 # This used to check that with trusted networks we reset login attempts # We no longer support trusted networks. resp = await client.get("/auth_true") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert app[KEY_FAILED_LOGIN_ATTEMPTS][remote_ip] == 2 diff --git a/tests/components/http/test_cors.py b/tests/components/http/test_cors.py index d03b40b2df3..141627c7763 100644 --- a/tests/components/http/test_cors.py +++ b/tests/components/http/test_cors.py @@ -1,4 +1,5 @@ """Test cors for the HTTP component.""" +from http import HTTPStatus from pathlib import Path from unittest.mock import patch @@ -59,28 +60,28 @@ def client(loop, aiohttp_client): async def test_cors_requests(client): """Test cross origin requests.""" req = await client.get("/", headers={ORIGIN: TRUSTED_ORIGIN}) - assert req.status == 200 + assert req.status == HTTPStatus.OK assert req.headers[ACCESS_CONTROL_ALLOW_ORIGIN] == TRUSTED_ORIGIN # With password in URL req = await client.get( "/", params={"api_password": "some-pass"}, headers={ORIGIN: TRUSTED_ORIGIN} ) - assert req.status == 200 + assert req.status == HTTPStatus.OK assert req.headers[ACCESS_CONTROL_ALLOW_ORIGIN] == TRUSTED_ORIGIN # With password in headers req = await client.get( "/", headers={HTTP_HEADER_HA_AUTH: "some-pass", ORIGIN: TRUSTED_ORIGIN} ) - assert req.status == 200 + assert req.status == HTTPStatus.OK assert req.headers[ACCESS_CONTROL_ALLOW_ORIGIN] == TRUSTED_ORIGIN # With auth token in headers req = await client.get( "/", headers={AUTHORIZATION: "Bearer some-token", ORIGIN: TRUSTED_ORIGIN} ) - assert req.status == 200 + assert req.status == HTTPStatus.OK assert req.headers[ACCESS_CONTROL_ALLOW_ORIGIN] == TRUSTED_ORIGIN @@ -95,7 +96,7 @@ async def test_cors_preflight_allowed(client): }, ) - assert req.status == 200 + assert req.status == HTTPStatus.OK assert req.headers[ACCESS_CONTROL_ALLOW_ORIGIN] == TRUSTED_ORIGIN assert req.headers[ACCESS_CONTROL_ALLOW_HEADERS] == "X-REQUESTED-WITH" @@ -139,7 +140,7 @@ async def test_cors_works_with_frontend(hass, hass_client): ) client = await hass_client() resp = await client.get("/") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK async def test_cors_on_static_files(hass, hass_client): @@ -157,5 +158,5 @@ async def test_cors_on_static_files(hass, hass_client): ACCESS_CONTROL_REQUEST_METHOD: "GET", }, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert resp.headers[ACCESS_CONTROL_ALLOW_ORIGIN] == "http://www.example.com" diff --git a/tests/components/http/test_data_validator.py b/tests/components/http/test_data_validator.py index a6e812ccdfe..4ff6d3e8c2a 100644 --- a/tests/components/http/test_data_validator.py +++ b/tests/components/http/test_data_validator.py @@ -1,4 +1,5 @@ """Test data validator decorator.""" +from http import HTTPStatus from unittest.mock import Mock from aiohttp import web @@ -35,13 +36,13 @@ async def test_validator(aiohttp_client): ) resp = await client.post("/", json={"test": "bla"}) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK resp = await client.post("/", json={"test": 100}) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST resp = await client.post("/") - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST async def test_validator_allow_empty(aiohttp_client): @@ -61,10 +62,10 @@ async def test_validator_allow_empty(aiohttp_client): ) resp = await client.post("/", json={"test": "bla"}) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK resp = await client.post("/", json={"test": 100}) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST resp = await client.post("/") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK diff --git a/tests/components/http/test_forwarded.py b/tests/components/http/test_forwarded.py index 42e67416044..c6eb421fcf3 100644 --- a/tests/components/http/test_forwarded.py +++ b/tests/components/http/test_forwarded.py @@ -1,4 +1,5 @@ """Test real forwarded middleware.""" +from http import HTTPStatus from ipaddress import ip_network from unittest.mock import Mock, patch @@ -34,7 +35,7 @@ async def test_x_forwarded_for_without_trusted_proxy(aiohttp_client, caplog): mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: "255.255.255.255"}) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST assert ( "Received X-Forwarded-For header from an untrusted proxy 127.0.0.1" in caplog.text @@ -81,7 +82,7 @@ async def test_x_forwarded_for_with_trusted_proxy( mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: x_forwarded_for}) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK async def test_x_forwarded_for_disabled_with_proxy(aiohttp_client, caplog): @@ -104,7 +105,7 @@ async def test_x_forwarded_for_disabled_with_proxy(aiohttp_client, caplog): mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: "255.255.255.255"}) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST assert ( "A request from a reverse proxy was received from 127.0.0.1, but your HTTP " "integration is not set-up for reverse proxies" in caplog.text @@ -132,7 +133,7 @@ async def test_x_forwarded_for_with_spoofed_header(aiohttp_client): "/", headers={X_FORWARDED_FOR: "222.222.222.222, 255.255.255.255"} ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK @pytest.mark.parametrize( @@ -160,7 +161,7 @@ async def test_x_forwarded_for_with_malformed_header( resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: x_forwarded_for}) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST assert "Invalid IP address in X-Forwarded-For" in caplog.text @@ -180,7 +181,7 @@ async def test_x_forwarded_for_with_multiple_headers(aiohttp_client, caplog): ], ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST assert "Too many headers for X-Forwarded-For" in caplog.text @@ -237,7 +238,7 @@ async def test_x_forwarded_proto_with_trusted_proxy( }, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK async def test_x_forwarded_proto_with_trusted_proxy_multiple_for(aiohttp_client): @@ -265,7 +266,7 @@ async def test_x_forwarded_proto_with_trusted_proxy_multiple_for(aiohttp_client) }, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK async def test_x_forwarded_proto_not_processed_without_for(aiohttp_client): @@ -287,7 +288,7 @@ async def test_x_forwarded_proto_not_processed_without_for(aiohttp_client): mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get("/", headers={X_FORWARDED_PROTO: "https"}) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK async def test_x_forwarded_proto_with_multiple_headers(aiohttp_client, caplog): @@ -306,7 +307,7 @@ async def test_x_forwarded_proto_with_multiple_headers(aiohttp_client, caplog): ], ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST assert "Too many headers for X-Forward-Proto" in caplog.text @@ -328,7 +329,7 @@ async def test_x_forwarded_proto_empty_element( headers={X_FORWARDED_FOR: "1.1.1.1", X_FORWARDED_PROTO: x_forwarded_proto}, ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST assert "Empty item received in X-Forward-Proto header" in caplog.text @@ -356,7 +357,7 @@ async def test_x_forwarded_proto_incorrect_number_of_elements( }, ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST assert ( f"Incorrect number of elements in X-Forward-Proto. Expected 1 or {expected}, got {got}" in caplog.text @@ -384,7 +385,7 @@ async def test_x_forwarded_host_with_trusted_proxy(aiohttp_client): headers={X_FORWARDED_FOR: "255.255.255.255", X_FORWARDED_HOST: "example.com"}, ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK async def test_x_forwarded_host_not_processed_without_for(aiohttp_client): @@ -406,7 +407,7 @@ async def test_x_forwarded_host_not_processed_without_for(aiohttp_client): mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get("/", headers={X_FORWARDED_HOST: "example.com"}) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK async def test_x_forwarded_host_with_multiple_headers(aiohttp_client, caplog): @@ -425,7 +426,7 @@ async def test_x_forwarded_host_with_multiple_headers(aiohttp_client, caplog): ], ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST assert "Too many headers for X-Forwarded-Host" in caplog.text @@ -440,7 +441,7 @@ async def test_x_forwarded_host_with_empty_header(aiohttp_client, caplog): "/", headers={X_FORWARDED_FOR: "222.222.222.222", X_FORWARDED_HOST: ""} ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST assert "Empty value received in X-Forward-Host header" in caplog.text @@ -460,4 +461,4 @@ async def test_x_forwarded_cloud(aiohttp_client, caplog): ) # This request would normally fail because it's invalid, now it works. - assert resp.status == 200 + assert resp.status == HTTPStatus.OK diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 446b6c218bb..c03c8143bad 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -1,5 +1,6 @@ """The tests for the Home Assistant HTTP component.""" from datetime import timedelta +from http import HTTPStatus from ipaddress import ip_network import logging from unittest.mock import Mock, patch @@ -72,7 +73,7 @@ async def test_not_log_password(hass, hass_client_no_auth, caplog, legacy_auth): resp = await client.get("/api/", params={"api_password": "test-password"}) - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED logs = caplog.text # Ensure we don't log API passwords diff --git a/tests/components/http/test_request_context.py b/tests/components/http/test_request_context.py index f511b860dca..57cf6d4e17d 100644 --- a/tests/components/http/test_request_context.py +++ b/tests/components/http/test_request_context.py @@ -1,5 +1,6 @@ """Test request context middleware.""" from contextvars import ContextVar +from http import HTTPStatus from aiohttp import web @@ -24,7 +25,7 @@ async def test_request_context_middleware(aiohttp_client): mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get("/") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK text = await resp.text() assert text == "hi!" diff --git a/tests/components/http/test_security_filter.py b/tests/components/http/test_security_filter.py index eb1ac54d8f6..5897da50929 100644 --- a/tests/components/http/test_security_filter.py +++ b/tests/components/http/test_security_filter.py @@ -1,4 +1,6 @@ """Test security filter middleware.""" +from http import HTTPStatus + from aiohttp import web import pytest import urllib3 @@ -31,7 +33,7 @@ async def test_ok_requests(request_path, request_params, aiohttp_client): mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get(request_path, params=request_params) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert await resp.text() == "OK" @@ -80,7 +82,7 @@ async def test_bad_requests( request_params, ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST message = "Filtered a potential harmful request to:" if fail_on_query_string: diff --git a/tests/components/http/test_view.py b/tests/components/http/test_view.py index 522344461cb..fdd4fbc7808 100644 --- a/tests/components/http/test_view.py +++ b/tests/components/http/test_view.py @@ -1,4 +1,5 @@ """Tests for Home Assistant View.""" +from http import HTTPStatus from unittest.mock import AsyncMock, Mock from aiohttp.web_exceptions import ( @@ -68,4 +69,4 @@ async def test_not_running(mock_request_with_stopping): response = await request_handler_factory( Mock(requires_auth=False), AsyncMock(side_effect=Unauthorized) )(mock_request_with_stopping) - assert response.status == 503 + assert response.status == HTTPStatus.SERVICE_UNAVAILABLE From 05671557f0ce6ae6a8ee33aead593616079d5b2a Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 23 Oct 2021 14:42:50 -0400 Subject: [PATCH 0743/1038] Use DeviceInfo Class I-K (#58300) --- .../components/ialarm/alarm_control_panel.py | 13 +++++---- .../components/iaqualink/__init__.py | 4 +-- .../components/icloud/device_tracker.py | 12 ++++---- homeassistant/components/icloud/sensor.py | 12 ++++---- .../components/insteon/insteon_entity.py | 6 ++-- homeassistant/components/ios/sensor.py | 17 +++++------ homeassistant/components/iotawatt/sensor.py | 14 ++++------ homeassistant/components/ipp/entity.py | 23 ++++++--------- homeassistant/components/izone/climate.py | 28 ++++++------------- homeassistant/components/juicenet/entity.py | 6 ++-- .../keenetic_ndms2/device_tracker.py | 17 +++++------ .../components/keenetic_ndms2/router.py | 17 +++++------ homeassistant/components/kodi/media_player.py | 13 +++++---- .../components/konnected/binary_sensor.py | 9 +++--- homeassistant/components/konnected/sensor.py | 3 +- homeassistant/components/konnected/switch.py | 8 ++---- .../components/kostal_plenticore/helper.py | 15 +++++----- homeassistant/components/kraken/sensor.py | 13 +++++---- homeassistant/components/kulersky/light.py | 13 +++++---- 19 files changed, 114 insertions(+), 129 deletions(-) diff --git a/homeassistant/components/ialarm/alarm_control_panel.py b/homeassistant/components/ialarm/alarm_control_panel.py index fc758dd6175..bb5f5386ecd 100644 --- a/homeassistant/components/ialarm/alarm_control_panel.py +++ b/homeassistant/components/ialarm/alarm_control_panel.py @@ -4,6 +4,7 @@ from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_HOME, ) +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DATA_COORDINATOR, DOMAIN @@ -19,13 +20,13 @@ class IAlarmPanel(CoordinatorEntity, AlarmControlPanelEntity): """Representation of an iAlarm device.""" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info for this device.""" - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "manufacturer": "Antifurto365 - Meian", - } + return DeviceInfo( + identifiers={(DOMAIN, self.unique_id)}, + manufacturer="Antifurto365 - Meian", + name=self.name, + ) @property def unique_id(self): diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 7eb7256aa99..856674f1345 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -240,8 +240,8 @@ class AqualinkEntity(Entity): """Return the device info.""" return DeviceInfo( identifiers={(DOMAIN, self.unique_id)}, - name=self.name, - model=self.dev.__class__.__name__.replace("Aqualink", ""), manufacturer="Jandy", + model=self.dev.__class__.__name__.replace("Aqualink", ""), + name=self.name, via_device=(DOMAIN, self.dev.system.serial), ) diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 233df6a7556..d255d29b6ee 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -115,12 +115,12 @@ class IcloudTrackerEntity(TrackerEntity): @property def device_info(self) -> DeviceInfo: """Return the device information.""" - return { - "identifiers": {(DOMAIN, self._device.unique_id)}, - "name": self._device.name, - "manufacturer": "Apple", - "model": self._device.device_model, - } + return DeviceInfo( + identifiers={(DOMAIN, self._device.unique_id)}, + manufacturer="Apple", + model=self._device.device_model, + name=self._device.name, + ) async def async_added_to_hass(self): """Register state update callback.""" diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index 5469eadc998..6de8a49daf1 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -93,12 +93,12 @@ class IcloudDeviceBatterySensor(SensorEntity): @property def device_info(self) -> DeviceInfo: """Return the device information.""" - return { - "identifiers": {(DOMAIN, self._device.unique_id)}, - "name": self._device.name, - "manufacturer": "Apple", - "model": self._device.device_model, - } + return DeviceInfo( + identifiers={(DOMAIN, self._device.unique_id)}, + manufacturer="Apple", + model=self._device.device_model, + name=self._device.name, + ) @property def should_poll(self) -> bool: diff --git a/homeassistant/components/insteon/insteon_entity.py b/homeassistant/components/insteon/insteon_entity.py index f3f71e37ecf..aa2d4367225 100644 --- a/homeassistant/components/insteon/insteon_entity.py +++ b/homeassistant/components/insteon/insteon_entity.py @@ -83,10 +83,10 @@ class InsteonEntity(Entity): """Return device information.""" return DeviceInfo( identifiers={(DOMAIN, str(self._insteon_device.address))}, - name=f"{self._insteon_device.description} {self._insteon_device.address}", - model=f"{self._insteon_device.model} ({self._insteon_device.cat!r}, 0x{self._insteon_device.subcat:02x})", - sw_version=f"{self._insteon_device.firmware:02x} Engine Version: {self._insteon_device.engine_version}", manufacturer="Smart Home", + model=f"{self._insteon_device.model} ({self._insteon_device.cat!r}, 0x{self._insteon_device.subcat:02x})", + name=f"{self._insteon_device.description} {self._insteon_device.address}", + sw_version=f"{self._insteon_device.firmware:02x} Engine Version: {self._insteon_device.engine_version}", via_device=(DOMAIN, str(devices.modem.address)), ) diff --git a/homeassistant/components/ios/sensor.py b/homeassistant/components/ios/sensor.py index c3c1ad2b8ce..d8d779155bd 100644 --- a/homeassistant/components/ios/sensor.py +++ b/homeassistant/components/ios/sensor.py @@ -6,6 +6,7 @@ from homeassistant.components.sensor import SensorEntity, SensorEntityDescriptio from homeassistant.const import PERCENTAGE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.icon import icon_for_battery_level from .const import DOMAIN @@ -59,20 +60,20 @@ class IOSSensor(SensorEntity): self._attr_unique_id = f"{description.key}_{device_id}" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return information about the device.""" - return { - "identifiers": { + return DeviceInfo( + identifiers={ ( ios.DOMAIN, self._device[ios.ATTR_DEVICE][ios.ATTR_DEVICE_PERMANENT_ID], ) }, - "name": self._device[ios.ATTR_DEVICE][ios.ATTR_DEVICE_NAME], - "manufacturer": "Apple", - "model": self._device[ios.ATTR_DEVICE][ios.ATTR_DEVICE_TYPE], - "sw_version": self._device[ios.ATTR_DEVICE][ios.ATTR_DEVICE_SYSTEM_VERSION], - } + manufacturer="Apple", + model=self._device[ios.ATTR_DEVICE][ios.ATTR_DEVICE_TYPE], + name=self._device[ios.ATTR_DEVICE][ios.ATTR_DEVICE_NAME], + sw_version=self._device[ios.ATTR_DEVICE][ios.ATTR_DEVICE_SYSTEM_VERSION], + ) @property def extra_state_attributes(self): diff --git a/homeassistant/components/iotawatt/sensor.py b/homeassistant/components/iotawatt/sensor.py index ec2918b0ce6..1da5100ea9f 100644 --- a/homeassistant/components/iotawatt/sensor.py +++ b/homeassistant/components/iotawatt/sensor.py @@ -191,15 +191,13 @@ class IotaWattSensor(update_coordinator.CoordinatorEntity, SensorEntity): return self._sensor_data.getName() @property - def device_info(self) -> entity.DeviceInfo | None: + def device_info(self) -> entity.DeviceInfo: """Return device info.""" - return { - "connections": { - (CONNECTION_NETWORK_MAC, self._sensor_data.hub_mac_address) - }, - "manufacturer": "IoTaWatt", - "model": "IoTaWatt", - } + return entity.DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, self._sensor_data.hub_mac_address)}, + manufacturer="IoTaWatt", + model="IoTaWatt", + ) @callback def _handle_coordinator_update(self) -> None: diff --git a/homeassistant/components/ipp/entity.py b/homeassistant/components/ipp/entity.py index 55a0e76a658..7bd01b4cd12 100644 --- a/homeassistant/components/ipp/entity.py +++ b/homeassistant/components/ipp/entity.py @@ -1,13 +1,6 @@ """Entities for The Internet Printing Protocol (IPP) integration.""" from __future__ import annotations -from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, - ATTR_SW_VERSION, -) from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -37,15 +30,15 @@ class IPPEntity(CoordinatorEntity): self._attr_entity_registry_enabled_default = enabled_default @property - def device_info(self) -> DeviceInfo: + def device_info(self) -> DeviceInfo | None: """Return device information about this IPP device.""" if self._device_id is None: return None - return { - ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)}, - ATTR_NAME: self.coordinator.data.info.name, - ATTR_MANUFACTURER: self.coordinator.data.info.manufacturer, - ATTR_MODEL: self.coordinator.data.info.model, - ATTR_SW_VERSION: self.coordinator.data.info.version, - } + return DeviceInfo( + identifiers={(DOMAIN, self._device_id)}, + manufacturer=self.coordinator.data.info.manufacturer, + model=self.coordinator.data.info.model, + name=self.coordinator.data.info.name, + sw_version=self.coordinator.data.info.version, + ) diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index ddbb59aedad..67d121d760e 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -161,12 +161,12 @@ class ControllerDevice(ClimateEntity): self._fan_to_pizone[_IZONE_FAN_TO_HA[fan]] = fan self._available = True - self._device_info = { - "identifiers": {(IZONE, self.unique_id)}, - "name": self.name, - "manufacturer": "IZone", - "model": self._controller.sys_type, - } + self._attr_device_info = DeviceInfo( + identifiers={(IZONE, self.unique_id)}, + manufacturer="IZone", + model=self._controller.sys_type, + name=self.name, + ) # Create the zones self.zones = {} @@ -246,11 +246,6 @@ class ControllerDevice(ClimateEntity): for zone in self.zones.values(): zone.async_schedule_update_ha_state() - @property - def device_info(self): - """Return the device info for the iZone system.""" - return self._device_info - @property def unique_id(self): """Return the ID of the controller device.""" @@ -484,12 +479,12 @@ class ZoneDevice(ClimateEntity): } self._supported_features |= SUPPORT_TARGET_TEMPERATURE - self._device_info = DeviceInfo( + self._attr_device_info = DeviceInfo( identifiers={(IZONE, controller.unique_id, zone.index)}, - name=self.name, manufacturer="IZone", - via_device=(IZONE, controller.unique_id), model=zone.type.name.title(), + name=self.name, + via_device=(IZONE, controller.unique_id), ) async def async_added_to_hass(self): @@ -517,11 +512,6 @@ class ZoneDevice(ClimateEntity): """Return True if unable to access real state of the entity.""" return self._controller.assumed_state - @property - def device_info(self): - """Return the device info for the iZone system.""" - return self._device_info - @property def unique_id(self): """Return the ID of the controller device.""" diff --git a/homeassistant/components/juicenet/entity.py b/homeassistant/components/juicenet/entity.py index d3b2aa41d19..4b4e5764a5e 100644 --- a/homeassistant/components/juicenet/entity.py +++ b/homeassistant/components/juicenet/entity.py @@ -24,8 +24,8 @@ class JuiceNetDevice(CoordinatorEntity): def device_info(self) -> DeviceInfo: """Return device information about this JuiceNet Device.""" return DeviceInfo( - identifiers={(DOMAIN, self.device.id)}, - name=self.device.name, - manufacturer="JuiceNet", configuration_url=f"https://home.juice.net/Portal/Details?unitID={self.device.id}", + identifiers={(DOMAIN, self.device.id)}, + manufacturer="JuiceNet", + name=self.device.name, ) diff --git a/homeassistant/components/keenetic_ndms2/device_tracker.py b/homeassistant/components/keenetic_ndms2/device_tracker.py index a08d8c72f0c..4ec353045c7 100644 --- a/homeassistant/components/keenetic_ndms2/device_tracker.py +++ b/homeassistant/components/keenetic_ndms2/device_tracker.py @@ -26,6 +26,7 @@ from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo import homeassistant.util.dt as dt_util from .const import ( @@ -217,17 +218,13 @@ class KeeneticTracker(ScannerEntity): return None @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return a client description for device registry.""" - info = { - "connections": {(CONNECTION_NETWORK_MAC, self._device.mac)}, - "identifiers": {(DOMAIN, self._device.mac)}, - } - - if self._device.name: - info["name"] = self._device.name - - return info + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, self._device.mac)}, + identifiers={(DOMAIN, self._device.mac)}, + name=self._device.name if self._device.name else None, + ) async def async_added_to_hass(self): """Client entity created.""" diff --git a/homeassistant/components/keenetic_ndms2/router.py b/homeassistant/components/keenetic_ndms2/router.py index 8da8034a162..fdab10ea55e 100644 --- a/homeassistant/components/keenetic_ndms2/router.py +++ b/homeassistant/components/keenetic_ndms2/router.py @@ -19,6 +19,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.event import async_call_later import homeassistant.util.dt as dt_util @@ -66,15 +67,15 @@ class KeeneticRouter: return self.config_entry.data[CONF_HOST] @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the host of this hub.""" - return { - "identifiers": {(DOMAIN, f"router-{self.config_entry.entry_id}")}, - "manufacturer": self.manufacturer, - "model": self.model, - "name": self.name, - "sw_version": self.firmware, - } + return DeviceInfo( + identifiers={(DOMAIN, f"router-{self.config_entry.entry_id}")}, + manufacturer=self.manufacturer, + model=self.model, + name=self.name, + sw_version=self.firmware, + ) @property def name(self): diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 9fd46de026c..1e4298c447e 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -62,6 +62,7 @@ from homeassistant.helpers import ( device_registry, entity_platform, ) +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.network import is_internal_request import homeassistant.util.dt as dt_util @@ -345,13 +346,13 @@ class KodiEntity(MediaPlayerEntity): return self._unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info for this device.""" - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "manufacturer": "Kodi", - } + return DeviceInfo( + identifiers={(DOMAIN, self.unique_id)}, + manufacturer="Kodi", + name=self.name, + ) @property def state(self): diff --git a/homeassistant/components/konnected/binary_sensor.py b/homeassistant/components/konnected/binary_sensor.py index dc7a15758d9..2647d43a44e 100644 --- a/homeassistant/components/konnected/binary_sensor.py +++ b/homeassistant/components/konnected/binary_sensor.py @@ -10,6 +10,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from .const import DOMAIN as KONNECTED_DOMAIN @@ -66,11 +67,11 @@ class KonnectedBinarySensor(BinarySensorEntity): return self._device_class @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" - return { - "identifiers": {(KONNECTED_DOMAIN, self._device_id)}, - } + return DeviceInfo( + identifiers={(KONNECTED_DOMAIN, self._device_id)}, + ) async def async_added_to_hass(self): """Store entity_id and register state change callback.""" diff --git a/homeassistant/components/konnected/sensor.py b/homeassistant/components/konnected/sensor.py index ae43e771068..0b835e3fdce 100644 --- a/homeassistant/components/konnected/sensor.py +++ b/homeassistant/components/konnected/sensor.py @@ -15,6 +15,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from .const import DOMAIN as KONNECTED_DOMAIN, SIGNAL_DS18B20_NEW @@ -111,7 +112,7 @@ class KonnectedSensor(SensorEntity): name += f" {description.name}" self._attr_name = name - self._attr_device_info = {"identifiers": {(KONNECTED_DOMAIN, device_id)}} + self._attr_device_info = DeviceInfo(identifiers={(KONNECTED_DOMAIN, device_id)}) @property def native_value(self): diff --git a/homeassistant/components/konnected/switch.py b/homeassistant/components/konnected/switch.py index 9c9f8193dcd..687d29f182c 100644 --- a/homeassistant/components/konnected/switch.py +++ b/homeassistant/components/konnected/switch.py @@ -11,7 +11,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity import DeviceInfo, ToggleEntity from .const import ( CONF_ACTIVATION, @@ -77,11 +77,9 @@ class KonnectedSwitch(ToggleEntity): return device_data.get("panel") @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" - return { - "identifiers": {(KONNECTED_DOMAIN, self._device_id)}, - } + return DeviceInfo(identifiers={(KONNECTED_DOMAIN, self._device_id)}) @property def available(self): diff --git a/homeassistant/components/kostal_plenticore/helper.py b/homeassistant/components/kostal_plenticore/helper.py index 2a21cb4ee55..32dfc9b2fd9 100644 --- a/homeassistant/components/kostal_plenticore/helper.py +++ b/homeassistant/components/kostal_plenticore/helper.py @@ -13,6 +13,7 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_ST from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.event import async_call_later from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -84,14 +85,14 @@ class Plenticore: prod1 = device_local["Branding:ProductName1"] prod2 = device_local["Branding:ProductName2"] - self.device_info = { - "identifiers": {(DOMAIN, device_local["Properties:SerialNo"])}, - "manufacturer": "Kostal", - "model": f"{prod1} {prod2}", - "name": settings["scb:network"]["Hostname"], - "sw_version": f'IOC: {device_local["Properties:VersionIOC"]}' + self.device_info = DeviceInfo( + identifiers={(DOMAIN, device_local["Properties:SerialNo"])}, + manufacturer="Kostal", + model=f"{prod1} {prod2}", + name=settings["scb:network"]["Hostname"], + sw_version=f'IOC: {device_local["Properties:VersionIOC"]}' + f' MC: {device_local["Properties:VersionMC"]}', - } + ) return True diff --git a/homeassistant/components/kraken/sensor.py b/homeassistant/components/kraken/sensor.py index b7d38d4796b..d2b2cfeae70 100644 --- a/homeassistant/components/kraken/sensor.py +++ b/homeassistant/components/kraken/sensor.py @@ -9,6 +9,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -112,12 +113,12 @@ class KrakenSensor(CoordinatorEntity[Optional[KrakenResponse]], SensorEntity): self._received_data_at_least_once = False self._available = True - self._attr_device_info = { - "identifiers": {(DOMAIN, f"{source_asset}_{self._target_asset}")}, - "name": self._device_name, - "manufacturer": "Kraken.com", - "entry_type": "service", - } + self._attr_device_info = DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, f"{source_asset}_{self._target_asset}")}, + manufacturer="Kraken.com", + name=self._device_name, + ) async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" diff --git a/homeassistant/components/kulersky/light.py b/homeassistant/components/kulersky/light.py index 6e04dbdfcfd..f8d85960550 100644 --- a/homeassistant/components/kulersky/light.py +++ b/homeassistant/components/kulersky/light.py @@ -15,6 +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.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval @@ -97,13 +98,13 @@ class KulerskyLight(LightEntity): return self._light.address @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Device info for this light.""" - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "manufacturer": "Brightech", - } + return DeviceInfo( + identifiers={(DOMAIN, self.unique_id)}, + manufacturer="Brightech", + name=self.name, + ) @property def is_on(self): From 50e0c58310b4d8ac2a69dd2484a34fda99acff72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 23 Oct 2021 21:49:04 +0300 Subject: [PATCH 0744/1038] Use http.HTTPStatus in components/s* (#58291) --- homeassistant/components/sendgrid/notify.py | 4 +- .../components/shelly/config_flow.py | 10 +-- homeassistant/components/sigfox/sensor.py | 7 ++- .../components/smartthings/__init__.py | 21 +++---- .../components/smartthings/config_flow.py | 13 ++-- homeassistant/components/splunk/__init__.py | 3 +- .../components/squeezebox/config_flow.py | 11 +--- homeassistant/components/startca/sensor.py | 4 +- homeassistant/components/stream/hls.py | 9 +-- .../components/synology_chat/notify.py | 5 +- tests/components/shelly/test_config_flow.py | 16 ++++- tests/components/shopping_list/test_init.py | 21 ++++--- tests/components/sigfox/test_sensor.py | 7 ++- .../signal_messenger/test_notify.py | 14 ++--- tests/components/sleepiq/test_init.py | 3 +- tests/components/smappee/test_config_flow.py | 3 +- .../components/smart_meter_texas/conftest.py | 3 +- .../smartthings/test_config_flow.py | 10 ++- tests/components/smartthings/test_init.py | 2 +- tests/components/somfy/test_config_flow.py | 3 +- tests/components/sonarr/__init__.py | 37 ++++++----- tests/components/spaceapi/test_init.py | 8 ++- tests/components/spotify/test_config_flow.py | 3 +- .../components/squeezebox/test_config_flow.py | 13 ++-- tests/components/stream/conftest.py | 3 +- tests/components/stream/test_hls.py | 34 +++++----- tests/components/stream/test_ll_hls.py | 62 ++++++++++--------- tests/components/stt/test_init.py | 6 +- tests/components/system_log/test_init.py | 3 +- 29 files changed, 180 insertions(+), 158 deletions(-) diff --git a/homeassistant/components/sendgrid/notify.py b/homeassistant/components/sendgrid/notify.py index 8d8907f2dcf..e17f6ae60c4 100644 --- a/homeassistant/components/sendgrid/notify.py +++ b/homeassistant/components/sendgrid/notify.py @@ -1,4 +1,5 @@ """SendGrid notification service.""" +from http import HTTPStatus import logging from sendgrid import SendGridAPIClient @@ -15,7 +16,6 @@ from homeassistant.const import ( CONF_RECIPIENT, CONF_SENDER, CONTENT_TYPE_TEXT_PLAIN, - HTTP_ACCEPTED, ) import homeassistant.helpers.config_validation as cv @@ -66,5 +66,5 @@ class SendgridNotificationService(BaseNotificationService): } response = self._sg.client.mail.send.post(request_body=data) - if response.status_code != HTTP_ACCEPTED: + if response.status_code != HTTPStatus.ACCEPTED: _LOGGER.error("Unable to send notification") diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 31f99b2b1fb..b77868296bd 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from http import HTTPStatus import logging from typing import Any, Final @@ -13,12 +14,7 @@ import async_timeout import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_USERNAME, - HTTP_UNAUTHORIZED, -) +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client @@ -155,7 +151,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.hass, self.host, self.info, user_input ) except aiohttp.ClientResponseError as error: - if error.status == HTTP_UNAUTHORIZED: + if error.status == HTTPStatus.UNAUTHORIZED: errors["base"] = "invalid_auth" else: errors["base"] = "cannot_connect" diff --git a/homeassistant/components/sigfox/sensor.py b/homeassistant/components/sigfox/sensor.py index 41fb3469293..5ab68424ad3 100644 --- a/homeassistant/components/sigfox/sensor.py +++ b/homeassistant/components/sigfox/sensor.py @@ -1,5 +1,6 @@ """Sensor for SigFox devices.""" import datetime +from http import HTTPStatus import json import logging from urllib.parse import urljoin @@ -8,7 +9,7 @@ import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import CONF_NAME, HTTP_OK, HTTP_UNAUTHORIZED +from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -65,8 +66,8 @@ class SigfoxAPI: """Check API credentials are valid.""" url = urljoin(API_URL, "devicetypes") response = requests.get(url, auth=self._auth, timeout=10) - if response.status_code != HTTP_OK: - if response.status_code == HTTP_UNAUTHORIZED: + if response.status_code != HTTPStatus.OK: + if response.status_code == HTTPStatus.UNAUTHORIZED: _LOGGER.error("Invalid credentials for Sigfox API") else: _LOGGER.error( diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 5612cd65732..eb3aa9cb0f0 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import Iterable +from http import HTTPStatus import importlib import logging @@ -12,13 +13,7 @@ from pysmartthings import Attribute, Capability, SmartThings from pysmartthings.device import DeviceEntity from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_ACCESS_TOKEN, - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, - HTTP_FORBIDDEN, - HTTP_UNAUTHORIZED, -) +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -164,7 +159,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][DATA_BROKERS][entry.entry_id] = broker except ClientResponseError as ex: - if ex.status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN): + if ex.status in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN): _LOGGER.exception( "Unable to setup configuration entry '%s' - please reconfigure the integration", entry.title, @@ -197,7 +192,7 @@ async def async_get_entry_scenes(entry: ConfigEntry, api): try: return await api.scenes(location_id=entry.data[CONF_LOCATION_ID]) except ClientResponseError as ex: - if ex.status == HTTP_FORBIDDEN: + if ex.status == HTTPStatus.FORBIDDEN: _LOGGER.exception( "Unable to load scenes for configuration entry '%s' because the access token does not have the required access", entry.title, @@ -220,12 +215,12 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Perform clean-up when entry is being removed.""" api = SmartThings(async_get_clientsession(hass), entry.data[CONF_ACCESS_TOKEN]) - # Remove the installed_app, which if already removed raises a HTTP_FORBIDDEN error. + # Remove the installed_app, which if already removed raises a HTTPStatus.FORBIDDEN error. installed_app_id = entry.data[CONF_INSTALLED_APP_ID] try: await api.delete_installed_app(installed_app_id) except ClientResponseError as ex: - if ex.status == HTTP_FORBIDDEN: + if ex.status == HTTPStatus.FORBIDDEN: _LOGGER.debug( "Installed app %s has already been removed", installed_app_id, @@ -236,7 +231,7 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: _LOGGER.debug("Removed installed app %s", installed_app_id) # Remove the app if not referenced by other entries, which if already - # removed raises a HTTP_FORBIDDEN error. + # removed raises a HTTPStatus.FORBIDDEN error. all_entries = hass.config_entries.async_entries(DOMAIN) app_id = entry.data[CONF_APP_ID] app_count = sum(1 for entry in all_entries if entry.data[CONF_APP_ID] == app_id) @@ -250,7 +245,7 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: try: await api.delete_app(app_id) except ClientResponseError as ex: - if ex.status == HTTP_FORBIDDEN: + if ex.status == HTTPStatus.FORBIDDEN: _LOGGER.debug("App %s has already been removed", app_id, exc_info=True) else: raise diff --git a/homeassistant/components/smartthings/config_flow.py b/homeassistant/components/smartthings/config_flow.py index b69ef5d43a1..9a11fe43e02 100644 --- a/homeassistant/components/smartthings/config_flow.py +++ b/homeassistant/components/smartthings/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure SmartThings.""" +from http import HTTPStatus import logging from aiohttp import ClientResponseError @@ -7,13 +8,7 @@ from pysmartthings.installedapp import format_install_url import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import ( - CONF_ACCESS_TOKEN, - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, - HTTP_FORBIDDEN, - HTTP_UNAUTHORIZED, -) +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( @@ -142,12 +137,12 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) return self._show_step_pat(errors) except ClientResponseError as ex: - if ex.status == HTTP_UNAUTHORIZED: + if ex.status == HTTPStatus.UNAUTHORIZED: errors[CONF_ACCESS_TOKEN] = "token_unauthorized" _LOGGER.debug( "Unauthorized error received setting up SmartApp", exc_info=True ) - elif ex.status == HTTP_FORBIDDEN: + elif ex.status == HTTPStatus.FORBIDDEN: errors[CONF_ACCESS_TOKEN] = "token_forbidden" _LOGGER.debug( "Forbidden error received setting up SmartApp", exc_info=True diff --git a/homeassistant/components/splunk/__init__.py b/homeassistant/components/splunk/__init__.py index a3ec307d67b..6b40f9b7d58 100644 --- a/homeassistant/components/splunk/__init__.py +++ b/homeassistant/components/splunk/__init__.py @@ -1,5 +1,6 @@ """Support to send data to a Splunk instance.""" import asyncio +from http import HTTPStatus import json import logging import time @@ -111,7 +112,7 @@ async def async_setup(hass, config): try: await event_collector.queue(json.dumps(payload, cls=JSONEncoder), send=True) except SplunkPayloadError as err: - if err.status == 401: + if err.status == HTTPStatus.UNAUTHORIZED: _LOGGER.error(err) else: _LOGGER.warning(err) diff --git a/homeassistant/components/squeezebox/config_flow.py b/homeassistant/components/squeezebox/config_flow.py index 4b05588e281..e7e0a691e85 100644 --- a/homeassistant/components/squeezebox/config_flow.py +++ b/homeassistant/components/squeezebox/config_flow.py @@ -1,5 +1,6 @@ """Config flow for Logitech Squeezebox integration.""" import asyncio +from http import HTTPStatus import logging from pysqueezebox import Server, async_discover @@ -8,13 +9,7 @@ import voluptuous as vol from homeassistant import config_entries, data_entry_flow from homeassistant.components.dhcp import MAC_ADDRESS from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, - HTTP_UNAUTHORIZED, -) +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.entity_registry import async_get @@ -115,7 +110,7 @@ class SqueezeboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: status = await server.async_query("serverstatus") if not status: - if server.http_status == HTTP_UNAUTHORIZED: + if server.http_status == HTTPStatus.UNAUTHORIZED: return "invalid_auth" return "cannot_connect" except Exception: # pylint: disable=broad-except diff --git a/homeassistant/components/startca/sensor.py b/homeassistant/components/startca/sensor.py index 8079ea42c4c..931e9eabfc0 100644 --- a/homeassistant/components/startca/sensor.py +++ b/homeassistant/components/startca/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import timedelta +from http import HTTPStatus import logging from xml.parsers.expat import ExpatError @@ -19,7 +20,6 @@ from homeassistant.const import ( CONF_MONITORED_VARIABLES, CONF_NAME, DATA_GIGABYTES, - HTTP_OK, PERCENTAGE, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -195,7 +195,7 @@ class StartcaData: url = f"https://www.start.ca/support/usage/api?key={self.api_key}" with async_timeout.timeout(REQUEST_TIMEOUT): req = await self.websession.get(url) - if req.status != HTTP_OK: + if req.status != HTTPStatus.OK: _LOGGER.error("Request failed with status: %u", req.status) return False diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index 50b002b52b4..a88ee465e25 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -1,6 +1,7 @@ """Provide functionality to stream HLS.""" from __future__ import annotations +from http import HTTPStatus from typing import TYPE_CHECKING, cast from aiohttp import web @@ -193,7 +194,7 @@ class HlsPlaylistView(StreamView): """Return a HTTP Bad Request response.""" return web.Response( body=None, - status=400, + status=HTTPStatus.BAD_REQUEST, # From Appendix B.1 of the RFC: # Successful responses to blocking Playlist requests should be cached # for six Target Durations. Unsuccessful responses (such as 404s) should @@ -211,7 +212,7 @@ class HlsPlaylistView(StreamView): """Return a HTTP Not Found response.""" return web.Response( body=None, - status=404, + status=HTTPStatus.NOT_FOUND, headers={ "Cache-Control": f"max-age={(4 if blocking else 1)*target_duration:.0f}" }, @@ -351,7 +352,7 @@ class HlsPartView(StreamView): ): return web.Response( body=None, - status=404, + status=HTTPStatus.NOT_FOUND, headers={"Cache-Control": f"max-age={track.target_duration:.0f}"}, ) # If the part is ready or has been hinted, @@ -399,7 +400,7 @@ class HlsSegmentView(StreamView): ): return web.Response( body=None, - status=404, + status=HTTPStatus.NOT_FOUND, headers={"Cache-Control": f"max-age={track.target_duration:.0f}"}, ) return web.Response( diff --git a/homeassistant/components/synology_chat/notify.py b/homeassistant/components/synology_chat/notify.py index f73fd65ba3f..e68968e94d0 100644 --- a/homeassistant/components/synology_chat/notify.py +++ b/homeassistant/components/synology_chat/notify.py @@ -1,4 +1,5 @@ """SynologyChat platform for notify component.""" +from http import HTTPStatus import json import logging @@ -10,7 +11,7 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) -from homeassistant.const import CONF_RESOURCE, CONF_VERIFY_SSL, HTTP_CREATED, HTTP_OK +from homeassistant.const import CONF_RESOURCE, CONF_VERIFY_SSL import homeassistant.helpers.config_validation as cv ATTR_FILE_URL = "file_url" @@ -57,7 +58,7 @@ class SynologyChatNotificationService(BaseNotificationService): self._resource, data=to_send, timeout=10, verify=self._verify_ssl ) - if response.status_code not in (HTTP_OK, HTTP_CREATED): + if response.status_code not in (HTTPStatus.OK, HTTPStatus.CREATED): _LOGGER.exception( "Error sending message. Response %d: %s:", response.status_code, diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index f17e01e118b..dc6921b9735 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Shelly config flow.""" import asyncio +from http import HTTPStatus from unittest.mock import AsyncMock, Mock, patch import aiohttp @@ -325,8 +326,14 @@ async def test_form_firmware_unsupported(hass): @pytest.mark.parametrize( "error", [ - (aiohttp.ClientResponseError(Mock(), (), status=400), "cannot_connect"), - (aiohttp.ClientResponseError(Mock(), (), status=401), "invalid_auth"), + ( + aiohttp.ClientResponseError(Mock(), (), status=HTTPStatus.BAD_REQUEST), + "cannot_connect", + ), + ( + aiohttp.ClientResponseError(Mock(), (), status=HTTPStatus.UNAUTHORIZED), + "invalid_auth", + ), (asyncio.TimeoutError, "cannot_connect"), (ValueError, "unknown"), ], @@ -480,7 +487,10 @@ async def test_zeroconf_sleeping_device(hass): @pytest.mark.parametrize( "error", [ - (aiohttp.ClientResponseError(Mock(), (), status=400), "cannot_connect"), + ( + aiohttp.ClientResponseError(Mock(), (), status=HTTPStatus.BAD_REQUEST), + "cannot_connect", + ), (asyncio.TimeoutError, "cannot_connect"), ], ) diff --git a/tests/components/shopping_list/test_init.py b/tests/components/shopping_list/test_init.py index 65fddec894e..3c18929f6a1 100644 --- a/tests/components/shopping_list/test_init.py +++ b/tests/components/shopping_list/test_init.py @@ -1,4 +1,5 @@ """Test shopping list component.""" +from http import HTTPStatus from homeassistant.components.shopping_list.const import ( DOMAIN, @@ -11,7 +12,7 @@ from homeassistant.components.websocket_api.const import ( ERR_NOT_FOUND, TYPE_RESULT, ) -from homeassistant.const import ATTR_NAME, HTTP_NOT_FOUND +from homeassistant.const import ATTR_NAME from homeassistant.helpers import intent @@ -115,7 +116,7 @@ async def test_deprecated_api_get_all(hass, hass_client, sl_setup): client = await hass_client() resp = await client.get("/api/shopping_list") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK data = await resp.json() assert len(data) == 2 assert data[0]["name"] == "beer" @@ -169,7 +170,7 @@ async def test_deprecated_api_update(hass, hass_client, sl_setup): f"/api/shopping_list/item/{beer_id}", json={"name": "soda"} ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK data = await resp.json() assert data == {"id": beer_id, "name": "soda", "complete": False} @@ -177,7 +178,7 @@ async def test_deprecated_api_update(hass, hass_client, sl_setup): f"/api/shopping_list/item/{wine_id}", json={"complete": True} ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK data = await resp.json() assert data == {"id": wine_id, "name": "wine", "complete": True} @@ -238,12 +239,12 @@ async def test_api_update_fails(hass, hass_client, sl_setup): client = await hass_client() resp = await client.post("/api/shopping_list/non_existing", json={"name": "soda"}) - assert resp.status == HTTP_NOT_FOUND + assert resp.status == HTTPStatus.NOT_FOUND beer_id = hass.data["shopping_list"].items[0]["id"] resp = await client.post(f"/api/shopping_list/item/{beer_id}", json={"name": 123}) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST async def test_ws_update_item_fail(hass, hass_ws_client, sl_setup): @@ -288,10 +289,10 @@ async def test_deprecated_api_clear_completed(hass, hass_client, sl_setup): resp = await client.post( f"/api/shopping_list/item/{beer_id}", json={"complete": True} ) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK resp = await client.post("/api/shopping_list/clear_completed") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK items = hass.data["shopping_list"].items assert len(items) == 1 @@ -334,7 +335,7 @@ async def test_deprecated_api_create(hass, hass_client, sl_setup): client = await hass_client() resp = await client.post("/api/shopping_list/item", json={"name": "soda"}) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK data = await resp.json() assert data["name"] == "soda" assert data["complete"] is False @@ -351,7 +352,7 @@ async def test_deprecated_api_create_fail(hass, hass_client, sl_setup): client = await hass_client() resp = await client.post("/api/shopping_list/item", json={"name": 1234}) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST assert len(hass.data["shopping_list"].items) == 0 diff --git a/tests/components/sigfox/test_sensor.py b/tests/components/sigfox/test_sensor.py index a7d293a052e..a5086d522b0 100644 --- a/tests/components/sigfox/test_sensor.py +++ b/tests/components/sigfox/test_sensor.py @@ -1,4 +1,5 @@ """Tests for the sigfox sensor.""" +from http import HTTPStatus import re import requests_mock @@ -34,7 +35,7 @@ async def test_invalid_credentials(hass): """Test for invalid credentials.""" with requests_mock.Mocker() as mock_req: url = re.compile(API_URL + "devicetypes") - mock_req.get(url, text="{}", status_code=401) + mock_req.get(url, text="{}", status_code=HTTPStatus.UNAUTHORIZED) assert await async_setup_component(hass, "sensor", VALID_CONFIG) await hass.async_block_till_done() assert len(hass.states.async_entity_ids()) == 0 @@ -44,7 +45,9 @@ async def test_valid_credentials(hass): """Test for valid credentials.""" with requests_mock.Mocker() as mock_req: url1 = re.compile(API_URL + "devicetypes") - mock_req.get(url1, text='{"data":[{"id":"fake_type"}]}', status_code=200) + mock_req.get( + url1, text='{"data":[{"id":"fake_type"}]}', status_code=HTTPStatus.OK + ) url2 = re.compile(API_URL + "devicetypes/fake_type/devices") mock_req.get(url2, text='{"data":[{"id":"fake_id"}]}') diff --git a/tests/components/signal_messenger/test_notify.py b/tests/components/signal_messenger/test_notify.py index d2dc93fea2e..61d13b8c60b 100644 --- a/tests/components/signal_messenger/test_notify.py +++ b/tests/components/signal_messenger/test_notify.py @@ -1,5 +1,5 @@ """The tests for the signal_messenger platform.""" - +from http import HTTPStatus import os import tempfile import unittest @@ -54,12 +54,12 @@ class TestSignalMesssenger(unittest.TestCase): mock.register_uri( "POST", "http://127.0.0.1:8080/v2/send", - status_code=201, + status_code=HTTPStatus.CREATED, ) mock.register_uri( "GET", "http://127.0.0.1:8080/v1/about", - status_code=200, + status_code=HTTPStatus.OK, json={"versions": ["v1", "v2"]}, ) with self.assertLogs( @@ -77,12 +77,12 @@ class TestSignalMesssenger(unittest.TestCase): mock.register_uri( "POST", "http://127.0.0.1:8080/v2/send", - status_code=201, + status_code=HTTPStatus.CREATED, ) mock.register_uri( "GET", "http://127.0.0.1:8080/v1/about", - status_code=200, + status_code=HTTPStatus.OK, json={"versions": ["v1", "v2"]}, ) with self.assertLogs( @@ -106,12 +106,12 @@ class TestSignalMesssenger(unittest.TestCase): mock.register_uri( "POST", "http://127.0.0.1:8080/v2/send", - status_code=201, + status_code=HTTPStatus.CREATED, ) mock.register_uri( "GET", "http://127.0.0.1:8080/v1/about", - status_code=200, + status_code=HTTPStatus.OK, json={"versions": ["v1", "v2"]}, ) with self.assertLogs( diff --git a/tests/components/sleepiq/test_init.py b/tests/components/sleepiq/test_init.py index a5e8e43ae07..68ca876504f 100644 --- a/tests/components/sleepiq/test_init.py +++ b/tests/components/sleepiq/test_init.py @@ -1,4 +1,5 @@ """The tests for the SleepIQ component.""" +from http import HTTPStatus from unittest.mock import MagicMock, patch from homeassistant import setup @@ -41,7 +42,7 @@ async def test_setup_login_failed(hass, requests_mock): mock_responses(requests_mock) requests_mock.put( "https://prod-api.sleepiq.sleepnumber.com/rest/login", - status_code=401, + status_code=HTTPStatus.UNAUTHORIZED, json=load_fixture("sleepiq-login-failed.json"), ) diff --git a/tests/components/smappee/test_config_flow.py b/tests/components/smappee/test_config_flow.py index 73fbf81fce0..d8efe8b2903 100644 --- a/tests/components/smappee/test_config_flow.py +++ b/tests/components/smappee/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Smappee component config flow module.""" +from http import HTTPStatus from unittest.mock import patch from homeassistant import data_entry_flow, setup @@ -424,7 +425,7 @@ async def test_full_user_flow( client = await hass_client_no_auth() resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert resp.headers["content-type"] == "text/html; charset=utf-8" aioclient_mock.post( diff --git a/tests/components/smart_meter_texas/conftest.py b/tests/components/smart_meter_texas/conftest.py index 8b089e5d31b..0d474503582 100644 --- a/tests/components/smart_meter_texas/conftest.py +++ b/tests/components/smart_meter_texas/conftest.py @@ -1,5 +1,6 @@ """Test configuration and mocks for Smart Meter Texas.""" import asyncio +from http import HTTPStatus import json from pathlib import Path @@ -67,7 +68,7 @@ def mock_connection( elif auth_fail: aioclient_mock.post( auth_endpoint, - status=400, + status=HTTPStatus.BAD_REQUEST, json={"errormessage": "ERR-USR-INVALIDPASSWORDERROR"}, ) else: # auth_timeout diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index be6385ccdd9..7f8950f18b5 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -551,7 +551,10 @@ async def test_webhook_problem_shows_error(hass, smartthings_mock): data = {"error": {}} request_info = Mock(real_url="http://example.com") error = APIResponseError( - request_info=request_info, history=None, data=data, status=422 + request_info=request_info, + history=None, + data=data, + status=HTTPStatus.UNPROCESSABLE_ENTITY, ) error.is_target_error = Mock(return_value=True) smartthings_mock.apps.side_effect = error @@ -591,7 +594,10 @@ async def test_api_error_shows_error(hass, smartthings_mock): data = {"error": {}} request_info = Mock(real_url="http://example.com") error = APIResponseError( - request_info=request_info, history=None, data=data, status=400 + request_info=request_info, + history=None, + data=data, + status=HTTPStatus.BAD_REQUEST, ) smartthings_mock.apps.side_effect = error diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index 43f88dfb7af..8696fb5956e 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -58,7 +58,7 @@ async def test_unrecoverable_api_errors_create_new_flow( config_entry.add_to_hass(hass) request_info = Mock(real_url="http://example.com") smartthings_mock.app.side_effect = ClientResponseError( - request_info=request_info, history=None, status=401 + request_info=request_info, history=None, status=HTTPStatus.UNAUTHORIZED ) # Assert setup returns false diff --git a/tests/components/somfy/test_config_flow.py b/tests/components/somfy/test_config_flow.py index bcfb617db96..752959802da 100644 --- a/tests/components/somfy/test_config_flow.py +++ b/tests/components/somfy/test_config_flow.py @@ -1,5 +1,6 @@ """Tests for the Somfy config flow.""" import asyncio +from http import HTTPStatus from unittest.mock import patch from homeassistant import config_entries, data_entry_flow, setup @@ -69,7 +70,7 @@ async def test_full_flow( client = await hass_client_no_auth() resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert resp.headers["content-type"] == "text/html; charset=utf-8" aioclient_mock.post( diff --git a/tests/components/sonarr/__init__.py b/tests/components/sonarr/__init__.py index e3ae6bfa837..8172cb4e0dd 100644 --- a/tests/components/sonarr/__init__.py +++ b/tests/components/sonarr/__init__.py @@ -1,4 +1,5 @@ """Tests for the Sonarr component.""" +from http import HTTPStatus from socket import gaierror as SocketGIAError from unittest.mock import patch @@ -148,13 +149,13 @@ def mock_connection_invalid_auth( """Mock Sonarr invalid auth errors.""" sonarr_url = f"http://{host}:{port}{base_path}" - aioclient_mock.get(f"{sonarr_url}/system/status", status=403) - aioclient_mock.get(f"{sonarr_url}/diskspace", status=403) - aioclient_mock.get(f"{sonarr_url}/calendar", status=403) - aioclient_mock.get(f"{sonarr_url}/command", status=403) - aioclient_mock.get(f"{sonarr_url}/queue", status=403) - aioclient_mock.get(f"{sonarr_url}/series", status=403) - aioclient_mock.get(f"{sonarr_url}/missing/wanted", status=403) + aioclient_mock.get(f"{sonarr_url}/system/status", status=HTTPStatus.FORBIDDEN) + aioclient_mock.get(f"{sonarr_url}/diskspace", status=HTTPStatus.FORBIDDEN) + aioclient_mock.get(f"{sonarr_url}/calendar", status=HTTPStatus.FORBIDDEN) + aioclient_mock.get(f"{sonarr_url}/command", status=HTTPStatus.FORBIDDEN) + aioclient_mock.get(f"{sonarr_url}/queue", status=HTTPStatus.FORBIDDEN) + aioclient_mock.get(f"{sonarr_url}/series", status=HTTPStatus.FORBIDDEN) + aioclient_mock.get(f"{sonarr_url}/missing/wanted", status=HTTPStatus.FORBIDDEN) def mock_connection_server_error( @@ -166,13 +167,21 @@ def mock_connection_server_error( """Mock Sonarr server errors.""" sonarr_url = f"http://{host}:{port}{base_path}" - aioclient_mock.get(f"{sonarr_url}/system/status", status=500) - aioclient_mock.get(f"{sonarr_url}/diskspace", status=500) - aioclient_mock.get(f"{sonarr_url}/calendar", status=500) - aioclient_mock.get(f"{sonarr_url}/command", status=500) - aioclient_mock.get(f"{sonarr_url}/queue", status=500) - aioclient_mock.get(f"{sonarr_url}/series", status=500) - aioclient_mock.get(f"{sonarr_url}/missing/wanted", status=500) + aioclient_mock.get( + f"{sonarr_url}/system/status", status=HTTPStatus.INTERNAL_SERVER_ERROR + ) + aioclient_mock.get( + f"{sonarr_url}/diskspace", status=HTTPStatus.INTERNAL_SERVER_ERROR + ) + aioclient_mock.get( + f"{sonarr_url}/calendar", status=HTTPStatus.INTERNAL_SERVER_ERROR + ) + aioclient_mock.get(f"{sonarr_url}/command", status=HTTPStatus.INTERNAL_SERVER_ERROR) + aioclient_mock.get(f"{sonarr_url}/queue", status=HTTPStatus.INTERNAL_SERVER_ERROR) + aioclient_mock.get(f"{sonarr_url}/series", status=HTTPStatus.INTERNAL_SERVER_ERROR) + aioclient_mock.get( + f"{sonarr_url}/missing/wanted", status=HTTPStatus.INTERNAL_SERVER_ERROR + ) async def setup_integration( diff --git a/tests/components/spaceapi/test_init.py b/tests/components/spaceapi/test_init.py index 0f33434254c..35da5bf27c7 100644 --- a/tests/components/spaceapi/test_init.py +++ b/tests/components/spaceapi/test_init.py @@ -1,4 +1,6 @@ """The tests for the Home Assistant SpaceAPI component.""" +from http import HTTPStatus + # pylint: disable=protected-access from unittest.mock import patch @@ -91,7 +93,7 @@ def mock_client(hass, hass_client): async def test_spaceapi_get(hass, mock_client): """Test response after start-up Home Assistant.""" resp = await mock_client.get(URL_API_SPACEAPI) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK data = await resp.json() @@ -137,7 +139,7 @@ async def test_spaceapi_state_get(hass, mock_client): hass.states.async_set("test.test_door", True) resp = await mock_client.get(URL_API_SPACEAPI) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK data = await resp.json() assert data["state"]["open"] == bool(1) @@ -146,7 +148,7 @@ async def test_spaceapi_state_get(hass, mock_client): async def test_spaceapi_sensors_get(hass, mock_client): """Test the response for the sensors.""" resp = await mock_client.get(URL_API_SPACEAPI) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK data = await resp.json() assert data["sensors"] == SENSOR_OUTPUT diff --git a/tests/components/spotify/test_config_flow.py b/tests/components/spotify/test_config_flow.py index 0d0d4a50a3d..8ff18882e8b 100644 --- a/tests/components/spotify/test_config_flow.py +++ b/tests/components/spotify/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Spotify config flow.""" +from http import HTTPStatus from unittest.mock import patch from spotipy import SpotifyException @@ -80,7 +81,7 @@ async def test_full_flow( client = await hass_client_no_auth() resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert resp.headers["content-type"] == "text/html; charset=utf-8" aioclient_mock.post( diff --git a/tests/components/squeezebox/test_config_flow.py b/tests/components/squeezebox/test_config_flow.py index 9460e2235be..7f07576427b 100644 --- a/tests/components/squeezebox/test_config_flow.py +++ b/tests/components/squeezebox/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Logitech Squeezebox config flow.""" +from http import HTTPStatus from unittest.mock import patch from pysqueezebox import Server @@ -6,13 +7,7 @@ from pysqueezebox import Server from homeassistant import config_entries from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.components.squeezebox.const import DOMAIN -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, - HTTP_UNAUTHORIZED, -) +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY, @@ -39,7 +34,7 @@ async def mock_failed_discover(_discovery_callback): async def patch_async_query_unauthorized(self, *args): """Mock an unauthorized query.""" - self.http_status = HTTP_UNAUTHORIZED + self.http_status = HTTPStatus.UNAUTHORIZED return False @@ -128,7 +123,7 @@ async def test_form_invalid_auth(hass): ) async def patch_async_query(self, *args): - self.http_status = HTTP_UNAUTHORIZED + self.http_status = HTTPStatus.UNAUTHORIZED return False with patch("pysqueezebox.Server.async_query", new=patch_async_query): diff --git a/tests/components/stream/conftest.py b/tests/components/stream/conftest.py index 746cc05fcbd..f5f66258f70 100644 --- a/tests/components/stream/conftest.py +++ b/tests/components/stream/conftest.py @@ -13,6 +13,7 @@ from __future__ import annotations import asyncio from collections import deque +from http import HTTPStatus import logging import threading from unittest.mock import patch @@ -171,7 +172,7 @@ class HLSSync: self.check_requests_ready() return self._original_not_found() - def response(self, body, headers, status=200): + def response(self, body, headers, status=HTTPStatus.OK): """Intercept the Response call so we know when the web handler is finished.""" self._num_finished += 1 self.check_requests_ready() diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index da040f6646a..c3c4779a948 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -1,5 +1,6 @@ """The tests for hls streams.""" from datetime import timedelta +from http import HTTPStatus from unittest.mock import patch from urllib.parse import urlparse @@ -15,7 +16,6 @@ from homeassistant.components.stream.const import ( NUM_PLAYLIST_SEGMENTS, ) from homeassistant.components.stream.core import Part -from homeassistant.const import HTTP_NOT_FOUND from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -138,23 +138,23 @@ async def test_hls_stream(hass, hls_stream, stream_worker_sync): # Fetch master playlist master_playlist_response = await hls_client.get() - assert master_playlist_response.status == 200 + assert master_playlist_response.status == HTTPStatus.OK # Fetch init master_playlist = await master_playlist_response.text() init_response = await hls_client.get("/init.mp4") - assert init_response.status == 200 + assert init_response.status == HTTPStatus.OK # Fetch playlist playlist_url = "/" + master_playlist.splitlines()[-1] playlist_response = await hls_client.get(playlist_url) - assert playlist_response.status == 200 + assert playlist_response.status == HTTPStatus.OK # Fetch segment playlist = await playlist_response.text() segment_url = "/" + [line for line in playlist.splitlines() if line][-1] segment_response = await hls_client.get(segment_url) - assert segment_response.status == 200 + assert segment_response.status == HTTPStatus.OK stream_worker_sync.resume() @@ -163,7 +163,7 @@ async def test_hls_stream(hass, hls_stream, stream_worker_sync): # Ensure playlist not accessible after stream ends fail_response = await hls_client.get() - assert fail_response.status == HTTP_NOT_FOUND + assert fail_response.status == HTTPStatus.NOT_FOUND async def test_stream_timeout(hass, hass_client, stream_worker_sync): @@ -186,7 +186,7 @@ async def test_stream_timeout(hass, hass_client, stream_worker_sync): # Fetch playlist parsed_url = urlparse(url) playlist_response = await http_client.get(parsed_url.path) - assert playlist_response.status == 200 + assert playlist_response.status == HTTPStatus.OK # Wait a minute future = dt_util.utcnow() + timedelta(minutes=1) @@ -194,7 +194,7 @@ async def test_stream_timeout(hass, hass_client, stream_worker_sync): # Fetch again to reset timer playlist_response = await http_client.get(parsed_url.path) - assert playlist_response.status == 200 + assert playlist_response.status == HTTPStatus.OK stream_worker_sync.resume() @@ -205,7 +205,7 @@ async def test_stream_timeout(hass, hass_client, stream_worker_sync): # Ensure playlist not accessible fail_response = await http_client.get(parsed_url.path) - assert fail_response.status == HTTP_NOT_FOUND + assert fail_response.status == HTTPStatus.NOT_FOUND async def test_stream_timeout_after_stop(hass, hass_client, stream_worker_sync): @@ -280,7 +280,7 @@ async def test_hls_playlist_view_no_output(hass, hls_stream): # Fetch playlist resp = await hls_client.get("/playlist.m3u8") - assert resp.status == 404 + assert resp.status == HTTPStatus.NOT_FOUND async def test_hls_playlist_view(hass, hls_stream, stream_worker_sync): @@ -298,7 +298,7 @@ async def test_hls_playlist_view(hass, hls_stream, stream_worker_sync): hls_client = await hls_stream(stream) resp = await hls_client.get("/playlist.m3u8") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert await resp.text() == make_playlist( sequence=0, segments=[make_segment(0), make_segment(1)] ) @@ -307,7 +307,7 @@ async def test_hls_playlist_view(hass, hls_stream, stream_worker_sync): hls.put(segment) await hass.async_block_till_done() resp = await hls_client.get("/playlist.m3u8") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert await resp.text() == make_playlist( sequence=0, segments=[make_segment(0), make_segment(1), make_segment(2)] ) @@ -333,7 +333,7 @@ async def test_hls_max_segments(hass, hls_stream, stream_worker_sync): await hass.async_block_till_done() resp = await hls_client.get("/playlist.m3u8") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK # Only NUM_PLAYLIST_SEGMENTS are returned in the playlist. start = MAX_SEGMENTS + 1 - NUM_PLAYLIST_SEGMENTS @@ -356,12 +356,12 @@ async def test_hls_max_segments(hass, hls_stream, stream_worker_sync): # The segment that fell off the buffer is not accessible with patch.object(hls.stream_settings, "hls_part_timeout", 0.1): segment_response = await hls_client.get("/segment/0.m4s") - assert segment_response.status == 404 + assert segment_response.status == HTTPStatus.NOT_FOUND # However all segments in the buffer are accessible, even those that were not in the playlist. for sequence in range(1, MAX_SEGMENTS + 1): segment_response = await hls_client.get(f"/segment/{sequence}.m4s") - assert segment_response.status == 200 + assert segment_response.status == HTTPStatus.OK stream_worker_sync.resume() stream.stop() @@ -390,7 +390,7 @@ async def test_hls_playlist_view_discontinuity(hass, hls_stream, stream_worker_s hls_client = await hls_stream(stream) resp = await hls_client.get("/playlist.m3u8") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert await resp.text() == make_playlist( sequence=0, segments=[ @@ -428,7 +428,7 @@ async def test_hls_max_segments_discontinuity(hass, hls_stream, stream_worker_sy await hass.async_block_till_done() resp = await hls_client.get("/playlist.m3u8") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK # Only NUM_PLAYLIST_SEGMENTS are returned in the playlist causing the # EXT-X-DISCONTINUITY tag to be omitted and EXT-X-DISCONTINUITY-SEQUENCE diff --git a/tests/components/stream/test_ll_hls.py b/tests/components/stream/test_ll_hls.py index ab1c01adce8..324b1435110 100644 --- a/tests/components/stream/test_ll_hls.py +++ b/tests/components/stream/test_ll_hls.py @@ -1,5 +1,6 @@ """The tests for hls streams.""" import asyncio +from http import HTTPStatus import itertools import re from urllib.parse import urlparse @@ -16,7 +17,6 @@ from homeassistant.components.stream.const import ( HLS_PROVIDER, ) from homeassistant.components.stream.core import Part -from homeassistant.const import HTTP_NOT_FOUND from homeassistant.setup import async_setup_component from .test_hls import SEGMENT_DURATION, STREAM_SOURCE, HlsClient, make_playlist @@ -143,17 +143,17 @@ async def test_ll_hls_stream(hass, hls_stream, stream_worker_sync): # Fetch playlist master_playlist_response = await hls_client.get() - assert master_playlist_response.status == 200 + assert master_playlist_response.status == HTTPStatus.OK # Fetch init master_playlist = await master_playlist_response.text() init_response = await hls_client.get("/init.mp4") - assert init_response.status == 200 + assert init_response.status == HTTPStatus.OK # Fetch playlist playlist_url = "/" + master_playlist.splitlines()[-1] playlist_response = await hls_client.get(playlist_url) - assert playlist_response.status == 200 + assert playlist_response.status == HTTPStatus.OK # Fetch segments playlist = await playlist_response.text() @@ -163,7 +163,7 @@ async def test_ll_hls_stream(hass, hls_stream, stream_worker_sync): if match: segment_url = "/" + match.group("segment_url") segment_response = await hls_client.get(segment_url) - assert segment_response.status == 200 + assert segment_response.status == HTTPStatus.OK def check_part_is_moof_mdat(data: bytes): if len(data) < 8 or data[4:8] != b"moof": @@ -200,7 +200,7 @@ async def test_ll_hls_stream(hass, hls_stream, stream_worker_sync): "Range": f'bytes={match.group("byterange_start")}-{byterange_end}' }, ) - assert part_segment_response.status == 206 + assert part_segment_response.status == HTTPStatus.PARTIAL_CONTENT assert check_part_is_moof_mdat(await part_segment_response.read()) stream_worker_sync.resume() @@ -210,7 +210,7 @@ async def test_ll_hls_stream(hass, hls_stream, stream_worker_sync): # Ensure playlist not accessible after stream ends fail_response = await hls_client.get() - assert fail_response.status == HTTP_NOT_FOUND + assert fail_response.status == HTTPStatus.NOT_FOUND async def test_ll_hls_playlist_view(hass, hls_stream, stream_worker_sync): @@ -244,7 +244,7 @@ async def test_ll_hls_playlist_view(hass, hls_stream, stream_worker_sync): hls_client = await hls_stream(stream) resp = await hls_client.get("/playlist.m3u8") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert await resp.text() == make_playlist( sequence=0, segments=[ @@ -265,7 +265,7 @@ async def test_ll_hls_playlist_view(hass, hls_stream, stream_worker_sync): await hass.async_block_till_done() resp = await hls_client.get("/playlist.m3u8") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert await resp.text() == make_playlist( sequence=0, segments=[ @@ -316,10 +316,10 @@ async def test_ll_hls_msn(hass, hls_stream, stream_worker_sync, hls_sync): msn_responses = await msn_requests - assert msn_responses[0].status == 200 - assert msn_responses[1].status == 200 - assert msn_responses[2].status == 400 - assert msn_responses[3].status == 400 + assert msn_responses[0].status == HTTPStatus.OK + assert msn_responses[1].status == HTTPStatus.OK + assert msn_responses[2].status == HTTPStatus.BAD_REQUEST + assert msn_responses[3].status == HTTPStatus.BAD_REQUEST # Sequence number is now 2. Create six more requests for sequences 0 through 5. # Calls for msn 0 through 4 should work, 5 should fail. @@ -334,12 +334,12 @@ async def test_ll_hls_msn(hass, hls_stream, stream_worker_sync, hls_sync): hls.put(segment) msn_responses = await msn_requests - assert msn_responses[0].status == 200 - assert msn_responses[1].status == 200 - assert msn_responses[2].status == 200 - assert msn_responses[3].status == 200 - assert msn_responses[4].status == 200 - assert msn_responses[5].status == 400 + assert msn_responses[0].status == HTTPStatus.OK + assert msn_responses[1].status == HTTPStatus.OK + assert msn_responses[2].status == HTTPStatus.OK + assert msn_responses[3].status == HTTPStatus.OK + assert msn_responses[4].status == HTTPStatus.OK + assert msn_responses[5].status == HTTPStatus.BAD_REQUEST stream_worker_sync.resume() @@ -369,7 +369,9 @@ async def test_ll_hls_playlist_bad_msn_part(hass, hls_stream, stream_worker_sync # If the Playlist URI contains an _HLS_part directive but no _HLS_msn # directive, the Server MUST return Bad Request, such as HTTP 400. - assert (await hls_client.get("/playlist.m3u8?_HLS_part=1")).status == 400 + assert ( + await hls_client.get("/playlist.m3u8?_HLS_part=1") + ).status == HTTPStatus.BAD_REQUEST # Seed hls with 1 complete segment and 1 in process segment segment = create_segment(sequence=0) @@ -398,12 +400,14 @@ async def test_ll_hls_playlist_bad_msn_part(hass, hls_stream, stream_worker_sync # The following two tests should fail immediately: # - request with a _HLS_msn of 4 # - request with a _HLS_msn of 1 and a _HLS_part of num_completed_parts-1+advance_part_limit - assert (await hls_client.get("/playlist.m3u8?_HLS_msn=4")).status == 400 + assert ( + await hls_client.get("/playlist.m3u8?_HLS_msn=4") + ).status == HTTPStatus.BAD_REQUEST assert ( await hls_client.get( f"/playlist.m3u8?_HLS_msn=1&_HLS_part={num_completed_parts-1+hass.data[DOMAIN][ATTR_SETTINGS].hls_advance_part_limit}" ) - ).status == 400 + ).status == HTTPStatus.BAD_REQUEST stream_worker_sync.resume() @@ -478,8 +482,8 @@ async def test_ll_hls_playlist_rollover_part( different_response, *same_responses = await requests - assert different_response.status == 200 - assert all(response.status == 200 for response in same_responses) + assert different_response.status == HTTPStatus.OK + assert all(response.status == HTTPStatus.OK for response in same_responses) different_playlist = await different_response.read() same_playlists = [await response.read() for response in same_responses] assert different_playlist != same_playlists[0] @@ -549,8 +553,8 @@ async def test_ll_hls_playlist_msn_part(hass, hls_stream, stream_worker_sync, hl msn_responses = await msn_requests # All the responses should succeed except the last one which fails - assert all(response.status == 200 for response in msn_responses[:-1]) - assert msn_responses[-1].status == 400 + assert all(response.status == HTTPStatus.OK for response in msn_responses[:-1]) + assert msn_responses[-1].status == HTTPStatus.BAD_REQUEST stream_worker_sync.resume() @@ -600,7 +604,7 @@ async def test_get_part_segments(hass, hls_stream, stream_worker_sync, hls_sync) ) ) responses = await requests - assert all(response.status == 200 for response in responses) + assert all(response.status == HTTPStatus.OK for response in responses) assert all( [ await responses[i].read() == segment.parts[i].data @@ -616,7 +620,7 @@ async def test_get_part_segments(hass, hls_stream, stream_worker_sync, hls_sync) await hls_sync.wait_for_handler() hls.part_put() response = await request - assert response.status == 404 + assert response.status == HTTPStatus.NOT_FOUND # Put the remaining parts and complete the segment while remaining_parts: @@ -641,7 +645,7 @@ async def test_get_part_segments(hass, hls_stream, stream_worker_sync, hls_sync) complete_segment(segment) # Check the response response = await request - assert response.status == 200 + assert response.status == HTTPStatus.OK assert ( await response.read() == ALT_SEQUENCE_BYTES[: len(hls.get_segment(2).parts[0].data)] diff --git a/tests/components/stt/test_init.py b/tests/components/stt/test_init.py index 9658ab92140..3b207fae01a 100644 --- a/tests/components/stt/test_init.py +++ b/tests/components/stt/test_init.py @@ -1,7 +1,7 @@ """Test STT component setup.""" +from http import HTTPStatus from homeassistant.components import stt -from homeassistant.const import HTTP_NOT_FOUND from homeassistant.setup import async_setup_component @@ -17,7 +17,7 @@ async def test_demo_settings_not_exists(hass, hass_client): response = await client.get("/api/stt/beer") - assert response.status == HTTP_NOT_FOUND + assert response.status == HTTPStatus.NOT_FOUND async def test_demo_speech_not_exists(hass, hass_client): @@ -27,4 +27,4 @@ async def test_demo_speech_not_exists(hass, hass_client): response = await client.post("/api/stt/beer", data=b"test") - assert response.status == HTTP_NOT_FOUND + assert response.status == HTTPStatus.NOT_FOUND diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py index ef54788d910..d10ee9bbb51 100644 --- a/tests/components/system_log/test_init.py +++ b/tests/components/system_log/test_init.py @@ -1,5 +1,6 @@ """Test system log component.""" import asyncio +from http import HTTPStatus import logging import queue from unittest.mock import MagicMock, patch @@ -40,7 +41,7 @@ async def get_error_log(hass, hass_client, expected_count): client = await hass_client() resp = await client.get("/api/error/all") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK data = await resp.json() From 5958e6a3f972aa0b01cdcce54ca1033a5e80beee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 23 Oct 2021 08:50:19 -1000 Subject: [PATCH 0745/1038] Ensure zeroconf uses the newest non-link local address in discovery (#58257) --- homeassistant/components/zeroconf/__init__.py | 19 ++++++-- .../components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zeroconf/test_init.py | 45 ++++++++++++++++++- 6 files changed, 62 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 1d72c7d20e9..8b845f303cd 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -469,13 +469,15 @@ def info_from_service(service: AsyncServiceInfo) -> HaServiceInfo | None: if isinstance(value, bytes): properties[key] = value.decode("utf-8") - if not service.addresses: + addresses = service.addresses + + if not addresses: + return None + if (host := _first_non_link_local_or_v6_address(addresses)) is None: return None - address = service.addresses[0] - return { - "host": str(ip_address(address)), + "host": str(host), "port": service.port, "hostname": service.server, "type": service.type, @@ -484,6 +486,15 @@ def info_from_service(service: AsyncServiceInfo) -> HaServiceInfo | None: } +def _first_non_link_local_or_v6_address(addresses: list[bytes]) -> str | None: + """Return the first ipv6 or non-link local ipv4 address.""" + for address in addresses: + ip_addr = ip_address(address) + if not ip_addr.is_link_local or ip_addr.version == 6: + return str(ip_addr) + return None + + def _suppress_invalid_properties(properties: dict) -> None: """Suppress any properties that will cause zeroconf to fail to startup.""" diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index e89697f2131..9870258027b 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.36.8"], + "requirements": ["zeroconf==0.36.9"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 63472852399..b4fd1d08e40 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,7 +32,7 @@ sqlalchemy==1.4.23 voluptuous-serialize==2.4.0 voluptuous==0.12.2 yarl==1.6.3 -zeroconf==0.36.8 +zeroconf==0.36.9 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 74435306616..b2a614b682b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2465,7 +2465,7 @@ youtube_dl==2021.06.06 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.36.8 +zeroconf==0.36.9 # homeassistant.components.zha zha-quirks==0.0.62 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9bc6da885d6..ef9d5eb9d6f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1427,7 +1427,7 @@ yeelight==0.7.8 youless-api==0.14 # homeassistant.components.zeroconf -zeroconf==0.36.8 +zeroconf==0.36.9 # homeassistant.components.zha zha-quirks==0.0.62 diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 0d3c5fc7792..edf29a32f69 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -1,5 +1,6 @@ """Test Zeroconf component setup process.""" from ipaddress import ip_address +from typing import Any from unittest.mock import call, patch from zeroconf import InterfaceChoice, IPVersion, ServiceStateChange @@ -39,7 +40,9 @@ def service_update_mock(ipv6, zeroconf, services, handlers, *, limit_service=Non handlers[0](zeroconf, service, f"_name.{service}", ServiceStateChange.Added) -def get_service_info_mock(service_type, name, *args, **kwargs): +def get_service_info_mock( + service_type: str, name: str, *args: Any, **kwargs: Any +) -> AsyncServiceInfo: """Return service info for get_service_info.""" return AsyncServiceInfo( service_type, @@ -53,7 +56,9 @@ def get_service_info_mock(service_type, name, *args, **kwargs): ) -def get_service_info_mock_without_an_address(service_type, name): +def get_service_info_mock_without_an_address( + service_type: str, name: str +) -> AsyncServiceInfo: """Return service info for get_service_info without any addresses.""" return AsyncServiceInfo( service_type, @@ -633,6 +638,42 @@ async def test_info_from_service_with_addresses(hass): assert info is None +async def test_info_from_service_with_link_local_address_first(hass): + """Test that the link local address is ignored.""" + service_type = "_test._tcp.local." + service_info = get_service_info_mock(service_type, f"test.{service_type}") + service_info.addresses = ["169.254.12.3", "192.168.66.12"] + info = zeroconf.info_from_service(service_info) + assert info["host"] == "192.168.66.12" + + +async def test_info_from_service_with_link_local_address_second(hass): + """Test that the link local address is ignored.""" + service_type = "_test._tcp.local." + service_info = get_service_info_mock(service_type, f"test.{service_type}") + service_info.addresses = ["192.168.66.12", "169.254.12.3"] + info = zeroconf.info_from_service(service_info) + assert info["host"] == "192.168.66.12" + + +async def test_info_from_service_with_link_local_address_only(hass): + """Test that the link local address is ignored.""" + service_type = "_test._tcp.local." + service_info = get_service_info_mock(service_type, f"test.{service_type}") + service_info.addresses = ["169.254.12.3"] + info = zeroconf.info_from_service(service_info) + assert info is None + + +async def test_info_from_service_prefers_ipv4(hass): + """Test that ipv4 addresses are preferred.""" + service_type = "_test._tcp.local." + service_info = get_service_info_mock(service_type, f"test.{service_type}") + service_info.addresses = ["2001:db8:3333:4444:5555:6666:7777:8888", "192.168.66.12"] + info = zeroconf.info_from_service(service_info) + assert info["host"] == "192.168.66.12" + + async def test_get_instance(hass, mock_async_zeroconf): """Test we get an instance.""" assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) From 5626cc45245f0387fd0ba4c9756202a6166d3f55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 23 Oct 2021 21:53:39 +0300 Subject: [PATCH 0746/1038] Use http.HTTPStatus in components/r* (#58288) --- .../components/rachio/config_flow.py | 7 +-- homeassistant/components/rachio/device.py | 7 +-- homeassistant/components/radarr/sensor.py | 4 +- homeassistant/components/rest/notify.py | 15 +++--- homeassistant/components/rest/switch.py | 9 ++-- .../components/rest_command/__init__.py | 4 +- homeassistant/components/rocketchat/notify.py | 11 ++--- homeassistant/components/route53/__init__.py | 5 +- tests/components/rest/test_binary_sensor.py | 23 ++++----- tests/components/rest/test_init.py | 19 +++---- tests/components/rest/test_sensor.py | 49 ++++++++++--------- tests/components/rest_command/test_init.py | 3 +- .../rituals_perfume_genie/test_config_flow.py | 5 +- tests/components/roku/__init__.py | 31 +++++++++--- .../components/rss_feed_template/test_init.py | 7 +-- 15 files changed, 112 insertions(+), 87 deletions(-) diff --git a/homeassistant/components/rachio/config_flow.py b/homeassistant/components/rachio/config_flow.py index ee03d042ec2..aa4efd971a6 100644 --- a/homeassistant/components/rachio/config_flow.py +++ b/homeassistant/components/rachio/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Rachio integration.""" +from http import HTTPStatus import logging from rachiopy import Rachio @@ -6,7 +7,7 @@ from requests.exceptions import ConnectTimeout import voluptuous as vol from homeassistant import config_entries, core, exceptions -from homeassistant.const import CONF_API_KEY, HTTP_OK +from homeassistant.const import CONF_API_KEY from homeassistant.core import callback from .const import ( @@ -33,13 +34,13 @@ async def validate_input(hass: core.HomeAssistant, data): try: data = await hass.async_add_executor_job(rachio.person.info) _LOGGER.debug("rachio.person.getInfo: %s", data) - if int(data[0][KEY_STATUS]) != HTTP_OK: + if int(data[0][KEY_STATUS]) != HTTPStatus.OK: raise InvalidAuth rachio_id = data[1][KEY_ID] data = await hass.async_add_executor_job(rachio.person.get, rachio_id) _LOGGER.debug("rachio.person.get: %s", data) - if int(data[0][KEY_STATUS]) != HTTP_OK: + if int(data[0][KEY_STATUS]) != HTTPStatus.OK: raise CannotConnect username = data[1][KEY_USERNAME] diff --git a/homeassistant/components/rachio/device.py b/homeassistant/components/rachio/device.py index 8ac4c92582f..2124e5736a2 100644 --- a/homeassistant/components/rachio/device.py +++ b/homeassistant/components/rachio/device.py @@ -1,11 +1,12 @@ """Adapter to wrap the rachiopy api for home assistant.""" from __future__ import annotations +from http import HTTPStatus import logging import voluptuous as vol -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, HTTP_OK +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import config_validation as cv from .const import ( @@ -123,12 +124,12 @@ class RachioPerson: rachio = self.rachio response = rachio.person.info() - assert int(response[0][KEY_STATUS]) == HTTP_OK, "API key error" + assert int(response[0][KEY_STATUS]) == HTTPStatus.OK, "API key error" self._id = response[1][KEY_ID] # Use user ID to get user data data = rachio.person.get(self._id) - assert int(data[0][KEY_STATUS]) == HTTP_OK, "User ID error" + assert int(data[0][KEY_STATUS]) == HTTPStatus.OK, "User ID error" self.username = data[1][KEY_USERNAME] devices = data[1][KEY_DEVICES] for controller in devices: diff --git a/homeassistant/components/radarr/sensor.py b/homeassistant/components/radarr/sensor.py index 8364dd670e4..96c0d2b905d 100644 --- a/homeassistant/components/radarr/sensor.py +++ b/homeassistant/components/radarr/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import datetime, timedelta +from http import HTTPStatus import logging import time from typing import Any @@ -29,7 +30,6 @@ from homeassistant.const import ( DATA_TERABYTES, DATA_YOTTABYTES, DATA_ZETTABYTES, - HTTP_OK, ) import homeassistant.helpers.config_validation as cv from homeassistant.util import dt as dt_util @@ -215,7 +215,7 @@ class RadarrSensor(SensorEntity): self._attr_native_value = None return - if res.status_code == HTTP_OK: + if res.status_code == HTTPStatus.OK: if sensor_type in ("upcoming", "movies", "commands"): self.data = res.json() self._attr_native_value = len(self.data) diff --git a/homeassistant/components/rest/notify.py b/homeassistant/components/rest/notify.py index 198e5b06c52..15ba73b7664 100644 --- a/homeassistant/components/rest/notify.py +++ b/homeassistant/components/rest/notify.py @@ -1,4 +1,5 @@ """RESTful platform for notify component.""" +from http import HTTPStatus import logging import requests @@ -22,11 +23,8 @@ from homeassistant.const import ( CONF_RESOURCE, CONF_USERNAME, CONF_VERIFY_SSL, - HTTP_BAD_REQUEST, HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, - HTTP_INTERNAL_SERVER_ERROR, - HTTP_OK, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.template import Template @@ -203,20 +201,23 @@ class RestNotificationService(BaseNotificationService): ) if ( - response.status_code >= HTTP_INTERNAL_SERVER_ERROR + response.status_code >= HTTPStatus.INTERNAL_SERVER_ERROR and response.status_code < 600 ): _LOGGER.exception( "Server error. Response %d: %s:", response.status_code, response.reason ) elif ( - response.status_code >= HTTP_BAD_REQUEST - and response.status_code < HTTP_INTERNAL_SERVER_ERROR + response.status_code >= HTTPStatus.BAD_REQUEST + and response.status_code < HTTPStatus.INTERNAL_SERVER_ERROR ): _LOGGER.exception( "Client error. Response %d: %s:", response.status_code, response.reason ) - elif response.status_code >= HTTP_OK and response.status_code < 300: + elif ( + response.status_code >= HTTPStatus.OK + and response.status_code < HTTPStatus.MULTIPLE_CHOICES + ): _LOGGER.debug( "Success. Response %d: %s:", response.status_code, response.reason ) diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index 66a5f21cc40..e6b16de40aa 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -1,5 +1,6 @@ """Support for RESTful switches.""" import asyncio +from http import HTTPStatus import logging import aiohttp @@ -22,8 +23,6 @@ from homeassistant.const import ( CONF_TIMEOUT, CONF_USERNAME, CONF_VERIFY_SSL, - HTTP_BAD_REQUEST, - HTTP_OK, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -111,7 +110,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) req = await switch.get_device_state(hass) - if req.status >= HTTP_BAD_REQUEST: + if req.status >= HTTPStatus.BAD_REQUEST: _LOGGER.error("Got non-ok response from resource: %s", req.status) else: async_add_entities([switch]) @@ -177,7 +176,7 @@ class RestSwitch(SwitchEntity): try: req = await self.set_device_state(body_on_t) - if req.status == HTTP_OK: + if req.status == HTTPStatus.OK: self._state = True else: _LOGGER.error( @@ -192,7 +191,7 @@ class RestSwitch(SwitchEntity): try: req = await self.set_device_state(body_off_t) - if req.status == HTTP_OK: + if req.status == HTTPStatus.OK: self._state = False else: _LOGGER.error( diff --git a/homeassistant/components/rest_command/__init__.py b/homeassistant/components/rest_command/__init__.py index 7ba46d3bf50..9f792d5c1a2 100644 --- a/homeassistant/components/rest_command/__init__.py +++ b/homeassistant/components/rest_command/__init__.py @@ -1,5 +1,6 @@ """Support for exposing regular REST commands as services.""" import asyncio +from http import HTTPStatus import logging import aiohttp @@ -15,7 +16,6 @@ from homeassistant.const import ( CONF_URL, CONF_USERNAME, CONF_VERIFY_SSL, - HTTP_BAD_REQUEST, ) from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -125,7 +125,7 @@ async def async_setup(hass, config): timeout=timeout, ) as response: - if response.status < HTTP_BAD_REQUEST: + if response.status < HTTPStatus.BAD_REQUEST: _LOGGER.debug( "Success. Url: %s. Status code: %d. Payload: %s", response.url, diff --git a/homeassistant/components/rocketchat/notify.py b/homeassistant/components/rocketchat/notify.py index 90495cc6eb2..51a25d90b6e 100644 --- a/homeassistant/components/rocketchat/notify.py +++ b/homeassistant/components/rocketchat/notify.py @@ -1,4 +1,5 @@ """Rocket.Chat notification service.""" +from http import HTTPStatus import logging from rocketchat_API.APIExceptions.RocketExceptions import ( @@ -13,13 +14,7 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) -from homeassistant.const import ( - CONF_PASSWORD, - CONF_ROOM, - CONF_URL, - CONF_USERNAME, - HTTP_OK, -) +from homeassistant.const import CONF_PASSWORD, CONF_ROOM, CONF_URL, CONF_USERNAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -68,7 +63,7 @@ class RocketChatNotificationService(BaseNotificationService): """Send a message to Rocket.Chat.""" data = kwargs.get(ATTR_DATA) or {} resp = self._server.chat_post_message(message, channel=self._room, **data) - if resp.status_code == HTTP_OK: + if resp.status_code == HTTPStatus.OK: if not resp.json()["success"]: _LOGGER.error("Unable to post Rocket.Chat message") else: diff --git a/homeassistant/components/route53/__init__.py b/homeassistant/components/route53/__init__.py index a2b6a854c62..1216f1d84c5 100644 --- a/homeassistant/components/route53/__init__.py +++ b/homeassistant/components/route53/__init__.py @@ -2,13 +2,14 @@ from __future__ import annotations from datetime import timedelta +from http import HTTPStatus import logging import boto3 import requests import voluptuous as vol -from homeassistant.const import CONF_DOMAIN, CONF_TTL, CONF_ZONE, HTTP_OK +from homeassistant.const import CONF_DOMAIN, CONF_TTL, CONF_ZONE import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_time_interval @@ -121,5 +122,5 @@ def _update_route53( ) _LOGGER.debug("Response is %s", response) - if response["ResponseMetadata"]["HTTPStatusCode"] != HTTP_OK: + if response["ResponseMetadata"]["HTTPStatusCode"] != HTTPStatus.OK: _LOGGER.warning(response) diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py index 0af82f9ab5a..8160a5976a7 100644 --- a/tests/components/rest/test_binary_sensor.py +++ b/tests/components/rest/test_binary_sensor.py @@ -1,6 +1,7 @@ """The tests for the REST binary sensor platform.""" import asyncio +from http import HTTPStatus from os import path from unittest.mock import MagicMock, patch @@ -92,7 +93,7 @@ async def test_setup_timeout(hass): @respx.mock async def test_setup_minimum(hass): """Test setup with minimum configuration.""" - respx.get("http://localhost") % 200 + respx.get("http://localhost") % HTTPStatus.OK assert await async_setup_component( hass, binary_sensor.DOMAIN, @@ -111,7 +112,7 @@ async def test_setup_minimum(hass): @respx.mock async def test_setup_minimum_resource_template(hass): """Test setup with minimum configuration (resource_template).""" - respx.get("http://localhost") % 200 + respx.get("http://localhost") % HTTPStatus.OK assert await async_setup_component( hass, binary_sensor.DOMAIN, @@ -129,7 +130,7 @@ async def test_setup_minimum_resource_template(hass): @respx.mock async def test_setup_duplicate_resource_template(hass): """Test setup with duplicate resources.""" - respx.get("http://localhost") % 200 + respx.get("http://localhost") % HTTPStatus.OK assert await async_setup_component( hass, binary_sensor.DOMAIN, @@ -148,7 +149,7 @@ async def test_setup_duplicate_resource_template(hass): @respx.mock async def test_setup_get(hass): """Test setup with valid configuration.""" - respx.get("http://localhost").respond(status_code=200, json={}) + respx.get("http://localhost").respond(status_code=HTTPStatus.OK, json={}) assert await async_setup_component( hass, "binary_sensor", @@ -181,7 +182,7 @@ async def test_setup_get(hass): @respx.mock async def test_setup_get_digest_auth(hass): """Test setup with valid configuration.""" - respx.get("http://localhost").respond(status_code=200, json={}) + respx.get("http://localhost").respond(status_code=HTTPStatus.OK, json={}) assert await async_setup_component( hass, "binary_sensor", @@ -209,7 +210,7 @@ async def test_setup_get_digest_auth(hass): @respx.mock async def test_setup_post(hass): """Test setup with valid configuration.""" - respx.post("http://localhost").respond(status_code=200, json={}) + respx.post("http://localhost").respond(status_code=HTTPStatus.OK, json={}) assert await async_setup_component( hass, "binary_sensor", @@ -238,7 +239,7 @@ async def test_setup_post(hass): async def test_setup_get_off(hass): """Test setup with valid off configuration.""" respx.get("http://localhost").respond( - status_code=200, + status_code=HTTPStatus.OK, headers={"content-type": "text/json"}, json={"dog": False}, ) @@ -268,7 +269,7 @@ async def test_setup_get_off(hass): async def test_setup_get_on(hass): """Test setup with valid on configuration.""" respx.get("http://localhost").respond( - status_code=200, + status_code=HTTPStatus.OK, headers={"content-type": "text/json"}, json={"dog": True}, ) @@ -297,7 +298,7 @@ async def test_setup_get_on(hass): @respx.mock async def test_setup_with_exception(hass): """Test setup with exception.""" - respx.get("http://localhost").respond(status_code=200, json={}) + respx.get("http://localhost").respond(status_code=HTTPStatus.OK, json={}) assert await async_setup_component( hass, "binary_sensor", @@ -340,7 +341,7 @@ async def test_setup_with_exception(hass): async def test_reload(hass): """Verify we can reload reset sensors.""" - respx.get("http://localhost") % 200 + respx.get("http://localhost") % HTTPStatus.OK await async_setup_component( hass, @@ -383,7 +384,7 @@ async def test_reload(hass): @respx.mock async def test_setup_query_params(hass): """Test setup with query params.""" - respx.get("http://localhost", params={"search": "something"}) % 200 + respx.get("http://localhost", params={"search": "something"}) % HTTPStatus.OK assert await async_setup_component( hass, binary_sensor.DOMAIN, diff --git a/tests/components/rest/test_init.py b/tests/components/rest/test_init.py index 2902addca0c..ddd5356525d 100644 --- a/tests/components/rest/test_init.py +++ b/tests/components/rest/test_init.py @@ -2,6 +2,7 @@ import asyncio from datetime import timedelta +from http import HTTPStatus from os import path from unittest.mock import patch @@ -67,7 +68,7 @@ async def test_setup_with_endpoint_timeout_with_recovery(hass): assert len(hass.states.async_all()) == 0 respx.get("http://localhost").respond( - status_code=200, + status_code=HTTPStatus.OK, json={ "sensor1": "1", "sensor2": "2", @@ -107,7 +108,7 @@ async def test_setup_with_endpoint_timeout_with_recovery(hass): # endpoint is working again respx.get("http://localhost").respond( - status_code=200, + status_code=HTTPStatus.OK, json={ "sensor1": "1", "sensor2": "2", @@ -133,7 +134,7 @@ async def test_setup_minimum_resource_template(hass): """Test setup with minimum configuration (resource_template).""" respx.get("http://localhost").respond( - status_code=200, + status_code=HTTPStatus.OK, json={ "sensor1": "1", "sensor2": "2", @@ -190,7 +191,7 @@ async def test_setup_minimum_resource_template(hass): async def test_reload(hass): """Verify we can reload.""" - respx.get("http://localhost") % 200 + respx.get("http://localhost") % HTTPStatus.OK assert await async_setup_component( hass, @@ -242,7 +243,7 @@ async def test_reload(hass): async def test_reload_and_remove_all(hass): """Verify we can reload and remove all.""" - respx.get("http://localhost") % 200 + respx.get("http://localhost") % HTTPStatus.OK assert await async_setup_component( hass, @@ -292,7 +293,7 @@ async def test_reload_and_remove_all(hass): async def test_reload_fails_to_read_configuration(hass): """Verify reload when configuration is missing or broken.""" - respx.get("http://localhost") % 200 + respx.get("http://localhost") % HTTPStatus.OK assert await async_setup_component( hass, @@ -345,7 +346,7 @@ async def test_multiple_rest_endpoints(hass): """Test multiple rest endpoints.""" respx.get("http://date.jsontest.com").respond( - status_code=200, + status_code=HTTPStatus.OK, json={ "date": "03-17-2021", "milliseconds_since_epoch": 1616008268573, @@ -354,7 +355,7 @@ async def test_multiple_rest_endpoints(hass): ) respx.get("http://time.jsontest.com").respond( - status_code=200, + status_code=HTTPStatus.OK, json={ "date": "03-17-2021", "milliseconds_since_epoch": 1616008299665, @@ -362,7 +363,7 @@ async def test_multiple_rest_endpoints(hass): }, ) respx.get("http://localhost").respond( - status_code=200, + status_code=HTTPStatus.OK, json={ "value": "1", }, diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index a59cb99bcdf..4ff8ca12dad 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -1,5 +1,6 @@ """The tests for the REST sensor platform.""" import asyncio +from http import HTTPStatus from os import path from unittest.mock import MagicMock, patch @@ -81,7 +82,7 @@ async def test_setup_timeout(hass): @respx.mock async def test_setup_minimum(hass): """Test setup with minimum configuration.""" - respx.get("http://localhost") % 200 + respx.get("http://localhost") % HTTPStatus.OK assert await async_setup_component( hass, sensor.DOMAIN, @@ -101,7 +102,9 @@ async def test_setup_minimum(hass): async def test_manual_update(hass): """Test setup with minimum configuration.""" await async_setup_component(hass, "homeassistant", {}) - respx.get("http://localhost").respond(status_code=200, json={"data": "first"}) + respx.get("http://localhost").respond( + status_code=HTTPStatus.OK, json={"data": "first"} + ) assert await async_setup_component( hass, sensor.DOMAIN, @@ -119,7 +122,9 @@ async def test_manual_update(hass): assert len(hass.states.async_all("sensor")) == 1 assert hass.states.get("sensor.mysensor").state == "first" - respx.get("http://localhost").respond(status_code=200, json={"data": "second"}) + respx.get("http://localhost").respond( + status_code=HTTPStatus.OK, json={"data": "second"} + ) await hass.services.async_call( "homeassistant", "update_entity", @@ -132,7 +137,7 @@ async def test_manual_update(hass): @respx.mock async def test_setup_minimum_resource_template(hass): """Test setup with minimum configuration (resource_template).""" - respx.get("http://localhost") % 200 + respx.get("http://localhost") % HTTPStatus.OK assert await async_setup_component( hass, sensor.DOMAIN, @@ -150,7 +155,7 @@ async def test_setup_minimum_resource_template(hass): @respx.mock async def test_setup_duplicate_resource_template(hass): """Test setup with duplicate resources.""" - respx.get("http://localhost") % 200 + respx.get("http://localhost") % HTTPStatus.OK assert await async_setup_component( hass, sensor.DOMAIN, @@ -169,7 +174,7 @@ async def test_setup_duplicate_resource_template(hass): @respx.mock async def test_setup_get(hass): """Test setup with valid configuration.""" - respx.get("http://localhost").respond(status_code=200, json={}) + respx.get("http://localhost").respond(status_code=HTTPStatus.OK, json={}) assert await async_setup_component( hass, "sensor", @@ -215,7 +220,7 @@ async def test_setup_get(hass): @respx.mock async def test_setup_get_digest_auth(hass): """Test setup with valid configuration.""" - respx.get("http://localhost").respond(status_code=200, json={}) + respx.get("http://localhost").respond(status_code=HTTPStatus.OK, json={}) assert await async_setup_component( hass, "sensor", @@ -244,7 +249,7 @@ async def test_setup_get_digest_auth(hass): @respx.mock async def test_setup_post(hass): """Test setup with valid configuration.""" - respx.post("http://localhost").respond(status_code=200, json={}) + respx.post("http://localhost").respond(status_code=HTTPStatus.OK, json={}) assert await async_setup_component( hass, "sensor", @@ -274,7 +279,7 @@ async def test_setup_post(hass): async def test_setup_get_xml(hass): """Test setup with valid xml configuration.""" respx.get("http://localhost").respond( - status_code=200, + status_code=HTTPStatus.OK, headers={"content-type": "text/xml"}, content="abc", ) @@ -305,7 +310,7 @@ async def test_setup_get_xml(hass): @respx.mock async def test_setup_query_params(hass): """Test setup with query params.""" - respx.get("http://localhost", params={"search": "something"}) % 200 + respx.get("http://localhost", params={"search": "something"}) % HTTPStatus.OK assert await async_setup_component( hass, sensor.DOMAIN, @@ -327,7 +332,7 @@ async def test_update_with_json_attrs(hass): """Test attributes get extracted from a JSON result.""" respx.get("http://localhost").respond( - status_code=200, + status_code=HTTPStatus.OK, json={"key": "some_json_value"}, ) assert await async_setup_component( @@ -360,7 +365,7 @@ async def test_update_with_no_template(hass): """Test update when there is no value template.""" respx.get("http://localhost").respond( - status_code=200, + status_code=HTTPStatus.OK, json={"key": "some_json_value"}, ) assert await async_setup_component( @@ -392,7 +397,7 @@ async def test_update_with_json_attrs_no_data(hass, caplog): """Test attributes when no JSON result fetched.""" respx.get("http://localhost").respond( - status_code=200, + status_code=HTTPStatus.OK, headers={"content-type": CONTENT_TYPE_JSON}, content="", ) @@ -428,7 +433,7 @@ async def test_update_with_json_attrs_not_dict(hass, caplog): """Test attributes get extracted from a JSON result.""" respx.get("http://localhost").respond( - status_code=200, + status_code=HTTPStatus.OK, json=["list", "of", "things"], ) assert await async_setup_component( @@ -463,7 +468,7 @@ async def test_update_with_json_attrs_bad_JSON(hass, caplog): """Test attributes get extracted from a JSON result.""" respx.get("http://localhost").respond( - status_code=200, + status_code=HTTPStatus.OK, headers={"content-type": CONTENT_TYPE_JSON}, content="This is text rather than JSON data.", ) @@ -499,7 +504,7 @@ async def test_update_with_json_attrs_with_json_attrs_path(hass): """Test attributes get extracted from a JSON result with a template for the attributes.""" respx.get("http://localhost").respond( - status_code=200, + status_code=HTTPStatus.OK, json={ "toplevel": { "master_value": "master", @@ -543,7 +548,7 @@ async def test_update_with_xml_convert_json_attrs_with_json_attrs_path(hass): """Test attributes get extracted from a JSON result that was converted from XML with a template for the attributes.""" respx.get("http://localhost").respond( - status_code=200, + status_code=HTTPStatus.OK, headers={"content-type": "text/xml"}, content="mastersome_json_valuesome_json_value2", ) @@ -579,7 +584,7 @@ async def test_update_with_xml_convert_json_attrs_with_jsonattr_template(hass): """Test attributes get extracted from a JSON result that was converted from XML.""" respx.get("http://localhost").respond( - status_code=200, + status_code=HTTPStatus.OK, headers={"content-type": "text/xml"}, content='01255648alexander000bogus000000000upupupup000x0XF0x0XF 0', ) @@ -620,7 +625,7 @@ async def test_update_with_application_xml_convert_json_attrs_with_jsonattr_temp """Test attributes get extracted from a JSON result that was converted from XML with application/xml mime type.""" respx.get("http://localhost").respond( - status_code=200, + status_code=HTTPStatus.OK, headers={"content-type": "application/xml"}, content="
13
", ) @@ -656,7 +661,7 @@ async def test_update_with_xml_convert_bad_xml(hass, caplog): """Test attributes get extracted from a XML result with bad xml.""" respx.get("http://localhost").respond( - status_code=200, + status_code=HTTPStatus.OK, headers={"content-type": "text/xml"}, content="", ) @@ -691,7 +696,7 @@ async def test_update_with_failed_get(hass, caplog): """Test attributes get extracted from a XML result with bad xml.""" respx.get("http://localhost").respond( - status_code=200, + status_code=HTTPStatus.OK, headers={"content-type": "text/xml"}, content="", ) @@ -725,7 +730,7 @@ async def test_update_with_failed_get(hass, caplog): async def test_reload(hass): """Verify we can reload reset sensors.""" - respx.get("http://localhost") % 200 + respx.get("http://localhost") % HTTPStatus.OK await async_setup_component( hass, diff --git a/tests/components/rest_command/test_init.py b/tests/components/rest_command/test_init.py index 80ede61be84..7cf94dcf846 100644 --- a/tests/components/rest_command/test_init.py +++ b/tests/components/rest_command/test_init.py @@ -1,5 +1,6 @@ """The tests for the rest command platform.""" import asyncio +from http import HTTPStatus import aiohttp @@ -104,7 +105,7 @@ class TestRestCommandComponent: with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) - aioclient_mock.get(self.url, status=400) + aioclient_mock.get(self.url, status=HTTPStatus.BAD_REQUEST) self.hass.services.call(rc.DOMAIN, "get_test", {}) self.hass.block_till_done() diff --git a/tests/components/rituals_perfume_genie/test_config_flow.py b/tests/components/rituals_perfume_genie/test_config_flow.py index df40405a56b..3582f49598c 100644 --- a/tests/components/rituals_perfume_genie/test_config_flow.py +++ b/tests/components/rituals_perfume_genie/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Rituals Perfume Genie config flow.""" +from http import HTTPStatus from unittest.mock import AsyncMock, MagicMock, patch from aiohttp import ClientResponseError @@ -103,7 +104,9 @@ async def test_form_cannot_connect(hass): with patch( "homeassistant.components.rituals_perfume_genie.config_flow.Account.authenticate", - side_effect=ClientResponseError(None, None, status=500), + side_effect=ClientResponseError( + None, None, status=HTTPStatus.INTERNAL_SERVER_ERROR + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/roku/__init__.py b/tests/components/roku/__init__.py index e9f2d5c85bf..0f508f3efc9 100644 --- a/tests/components/roku/__init__.py +++ b/tests/components/roku/__init__.py @@ -1,4 +1,5 @@ """Tests for the Roku component.""" +from http import HTTPStatus import re from socket import gaierror as SocketGIAError @@ -150,15 +151,29 @@ def mock_connection_server_error( """Mock the Roku server error.""" roku_url = f"http://{host}:8060" - aioclient_mock.get(f"{roku_url}/query/device-info", status=500) - aioclient_mock.get(f"{roku_url}/query/apps", status=500) - aioclient_mock.get(f"{roku_url}/query/active-app", status=500) - aioclient_mock.get(f"{roku_url}/query/tv-active-channel", status=500) - aioclient_mock.get(f"{roku_url}/query/tv-channels", status=500) + aioclient_mock.get( + f"{roku_url}/query/device-info", status=HTTPStatus.INTERNAL_SERVER_ERROR + ) + aioclient_mock.get( + f"{roku_url}/query/apps", status=HTTPStatus.INTERNAL_SERVER_ERROR + ) + aioclient_mock.get( + f"{roku_url}/query/active-app", status=HTTPStatus.INTERNAL_SERVER_ERROR + ) + aioclient_mock.get( + f"{roku_url}/query/tv-active-channel", status=HTTPStatus.INTERNAL_SERVER_ERROR + ) + aioclient_mock.get( + f"{roku_url}/query/tv-channels", status=HTTPStatus.INTERNAL_SERVER_ERROR + ) - aioclient_mock.post(re.compile(f"{roku_url}/keypress/.*"), status=500) - aioclient_mock.post(re.compile(f"{roku_url}/launch/.*"), status=500) - aioclient_mock.post(f"{roku_url}/search", status=500) + aioclient_mock.post( + re.compile(f"{roku_url}/keypress/.*"), status=HTTPStatus.INTERNAL_SERVER_ERROR + ) + aioclient_mock.post( + re.compile(f"{roku_url}/launch/.*"), status=HTTPStatus.INTERNAL_SERVER_ERROR + ) + aioclient_mock.post(f"{roku_url}/search", status=HTTPStatus.INTERNAL_SERVER_ERROR) async def setup_integration( diff --git a/tests/components/rss_feed_template/test_init.py b/tests/components/rss_feed_template/test_init.py index e0637b709e0..bdc894c3343 100644 --- a/tests/components/rss_feed_template/test_init.py +++ b/tests/components/rss_feed_template/test_init.py @@ -1,8 +1,9 @@ """The tests for the rss_feed_api component.""" +from http import HTTPStatus + from defusedxml import ElementTree import pytest -from homeassistant.const import HTTP_NOT_FOUND from homeassistant.setup import async_setup_component @@ -30,7 +31,7 @@ def mock_http_client(loop, hass, hass_client): async def test_get_nonexistant_feed(mock_http_client): """Test if we can retrieve the correct rss feed.""" resp = await mock_http_client.get("/api/rss_template/otherfeed") - assert resp.status == HTTP_NOT_FOUND + assert resp.status == HTTPStatus.NOT_FOUND async def test_get_rss_feed(mock_http_client, hass): @@ -40,7 +41,7 @@ async def test_get_rss_feed(mock_http_client, hass): hass.states.async_set("test.test3", "a_state_3") resp = await mock_http_client.get("/api/rss_template/testfeed") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK text = await resp.text() From 380cff167e0971e34190913fbb5d578a322e993e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 23 Oct 2021 21:56:30 +0300 Subject: [PATCH 0747/1038] Use HTTPStatus in components/[nop]* (#58279) --- homeassistant/components/nest/legacy/local_auth.py | 4 ++-- homeassistant/components/nexia/util.py | 4 ++-- homeassistant/components/nissan_leaf/__init__.py | 5 +++-- homeassistant/components/nuheat/__init__.py | 12 ++++-------- homeassistant/components/nuheat/config_flow.py | 12 ++++-------- .../components/openalpr_cloud/image_processing.py | 5 +++-- .../components/openexchangerates/sensor.py | 4 ++-- homeassistant/components/prowl/notify.py | 5 +++-- homeassistant/components/pushsafer/notify.py | 5 +++-- tests/components/nest/test_camera_sdm.py | 11 ++++++----- tests/components/nexia/test_util.py | 8 ++++---- tests/components/nightscout/test_config_flow.py | 3 ++- tests/components/nuheat/test_config_flow.py | 5 +++-- tests/components/onboarding/test_views.py | 6 +++--- tests/components/plex/test_browse_media.py | 4 +++- tests/components/plex/test_config_flow.py | 3 ++- tests/components/plex/test_init.py | 9 +++++++-- tests/components/plex/test_playback.py | 5 +++-- tests/components/plex/test_sensor.py | 4 +++- tests/components/plex/test_server.py | 3 ++- tests/components/plex/test_services.py | 13 ++++++++++--- tests/components/plugwise/conftest.py | 9 +++++---- tests/components/prometheus/test_init.py | 5 +++-- tests/components/push/test_camera.py | 5 +++-- tests/components/pushbullet/test_notify.py | 13 +++++++------ 25 files changed, 92 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/nest/legacy/local_auth.py b/homeassistant/components/nest/legacy/local_auth.py index f5fb286df7e..6c7f1043093 100644 --- a/homeassistant/components/nest/legacy/local_auth.py +++ b/homeassistant/components/nest/legacy/local_auth.py @@ -1,10 +1,10 @@ """Local Nest authentication for the legacy api.""" import asyncio from functools import partial +from http import HTTPStatus from nest.nest import AUTHORIZE_URL, AuthorizationError, NestAuth -from homeassistant.const import HTTP_UNAUTHORIZED from homeassistant.core import callback from ..config_flow import CodeInvalid, NestAuthError, register_flow_implementation @@ -43,7 +43,7 @@ async def resolve_auth_code(hass, client_id, client_secret, code): await hass.async_add_executor_job(auth.login) return await result except AuthorizationError as err: - if err.response.status_code == HTTP_UNAUTHORIZED: + if err.response.status_code == HTTPStatus.UNAUTHORIZED: raise CodeInvalid() from err raise NestAuthError( f"Unknown error: {err} ({err.response.status_code})" diff --git a/homeassistant/components/nexia/util.py b/homeassistant/components/nexia/util.py index 74272a3c7fd..03dd52f7f02 100644 --- a/homeassistant/components/nexia/util.py +++ b/homeassistant/components/nexia/util.py @@ -1,11 +1,11 @@ """Utils for Nexia / Trane XL Thermostats.""" -from homeassistant.const import HTTP_FORBIDDEN, HTTP_UNAUTHORIZED +from http import HTTPStatus def is_invalid_auth_code(http_status_code): """HTTP status codes that mean invalid auth.""" - if http_status_code in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN): + if http_status_code in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN): return True return False diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index a0a39542a20..44727ebccdd 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -1,13 +1,14 @@ """Support for the Nissan Leaf Carwings/Nissan Connect API.""" import asyncio from datetime import datetime, timedelta +from http import HTTPStatus import logging import sys from pycarwings2 import CarwingsError, Session import voluptuous as vol -from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME, HTTP_OK +from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform @@ -293,7 +294,7 @@ class LeafDataStore: if server_response is not None: _LOGGER.debug("Server Response: %s", server_response.__dict__) - if server_response.answer["status"] == HTTP_OK: + if server_response.answer["status"] == HTTPStatus.OK: self.data[DATA_BATTERY] = server_response.battery_percent # pycarwings2 library doesn't always provide cruising rnages diff --git a/homeassistant/components/nuheat/__init__.py b/homeassistant/components/nuheat/__init__.py index fee23ac496c..60dc0a5d9e2 100644 --- a/homeassistant/components/nuheat/__init__.py +++ b/homeassistant/components/nuheat/__init__.py @@ -1,17 +1,13 @@ """Support for NuHeat thermostats.""" from datetime import timedelta +from http import HTTPStatus import logging import nuheat import requests from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_PASSWORD, - CONF_USERNAME, - HTTP_BAD_REQUEST, - HTTP_INTERNAL_SERVER_ERROR, -) +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv @@ -49,8 +45,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady from ex except requests.exceptions.HTTPError as ex: if ( - ex.response.status_code > HTTP_BAD_REQUEST - and ex.response.status_code < HTTP_INTERNAL_SERVER_ERROR + ex.response.status_code > HTTPStatus.BAD_REQUEST + and ex.response.status_code < HTTPStatus.INTERNAL_SERVER_ERROR ): _LOGGER.error("Failed to login to nuheat: %s", ex) return False diff --git a/homeassistant/components/nuheat/config_flow.py b/homeassistant/components/nuheat/config_flow.py index e47f3c8eb6e..0959466244d 100644 --- a/homeassistant/components/nuheat/config_flow.py +++ b/homeassistant/components/nuheat/config_flow.py @@ -1,4 +1,5 @@ """Config flow for NuHeat integration.""" +from http import HTTPStatus import logging import nuheat @@ -6,12 +7,7 @@ import requests.exceptions import voluptuous as vol from homeassistant import config_entries, core, exceptions -from homeassistant.const import ( - CONF_PASSWORD, - CONF_USERNAME, - HTTP_BAD_REQUEST, - HTTP_INTERNAL_SERVER_ERROR, -) +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from .const import CONF_SERIAL_NUMBER, DOMAIN @@ -39,8 +35,8 @@ async def validate_input(hass: core.HomeAssistant, data): raise CannotConnect from ex except requests.exceptions.HTTPError as ex: if ( - ex.response.status_code > HTTP_BAD_REQUEST - and ex.response.status_code < HTTP_INTERNAL_SERVER_ERROR + ex.response.status_code > HTTPStatus.BAD_REQUEST + and ex.response.status_code < HTTPStatus.INTERNAL_SERVER_ERROR ): raise InvalidAuth from ex raise CannotConnect from ex diff --git a/homeassistant/components/openalpr_cloud/image_processing.py b/homeassistant/components/openalpr_cloud/image_processing.py index bc33832bba1..dedf242e0c7 100644 --- a/homeassistant/components/openalpr_cloud/image_processing.py +++ b/homeassistant/components/openalpr_cloud/image_processing.py @@ -1,6 +1,7 @@ """Component that will help set the OpenALPR cloud for ALPR processing.""" import asyncio from base64 import b64encode +from http import HTTPStatus import logging import aiohttp @@ -17,7 +18,7 @@ from homeassistant.components.image_processing import ( from homeassistant.components.openalpr_local.image_processing import ( ImageProcessingAlprEntity, ) -from homeassistant.const import CONF_API_KEY, CONF_REGION, HTTP_OK +from homeassistant.const import CONF_API_KEY, CONF_REGION from homeassistant.core import split_entity_id from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -119,7 +120,7 @@ class OpenAlprCloudEntity(ImageProcessingAlprEntity): data = await request.json() - if request.status != HTTP_OK: + if request.status != HTTPStatus.OK: _LOGGER.error("Error %d -> %s", request.status, data.get("error")) return diff --git a/homeassistant/components/openexchangerates/sensor.py b/homeassistant/components/openexchangerates/sensor.py index 803123a88c3..8c37346ebee 100644 --- a/homeassistant/components/openexchangerates/sensor.py +++ b/homeassistant/components/openexchangerates/sensor.py @@ -1,5 +1,6 @@ """Support for openexchangerates.org exchange rates service.""" from datetime import timedelta +from http import HTTPStatus import logging import requests @@ -12,7 +13,6 @@ from homeassistant.const import ( CONF_BASE, CONF_NAME, CONF_QUOTE, - HTTP_OK, ) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle @@ -49,7 +49,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): rest = OpenexchangeratesData(_RESOURCE, parameters, quote) response = requests.get(_RESOURCE, params=parameters, timeout=10) - if response.status_code != HTTP_OK: + if response.status_code != HTTPStatus.OK: _LOGGER.error("Check your OpenExchangeRates API key") return False diff --git a/homeassistant/components/prowl/notify.py b/homeassistant/components/prowl/notify.py index 802679ab03d..91dd8eca5ca 100644 --- a/homeassistant/components/prowl/notify.py +++ b/homeassistant/components/prowl/notify.py @@ -1,5 +1,6 @@ """Prowl notification service.""" import asyncio +from http import HTTPStatus import logging import async_timeout @@ -12,7 +13,7 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) -from homeassistant.const import CONF_API_KEY, HTTP_OK +from homeassistant.const import CONF_API_KEY from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -59,7 +60,7 @@ class ProwlNotificationService(BaseNotificationService): response = await session.post(url, data=payload) result = await response.text() - if response.status != HTTP_OK or "error" in result: + if response.status != HTTPStatus.OK or "error" in result: _LOGGER.error( "Prowl service returned http status %d, response %s", response.status, diff --git a/homeassistant/components/pushsafer/notify.py b/homeassistant/components/pushsafer/notify.py index 3337af0f8b0..521c1b2929a 100644 --- a/homeassistant/components/pushsafer/notify.py +++ b/homeassistant/components/pushsafer/notify.py @@ -1,5 +1,6 @@ """Pushsafer platform for notify component.""" import base64 +from http import HTTPStatus import logging import mimetypes @@ -15,7 +16,7 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) -from homeassistant.const import ATTR_ICON, HTTP_OK +from homeassistant.const import ATTR_ICON import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -118,7 +119,7 @@ class PushsaferNotificationService(BaseNotificationService): for target in targets: payload["d"] = target response = requests.post(_RESOURCE, data=payload, timeout=CONF_TIMEOUT) - if response.status_code != HTTP_OK: + if response.status_code != HTTPStatus.OK: _LOGGER.error("Pushsafer failed with: %s", response.text) else: _LOGGER.debug("Push send: %s", response.json()) diff --git a/tests/components/nest/test_camera_sdm.py b/tests/components/nest/test_camera_sdm.py index 3cab5eea171..b7637bf3e2e 100644 --- a/tests/components/nest/test_camera_sdm.py +++ b/tests/components/nest/test_camera_sdm.py @@ -6,6 +6,7 @@ pubsub subscriber. """ import datetime +from http import HTTPStatus from unittest.mock import patch import aiohttp @@ -237,7 +238,7 @@ async def test_camera_ws_stream(hass, auth, hass_ws_client): async def test_camera_ws_stream_failure(hass, auth, hass_ws_client): """Test a basic camera that supports web rtc.""" - auth.responses = [aiohttp.web.Response(status=400)] + auth.responses = [aiohttp.web.Response(status=HTTPStatus.BAD_REQUEST)] await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) assert len(hass.states.async_all()) == 1 @@ -438,7 +439,7 @@ async def test_refresh_expired_stream_failure(hass, auth): auth.responses = [ make_stream_url_response(expiration=stream_1_expiration, token_num=1), # Extending the stream fails with arbitrary error - aiohttp.web.Response(status=500), + aiohttp.web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR), # Next attempt to get a stream fetches a new url make_stream_url_response(expiration=stream_2_expiration, token_num=2), ] @@ -544,7 +545,7 @@ async def test_generate_event_image_url_failure(hass, auth): auth.responses = [ # Fail to generate the image url - aiohttp.web.Response(status=500), + aiohttp.web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR), # Camera fetches a stream url as a fallback make_stream_url_response(), ] @@ -566,7 +567,7 @@ async def test_fetch_event_image_failure(hass, auth): # Fake response from API that returns url image aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), # Fail to download the image - aiohttp.web.Response(status=500), + aiohttp.web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR), # Camera fetches a stream url as a fallback make_stream_url_response(), ] @@ -756,7 +757,7 @@ async def test_camera_web_rtc_unsupported(hass, auth, hass_ws_client): async def test_camera_web_rtc_offer_failure(hass, auth, hass_ws_client): """Test a basic camera that supports web rtc.""" auth.responses = [ - aiohttp.web.Response(status=400), + aiohttp.web.Response(status=HTTPStatus.BAD_REQUEST), ] device_traits = { "sdm.devices.traits.Info": { diff --git a/tests/components/nexia/test_util.py b/tests/components/nexia/test_util.py index 0820c7caf0c..9982970055d 100644 --- a/tests/components/nexia/test_util.py +++ b/tests/components/nexia/test_util.py @@ -1,16 +1,16 @@ """The sensor tests for the nexia platform.""" +from http import HTTPStatus from homeassistant.components.nexia import util -from homeassistant.const import HTTP_FORBIDDEN, HTTP_NOT_FOUND, HTTP_UNAUTHORIZED async def test_is_invalid_auth_code(): """Test for invalid auth.""" - assert util.is_invalid_auth_code(HTTP_UNAUTHORIZED) is True - assert util.is_invalid_auth_code(HTTP_FORBIDDEN) is True - assert util.is_invalid_auth_code(HTTP_NOT_FOUND) is False + assert util.is_invalid_auth_code(HTTPStatus.UNAUTHORIZED) is True + assert util.is_invalid_auth_code(HTTPStatus.FORBIDDEN) is True + assert util.is_invalid_auth_code(HTTPStatus.NOT_FOUND) is False async def test_percent_conv(): diff --git a/tests/components/nightscout/test_config_flow.py b/tests/components/nightscout/test_config_flow.py index 61f064a3e29..d7a54ba28fb 100644 --- a/tests/components/nightscout/test_config_flow.py +++ b/tests/components/nightscout/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Nightscout config flow.""" +from http import HTTPStatus from unittest.mock import patch from aiohttp import ClientConnectionError, ClientResponseError @@ -70,7 +71,7 @@ async def test_user_form_api_key_required(hass): return_value=SERVER_STATUS_STATUS_ONLY, ), patch( "homeassistant.components.nightscout.NightscoutAPI.get_sgvs", - side_effect=ClientResponseError(None, None, status=401), + side_effect=ClientResponseError(None, None, status=HTTPStatus.UNAUTHORIZED), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/nuheat/test_config_flow.py b/tests/components/nuheat/test_config_flow.py index b89147a0024..70a9a749657 100644 --- a/tests/components/nuheat/test_config_flow.py +++ b/tests/components/nuheat/test_config_flow.py @@ -1,11 +1,12 @@ """Test the NuHeat config flow.""" +from http import HTTPStatus from unittest.mock import MagicMock, patch import requests from homeassistant import config_entries from homeassistant.components.nuheat.const import CONF_SERIAL_NUMBER, DOMAIN -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, HTTP_INTERNAL_SERVER_ERROR +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from .mocks import _get_mock_thermostat_run @@ -98,7 +99,7 @@ async def test_form_invalid_thermostat(hass): ) response_mock = MagicMock() - type(response_mock).status_code = HTTP_INTERNAL_SERVER_ERROR + type(response_mock).status_code = HTTPStatus.INTERNAL_SERVER_ERROR with patch( "homeassistant.components.nuheat.config_flow.nuheat.NuHeat.authenticate", diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 582ad5a436b..45fe9a19546 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -1,5 +1,6 @@ """Test the onboarding views.""" import asyncio +from http import HTTPStatus import os from unittest.mock import patch @@ -7,7 +8,6 @@ import pytest from homeassistant.components import onboarding from homeassistant.components.onboarding import const, views -from homeassistant.const import HTTP_FORBIDDEN from homeassistant.helpers import area_registry as ar from homeassistant.setup import async_setup_component @@ -130,7 +130,7 @@ async def test_onboarding_user_already_done(hass, hass_storage, hass_client_no_a }, ) - assert resp.status == HTTP_FORBIDDEN + assert resp.status == HTTPStatus.FORBIDDEN async def test_onboarding_user(hass, hass_storage, hass_client_no_auth): @@ -247,7 +247,7 @@ async def test_onboarding_user_race(hass, hass_storage, hass_client_no_auth): res1, res2 = await asyncio.gather(resp1, resp2) - assert sorted([res1.status, res2.status]) == [200, HTTP_FORBIDDEN] + assert sorted([res1.status, res2.status]) == [HTTPStatus.OK, HTTPStatus.FORBIDDEN] async def test_onboarding_integration(hass, hass_storage, hass_client, hass_admin_user): diff --git a/tests/components/plex/test_browse_media.py b/tests/components/plex/test_browse_media.py index d4ea73f6a97..5bbd29f35c0 100644 --- a/tests/components/plex/test_browse_media.py +++ b/tests/components/plex/test_browse_media.py @@ -1,4 +1,5 @@ """Tests for Plex media browser.""" +from http import HTTPStatus from unittest.mock import patch from homeassistant.components.media_player.const import ( @@ -359,7 +360,8 @@ async def test_browse_media( # Browse into a non-existent TV season unknown_key = 99999999999999 requests_mock.get( - f"{mock_plex_server.url_in_use}/library/metadata/{unknown_key}", status_code=404 + f"{mock_plex_server.url_in_use}/library/metadata/{unknown_key}", + status_code=HTTPStatus.NOT_FOUND, ) msg_id += 1 diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 1760a498a6f..c18d38fdba1 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -1,5 +1,6 @@ """Tests for Plex config flow.""" import copy +from http import HTTPStatus import ssl from unittest.mock import patch @@ -528,7 +529,7 @@ async def test_callback_view(hass, hass_client_no_auth, current_request_with_hos forward_url = f'{config_flow.AUTH_CALLBACK_PATH}?flow_id={result["flow_id"]}' resp = await client.get(forward_url) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK async def test_manual_config(hass, mock_plex_calls, current_request_with_host): diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index c9bcce0ac83..fced4bae58a 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -1,6 +1,7 @@ """Tests for Plex setup.""" import copy from datetime import timedelta +from http import HTTPStatus import ssl from unittest.mock import patch @@ -212,7 +213,9 @@ async def test_setup_when_certificate_changed( requests_mock.get(old_url, exc=WrongCertHostnameException) # Test with account failure - requests_mock.get("https://plex.tv/users/account", status_code=401) + requests_mock.get( + "https://plex.tv/users/account", status_code=HTTPStatus.UNAUTHORIZED + ) old_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(old_entry.entry_id) is False await hass.async_block_till_done() @@ -262,7 +265,9 @@ async def test_bad_token_with_tokenless_server( hass, entry, mock_websocket, setup_plex_server, requests_mock ): """Test setup with a bad token and a server with token auth disabled.""" - requests_mock.get("https://plex.tv/users/account", status_code=401) + requests_mock.get( + "https://plex.tv/users/account", status_code=HTTPStatus.UNAUTHORIZED + ) await setup_plex_server() diff --git a/tests/components/plex/test_playback.py b/tests/components/plex/test_playback.py index 86e55dab613..7ab0fc0f434 100644 --- a/tests/components/plex/test_playback.py +++ b/tests/components/plex/test_playback.py @@ -1,4 +1,5 @@ """Tests for Plex player playback methods/services.""" +from http import HTTPStatus from unittest.mock import patch from homeassistant.components.media_player.const import ( @@ -23,7 +24,7 @@ async def test_media_player_playback( media_player = "media_player.plex_plex_web_chrome" requests_mock.post("/playqueues", text=playqueue_created) - requests_mock.get("/player/playback/playMedia", status_code=200) + requests_mock.get("/player/playback/playMedia", status_code=HTTPStatus.OK) # Test movie success assert await hass.services.async_call( @@ -111,7 +112,7 @@ async def test_media_player_playback( ) # Test media lookup failure by key - requests_mock.get("/library/metadata/999", status_code=404) + requests_mock.get("/library/metadata/999", status_code=HTTPStatus.NOT_FOUND) assert await hass.services.async_call( MP_DOMAIN, SERVICE_PLAY_MEDIA, diff --git a/tests/components/plex/test_sensor.py b/tests/components/plex/test_sensor.py index 0e87f25850f..c07693cf073 100644 --- a/tests/components/plex/test_sensor.py +++ b/tests/components/plex/test_sensor.py @@ -1,5 +1,6 @@ """Tests for Plex sensors.""" from datetime import datetime, timedelta +from http import HTTPStatus from unittest.mock import patch import requests.exceptions @@ -165,7 +166,8 @@ async def test_library_sensor_values( # Handle library deletion requests_mock.get( - "/library/sections/2/all?includeCollections=0&type=2", status_code=404 + "/library/sections/2/all?includeCollections=0&type=2", + status_code=HTTPStatus.NOT_FOUND, ) trigger_plex_update( mock_websocket, msgtype="status", payload=LIBRARY_UPDATE_PAYLOAD diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py index f9b34088601..5a8a9869f59 100644 --- a/tests/components/plex/test_server.py +++ b/tests/components/plex/test_server.py @@ -1,5 +1,6 @@ """Tests for Plex server.""" import copy +from http import HTTPStatus from unittest.mock import patch from plexapi.exceptions import BadRequest, NotFound @@ -188,7 +189,7 @@ async def test_media_lookups(hass, mock_plex_server, requests_mock, playqueue_cr # Plex Key searches media_player_id = hass.states.async_entity_ids("media_player")[0] requests_mock.post("/playqueues", text=playqueue_created) - requests_mock.get("/player/playback/playMedia", status_code=200) + requests_mock.get("/player/playback/playMedia", status_code=HTTPStatus.OK) assert await hass.services.async_call( MP_DOMAIN, SERVICE_PLAY_MEDIA, diff --git a/tests/components/plex/test_services.py b/tests/components/plex/test_services.py index cf8bc63c5da..7ad7b033caa 100644 --- a/tests/components/plex/test_services.py +++ b/tests/components/plex/test_services.py @@ -1,4 +1,5 @@ """Tests for various Plex services.""" +from http import HTTPStatus from unittest.mock import patch from plexapi.exceptions import NotFound @@ -33,7 +34,9 @@ async def test_refresh_library( ): """Test refresh_library service call.""" url = mock_plex_server.url_in_use - refresh = requests_mock.get(f"{url}/library/sections/1/refresh", status_code=200) + refresh = requests_mock.get( + f"{url}/library/sections/1/refresh", status_code=HTTPStatus.OK + ) # Test with non-existent server with pytest.raises(HomeAssistantError): @@ -126,7 +129,9 @@ async def test_sonos_play_media( requests_mock.get("https://plex.tv/users/account", text=plextv_account) requests_mock.post("/playqueues", text=playqueue_created) - playback_mock = requests_mock.get("/player/playback/playMedia", status_code=200) + playback_mock = requests_mock.get( + "/player/playback/playMedia", status_code=HTTPStatus.OK + ) # Test with no Plex integration available with pytest.raises(HomeAssistantError) as excinfo: @@ -187,7 +192,9 @@ async def test_sonos_play_media( assert playback_mock.call_count == 4 # Test with speakers available and invalid playqueue - requests_mock.get("https://1.2.3.4:32400/playQueues/1235", status_code=404) + requests_mock.get( + "https://1.2.3.4:32400/playQueues/1235", status_code=HTTPStatus.NOT_FOUND + ) content_id_with_playqueue = '{"playqueue_id": 1235}' with pytest.raises(HomeAssistantError) as excinfo: play_on_sonos( diff --git a/tests/components/plugwise/conftest.py b/tests/components/plugwise/conftest.py index 615cfc55eeb..06f9b56e689 100644 --- a/tests/components/plugwise/conftest.py +++ b/tests/components/plugwise/conftest.py @@ -1,6 +1,7 @@ """Setup mocks for the Plugwise integration tests.""" from functools import partial +from http import HTTPStatus import re from unittest.mock import AsyncMock, Mock, patch @@ -38,15 +39,15 @@ def mock_smile(): @pytest.fixture(name="mock_smile_unauth") def mock_smile_unauth(aioclient_mock: AiohttpClientMocker) -> None: """Mock the Plugwise Smile unauthorized for Home Assistant.""" - aioclient_mock.get(re.compile(".*"), status=401) - aioclient_mock.put(re.compile(".*"), status=401) + aioclient_mock.get(re.compile(".*"), status=HTTPStatus.UNAUTHORIZED) + aioclient_mock.put(re.compile(".*"), status=HTTPStatus.UNAUTHORIZED) @pytest.fixture(name="mock_smile_error") def mock_smile_error(aioclient_mock: AiohttpClientMocker) -> None: """Mock the Plugwise Smile server failure for Home Assistant.""" - aioclient_mock.get(re.compile(".*"), status=500) - aioclient_mock.put(re.compile(".*"), status=500) + aioclient_mock.get(re.compile(".*"), status=HTTPStatus.INTERNAL_SERVER_ERROR) + aioclient_mock.put(re.compile(".*"), status=HTTPStatus.INTERNAL_SERVER_ERROR) @pytest.fixture(name="mock_smile_notconnect") diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index 6f89c91a245..827190a4b41 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -1,6 +1,7 @@ """The tests for the Prometheus exporter.""" from dataclasses import dataclass import datetime +from http import HTTPStatus import unittest.mock as mock import pytest @@ -106,7 +107,7 @@ async def test_view_empty_namespace(hass, hass_client): client = await prometheus_client(hass, hass_client, "") resp = await client.get(prometheus.API_ENDPOINT) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert resp.headers["content-type"] == CONTENT_TYPE_TEXT_PLAIN body = await resp.text() body = body.split("\n") @@ -234,7 +235,7 @@ async def test_view_default_namespace(hass, hass_client): client = await prometheus_client(hass, hass_client, None) resp = await client.get(prometheus.API_ENDPOINT) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert resp.headers["content-type"] == CONTENT_TYPE_TEXT_PLAIN body = await resp.text() body = body.split("\n") diff --git a/tests/components/push/test_camera.py b/tests/components/push/test_camera.py index d4759350341..5a1d784be67 100644 --- a/tests/components/push/test_camera.py +++ b/tests/components/push/test_camera.py @@ -1,5 +1,6 @@ """The tests for generic camera component.""" from datetime import timedelta +from http import HTTPStatus import io from homeassistant.config import async_process_ha_core_config @@ -34,7 +35,7 @@ async def test_bad_posting(hass, hass_client_no_auth): # missing file async with client.post("/api/webhook/camera.config_test") as resp: - assert resp.status == 200 # webhooks always return 200 + assert resp.status == HTTPStatus.OK # webhooks always return OK camera_state = hass.states.get("camera.config_test") assert camera_state.state == "idle" # no file supplied we are still idle @@ -69,7 +70,7 @@ async def test_posting_url(hass, hass_client_no_auth): # post image resp = await client.post("/api/webhook/camera.config_test", data=files) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK # state recording camera_state = hass.states.get("camera.config_test") diff --git a/tests/components/pushbullet/test_notify.py b/tests/components/pushbullet/test_notify.py index 3eec106019c..6e1de0b9824 100644 --- a/tests/components/pushbullet/test_notify.py +++ b/tests/components/pushbullet/test_notify.py @@ -1,4 +1,5 @@ """The tests for the pushbullet notification platform.""" +from http import HTTPStatus import json from unittest.mock import patch @@ -62,7 +63,7 @@ async def test_pushbullet_push_default(hass, requests_mock, mock_pushbullet): requests_mock.register_uri( "POST", "https://api.pushbullet.com/v2/pushes", - status_code=200, + status_code=HTTPStatus.OK, json={"mock_response": "Ok"}, ) data = {"title": "Test Title", "message": "Test Message"} @@ -91,7 +92,7 @@ async def test_pushbullet_push_device(hass, requests_mock, mock_pushbullet): requests_mock.register_uri( "POST", "https://api.pushbullet.com/v2/pushes", - status_code=200, + status_code=HTTPStatus.OK, json={"mock_response": "Ok"}, ) data = { @@ -129,7 +130,7 @@ async def test_pushbullet_push_devices(hass, requests_mock, mock_pushbullet): requests_mock.register_uri( "POST", "https://api.pushbullet.com/v2/pushes", - status_code=200, + status_code=HTTPStatus.OK, json={"mock_response": "Ok"}, ) data = { @@ -175,7 +176,7 @@ async def test_pushbullet_push_email(hass, requests_mock, mock_pushbullet): requests_mock.register_uri( "POST", "https://api.pushbullet.com/v2/pushes", - status_code=200, + status_code=HTTPStatus.OK, json={"mock_response": "Ok"}, ) data = { @@ -214,7 +215,7 @@ async def test_pushbullet_push_mixed(hass, requests_mock, mock_pushbullet): requests_mock.register_uri( "POST", "https://api.pushbullet.com/v2/pushes", - status_code=200, + status_code=HTTPStatus.OK, json={"mock_response": "Ok"}, ) data = { @@ -260,7 +261,7 @@ async def test_pushbullet_push_no_file(hass, requests_mock, mock_pushbullet): requests_mock.register_uri( "POST", "https://api.pushbullet.com/v2/pushes", - status_code=200, + status_code=HTTPStatus.OK, json={"mock_response": "Ok"}, ) data = { From 9ae7f0ecd7cc7a7890adb7fd6a737062de3b68e2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 23 Oct 2021 21:01:34 +0200 Subject: [PATCH 0748/1038] Add new attribute constants for DeviceInfo registration (#58289) Co-authored-by: epenet --- homeassistant/components/apple_tv/__init__.py | 25 +++++++---- homeassistant/components/bond/entity.py | 10 ++++- homeassistant/components/isy994/entity.py | 44 +++++++++++-------- homeassistant/components/netatmo/climate.py | 5 +-- homeassistant/components/plugwise/gateway.py | 11 +++-- homeassistant/components/zwave_js/__init__.py | 23 +++++++--- homeassistant/const.py | 4 ++ 7 files changed, 81 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index dd13375080e..fbf02fcfdff 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -10,6 +10,13 @@ from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import ( + ATTR_CONNECTIONS, + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + ATTR_SUGGESTED_AREA, + ATTR_SW_VERSION, CONF_ADDRESS, CONF_NAME, CONF_PROTOCOL, @@ -322,25 +329,27 @@ class AppleTVManager: async def _async_setup_device_registry(self): attrs = { - "identifiers": {(DOMAIN, self.config_entry.unique_id)}, - "manufacturer": "Apple", - "name": self.config_entry.data[CONF_NAME], + ATTR_IDENTIFIERS: {(DOMAIN, self.config_entry.unique_id)}, + ATTR_MANUFACTURER: "Apple", + ATTR_NAME: self.config_entry.data[CONF_NAME], } - area = attrs["name"] + area = attrs[ATTR_NAME] name_trailer = f" {DEFAULT_NAME}" if area.endswith(name_trailer): area = area[: -len(name_trailer)] - attrs["suggested_area"] = area + attrs[ATTR_SUGGESTED_AREA] = area if self.atv: dev_info = self.atv.device_info - attrs["model"] = DEFAULT_NAME + " " + dev_info.model.name.replace("Gen", "") - attrs["sw_version"] = dev_info.version + attrs[ATTR_MODEL] = ( + DEFAULT_NAME + " " + dev_info.model.name.replace("Gen", "") + ) + attrs[ATTR_SW_VERSION] = dev_info.version if dev_info.mac: - attrs["connections"] = {(dr.CONNECTION_NETWORK_MAC, dev_info.mac)} + attrs[ATTR_CONNECTIONS] = {(dr.CONNECTION_NETWORK_MAC, dev_info.mac)} device_registry = await dr.async_get_registry(self.hass) device_registry.async_get_or_create( diff --git a/homeassistant/components/bond/entity.py b/homeassistant/components/bond/entity.py index e2957984dc7..5f37de4fa19 100644 --- a/homeassistant/components/bond/entity.py +++ b/homeassistant/components/bond/entity.py @@ -10,7 +10,13 @@ from typing import Any from aiohttp import ClientError from bond_api import BPUPSubscriptions -from homeassistant.const import ATTR_MODEL, ATTR_NAME, ATTR_SW_VERSION, ATTR_VIA_DEVICE +from homeassistant.const import ( + ATTR_MODEL, + ATTR_NAME, + ATTR_SUGGESTED_AREA, + ATTR_SW_VERSION, + ATTR_VIA_DEVICE, +) from homeassistant.core import callback from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.event import async_track_time_interval @@ -65,7 +71,7 @@ class BondEntity(Entity): if self._hub.bond_id is not None: device_info[ATTR_VIA_DEVICE] = (DOMAIN, self._hub.bond_id) if self._device.location is not None: - device_info["suggested_area"] = self._device.location + device_info[ATTR_SUGGESTED_AREA] = self._device.location if not self._hub.is_bridge: if self._hub.model is not None: device_info[ATTR_MODEL] = self._hub.model diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index 0406fc45cba..a0a898206f5 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -10,10 +10,18 @@ from pyisy.constants import ( ) from pyisy.helpers import NodeProperty -from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.const import ( + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + ATTR_SUGGESTED_AREA, + STATE_OFF, + STATE_ON, +) from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN @@ -63,7 +71,7 @@ class ISYEntity(Entity): self.hass.bus.fire("isy994_control", event_data) @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" if hasattr(self._node, "protocol") and self._node.protocol == PROTO_GROUP: # not a device @@ -77,36 +85,36 @@ class ISYEntity(Entity): node = self._node.parent_node basename = node.name - device_info = { - "name": basename, - "identifiers": {}, - "model": "Unknown", - "manufacturer": "Unknown", - "via_device": (DOMAIN, uuid), - } + device_info = DeviceInfo( + identifiers={}, + manufacturer="Unknown", + model="Unknown", + name=basename, + via_device=(DOMAIN, uuid), + ) if hasattr(node, "address"): - device_info["name"] += f" ({node.address})" + device_info[ATTR_NAME] += f" ({node.address})" if hasattr(node, "primary_node"): - device_info["identifiers"] = {(DOMAIN, f"{uuid}_{node.address}")} + device_info[ATTR_IDENTIFIERS] = {(DOMAIN, f"{uuid}_{node.address}")} # ISYv5 Device Types if hasattr(node, "node_def_id") and node.node_def_id is not None: - device_info["model"] = node.node_def_id + device_info[ATTR_MODEL] = node.node_def_id # Numerical Device Type if hasattr(node, "type") and node.type is not None: - device_info["model"] += f" {node.type}" + device_info[ATTR_MODEL] += f" {node.type}" if hasattr(node, "protocol"): - device_info["manufacturer"] = node.protocol + device_info[ATTR_MANUFACTURER] = node.protocol if node.protocol == PROTO_ZWAVE: # Get extra information for Z-Wave Devices - device_info["manufacturer"] += f" MfrID:{node.zwave_props.mfr_id}" - device_info["model"] += ( + device_info[ATTR_MANUFACTURER] += f" MfrID:{node.zwave_props.mfr_id}" + device_info[ATTR_MODEL] += ( f" Type:{node.zwave_props.devtype_gen} " f"ProductTypeID:{node.zwave_props.prod_type_id} " f"ProductID:{node.zwave_props.product_id}" ) if hasattr(node, "folder") and node.folder is not None: - device_info["suggested_area"] = node.folder + device_info[ATTR_SUGGESTED_AREA] = node.folder # Note: sw_version is not exposed by the ISY for the individual devices. return device_info diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 1e2dc3f0195..4a43267852f 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -23,6 +23,7 @@ from homeassistant.components.climate.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_BATTERY_LEVEL, + ATTR_SUGGESTED_AREA, ATTR_TEMPERATURE, PRECISION_HALVES, STATE_OFF, @@ -116,8 +117,6 @@ DEFAULT_MAX_TEMP = 30 NA_THERM = "NATherm1" NA_VALVE = "NRV" -SUGGESTED_AREA = "suggested_area" - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -616,5 +615,5 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): def device_info(self) -> DeviceInfo: """Return the device info for the thermostat.""" device_info: DeviceInfo = super().device_info - device_info["suggested_area"] = self._room_data["name"] + device_info[ATTR_SUGGESTED_AREA] = self._room_data["name"] return device_info diff --git a/homeassistant/components/plugwise/gateway.py b/homeassistant/components/plugwise/gateway.py index 976accfdfa0..05d8925aeb0 100644 --- a/homeassistant/components/plugwise/gateway.py +++ b/homeassistant/components/plugwise/gateway.py @@ -15,6 +15,9 @@ from plugwise.smile import Smile from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + ATTR_CONFIGURATION_URL, + ATTR_MODEL, + ATTR_VIA_DEVICE, CONF_HOST, CONF_PASSWORD, CONF_PORT, @@ -199,13 +202,15 @@ class SmileGateway(CoordinatorEntity): ) if entry := self.coordinator.config_entry: - device_information["configuration_url"] = f"http://{entry.data[CONF_HOST]}" + device_information[ + ATTR_CONFIGURATION_URL + ] = f"http://{entry.data[CONF_HOST]}" if self._model is not None: - device_information["model"] = self._model.replace("_", " ").title() + device_information[ATTR_MODEL] = self._model.replace("_", " ").title() if self._dev_id != self._api.gateway_id: - device_information["via_device"] = (DOMAIN, self._api.gateway_id) + device_information[ATTR_VIA_DEVICE] = (DOMAIN, self._api.gateway_id) return device_information diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 37ffdf5f216..99b7684a9ad 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -18,9 +18,16 @@ from zwave_js_server.model.value import Value, ValueNotification from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + ATTR_CONFIG_ENTRY_ID, ATTR_DEVICE_ID, ATTR_DOMAIN, ATTR_ENTITY_ID, + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + ATTR_SUGGESTED_AREA, + ATTR_SW_VERSION, CONF_URL, EVENT_HOMEASSISTANT_STOP, ) @@ -120,15 +127,17 @@ def register_node_in_dev_reg( ): remove_device_func(device) params = { - "config_entry_id": entry.entry_id, - "identifiers": {device_id}, - "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, + ATTR_CONFIG_ENTRY_ID: entry.entry_id, + ATTR_IDENTIFIERS: {device_id}, + ATTR_SW_VERSION: node.firmware_version, + ATTR_NAME: node.name + or node.device_config.description + or f"Node {node.node_id}", + ATTR_MODEL: node.device_config.label, + ATTR_MANUFACTURER: node.device_config.manufacturer, } if node.location: - params["suggested_area"] = node.location + params[ATTR_SUGGESTED_AREA] = node.location device = dev_reg.async_get_or_create(**params) async_dispatcher_send(hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device) diff --git a/homeassistant/const.py b/homeassistant/const.py index 470acf06ec6..eedfc13067e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -352,8 +352,12 @@ ATTR_LOCATION: Final = "location" ATTR_MODE: Final = "mode" +ATTR_CONFIG_ENTRY_ID: Final = "config_entry_id" +ATTR_CONFIGURATION_URL: Final = "configuration_url" +ATTR_CONNECTIONS: Final = "connections" ATTR_MANUFACTURER: Final = "manufacturer" ATTR_MODEL: Final = "model" +ATTR_SUGGESTED_AREA: Final = "suggested_area" ATTR_SW_VERSION: Final = "sw_version" ATTR_VIA_DEVICE: Final = "via_device" From e961d92b5ef2eb3c9d24b4b349898117a5541e2b Mon Sep 17 00:00:00 2001 From: Chris Browet Date: Sat, 23 Oct 2021 21:10:30 +0200 Subject: [PATCH 0749/1038] Allow service data template to return a dict (#57105) --- homeassistant/helpers/config_validation.py | 6 ++-- homeassistant/helpers/service.py | 7 ++++- tests/components/automation/test_init.py | 32 ++++++++++++++++++++-- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 4bfcb98e9d4..8f0c93f5c9f 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -929,8 +929,10 @@ SERVICE_SCHEMA = vol.All( vol.Exclusive(CONF_SERVICE_TEMPLATE, "service name"): vol.Any( service, dynamic_template ), - vol.Optional("data"): vol.All(dict, template_complex), - vol.Optional("data_template"): vol.All(dict, template_complex), + vol.Optional("data"): vol.Any(template, vol.All(dict, template_complex)), + vol.Optional("data_template"): vol.Any( + template, vol.All(dict, template_complex) + ), vol.Optional(CONF_ENTITY_ID): comp_entity_ids, vol.Optional(CONF_TARGET): vol.Any(ENTITY_SERVICE_FIELDS, dynamic_template), } diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 57bcefe56e3..c2720c02f47 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -234,7 +234,12 @@ def async_prepare_call_from_config( continue try: template.attach(hass, config[conf]) - service_data.update(template.render_complex(config[conf], variables)) + render = template.render_complex(config[conf], variables) + if not isinstance(render, dict): + raise HomeAssistantError( + "Error rendering data template: Result is not a Dictionary" + ) + service_data.update(render) except TemplateError as ex: raise HomeAssistantError(f"Error rendering data template: {ex}") from ex diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 214b2ea20e8..b90c6a90819 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -45,9 +45,9 @@ def calls(hass): return async_mock_service(hass, "test", "automation") -async def test_service_data_not_a_dict(hass, calls): +async def test_service_data_not_a_dict(hass, caplog, calls): """Test service data not dict.""" - with assert_setup_component(0, automation.DOMAIN): + with assert_setup_component(1, automation.DOMAIN): assert await async_setup_component( hass, automation.DOMAIN, @@ -59,6 +59,34 @@ async def test_service_data_not_a_dict(hass, calls): }, ) + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 0 + assert "Result is not a Dictionary" in caplog.text + + +async def test_service_data_single_template(hass, calls): + """Test service data not dict.""" + with assert_setup_component(1, automation.DOMAIN): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": { + "service": "test.automation", + "data": "{{ { 'foo': 'bar' } }}", + }, + } + }, + ) + + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["foo"] == "bar" + async def test_service_specify_data(hass, calls): """Test service data.""" From 75e561f1fab16088a29429dc21b98d9ff2ea5e99 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 23 Oct 2021 21:24:40 +0200 Subject: [PATCH 0750/1038] Complete Smart Camera (sp) device support to Tuya (#58301) --- homeassistant/components/tuya/const.py | 29 ++++++++ homeassistant/components/tuya/light.py | 15 ++++ homeassistant/components/tuya/number.py | 10 +++ homeassistant/components/tuya/select.py | 51 ++++++++++++++ homeassistant/components/tuya/sensor.py | 23 ++++++ homeassistant/components/tuya/siren.py | 8 +++ .../components/tuya/strings.select.json | 27 +++++++ homeassistant/components/tuya/switch.py | 70 +++++++++++++++++++ .../tuya/translations/select.en.json | 27 +++++++ 9 files changed, 260 insertions(+) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 364febe5703..0cf46390601 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -75,8 +75,14 @@ CONF_PASSWORD = "password" CONF_COUNTRY_CODE = "country_code" CONF_APP_TYPE = "tuya_app_type" +DEVICE_CLASS_TUYA_BASIC_ANTI_FLICKR = "tuya__basic_anti_flickr" +DEVICE_CLASS_TUYA_BASIC_NIGHTVISION = "tuya__basic_nightvision" +DEVICE_CLASS_TUYA_DECIBEL_SENSITIVITY = "tuya__decibel_sensitivity" +DEVICE_CLASS_TUYA_IPC_WORK_MODE = "tuya__ipc_work_mode" DEVICE_CLASS_TUYA_LED_TYPE = "tuya__led_type" DEVICE_CLASS_TUYA_LIGHT_MODE = "tuya__light_mode" +DEVICE_CLASS_TUYA_MOTION_SENSITIVITY = "tuya__motion_sensitivity" +DEVICE_CLASS_TUYA_RECORD_MODE = "tuya__record_mode" DEVICE_CLASS_TUYA_RELAY_STATUS = "tuya__relay_status" TUYA_DISCOVERY_NEW = "tuya_discovery_new" @@ -130,6 +136,14 @@ class DPCode(str, Enum): ANGLE_HORIZONTAL = "angle_horizontal" ANGLE_VERTICAL = "angle_vertical" ANION = "anion" # Ionizer unit + BASIC_ANTI_FLICKER = "basic_anti_flicker" + BASIC_DEVICE_VOLUME = "basic_device_volume" + BASIC_FLIP = "basic_flip" + BASIC_INDICATOR = "basic_indicator" + BASIC_NIGHTVISION = "basic_nightvision" + BASIC_OSD = "basic_osd" + BASIC_PRIVATE = "basic_private" + BASIC_WDR = "basic_wdr" BATTERY_PERCENTAGE = "battery_percentage" # Battery percentage BATTERY_STATE = "battery_state" # Battery state BATTERY_VALUE = "battery_value" # Battery value @@ -162,10 +176,13 @@ class DPCode(str, Enum): CONTROL = "control" CONTROL_2 = "control_2" CONTROL_3 = "control_3" + CRY_DETECTION_SWITCH = "cry_detection_switch" CUP_NUMBER = "cup_number" # NUmber of cups CUR_CURRENT = "cur_current" # Actual current CUR_POWER = "cur_power" # Actual power CUR_VOLTAGE = "cur_voltage" # Actual voltage + DECIBEL_SENSITIVITY = "decibel_sensitivity" + DECIBEL_SWITCH = "decibel_switch" DEHUMIDITY_SET_VALUE = "dehumidify_set_value" DO_NOT_DISTURB = "do_not_disturb" DOORCONTACT_STATE = "doorcontact_state" # Status of door window sensor @@ -177,12 +194,15 @@ class DPCode(str, Enum): FAN_SPEED_PERCENT = "fan_speed_percent" # Stepless speed FAR_DETECTION = "far_detection" FILTER_RESET = "filter_reset" # Filter (cartridge) reset + FLOODLIGHT_LIGHTNESS = "floodlight_lightness" + FLOODLIGHT_SWITCH = "floodlight_switch" GAS_SENSOR_STATE = "gas_sensor_state" GAS_SENSOR_STATUS = "gas_sensor_status" GAS_SENSOR_VALUE = "gas_sensor_value" HUMIDITY_CURRENT = "humidity_current" # Current humidity HUMIDITY_SET = "humidity_set" # Humidity setting HUMIDITY_VALUE = "humidity_value" # Humidity + IPC_WORK_MODE = "ipc_work_mode" LED_TYPE_1 = "led_type_1" LED_TYPE_2 = "led_type_2" LED_TYPE_3 = "led_type_3" @@ -191,7 +211,10 @@ class DPCode(str, Enum): LOCK = "lock" # Lock / Child lock MATERIAL = "material" # Material MODE = "mode" # Working mode / Mode + MOTION_RECORD = "motion_record" + MOTION_SENSITIVITY = "motion_sensitivity" MOTION_SWITCH = "motion_switch" # Motion switch + MOTION_TRACKING = "motion_tracking" MUFFLING = "muffling" # Muffling NEAR_DETECTION = "near_detection" PAUSE = "pause" @@ -209,12 +232,16 @@ class DPCode(str, Enum): PRESSURE_STATE = "pressure_state" PRESSURE_VALUE = "pressure_value" PUMP_RESET = "pump_reset" # Water pump reset + RECORD_MODE = "record_mode" RECORD_SWITCH = "record_switch" # Recording switch RELAY_STATUS = "relay_status" SEEK = "seek" SENSITIVITY = "sensitivity" # Sensitivity + SENSOR_HUMIDITY = "sensor_humidity" + SENSOR_TEMPERATURE = "sensor_temperature" SHAKE = "shake" # Oscillating SHOCK_STATE = "shock_state" # Vibration status + SIREN_SWITCH = "siren_switch" SITUATION_SET = "situation_set" SMOKE_SENSOR_STATE = "smoke_sensor_state" SMOKE_SENSOR_STATUS = "smoke_sensor_status" @@ -270,6 +297,8 @@ class DPCode(str, Enum): WATER_SET = "water_set" # Water level WATERSENSOR_STATE = "watersensor_state" WET = "wet" # Humidification + WIRELESS_BATTERYLOCK = "wireless_batterylock" + WIRELESS_ELECTRICITY = "wireless_electricity" WORK_MODE = "work_mode" # Working mode WORK_POWER = "work_power" diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 6a194ed94b2..437c1a1262f 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -19,6 +19,7 @@ from homeassistant.components.light import ( LightEntityDescription, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -116,6 +117,20 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { name="Backlight", ), ), + # Smart Camera + # https://developer.tuya.com/en/docs/iot/categorysp?id=Kaiuz35leyo12 + "sp": ( + TuyaLightEntityDescription( + key=DPCode.FLOODLIGHT_SWITCH, + brightness=DPCode.FLOODLIGHT_LIGHTNESS, + name="Floodlight", + ), + TuyaLightEntityDescription( + key=DPCode.BASIC_INDICATOR, + name="Indicator Light", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), # Dimmer Switch # https://developer.tuya.com/en/docs/iot/categorytgkg?id=Kaiuz0ktx7m0o "tgkg": ( diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index 922012412b7..c69756d2998 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -78,6 +78,16 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { entity_category=ENTITY_CATEGORY_CONFIG, ), ), + # Smart Camera + # https://developer.tuya.com/en/docs/iot/categorysp?id=Kaiuz35leyo12 + "sp": ( + NumberEntityDescription( + key=DPCode.BASIC_DEVICE_VOLUME, + name="Volume", + icon="mdi:volume-high", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), # Dimmer Switch # https://developer.tuya.com/en/docs/iot/categorytgkg?id=Kaiuz0ktx7m0o "tgkg": ( diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index fe6c030f0bc..f56d2929a84 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -16,8 +16,14 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantTuyaData from .base import EnumTypeData, TuyaEntity from .const import ( + DEVICE_CLASS_TUYA_BASIC_ANTI_FLICKR, + DEVICE_CLASS_TUYA_BASIC_NIGHTVISION, + DEVICE_CLASS_TUYA_DECIBEL_SENSITIVITY, + DEVICE_CLASS_TUYA_IPC_WORK_MODE, DEVICE_CLASS_TUYA_LED_TYPE, DEVICE_CLASS_TUYA_LIGHT_MODE, + DEVICE_CLASS_TUYA_MOTION_SENSITIVITY, + DEVICE_CLASS_TUYA_RECORD_MODE, DEVICE_CLASS_TUYA_RELAY_STATUS, DOMAIN, TUYA_DISCOVERY_NEW, @@ -83,6 +89,51 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { entity_category=ENTITY_CATEGORY_CONFIG, ), ), + # Smart Camera + # https://developer.tuya.com/en/docs/iot/categorysp?id=Kaiuz35leyo12 + "sp": ( + SelectEntityDescription( + key=DPCode.IPC_WORK_MODE, + name="IPC Mode", + device_class=DEVICE_CLASS_TUYA_IPC_WORK_MODE, + entity_category=ENTITY_CATEGORY_CONFIG, + ), + SelectEntityDescription( + key=DPCode.DECIBEL_SENSITIVITY, + name="Sound Detection Sensitivity", + icon="mdi:volume-vibrate", + device_class=DEVICE_CLASS_TUYA_DECIBEL_SENSITIVITY, + entity_category=ENTITY_CATEGORY_CONFIG, + ), + SelectEntityDescription( + key=DPCode.RECORD_MODE, + name="Record Mode", + icon="mdi:record-rec", + device_class=DEVICE_CLASS_TUYA_RECORD_MODE, + entity_category=ENTITY_CATEGORY_CONFIG, + ), + SelectEntityDescription( + key=DPCode.BASIC_NIGHTVISION, + name="Night Vision", + icon="mdi:theme-light-dark", + device_class=DEVICE_CLASS_TUYA_BASIC_NIGHTVISION, + entity_category=ENTITY_CATEGORY_CONFIG, + ), + SelectEntityDescription( + key=DPCode.BASIC_ANTI_FLICKER, + name="Anti-flicker", + icon="mdi:image-outline", + device_class=DEVICE_CLASS_TUYA_BASIC_ANTI_FLICKR, + entity_category=ENTITY_CATEGORY_CONFIG, + ), + SelectEntityDescription( + key=DPCode.MOTION_SENSITIVITY, + name="Motion Detection Sensitivity", + icon="mdi:motion-sensor", + device_class=DEVICE_CLASS_TUYA_MOTION_SENSITIVITY, + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), # Dimmer Switch # https://developer.tuya.com/en/docs/iot/categorytgkg?id=Kaiuz0ktx7m0o "tgkg": ( diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 2e0b1562ff1..f67d8f1c242 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -238,6 +238,29 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { # Emergency Button # https://developer.tuya.com/en/docs/iot/categorysos?id=Kaiuz3oi6agjy "sos": BATTERY_SENSORS, + # Smart Camera + # https://developer.tuya.com/en/docs/iot/categorysp?id=Kaiuz35leyo12 + "sp": ( + SensorEntityDescription( + key=DPCode.SENSOR_TEMPERATURE, + name="Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.SENSOR_HUMIDITY, + name="Humidity", + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.WIRELESS_ELECTRICITY, + name="Battery", + device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=STATE_CLASS_MEASUREMENT, + ), + ), # Pressure Sensor # https://developer.tuya.com/en/docs/iot/categoryylcg?id=Kaiuz3kc2e4gm "ylcg": ( diff --git a/homeassistant/components/tuya/siren.py b/homeassistant/components/tuya/siren.py index 42b1f6839f5..bfdca70ac25 100644 --- a/homeassistant/components/tuya/siren.py +++ b/homeassistant/components/tuya/siren.py @@ -27,6 +27,14 @@ SIRENS: dict[str, tuple[SirenEntityDescription, ...]] = { name="Siren", ), ), + # Smart Camera + # https://developer.tuya.com/en/docs/iot/categorysp?id=Kaiuz35leyo12 + "sp": ( + SirenEntityDescription( + key=DPCode.SIREN_SWITCH, + name="Siren", + ), + ), } diff --git a/homeassistant/components/tuya/strings.select.json b/homeassistant/components/tuya/strings.select.json index 38d3e7c4a20..ccc78704166 100644 --- a/homeassistant/components/tuya/strings.select.json +++ b/homeassistant/components/tuya/strings.select.json @@ -1,5 +1,23 @@ { "state": { + "tuya__basic_anti_flickr": { + "0": "Disabled", + "1": "50 Hz", + "2": "60 Hz" + }, + "tuya__basic_nightvision": { + "0": "Automatic", + "1": "[%key:common::state::off%]", + "2": "[%key:common::state::on%]" + }, + "tuya__decibel_sensitivity": { + "0": "Low sensitivity", + "1": "High sensitivity" + }, + "tuya__ipc_work_mode": { + "0": "Low power mode", + "1": "Continuous working mode" + }, "tuya__led_type": { "halogen": "Halogen", "incandescent": "Incandescent", @@ -10,6 +28,15 @@ "pos": "Indicate switch location", "relay": "Indicate switch on/off state" }, + "tuya__motion_sensitivity": { + "0": "Low sensitivity", + "1": "Medium sensitivity", + "2": "High sensitivity" + }, + "tuya__record_mode": { + "1": "Record events only", + "2": "Continuous recording" + }, "tuya__relay_status": { "last": "Remember last state", "memory": "Remember last state", diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 8f5b2715731..d27fe764541 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -272,6 +272,76 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { entity_category=ENTITY_CATEGORY_CONFIG, ), ), + # Smart Camera + # https://developer.tuya.com/en/docs/iot/categorysp?id=Kaiuz35leyo12 + "sp": ( + SwitchEntityDescription( + key=DPCode.WIRELESS_BATTERYLOCK, + name="Battery Lock", + icon="mdi:battery-lock", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + SwitchEntityDescription( + key=DPCode.CRY_DETECTION_SWITCH, + icon="mdi:emoticon-cry", + name="Cry Detection", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + SwitchEntityDescription( + key=DPCode.DECIBEL_SWITCH, + icon="mdi:microphone-outline", + name="Sound Detection", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + SwitchEntityDescription( + key=DPCode.RECORD_SWITCH, + icon="mdi:record-rec", + name="Video Recording", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + SwitchEntityDescription( + key=DPCode.MOTION_RECORD, + icon="mdi:record-rec", + name="Motion Recording", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + SwitchEntityDescription( + key=DPCode.BASIC_PRIVATE, + icon="mdi:eye-off", + name="Privacy Mode", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + SwitchEntityDescription( + key=DPCode.BASIC_FLIP, + icon="mdi:flip-horizontal", + name="Flip", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + SwitchEntityDescription( + key=DPCode.BASIC_OSD, + icon="mdi:watermark", + name="Time Watermark", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + SwitchEntityDescription( + key=DPCode.BASIC_WDR, + icon="mdi:watermark", + name="Wide Dynamic Range", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + SwitchEntityDescription( + key=DPCode.MOTION_TRACKING, + icon="mdi:motion-sensor", + name="Motion Tracking", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + SwitchEntityDescription( + key=DPCode.MOTION_SWITCH, + icon="mdi:motion-sensor", + name="Motion Alarm", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), # Ceiling Light # https://developer.tuya.com/en/docs/iot/ceiling-light?id=Kaiuz03xxfc4r "xdd": ( diff --git a/homeassistant/components/tuya/translations/select.en.json b/homeassistant/components/tuya/translations/select.en.json index 70ef9486196..7ac1f656d87 100644 --- a/homeassistant/components/tuya/translations/select.en.json +++ b/homeassistant/components/tuya/translations/select.en.json @@ -1,5 +1,23 @@ { "state": { + "tuya__basic_anti_flickr": { + "0": "Disabled", + "1": "50 Hz", + "2": "60 Hz" + }, + "tuya__basic_nightvision": { + "0": "Automatic", + "1": "Off", + "2": "On" + }, + "tuya__decibel_sensitivity": { + "0": "Low sensitivity", + "1": "High sensitivity" + }, + "tuya__ipc_work_mode": { + "0": "Low power mode", + "1": "Continuous working mode" + }, "tuya__led_type": { "halogen": "Halogen", "incandescent": "Incandescent", @@ -10,6 +28,15 @@ "pos": "Indicate switch location", "relay": "Indicate switch on/off state" }, + "tuya__motion_sensitivity": { + "0": "Low sensitivity", + "1": "Medium sensitivity", + "2": "High sensitivity" + }, + "tuya__record_mode": { + "1": "Record events only", + "2": "Continuous recording" + }, "tuya__relay_status": { "last": "Remember last state", "memory": "Remember last state", From 8bfd5e4d06a353fa34472ada8780f148a5b32c74 Mon Sep 17 00:00:00 2001 From: fOmey Date: Sun, 24 Oct 2021 07:44:22 +1100 Subject: [PATCH 0751/1038] Add switch platform to Tuya Light (dj) devices (#58196) Co-authored-by: Franck Nijhof --- homeassistant/components/tuya/switch.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index d27fe764541..95688f7dc78 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -64,6 +64,17 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { entity_category=ENTITY_CATEGORY_CONFIG, ), ), + # Light + # https://developer.tuya.com/en/docs/iot/f?id=K9i5ql3v98hn3 + "dj": ( + # There are sockets available with an RGB light + # that advertise as `dj`, but provide an additional + # switch to control the plug. + SwitchEntityDescription( + key=DPCode.SWITCH, + name="Plug", + ), + ), # Cirquit Breaker "dlq": ( SwitchEntityDescription( From 26f0ea4a246603877b0102bf28959e8718f65826 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Sat, 23 Oct 2021 22:53:49 +0200 Subject: [PATCH 0752/1038] OpenGarage binary sensor (#58030) Co-authored-by: Martin Hjelmare --- .coveragerc | 1 + .../components/opengarage/__init__.py | 2 +- .../components/opengarage/binary_sensor.py | 67 +++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/opengarage/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index 713f7e98cc3..8f6db3b3935 100644 --- a/.coveragerc +++ b/.coveragerc @@ -761,6 +761,7 @@ omit = homeassistant/components/openevse/sensor.py homeassistant/components/openexchangerates/sensor.py homeassistant/components/opengarage/__init__.py + homeassistant/components/opengarage/binary_sensor.py homeassistant/components/opengarage/cover.py homeassistant/components/opengarage/entity.py homeassistant/components/opengarage/sensor.py diff --git a/homeassistant/components/opengarage/__init__.py b/homeassistant/components/opengarage/__init__.py index 4f890d07f9a..a8c8d50ce63 100644 --- a/homeassistant/components/opengarage/__init__.py +++ b/homeassistant/components/opengarage/__init__.py @@ -16,7 +16,7 @@ from .const import CONF_DEVICE_KEY, DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["cover", "sensor"] +PLATFORMS = ["binary_sensor", "cover", "sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/opengarage/binary_sensor.py b/homeassistant/components/opengarage/binary_sensor.py new file mode 100644 index 00000000000..ae1ceab6805 --- /dev/null +++ b/homeassistant/components/opengarage/binary_sensor.py @@ -0,0 +1,67 @@ +"""Platform for the opengarage.io binary sensor component.""" +from __future__ import annotations + +import logging + +from homeassistant.components.binary_sensor import ( + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.core import callback + +from .const import DOMAIN +from .entity import OpenGarageEntity + +_LOGGER = logging.getLogger(__name__) + + +SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( + BinarySensorEntityDescription( + key="vehicle", + ), +) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the OpenGarage binary sensors.""" + open_garage_data_coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + [ + OpenGarageBinarySensor( + open_garage_data_coordinator, + entry.unique_id, + description, + ) + for description in SENSOR_TYPES + ], + ) + + +class OpenGarageBinarySensor(OpenGarageEntity, BinarySensorEntity): + """Representation of a OpenGarage binary sensor.""" + + def __init__(self, open_garage_data_coordinator, device_id, description): + """Initialize the entity.""" + self._available = False + super().__init__(open_garage_data_coordinator, device_id, description) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return super().available and self._available + + @callback + def _update_attr(self) -> None: + """Handle updated data from the coordinator.""" + self._attr_name = ( + f'{self.coordinator.data["name"]} {self.entity_description.key}' + ) + state = self.coordinator.data.get(self.entity_description.key) + if state == 1: + self._attr_is_on = True + self._available = True + elif state == 0: + self._attr_is_on = False + self._available = True + else: + self._available = False From 21daffe905e3b82980bf170635c4562204242b59 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Sat, 23 Oct 2021 15:07:28 -0600 Subject: [PATCH 0753/1038] Bump pylitterbot to 2021.10.1 (#58307) --- 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 543a15736fe..7b864948569 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==2021.8.1"], + "requirements": ["pylitterbot==2021.10.1"], "codeowners": ["@natekspencer"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index b2a614b682b..333a57d585b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1598,7 +1598,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2021.8.1 +pylitterbot==2021.10.1 # homeassistant.components.loopenergy pyloopenergy==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ef9d5eb9d6f..65f13eff966 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -950,7 +950,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2021.8.1 +pylitterbot==2021.10.1 # homeassistant.components.lutron_caseta pylutron-caseta==0.11.0 From 084fd2d19fef8f893b17fd6f104834b7fcac3252 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sat, 23 Oct 2021 16:11:27 -0500 Subject: [PATCH 0754/1038] Expose Sonos features as switch entities (#54502) Co-authored-by: Tobias Sauerwein --- .../components/sonos/binary_sensor.py | 2 +- homeassistant/components/sonos/const.py | 1 + homeassistant/components/sonos/entity.py | 14 +- .../components/sonos/media_player.py | 38 +---- homeassistant/components/sonos/sensor.py | 2 +- homeassistant/components/sonos/services.yaml | 27 ---- homeassistant/components/sonos/speaker.py | 18 ++- homeassistant/components/sonos/switch.py | 132 +++++++++++++++++- tests/components/sonos/test_media_player.py | 2 - tests/components/sonos/test_switch.py | 30 +++- 10 files changed, 184 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/sonos/binary_sensor.py b/homeassistant/components/sonos/binary_sensor.py index 730a6367edd..9e35fc59616 100644 --- a/homeassistant/components/sonos/binary_sensor.py +++ b/homeassistant/components/sonos/binary_sensor.py @@ -46,7 +46,7 @@ class SonosPowerEntity(SonosEntity, BinarySensorEntity): """Return the entity's device class.""" return DEVICE_CLASS_BATTERY_CHARGING - async def async_update(self) -> None: + async def _async_poll(self) -> None: """Poll the device for the current state.""" await self.speaker.async_poll_battery() diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index c5a630d73bd..abd04652936 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -137,6 +137,7 @@ PLAYABLE_MEDIA_TYPES = [ SONOS_CREATE_ALARM = "sonos_create_alarm" SONOS_CREATE_BATTERY = "sonos_create_battery" +SONOS_CREATE_SWITCHES = "sonos_create_switches" SONOS_CREATE_MEDIA_PLAYER = "sonos_create_media_player" SONOS_ENTITY_CREATED = "sonos_entity_created" SONOS_POLL_UPDATE = "sonos_poll_update" diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py index 1d4eb6e1cdd..0579c4f5c9b 100644 --- a/homeassistant/components/sonos/entity.py +++ b/homeassistant/components/sonos/entity.py @@ -1,6 +1,7 @@ """Entity representing a Sonos player.""" from __future__ import annotations +from abc import abstractmethod import datetime import logging @@ -30,6 +31,8 @@ _LOGGER = logging.getLogger(__name__) class SonosEntity(Entity): """Representation of a Sonos entity.""" + _attr_should_poll = False + def __init__(self, speaker: SonosSpeaker) -> None: """Initialize a SonosEntity.""" self.speaker = speaker @@ -78,10 +81,14 @@ class SonosEntity(Entity): self.speaker.subscriptions_failed = True await self.speaker.async_unsubscribe() try: - await self.async_update() # pylint: disable=no-member + await self._async_poll() except (OSError, SoCoException) as ex: _LOGGER.debug("Error connecting to %s: %s", self.entity_id, ex) + @abstractmethod + async def _async_poll(self) -> None: + """Poll the specific functionality. Should be implemented by platforms if needed.""" + @property def soco(self) -> SoCo: """Return the speaker SoCo instance.""" @@ -108,8 +115,3 @@ class SonosEntity(Entity): def available(self) -> bool: """Return whether this device is available.""" return self.speaker.available - - @property - def should_poll(self) -> bool: - """Return that we should not be polled (we handle that internally).""" - return False diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 5cb6e225510..9f2bc829eac 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -119,12 +119,7 @@ ATTR_ENABLED = "enabled" ATTR_INCLUDE_LINKED_ZONES = "include_linked_zones" ATTR_MASTER = "master" ATTR_WITH_GROUP = "with_group" -ATTR_BUTTONS_ENABLED = "buttons_enabled" -ATTR_CROSSFADE = "crossfade" -ATTR_NIGHT_SOUND = "night_sound" -ATTR_SPEECH_ENHANCE = "speech_enhance" ATTR_QUEUE_POSITION = "queue_position" -ATTR_STATUS_LIGHT = "status_light" ATTR_EQ_BASS = "bass_level" ATTR_EQ_TREBLE = "treble_level" @@ -233,11 +228,6 @@ async def async_setup_entry( platform.async_register_entity_service( # type: ignore SERVICE_SET_OPTION, { - vol.Optional(ATTR_BUTTONS_ENABLED): cv.boolean, - vol.Optional(ATTR_CROSSFADE): cv.boolean, - vol.Optional(ATTR_NIGHT_SOUND): cv.boolean, - vol.Optional(ATTR_SPEECH_ENHANCE): cv.boolean, - vol.Optional(ATTR_STATUS_LIGHT): cv.boolean, vol.Optional(ATTR_EQ_BASS): vol.All( vol.Coerce(int), vol.Range(min=-10, max=10) ), @@ -302,7 +292,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): return STATE_PLAYING return STATE_IDLE - async def async_update(self) -> None: + async def _async_poll(self) -> None: """Retrieve latest state by polling.""" await self.hass.data[DATA_SONOS].favorites[ self.speaker.household_id @@ -618,30 +608,10 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): @soco_error() def set_option( self, - buttons_enabled: bool | None = None, - crossfade: bool | None = None, - night_sound: bool | None = None, - speech_enhance: bool | None = None, - status_light: bool | None = None, bass_level: int | None = None, treble_level: int | None = None, ) -> None: """Modify playback options.""" - if buttons_enabled is not None: - self.soco.buttons_enabled = buttons_enabled - - if crossfade is not None: - self.soco.cross_fade = crossfade - - if night_sound is not None and self.speaker.night_mode is not None: - self.soco.night_mode = night_sound - - if speech_enhance is not None and self.speaker.dialog_mode is not None: - self.soco.dialog_mode = speech_enhance - - if status_light is not None: - self.soco.status_light = status_light - if bass_level is not None: self.soco.bass = bass_level @@ -671,12 +641,6 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): if self.speaker.treble_level is not None: attributes[ATTR_EQ_TREBLE] = self.speaker.treble_level - if self.speaker.night_mode is not None: - attributes[ATTR_NIGHT_SOUND] = self.speaker.night_mode - - if self.speaker.dialog_mode is not None: - attributes[ATTR_SPEECH_ENHANCE] = self.speaker.dialog_mode - if self.media.queue_position is not None: attributes[ATTR_QUEUE_POSITION] = self.media.queue_position diff --git a/homeassistant/components/sonos/sensor.py b/homeassistant/components/sonos/sensor.py index a71ac7cef21..f8e5142c123 100644 --- a/homeassistant/components/sonos/sensor.py +++ b/homeassistant/components/sonos/sensor.py @@ -45,7 +45,7 @@ class SonosBatteryEntity(SonosEntity, SensorEntity): """Get the unit of measurement.""" return PERCENTAGE - async def async_update(self) -> None: + async def _async_poll(self) -> None: """Poll the device for the current state.""" await self.speaker.async_poll_battery() diff --git a/homeassistant/components/sonos/services.yaml b/homeassistant/components/sonos/services.yaml index 9858eb7f8ed..af664f0b367 100644 --- a/homeassistant/components/sonos/services.yaml +++ b/homeassistant/components/sonos/services.yaml @@ -94,33 +94,6 @@ set_option: device: integration: sonos fields: - buttons_enabled: - name: Buttons enabled - description: Enable control buttons on the device - example: "true" - selector: - boolean: - crossfade: - name: Crossfade - description: Enable crossfade on the device - example: "true" - selector: - boolean: - night_sound: - name: Night sound - description: Enable Night Sound mode - selector: - boolean: - speech_enhance: - name: Speech enhance - description: Enable Speech Enhancement mode - selector: - boolean: - status_light: - name: Status light - description: Enable Status (LED) Light - selector: - boolean: bass_level: name: Bass Level description: Bass level for EQ. diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 549e4bacc9d..66a2b46eb12 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -14,7 +14,7 @@ import async_timeout from soco.core import MUSIC_SRC_LINE_IN, MUSIC_SRC_RADIO, MUSIC_SRC_TV, SoCo from soco.data_structures import DidlAudioBroadcast, DidlPlaylistContainer from soco.events_base import Event as SonosEvent, SubscriptionBase -from soco.exceptions import SoCoException, SoCoUPnPException +from soco.exceptions import SoCoException, SoCoSlaveException, SoCoUPnPException from soco.music_library import MusicLibrary from soco.plugins.sharelink import ShareLinkPlugin from soco.snapshot import Snapshot @@ -46,6 +46,7 @@ from .const import ( SONOS_CREATE_ALARM, SONOS_CREATE_BATTERY, SONOS_CREATE_MEDIA_PLAYER, + SONOS_CREATE_SWITCHES, SONOS_ENTITY_CREATED, SONOS_POLL_UPDATE, SONOS_REBOOTED, @@ -191,9 +192,14 @@ class SonosSpeaker: self.muted: bool | None = None self.night_mode: bool | None = None self.dialog_mode: bool | None = None + self.cross_fade: bool | None = None self.bass_level: int | None = None self.treble_level: int | None = None + # Misc features + self.buttons_enabled: bool | None = None + self.status_light: bool | None = None + # Grouping self.coordinator: SonosSpeaker | None = None self.sonos_group: list[SonosSpeaker] = [self] @@ -240,6 +246,8 @@ class SonosSpeaker: else: self._platforms_ready.add(SWITCH_DOMAIN) + dispatcher_send(self.hass, SONOS_CREATE_SWITCHES, self) + self._event_dispatchers = { "AlarmClock": self.async_dispatch_alarms, "AVTransport": self.async_dispatch_media_update, @@ -458,6 +466,9 @@ class SonosSpeaker: @callback def async_dispatch_media_update(self, event: SonosEvent) -> None: """Update information about currently playing media from an event.""" + if crossfade := event.variables.get("current_crossfade_mode"): + self.cross_fade = bool(int(crossfade)) + self.hass.async_add_executor_job(self.update_media, event) @callback @@ -982,6 +993,11 @@ class SonosSpeaker: self.bass_level = self.soco.bass self.treble_level = self.soco.treble + try: + self.cross_fade = self.soco.cross_fade + except SoCoSlaveException: + pass + def update_media(self, event: SonosEvent | None = None) -> None: """Update information about currently playing media.""" variables = event and event.variables diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index cee60cbbafa..3e9d5484784 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -4,10 +4,10 @@ from __future__ import annotations import datetime import logging -from soco.exceptions import SoCoException, SoCoUPnPException +from soco.exceptions import SoCoException, SoCoSlaveException, SoCoUPnPException from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity -from homeassistant.const import ATTR_TIME +from homeassistant.const import ATTR_TIME, ENTITY_CATEGORY_CONFIG from homeassistant.core import callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -17,6 +17,7 @@ from .const import ( DOMAIN as SONOS_DOMAIN, SONOS_ALARMS_UPDATED, SONOS_CREATE_ALARM, + SONOS_CREATE_SWITCHES, ) from .entity import SonosEntity from .speaker import SonosSpeaker @@ -31,11 +32,48 @@ ATTR_SCHEDULED_TODAY = "scheduled_today" ATTR_VOLUME = "volume" ATTR_INCLUDE_LINKED_ZONES = "include_linked_zones" +ATTR_CROSSFADE = "cross_fade" +ATTR_NIGHT_SOUND = "night_mode" +ATTR_SPEECH_ENHANCEMENT = "dialog_mode" +ATTR_STATUS_LIGHT = "status_light" +ATTR_TOUCH_CONTROLS = "buttons_enabled" + +ALL_FEATURES = ( + ATTR_TOUCH_CONTROLS, + ATTR_CROSSFADE, + ATTR_NIGHT_SOUND, + ATTR_SPEECH_ENHANCEMENT, + ATTR_STATUS_LIGHT, +) + +COORDINATOR_FEATURES = ATTR_CROSSFADE + +POLL_REQUIRED = ( + ATTR_TOUCH_CONTROLS, + ATTR_STATUS_LIGHT, +) + +FRIENDLY_NAMES = { + ATTR_CROSSFADE: "Crossfade", + ATTR_NIGHT_SOUND: "Night Sound", + ATTR_SPEECH_ENHANCEMENT: "Speech Enhancement", + ATTR_STATUS_LIGHT: "Status Light", + ATTR_TOUCH_CONTROLS: "Touch Controls", +} + +FEATURE_ICONS = { + ATTR_NIGHT_SOUND: "mdi:chat-sleep", + ATTR_SPEECH_ENHANCEMENT: "mdi:ear-hearing", + ATTR_CROSSFADE: "mdi:swap-horizontal", + ATTR_STATUS_LIGHT: "mdi:led-on", + ATTR_TOUCH_CONTROLS: "mdi:gesture-tap", +} + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Sonos from a config entry.""" - async def _async_create_entity(speaker: SonosSpeaker, alarm_ids: list[str]) -> None: + async def _async_create_alarms(speaker: SonosSpeaker, alarm_ids: list[str]) -> None: entities = [] created_alarms = ( hass.data[DATA_SONOS].alarms[speaker.household_id].created_alarm_ids @@ -48,9 +86,93 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities.append(SonosAlarmEntity(alarm_id, speaker)) async_add_entities(entities) + def available_soco_attributes(speaker: SonosSpeaker) -> list[tuple[str, bool]]: + features = [] + for feature_type in ALL_FEATURES: + try: + if (state := getattr(speaker.soco, feature_type, None)) is not None: + setattr(speaker, feature_type, state) + features.append(feature_type) + except SoCoSlaveException: + features.append(feature_type) + return features + + async def _async_create_switches(speaker: SonosSpeaker) -> None: + entities = [] + available_features = await hass.async_add_executor_job( + available_soco_attributes, speaker + ) + for feature_type in available_features: + _LOGGER.debug( + "Creating %s switch on %s", + FRIENDLY_NAMES[feature_type], + speaker.zone_name, + ) + entities.append(SonosSwitchEntity(feature_type, speaker)) + async_add_entities(entities) + config_entry.async_on_unload( - async_dispatcher_connect(hass, SONOS_CREATE_ALARM, _async_create_entity) + async_dispatcher_connect(hass, SONOS_CREATE_ALARM, _async_create_alarms) ) + config_entry.async_on_unload( + async_dispatcher_connect(hass, SONOS_CREATE_SWITCHES, _async_create_switches) + ) + + +class SonosSwitchEntity(SonosEntity, SwitchEntity): + """Representation of a Sonos feature switch.""" + + def __init__(self, feature_type: str, speaker: SonosSpeaker) -> None: + """Initialize the switch.""" + super().__init__(speaker) + self.feature_type = feature_type + self.entity_id = ENTITY_ID_FORMAT.format( + f"sonos_{speaker.zone_name}_{FRIENDLY_NAMES[feature_type]}" + ) + self.needs_coordinator = feature_type in COORDINATOR_FEATURES + self._attr_entity_category = ENTITY_CATEGORY_CONFIG + self._attr_name = f"{speaker.zone_name} {FRIENDLY_NAMES[feature_type]}" + self._attr_unique_id = f"{speaker.soco.uid}-{feature_type}" + self._attr_icon = FEATURE_ICONS.get(feature_type) + + if feature_type in POLL_REQUIRED: + self._attr_should_poll = True + + async def _async_poll(self) -> None: + """Handle polling for subscription-based switches when subscription fails.""" + if not self.should_poll: + await self.hass.async_add_executor_job(self.update) + + def update(self) -> None: + """Fetch switch state if necessary.""" + state = getattr(self.soco, self.feature_type) + setattr(self.speaker, self.feature_type, state) + + @property + 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) + + def turn_on(self, **kwargs) -> None: + """Turn the entity on.""" + self.send_command(True) + + def turn_off(self, **kwargs) -> None: + """Turn the entity off.""" + self.send_command(False) + + def send_command(self, enable: bool) -> None: + """Enable or disable the feature on the device.""" + if self.needs_coordinator: + soco = self.soco.group.coordinator + else: + soco = self.soco + try: + setattr(soco, self.feature_type, enable) + except SoCoUPnPException as exc: + _LOGGER.warning("Could not toggle %s: %s", self.entity_id, exc) class SonosAlarmEntity(SonosEntity, SwitchEntity): @@ -99,7 +221,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): str(self.alarm.start_time)[0:5], ) - async def async_update(self) -> None: + async def _async_poll(self) -> None: """Call the central alarm polling method.""" await self.hass.data[DATA_SONOS].alarms[self.household_id].async_poll() diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index 4de0f37d333..9fb1d7639eb 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -95,6 +95,4 @@ async def test_entity_basic(hass, config_entry, discover): attributes = state.attributes assert attributes["friendly_name"] == "Zone A" assert attributes["is_volume_muted"] is False - assert attributes["night_sound"] is True - assert attributes["speech_enhance"] is True assert attributes["volume_level"] == 0.19 diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py index d71d403fd8a..906695bdbaf 100644 --- a/tests/components/sonos/test_switch.py +++ b/tests/components/sonos/test_switch.py @@ -30,10 +30,14 @@ async def test_entity_registry(hass, config_entry, config): assert "media_player.zone_a" in entity_registry.entities assert "switch.sonos_alarm_14" in entity_registry.entities + assert "switch.sonos_zone_a_status_light" in entity_registry.entities + assert "switch.sonos_zone_a_night_sound" in entity_registry.entities + assert "switch.sonos_zone_a_speech_enhancement" in entity_registry.entities + assert "switch.sonos_zone_a_touch_controls" in entity_registry.entities -async def test_alarm_attributes(hass, config_entry, config): - """Test for correct sonos alarm state.""" +async def test_switch_attributes(hass, config_entry, config, soco): + """Test for correct Sonos switch states.""" await setup_platform(hass, config_entry, config) entity_registry = await hass.helpers.entity_registry.async_get_registry() @@ -49,6 +53,28 @@ async def test_alarm_attributes(hass, config_entry, config): assert alarm_state.attributes.get(ATTR_PLAY_MODE) == "SHUFFLE_NOREPEAT" assert not alarm_state.attributes.get(ATTR_INCLUDE_LINKED_ZONES) + night_sound = entity_registry.entities["switch.sonos_zone_a_night_sound"] + night_sound_state = hass.states.get(night_sound.entity_id) + assert night_sound_state.state == STATE_ON + + speech_enhancement = entity_registry.entities[ + "switch.sonos_zone_a_speech_enhancement" + ] + speech_enhancement_state = hass.states.get(speech_enhancement.entity_id) + assert speech_enhancement_state.state == STATE_ON + + crossfade = entity_registry.entities["switch.sonos_zone_a_crossfade"] + crossfade_state = hass.states.get(crossfade.entity_id) + assert crossfade_state.state == STATE_ON + + status_light = entity_registry.entities["switch.sonos_zone_a_status_light"] + status_light_state = hass.states.get(status_light.entity_id) + assert status_light_state.state == STATE_ON + + touch_controls = entity_registry.entities["switch.sonos_zone_a_touch_controls"] + touch_controls_state = hass.states.get(touch_controls.entity_id) + assert touch_controls_state.state == STATE_ON + async def test_alarm_create_delete( hass, config_entry, config, soco, alarm_clock, alarm_clock_extended, alarm_event From 30fb6190954d9f2772cacf94de6e27333be4b900 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 24 Oct 2021 00:12:57 +0000 Subject: [PATCH 0755/1038] [ci skip] Translation update --- .../components/abode/translations/hu.json | 2 +- .../components/agent_dvr/translations/hu.json | 2 +- .../components/airvisual/translations/hu.json | 2 +- .../components/ambee/translations/hu.json | 2 +- .../components/apple_tv/translations/hu.json | 8 ++--- .../components/arcam_fmj/translations/hu.json | 2 +- .../components/august/translations/hu.json | 4 +-- .../components/auth/translations/hu.json | 2 +- .../components/awair/translations/hu.json | 2 +- .../components/axis/translations/hu.json | 2 +- .../azure_devops/translations/hu.json | 4 +-- .../components/blebox/translations/hu.json | 2 +- .../components/bosch_shc/translations/hu.json | 2 +- .../components/broadlink/translations/hu.json | 2 +- .../components/climate/translations/hu.json | 4 +-- .../components/deconz/translations/hu.json | 4 +-- .../components/denonavr/translations/hu.json | 2 +- .../components/dlna_dmr/translations/ca.json | 17 +++++++++-- .../components/dlna_dmr/translations/de.json | 15 ++++++++-- .../components/dlna_dmr/translations/et.json | 17 +++++++++-- .../components/dlna_dmr/translations/hu.json | 11 +++++++ .../components/dlna_dmr/translations/ru.json | 15 ++++++++-- .../dlna_dmr/translations/zh-Hant.json | 15 ++++++++-- .../components/doorbird/translations/bg.json | 1 + .../components/efergy/translations/hu.json | 2 +- .../components/esphome/translations/hu.json | 10 +++---- .../fireservicerota/translations/hu.json | 2 +- .../components/flume/translations/bg.json | 20 +++++++++++++ .../components/flume/translations/hu.json | 2 +- .../components/flux_led/translations/hu.json | 4 +-- .../components/fritzbox/translations/hu.json | 6 ++-- .../components/guardian/translations/hu.json | 2 +- .../components/hive/translations/hu.json | 2 +- .../home_plus_control/translations/hu.json | 2 +- .../homekit_controller/translations/hu.json | 2 +- .../huawei_lte/translations/hu.json | 2 +- .../components/hue/translations/hu.json | 2 +- .../humidifier/translations/hu.json | 2 +- .../translations/bg.json | 3 ++ .../components/hyperion/translations/hu.json | 4 +-- .../components/icloud/translations/hu.json | 4 +-- .../components/konnected/translations/hu.json | 2 +- .../components/lookin/translations/hu.json | 2 +- .../lutron_caseta/translations/hu.json | 2 +- .../components/mazda/translations/hu.json | 2 +- .../modem_callerid/translations/hu.json | 2 +- .../motion_blinds/translations/hu.json | 2 +- .../components/myq/translations/hu.json | 2 +- .../components/neato/translations/hu.json | 2 +- .../components/nest/translations/hu.json | 2 +- .../components/netatmo/translations/hu.json | 2 +- .../components/netgear/translations/bg.json | 4 +++ .../components/notion/translations/hu.json | 2 +- .../components/nut/translations/bg.json | 10 +++++++ .../components/octoprint/translations/de.json | 29 +++++++++++++++++++ .../components/octoprint/translations/hu.json | 29 +++++++++++++++++++ .../octoprint/translations/zh-Hant.json | 29 +++++++++++++++++++ .../components/onvif/translations/hu.json | 4 +-- .../components/ozw/translations/hu.json | 2 +- .../panasonic_viera/translations/bg.json | 3 +- .../components/plex/translations/hu.json | 4 +-- .../components/powerwall/translations/hu.json | 2 +- .../components/roku/translations/hu.json | 2 +- .../components/roomba/translations/hu.json | 2 +- .../components/samsungtv/translations/hu.json | 6 ++-- .../components/select/translations/hu.json | 2 +- .../components/sharkiq/translations/hu.json | 2 +- .../simplisafe/translations/hu.json | 4 +-- .../components/smarttub/translations/hu.json | 2 +- .../somfy_mylink/translations/hu.json | 2 +- .../components/sonarr/translations/hu.json | 4 +-- .../components/sonos/translations/hu.json | 4 +-- .../synology_dsm/translations/bg.json | 15 +++++++--- .../system_bridge/translations/hu.json | 2 +- .../totalconnect/translations/hu.json | 2 +- .../components/tradfri/translations/hu.json | 2 +- .../components/tuya/translations/hu.json | 2 +- .../components/unifi/translations/hu.json | 2 +- .../components/verisure/translations/hu.json | 2 +- .../vlc_telnet/translations/hu.json | 2 +- .../components/watttime/translations/hu.json | 2 +- .../components/wled/translations/hu.json | 2 +- .../xiaomi_aqara/translations/hu.json | 2 +- .../xiaomi_miio/translations/hu.json | 4 +-- .../components/zwave_js/translations/hu.json | 8 ++--- 85 files changed, 312 insertions(+), 113 deletions(-) create mode 100644 homeassistant/components/flume/translations/bg.json create mode 100644 homeassistant/components/octoprint/translations/de.json create mode 100644 homeassistant/components/octoprint/translations/hu.json create mode 100644 homeassistant/components/octoprint/translations/zh-Hant.json diff --git a/homeassistant/components/abode/translations/hu.json b/homeassistant/components/abode/translations/hu.json index a4ce211d21a..8f835cbbe2d 100644 --- a/homeassistant/components/abode/translations/hu.json +++ b/homeassistant/components/abode/translations/hu.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "error": { diff --git a/homeassistant/components/agent_dvr/translations/hu.json b/homeassistant/components/agent_dvr/translations/hu.json index b8fec1c281d..83751d72eaf 100644 --- a/homeassistant/components/agent_dvr/translations/hu.json +++ b/homeassistant/components/agent_dvr/translations/hu.json @@ -4,7 +4,7 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, "error": { - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { diff --git a/homeassistant/components/airvisual/translations/hu.json b/homeassistant/components/airvisual/translations/hu.json index 48d4f5b98eb..681f32ca3bc 100644 --- a/homeassistant/components/airvisual/translations/hu.json +++ b/homeassistant/components/airvisual/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "A hely m\u00e1r konfigur\u00e1lva van vagy a Node/Pro azonos\u00edt\u00f3 m\u00e1r regisztr\u00e1lva van.", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", diff --git a/homeassistant/components/ambee/translations/hu.json b/homeassistant/components/ambee/translations/hu.json index 6cb59bba925..e4cef44c5ba 100644 --- a/homeassistant/components/ambee/translations/hu.json +++ b/homeassistant/components/ambee/translations/hu.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", diff --git a/homeassistant/components/apple_tv/translations/hu.json b/homeassistant/components/apple_tv/translations/hu.json index 3d254422baf..3ee74bbf419 100644 --- a/homeassistant/components/apple_tv/translations/hu.json +++ b/homeassistant/components/apple_tv/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "backoff": "Az eszk\u00f6z jelenleg nem fogadja el a p\u00e1ros\u00edt\u00e1si k\u00e9relmeket (lehet, hogy t\u00fal sokszor adott meg \u00e9rv\u00e9nytelen PIN-k\u00f3dot), pr\u00f3b\u00e1lkozzon \u00fajra k\u00e9s\u0151bb.", "device_did_not_pair": "A p\u00e1ros\u00edt\u00e1s folyamat\u00e1t az eszk\u00f6zr\u0151l nem pr\u00f3b\u00e1lt\u00e1k befejezni.", "invalid_config": "Az eszk\u00f6z konfigur\u00e1l\u00e1sa nem teljes. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra hozz\u00e1adni.", @@ -23,14 +23,14 @@ "title": "Apple TV sikeresen hozz\u00e1adva" }, "pair_no_pin": { - "description": "P\u00e1ros\u00edt\u00e1sra van sz\u00fcks\u00e9g a(z) {protocol} szolg\u00e1ltat\u00e1shoz. A folytat\u00e1shoz k\u00e9rj\u00fck, \u00edrja be az Apple TV {pin} -t.", + "description": "P\u00e1ros\u00edt\u00e1sra van sz\u00fcks\u00e9g a {protocol} szolg\u00e1ltat\u00e1shoz. A folytat\u00e1shoz k\u00e9rj\u00fck, \u00edrja be az Apple TV {pin}-t.", "title": "P\u00e1ros\u00edt\u00e1s" }, "pair_with_pin": { "data": { "pin": "PIN-k\u00f3d" }, - "description": "P\u00e1ros\u00edt\u00e1sra van sz\u00fcks\u00e9g a(z) {protocol} protokollhoz. K\u00e9rj\u00fck, adja meg a k\u00e9perny\u0151n megjelen\u0151 PIN-k\u00f3dot. A vezet\u0151 null\u00e1kat el kell hagyni, pl. \u00edrja be a 123 \u00e9rt\u00e9ket, ha a megjelen\u00edtett k\u00f3d 0123.", + "description": "P\u00e1ros\u00edt\u00e1sra van sz\u00fcks\u00e9g a {protocol} protokollhoz. K\u00e9rj\u00fck, adja meg a k\u00e9perny\u0151n megjelen\u0151 PIN-k\u00f3dot. A vezet\u0151 null\u00e1kat el kell hagyni, pl. \u00edrja be a 123 \u00e9rt\u00e9ket, ha a megjelen\u00edtett k\u00f3d 0123.", "title": "P\u00e1ros\u00edt\u00e1s" }, "reconfigure": { @@ -38,7 +38,7 @@ "title": "Eszk\u00f6z \u00fajrakonfigur\u00e1l\u00e1sa" }, "service_problem": { - "description": "Hiba t\u00f6rt\u00e9nt a(z) \" {protocol} \" protokoll p\u00e1ros\u00edt\u00e1sakor. Ez figyelmen k\u00edv\u00fcl lett hagyva.", + "description": "Hiba t\u00f6rt\u00e9nt a `{protocol}` protokoll p\u00e1ros\u00edt\u00e1sakor. Figyelmen k\u00edv\u00fcl lesz hagyva.", "title": "Nem siker\u00fclt hozz\u00e1adni a szolg\u00e1ltat\u00e1st" }, "user": { diff --git a/homeassistant/components/arcam_fmj/translations/hu.json b/homeassistant/components/arcam_fmj/translations/hu.json index c7532f24b76..964ebe2a33d 100644 --- a/homeassistant/components/arcam_fmj/translations/hu.json +++ b/homeassistant/components/arcam_fmj/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "error": { diff --git a/homeassistant/components/august/translations/hu.json b/homeassistant/components/august/translations/hu.json index 22e16dda305..42f9860bdc2 100644 --- a/homeassistant/components/august/translations/hu.json +++ b/homeassistant/components/august/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -14,7 +14,7 @@ "data": { "password": "Jelsz\u00f3" }, - "description": "Adja meg a(z) {username} jelszav\u00e1t.", + "description": "Adja meg {username} jelszav\u00e1t.", "title": "August fi\u00f3k \u00fajrahiteles\u00edt\u00e9se" }, "user_validate": { diff --git a/homeassistant/components/auth/translations/hu.json b/homeassistant/components/auth/translations/hu.json index 47ecf846e0f..99504c1b7a7 100644 --- a/homeassistant/components/auth/translations/hu.json +++ b/homeassistant/components/auth/translations/hu.json @@ -13,7 +13,7 @@ "title": "\u00c1ll\u00edtsa be az \u00e9rtes\u00edt\u00e9si \u00f6sszetev\u0151 \u00e1ltal megadott egyszeri jelsz\u00f3t" }, "setup": { - "description": "Az egyszeri jelsz\u00f3 el lett k\u00fcldve a(z) **notify.{notify_service}** szolg\u00e1ltat\u00e1ssal. K\u00e9rem, adja meg al\u00e1bb:", + "description": "Az egyszeri jelsz\u00f3 el lett k\u00fcldve **notify.{notify_service}** szolg\u00e1ltat\u00e1ssal. K\u00e9rem, adja meg al\u00e1bb:", "title": "Be\u00e1ll\u00edt\u00e1s ellen\u0151rz\u00e9se" } }, diff --git a/homeassistant/components/awair/translations/hu.json b/homeassistant/components/awair/translations/hu.json index 62187becd37..e3994430a8b 100644 --- a/homeassistant/components/awair/translations/hu.json +++ b/homeassistant/components/awair/translations/hu.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "A fi\u00f3k 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." }, "error": { "invalid_access_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token", diff --git a/homeassistant/components/axis/translations/hu.json b/homeassistant/components/axis/translations/hu.json index 0cddf167437..cb2f9a17c93 100644 --- a/homeassistant/components/axis/translations/hu.json +++ b/homeassistant/components/axis/translations/hu.json @@ -7,7 +7,7 @@ }, "error": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, diff --git a/homeassistant/components/azure_devops/translations/hu.json b/homeassistant/components/azure_devops/translations/hu.json index e42ebc8d8e2..2d8879b9d68 100644 --- a/homeassistant/components/azure_devops/translations/hu.json +++ b/homeassistant/components/azure_devops/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -15,7 +15,7 @@ "data": { "personal_access_token": "Szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si token (PAT)" }, - "description": "A(z) {project_url} hiteles\u00edt\u00e9se nem siker\u00fclt. K\u00e9rj\u00fck, adja meg jelenlegi hiteles\u00edt\u0151 adatait.", + "description": "{project_url} hiteles\u00edt\u00e9se nem siker\u00fclt. K\u00e9rj\u00fck, adja meg jelenlegi hiteles\u00edt\u0151 adatait.", "title": "\u00dajrahiteles\u00edt\u00e9s" }, "user": { diff --git a/homeassistant/components/blebox/translations/hu.json b/homeassistant/components/blebox/translations/hu.json index 056402ea13f..0d9e2b5a3ff 100644 --- a/homeassistant/components/blebox/translations/hu.json +++ b/homeassistant/components/blebox/translations/hu.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "address_already_configured": "Egy BleBox-eszk\u00f6z m\u00e1r konfigur\u00e1lva van a(z) {address} c\u00edmen.", + "address_already_configured": "Egy BleBox-eszk\u00f6z m\u00e1r konfigur\u00e1lva van a {address} c\u00edmen.", "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, "error": { diff --git a/homeassistant/components/bosch_shc/translations/hu.json b/homeassistant/components/bosch_shc/translations/hu.json index cf0090475b7..b3e0ed77815 100644 --- a/homeassistant/components/bosch_shc/translations/hu.json +++ b/homeassistant/components/bosch_shc/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", diff --git a/homeassistant/components/broadlink/translations/hu.json b/homeassistant/components/broadlink/translations/hu.json index d3a59a03cea..0bab0c1752f 100644 --- a/homeassistant/components/broadlink/translations/hu.json +++ b/homeassistant/components/broadlink/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm", "not_supported": "Az eszk\u00f6z nem t\u00e1mogatott", diff --git a/homeassistant/components/climate/translations/hu.json b/homeassistant/components/climate/translations/hu.json index 400c1af877d..a31792fc016 100644 --- a/homeassistant/components/climate/translations/hu.json +++ b/homeassistant/components/climate/translations/hu.json @@ -2,11 +2,11 @@ "device_automation": { "action_type": { "set_hvac_mode": "F\u0171t\u00e9s- \u00e9s l\u00e9gtechnikai (HVAC) \u00fczemm\u00f3d m\u00f3dos\u00edt\u00e1sa a k\u00f6vetkez\u0151n: {entity_name}", - "set_preset_mode": "A(z) {entity_name} be\u00e1ll\u00edt\u00e1s\u00e1nak v\u00e1lt\u00e1sa" + "set_preset_mode": "{entity_name} \u00fczemm\u00f3dj\u00e1nak v\u00e1lt\u00e1sa" }, "condition_type": { "is_hvac_mode": "{entity_name} speci\u00e1lis f\u0171t\u00e9s, szell\u0151z\u00e9s \u00e9s l\u00e9gkondicion\u00e1l\u00e1s (HVAC) \u00fczemm\u00f3dra van be\u00e1ll\u00edtva", - "is_preset_mode": "A(z) {entity_name} el\u0151re be\u00e1ll\u00edtott m\u00f3dja van kiv\u00e1lasztva" + "is_preset_mode": "{entity_name} \u00fczemm\u00f3dja van kiv\u00e1lasztva" }, "trigger_type": { "current_humidity_changed": "{entity_name} m\u00e9rt p\u00e1ratartalma megv\u00e1ltozott", diff --git a/homeassistant/components/deconz/translations/hu.json b/homeassistant/components/deconz/translations/hu.json index a78a6ef1961..c71689a555d 100644 --- a/homeassistant/components/deconz/translations/hu.json +++ b/homeassistant/components/deconz/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "A bridge m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "no_bridges": "Nem tal\u00e1lhat\u00f3 deCONZ \u00e1tj\u00e1r\u00f3", "no_hardware_available": "Nincs deCONZ-hoz csatlakoztatott r\u00e1di\u00f3hardver", "not_deconz_bridge": "Nem egy deCONZ \u00e1tj\u00e1r\u00f3", @@ -71,7 +71,7 @@ "remote_button_quintuple_press": "\"{subtype}\" gombra \u00f6tsz\u00f6r kattintottak", "remote_button_rotated": "A gomb elforgatva: \"{subtype}\"", "remote_button_rotated_fast": "A gomb gyorsan elfordult: \"{subtype}\"", - "remote_button_rotation_stopped": "A(z) \"{subtype}\" gomb forg\u00e1sa le\u00e1llt", + "remote_button_rotation_stopped": "\"{subtype}\" gomb forg\u00e1sa le\u00e1llt", "remote_button_short_press": "\"{subtype}\" gomb lenyomva", "remote_button_short_release": "\"{subtype}\" gomb elengedve", "remote_button_triple_press": "\"{subtype}\" gombra h\u00e1romszor kattintottak", diff --git a/homeassistant/components/denonavr/translations/hu.json b/homeassistant/components/denonavr/translations/hu.json index c22d392dc8a..874f190ff01 100644 --- a/homeassistant/components/denonavr/translations/hu.json +++ b/homeassistant/components/denonavr/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "cannot_connect": "Nem siker\u00fclt csatlakozni, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra. A h\u00e1l\u00f3zati \u00e9s Ethernet k\u00e1belek kih\u00faz\u00e1sa \u00e9s \u00fajracsatlakoztat\u00e1sa seg\u00edthet", "not_denonavr_manufacturer": "Nem egy Denon AVR h\u00e1l\u00f3zati vev\u0151, felfedezett gy\u00e1rt\u00f3 nem egyezik", "not_denonavr_missing": "Nem Denon AVR h\u00e1l\u00f3zati vev\u0151, a felfedez\u00e9si inform\u00e1ci\u00f3k nem teljesek" diff --git a/homeassistant/components/dlna_dmr/translations/ca.json b/homeassistant/components/dlna_dmr/translations/ca.json index 686d20fbaab..feeb4912fff 100644 --- a/homeassistant/components/dlna_dmr/translations/ca.json +++ b/homeassistant/components/dlna_dmr/translations/ca.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", + "alternative_integration": "El dispositiu t\u00e9 millor compatibilitat amb una altra integraci\u00f3", + "cannot_connect": "Ha fallat la connexi\u00f3", "could_not_connect": "No s'ha pogut connectar amb el dispositiu DLNA", "discovery_error": "No s'ha pogut descobrir cap dispositiu DLNA coincident", "incomplete_config": "Falta una variable obligat\u00f2ria a la configuraci\u00f3", @@ -9,6 +11,7 @@ "not_dmr": "El dispositiu no \u00e9s un renderitzador de mitjans digitals" }, "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", "could_not_connect": "No s'ha pogut connectar amb el dispositiu DLNA", "not_dmr": "El dispositiu no \u00e9s un renderitzador de mitjans digitals" }, @@ -20,12 +23,20 @@ "import_turn_on": { "description": "Engega el dispositiu i fes clic a Envia per continuar la migraci\u00f3" }, - "user": { + "manual": { "data": { "url": "URL" }, - "description": "URL al fitxer XML de descripci\u00f3 de dispositiu", - "title": "Renderitzador de mitjans digitals DLNA" + "description": "URL al fitxer XML de descripci\u00f3 del dispositiu", + "title": "Connexi\u00f3 manual de dispositiu DLNA DMR" + }, + "user": { + "data": { + "host": "Amfitri\u00f3", + "url": "URL" + }, + "description": "Tria un dispositiu a configurar o deixeu-ho en blanc per introduir un URL", + "title": "Dispositius descoberts DLNA DMR" } } }, diff --git a/homeassistant/components/dlna_dmr/translations/de.json b/homeassistant/components/dlna_dmr/translations/de.json index 58848ea69e8..74083d15b56 100644 --- a/homeassistant/components/dlna_dmr/translations/de.json +++ b/homeassistant/components/dlna_dmr/translations/de.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "alternative_integration": "Das Ger\u00e4t wird besser durch eine andere Integration unterst\u00fctzt", + "cannot_connect": "Verbindung fehlgeschlagen", "could_not_connect": "Verbindung zum DLNA-Ger\u00e4t fehlgeschlagen", "discovery_error": "Ein passendes DLNA-Ger\u00e4t konnte nicht gefunden werden", "incomplete_config": "In der Konfiguration fehlt eine erforderliche Variable", @@ -9,6 +11,7 @@ "not_dmr": "Ger\u00e4t ist kein Digital Media Renderer" }, "error": { + "cannot_connect": "Verbindung fehlgeschlagen", "could_not_connect": "Verbindung zum DLNA-Ger\u00e4t fehlgeschlagen", "not_dmr": "Ger\u00e4t ist kein Digital Media Renderer" }, @@ -20,12 +23,20 @@ "import_turn_on": { "description": "Bitte schalte das Ger\u00e4t ein und klicke auf Senden, um die Migration fortzusetzen" }, - "user": { + "manual": { "data": { "url": "URL" }, "description": "URL zu einer XML-Datei mit Ger\u00e4tebeschreibung", - "title": "DLNA Digital Media Renderer" + "title": "Manuelle DLNA DMR-Ger\u00e4teverbindung" + }, + "user": { + "data": { + "host": "Host", + "url": "URL" + }, + "description": "W\u00e4hle ein zu konfigurierendes Ger\u00e4t oder lasse es leer, um eine URL einzugeben.", + "title": "Erkannte DLNA-DMR-Ger\u00e4te" } } }, diff --git a/homeassistant/components/dlna_dmr/translations/et.json b/homeassistant/components/dlna_dmr/translations/et.json index 033ff3e42c5..b9757648e1d 100644 --- a/homeassistant/components/dlna_dmr/translations/et.json +++ b/homeassistant/components/dlna_dmr/translations/et.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "alternative_integration": "Seadet toetab paremini teine sidumine", + "cannot_connect": "\u00dchendamine nurjus", "could_not_connect": "DLNA seadmega \u00fchenduse loomine nurjus", "discovery_error": "Sobiva DLNA -seadme leidmine nurjus", "incomplete_config": "Seadetes puudub n\u00f5utav muutuja", @@ -9,6 +11,7 @@ "not_dmr": "Seade ei ole digitaalse meedia renderdaja" }, "error": { + "cannot_connect": "\u00dchendamine nurjus", "could_not_connect": "DLNA seadmega \u00fchenduse loomine nurjus", "not_dmr": "Seade ei ole digitaalse meedia renderdaja" }, @@ -20,12 +23,20 @@ "import_turn_on": { "description": "L\u00fclita seade sisse ja kl\u00f5psa migreerimise j\u00e4tkamiseks nuppu Edasta" }, - "user": { + "manual": { "data": { "url": "URL" }, - "description": "URL aadress seadme kirjelduse XML-failile", - "title": "DLNA digitaalse meediumi renderdaja" + "description": "Seadme kirjelduse XML-faili URL", + "title": "DLNA DMR seadme k\u00e4sitsi \u00fchendamine" + }, + "user": { + "data": { + "host": "Host", + "url": "URL" + }, + "description": "Vali h\u00e4\u00e4lestatav seade v\u00f5i j\u00e4ta URL -i sisestamiseks t\u00fchjaks", + "title": "Avastatud DLNA DMR-seadmed" } } }, diff --git a/homeassistant/components/dlna_dmr/translations/hu.json b/homeassistant/components/dlna_dmr/translations/hu.json index af27382f511..603e14b9e30 100644 --- a/homeassistant/components/dlna_dmr/translations/hu.json +++ b/homeassistant/components/dlna_dmr/translations/hu.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r be van konfigur\u00e1lva", + "alternative_integration": "Az eszk\u00f6zt jobban t\u00e1mogatja egy m\u00e1sik integr\u00e1ci\u00f3", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", "could_not_connect": "Nem siker\u00fclt csatlakozni a DLNA-eszk\u00f6zh\u00f6z", "discovery_error": "Nem siker\u00fclt megfelel\u0151 DLNA-eszk\u00f6zt tal\u00e1lni", "incomplete_config": "A konfigur\u00e1ci\u00f3b\u00f3l hi\u00e1nyzik egy sz\u00fcks\u00e9ges \u00e9rt\u00e9k", @@ -9,6 +11,7 @@ "not_dmr": "Az eszk\u00f6z nem digit\u00e1lis m\u00e9dia renderel\u0151" }, "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", "could_not_connect": "Nem siker\u00fclt csatlakozni a DLNA-eszk\u00f6zh\u00f6z", "not_dmr": "Az eszk\u00f6z nem digit\u00e1lis m\u00e9dia renderel\u0151" }, @@ -20,8 +23,16 @@ "import_turn_on": { "description": "Kapcsolja be az eszk\u00f6zt, \u00e9s kattintson a K\u00fcld\u00e9s gombra a migr\u00e1ci\u00f3 folytat\u00e1s\u00e1hoz" }, + "manual": { + "data": { + "url": "URL" + }, + "description": "URL egy eszk\u00f6zle\u00edr\u00f3 XML f\u00e1jlhoz", + "title": "DLNA DMR eszk\u00f6z manu\u00e1lis csatlakoztat\u00e1sa" + }, "user": { "data": { + "host": "C\u00edm", "url": "URL" }, "description": "Az eszk\u00f6z le\u00edr\u00e1s\u00e1nak XML-f\u00e1jl URL-c\u00edme", diff --git a/homeassistant/components/dlna_dmr/translations/ru.json b/homeassistant/components/dlna_dmr/translations/ru.json index 91d0d0dbff0..327631b7af1 100644 --- a/homeassistant/components/dlna_dmr/translations/ru.json +++ b/homeassistant/components/dlna_dmr/translations/ru.json @@ -2,6 +2,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.", + "alternative_integration": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043b\u0443\u0447\u0448\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0434\u0440\u0443\u0433\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0435\u0439.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "could_not_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443.", "discovery_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c \u043f\u043e\u0434\u0445\u043e\u0434\u044f\u0449\u0435\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e DLNA.", "incomplete_config": "\u0412 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u0430\u044f.", @@ -9,6 +11,7 @@ "not_dmr": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043c\u0435\u0434\u0438\u0430\u0440\u0435\u043d\u0434\u0435\u0440\u0435\u0440\u043e\u043c (DMR)." }, "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "could_not_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443.", "not_dmr": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043c\u0435\u0434\u0438\u0430\u0440\u0435\u043d\u0434\u0435\u0440\u0435\u0440\u043e\u043c (DMR)." }, @@ -20,12 +23,20 @@ "import_turn_on": { "description": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 '\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c' \u0434\u043b\u044f \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0435\u043d\u0438\u044f \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438" }, - "user": { + "manual": { "data": { "url": "URL-\u0430\u0434\u0440\u0435\u0441" }, "description": "URL-\u0430\u0434\u0440\u0435\u0441 XML-\u0444\u0430\u0439\u043b\u0430 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", - "title": "\u041c\u0435\u0434\u0438\u0430\u0440\u0435\u043d\u0434\u0435\u0440\u0435\u0440 DLNA" + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 DLNA DMR" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "url": "URL-\u0430\u0434\u0440\u0435\u0441" + }, + "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 \u0438\u043b\u0438 \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0447\u0442\u043e\u0431\u044b \u0432\u0432\u0435\u0441\u0442\u0438 URL.", + "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 DLNA DMR" } } }, diff --git a/homeassistant/components/dlna_dmr/translations/zh-Hant.json b/homeassistant/components/dlna_dmr/translations/zh-Hant.json index 04c48c833e8..a4f12030c09 100644 --- a/homeassistant/components/dlna_dmr/translations/zh-Hant.json +++ b/homeassistant/components/dlna_dmr/translations/zh-Hant.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "alternative_integration": "\u4f7f\u7528\u5176\u4ed6\u6574\u5408\u4ee5\u53d6\u5f97\u66f4\u4f73\u7684\u88dd\u7f6e\u652f\u63f4", + "cannot_connect": "\u9023\u7dda\u5931\u6557", "could_not_connect": "DLNA \u88dd\u7f6e\u9023\u7dda\u5931\u6557\u3002", "discovery_error": "DLNA \u88dd\u7f6e\u63a2\u7d22\u5931\u6557", "incomplete_config": "\u6240\u7f3a\u5c11\u7684\u8a2d\u5b9a\u70ba\u5fc5\u9808\u8b8a\u6578", @@ -9,6 +11,7 @@ "not_dmr": "\u88dd\u7f6e\u4e26\u975e Digital Media Renderer" }, "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", "could_not_connect": "DLNA \u88dd\u7f6e\u9023\u7dda\u5931\u6557\u3002", "not_dmr": "\u88dd\u7f6e\u4e26\u975e Digital Media Renderer" }, @@ -20,12 +23,20 @@ "import_turn_on": { "description": "\u8acb\u958b\u555f\u88dd\u7f6e\u4e26\u9ede\u9078\u50b3\u9001\u4ee5\u7e7c\u7e8c\u9077\u79fb" }, - "user": { + "manual": { "data": { "url": "\u7db2\u5740" }, "description": "\u88dd\u7f6e\u8aaa\u660e XML \u6a94\u6848\u4e4b URL", - "title": "DLNA Digital Media Renderer" + "title": "\u624b\u52d5 DLNA DMR \u88dd\u7f6e\u9023\u7dda" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "url": "\u7db2\u5740" + }, + "description": "\u9078\u64c7\u88dd\u7f6e\u9032\u884c\u8a2d\u5b9a\u6216\u4fdd\u7559\u7a7a\u767d\u4ee5\u8f38\u5165 URL", + "title": "\u5df2\u63a2\u7d22\u5230\u7684 DLNA DMR \u88dd\u7f6e" } } }, diff --git a/homeassistant/components/doorbird/translations/bg.json b/homeassistant/components/doorbird/translations/bg.json index d152ddfcf20..628eaf62894 100644 --- a/homeassistant/components/doorbird/translations/bg.json +++ b/homeassistant/components/doorbird/translations/bg.json @@ -4,6 +4,7 @@ "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" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/efergy/translations/hu.json b/homeassistant/components/efergy/translations/hu.json index ad41d4025bb..032ef05d527 100644 --- a/homeassistant/components/efergy/translations/hu.json +++ b/homeassistant/components/efergy/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", diff --git a/homeassistant/components/esphome/translations/hu.json b/homeassistant/components/esphome/translations/hu.json index a35248864ff..17af0e57d26 100644 --- a/homeassistant/components/esphome/translations/hu.json +++ b/homeassistant/components/esphome/translations/hu.json @@ -2,8 +2,8 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "connection_error": "Nem lehet csatlakozni az ESP-hez. K\u00e9rem, gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy a YAML konfigur\u00e1ci\u00f3 tartalmaz egy \"api:\" sort.", @@ -17,7 +17,7 @@ "data": { "password": "Jelsz\u00f3" }, - "description": "K\u00e9rem, adja meg {name} konfigur\u00e1ci\u00f3j\u00e1ban be\u00e1ll\u00edtott API jelsz\u00f3t." + "description": "K\u00e9rem, adja meg a konfigur\u00e1ci\u00f3ban l\u00e9v\u0151, {name} jelszav\u00e1t." }, "discovery_confirm": { "description": "Szeretn\u00e9 hozz\u00e1adni `{name}` ESPHome csom\u00f3pontot Home Assistanthoz?", @@ -27,13 +27,13 @@ "data": { "noise_psk": "Titkos\u00edt\u00e1si kulcs" }, - "description": "K\u00e9rem, adja meg {name} konfigur\u00e1ci\u00f3j\u00e1ban be\u00e1ll\u00edtott titkos\u00edt\u00e1si kulcsot." + "description": "K\u00e9rj\u00fck, adja meg a konfigur\u00e1ci\u00f3ban l\u00e9v\u0151, {name} titkos\u00edt\u00e1si kulcs\u00e1t." }, "reauth_confirm": { "data": { "noise_psk": "Titkos\u00edt\u00e1si kulcs" }, - "description": "{name} ESPHome eszk\u00f6z aktiv\u00e1lta az adat\u00e1tviteli titkos\u00edt\u00e1st vagy megv\u00e1ltoztatta a titkos\u00edt\u00e1si kulcsot. K\u00e9rj\u00fck, adja meg az aktu\u00e1lis kulcsot." + "description": "{name} ESPHome v\u00e9gpont aktiv\u00e1lta az adat\u00e1tviteli titkos\u00edt\u00e1st vagy megv\u00e1ltoztatta a titkos\u00edt\u00e1si kulcsot. K\u00e9rj\u00fck, adja meg az aktu\u00e1lis kulcsot." }, "user": { "data": { diff --git a/homeassistant/components/fireservicerota/translations/hu.json b/homeassistant/components/fireservicerota/translations/hu.json index 54461091c93..3bda2225400 100644 --- a/homeassistant/components/fireservicerota/translations/hu.json +++ b/homeassistant/components/fireservicerota/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "create_entry": { "default": "Sikeres hiteles\u00edt\u00e9s" diff --git a/homeassistant/components/flume/translations/bg.json b/homeassistant/components/flume/translations/bg.json new file mode 100644 index 00000000000..6eca91e8ed2 --- /dev/null +++ b/homeassistant/components/flume/translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "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": { + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flume/translations/hu.json b/homeassistant/components/flume/translations/hu.json index e1780be5654..d1cab31095d 100644 --- a/homeassistant/components/flume/translations/hu.json +++ b/homeassistant/components/flume/translations/hu.json @@ -14,7 +14,7 @@ "data": { "password": "Jelsz\u00f3" }, - "description": "A(z) {username} jelszava m\u00e1r nem \u00e9rv\u00e9nyes.", + "description": "{username} jelszava m\u00e1r nem \u00e9rv\u00e9nyes.", "title": "Hiteles\u00edtse \u00fajra Flume-fi\u00f3kj\u00e1t" }, "user": { diff --git a/homeassistant/components/flux_led/translations/hu.json b/homeassistant/components/flux_led/translations/hu.json index 4afa3d99f74..1208f87fe70 100644 --- a/homeassistant/components/flux_led/translations/hu.json +++ b/homeassistant/components/flux_led/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" }, "error": { @@ -11,7 +11,7 @@ "flow_title": "{model} {id} ({ipaddr})", "step": { "discovery_confirm": { - "description": "Be szeretn\u00e9 \u00e1ll\u00edtani: {model} {id} ({ipaddr}) ?" + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {model} {id} ({ipaddr}) ?" }, "user": { "data": { diff --git a/homeassistant/components/fritzbox/translations/hu.json b/homeassistant/components/fritzbox/translations/hu.json index c5d5e495131..c1cf8154aea 100644 --- a/homeassistant/components/fritzbox/translations/hu.json +++ b/homeassistant/components/fritzbox/translations/hu.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", "not_supported": "Csatlakoztatva az AVM FRITZ! Boxhoz, de nem tudja vez\u00e9relni az intelligens otthoni eszk\u00f6z\u00f6ket.", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" @@ -24,7 +24,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "Friss\u00edtse a(z) {name} bejelentkez\u00e9si adatait." + "description": "Friss\u00edtse {name} bejelentkez\u00e9si adatait." }, "user": { "data": { diff --git a/homeassistant/components/guardian/translations/hu.json b/homeassistant/components/guardian/translations/hu.json index ecd1b7de01b..04b62bb660e 100644 --- a/homeassistant/components/guardian/translations/hu.json +++ b/homeassistant/components/guardian/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { diff --git a/homeassistant/components/hive/translations/hu.json b/homeassistant/components/hive/translations/hu.json index 469b99debe1..9b0d3c21590 100644 --- a/homeassistant/components/hive/translations/hu.json +++ b/homeassistant/components/hive/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", "unknown_entry": "Nem tal\u00e1lhat\u00f3 megl\u00e9v\u0151 bejegyz\u00e9s." }, "error": { diff --git a/homeassistant/components/home_plus_control/translations/hu.json b/homeassistant/components/home_plus_control/translations/hu.json index 2dc22c7a729..09625a222f2 100644 --- a/homeassistant/components/home_plus_control/translations/hu.json +++ b/homeassistant/components/home_plus_control/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.", diff --git a/homeassistant/components/homekit_controller/translations/hu.json b/homeassistant/components/homekit_controller/translations/hu.json index 7703925ae67..6fad9050a20 100644 --- a/homeassistant/components/homekit_controller/translations/hu.json +++ b/homeassistant/components/homekit_controller/translations/hu.json @@ -3,7 +3,7 @@ "abort": { "accessory_not_found_error": "Nem adhat\u00f3 hozz\u00e1 p\u00e1ros\u00edt\u00e1s, mert az eszk\u00f6z m\u00e1r nem tal\u00e1lhat\u00f3.", "already_configured": "A tartoz\u00e9k m\u00e1r konfigur\u00e1lva van ezzel a vez\u00e9rl\u0151vel.", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "already_paired": "Ez a tartoz\u00e9k m\u00e1r p\u00e1ros\u00edtva van egy m\u00e1sik eszk\u00f6zzel. \u00c1ll\u00edtsa alaphelyzetbe a tartoz\u00e9kot, majd pr\u00f3b\u00e1lkozzon \u00fajra.", "ignored_model": "A HomeKit t\u00e1mogat\u00e1sa e modelln\u00e9l blokkolva van, mivel a szolg\u00e1ltat\u00e1shoz teljes nat\u00edv integr\u00e1ci\u00f3 \u00e9rhet\u0151 el.", "invalid_config_entry": "Ez az eszk\u00f6z k\u00e9szen \u00e1ll a p\u00e1ros\u00edt\u00e1sra, de m\u00e1r van egy \u00fctk\u00f6z\u0151 konfigur\u00e1ci\u00f3s bejegyz\u00e9s Home Assistantban, amelyet el\u0151sz\u00f6r el kell t\u00e1vol\u00edtani.", diff --git a/homeassistant/components/huawei_lte/translations/hu.json b/homeassistant/components/huawei_lte/translations/hu.json index 91f70a17e46..36d08438fca 100644 --- a/homeassistant/components/huawei_lte/translations/hu.json +++ b/homeassistant/components/huawei_lte/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "not_huawei_lte": "Nem Huawei LTE eszk\u00f6z" }, "error": { diff --git a/homeassistant/components/hue/translations/hu.json b/homeassistant/components/hue/translations/hu.json index a114fc2c890..917ec094ced 100644 --- a/homeassistant/components/hue/translations/hu.json +++ b/homeassistant/components/hue/translations/hu.json @@ -3,7 +3,7 @@ "abort": { "all_configured": "M\u00e1r minden Philips Hue bridge konfigur\u00e1lt", "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "discover_timeout": "Nem tal\u00e1lhat\u00f3 a Hue bridge", "no_bridges": "Nem tal\u00e1lhat\u00f3 Philips Hue bridget", diff --git a/homeassistant/components/humidifier/translations/hu.json b/homeassistant/components/humidifier/translations/hu.json index 7dd723df738..bbc5e8a0d77 100644 --- a/homeassistant/components/humidifier/translations/hu.json +++ b/homeassistant/components/humidifier/translations/hu.json @@ -8,7 +8,7 @@ "turn_on": "{entity_name} bekapcsol\u00e1sa" }, "condition_type": { - "is_mode": "A(z) {entity_name} egy adott m\u00f3dra van \u00e1ll\u00edtva", + "is_mode": "{entity_name} egy adott m\u00f3dra van \u00e1ll\u00edtva", "is_off": "{entity_name} ki van kapcsolva", "is_on": "{entity_name} be van kapcsolva" }, diff --git a/homeassistant/components/hunterdouglas_powerview/translations/bg.json b/homeassistant/components/hunterdouglas_powerview/translations/bg.json index 17ef87d7d8b..4eca0883892 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/bg.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/bg.json @@ -9,6 +9,9 @@ }, "flow_title": "{name} ({host})", "step": { + "link": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name} ({host})?" + }, "user": { "data": { "host": "IP \u0430\u0434\u0440\u0435\u0441" diff --git a/homeassistant/components/hyperion/translations/hu.json b/homeassistant/components/hyperion/translations/hu.json index 3fa440c41d5..750f2380624 100644 --- a/homeassistant/components/hyperion/translations/hu.json +++ b/homeassistant/components/hyperion/translations/hu.json @@ -2,13 +2,13 @@ "config": { "abort": { "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "auth_new_token_not_granted_error": "Az \u00fajonnan l\u00e9trehozott tokent nem hagyt\u00e1k j\u00f3v\u00e1 a Hyperion felhaszn\u00e1l\u00f3i fel\u00fclet\u00e9n", "auth_new_token_not_work_error": "Nem siker\u00fclt hiteles\u00edteni az \u00fajonnan l\u00e9trehozott token haszn\u00e1lat\u00e1val", "auth_required_error": "Nem siker\u00fclt meghat\u00e1rozni, hogy sz\u00fcks\u00e9ges-e enged\u00e9ly", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "no_id": "A Hyperion Ambilight p\u00e9ld\u00e1ny nem szolg\u00e1ltatja az azonos\u00edt\u00f3j\u00e1t", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", diff --git a/homeassistant/components/icloud/translations/hu.json b/homeassistant/components/icloud/translations/hu.json index e858eedb757..cff637ae03f 100644 --- a/homeassistant/components/icloud/translations/hu.json +++ b/homeassistant/components/icloud/translations/hu.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", "no_device": "Egyik k\u00e9sz\u00fcl\u00e9ke sem aktiv\u00e1lta az \"iPhone keres\u00e9se\" funkci\u00f3t", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", @@ -15,7 +15,7 @@ "data": { "password": "Jelsz\u00f3" }, - "description": "A(z) {username} kor\u00e1bban megadott jelszava m\u00e1r nem m\u0171k\u00f6dik. Az integr\u00e1ci\u00f3 haszn\u00e1lat\u00e1hoz friss\u00edtse jelszav\u00e1t.", + "description": "{username} kor\u00e1bban megadott jelszava m\u00e1r nem m\u0171k\u00f6dik. Az integr\u00e1ci\u00f3 haszn\u00e1lat\u00e1hoz friss\u00edtse jelszav\u00e1t.", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" }, "trusted_device": { diff --git a/homeassistant/components/konnected/translations/hu.json b/homeassistant/components/konnected/translations/hu.json index f5431480ebb..65a1c88b8d5 100644 --- a/homeassistant/components/konnected/translations/hu.json +++ b/homeassistant/components/konnected/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "not_konn_panel": "Nem felismert Konnected.io eszk\u00f6z", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, diff --git a/homeassistant/components/lookin/translations/hu.json b/homeassistant/components/lookin/translations/hu.json index 4d9d53ee33d..ab18b579bd4 100644 --- a/homeassistant/components/lookin/translations/hu.json +++ b/homeassistant/components/lookin/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" }, diff --git a/homeassistant/components/lutron_caseta/translations/hu.json b/homeassistant/components/lutron_caseta/translations/hu.json index a891a1b4b72..1cbca645b6c 100644 --- a/homeassistant/components/lutron_caseta/translations/hu.json +++ b/homeassistant/components/lutron_caseta/translations/hu.json @@ -15,7 +15,7 @@ "title": "Nem siker\u00fclt import\u00e1lni a Cas\u00e9ta h\u00edd konfigur\u00e1ci\u00f3j\u00e1t." }, "link": { - "description": "A(z) {name} ({host}) p\u00e1ros\u00edt\u00e1s\u00e1hoz az \u0171rlap elk\u00fcld\u00e9se ut\u00e1n nyomja meg a h\u00edd h\u00e1tulj\u00e1n tal\u00e1lhat\u00f3 fekete gombot.", + "description": "{name} ({host}) p\u00e1ros\u00edt\u00e1s\u00e1hoz az \u0171rlap elk\u00fcld\u00e9se ut\u00e1n nyomja meg a bridge h\u00e1tulj\u00e1n tal\u00e1lhat\u00f3 fekete gombot.", "title": "P\u00e1ros\u00edt\u00e1s a h\u00edddal" }, "user": { diff --git a/homeassistant/components/mazda/translations/hu.json b/homeassistant/components/mazda/translations/hu.json index c3f00040ea3..ec62b456021 100644 --- a/homeassistant/components/mazda/translations/hu.json +++ b/homeassistant/components/mazda/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "account_locked": "Fi\u00f3k z\u00e1rolva. K\u00e9rem, pr\u00f3b\u00e1lja \u00fajra k\u00e9s\u0151bb.", diff --git a/homeassistant/components/modem_callerid/translations/hu.json b/homeassistant/components/modem_callerid/translations/hu.json index cb8433e0028..3a75df30a7d 100644 --- a/homeassistant/components/modem_callerid/translations/hu.json +++ b/homeassistant/components/modem_callerid/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "no_devices_found": "Nem tal\u00e1lhat\u00f3 egy\u00e9b eszk\u00f6z" }, "error": { diff --git a/homeassistant/components/motion_blinds/translations/hu.json b/homeassistant/components/motion_blinds/translations/hu.json index e65c43fcf8a..9d0f41db143 100644 --- a/homeassistant/components/motion_blinds/translations/hu.json +++ b/homeassistant/components/motion_blinds/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "connection_error": "Sikertelen csatlakoz\u00e1s" }, "error": { diff --git a/homeassistant/components/myq/translations/hu.json b/homeassistant/components/myq/translations/hu.json index f50099f023b..e9d17985676 100644 --- a/homeassistant/components/myq/translations/hu.json +++ b/homeassistant/components/myq/translations/hu.json @@ -14,7 +14,7 @@ "data": { "password": "Jelsz\u00f3" }, - "description": "A(z) {username} jelszava m\u00e1r nem \u00e9rv\u00e9nyes.", + "description": "{username} jelszava m\u00e1r nem \u00e9rv\u00e9nyes.", "title": "Hiteles\u00edtse \u00fajra MyQ-fi\u00f3kj\u00e1t" }, "user": { diff --git a/homeassistant/components/neato/translations/hu.json b/homeassistant/components/neato/translations/hu.json index 20bc76ca6c0..281c5cd61a9 100644 --- a/homeassistant/components/neato/translations/hu.json +++ b/homeassistant/components/neato/translations/hu.json @@ -5,7 +5,7 @@ "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "create_entry": { "default": "Sikeres hiteles\u00edt\u00e9s" diff --git a/homeassistant/components/nest/translations/hu.json b/homeassistant/components/nest/translations/hu.json index 58f8ea30caf..95f284b9a81 100644 --- a/homeassistant/components/nest/translations/hu.json +++ b/homeassistant/components/nest/translations/hu.json @@ -4,7 +4,7 @@ "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.", "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3t [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lsz.", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", "unknown_authorize_url_generation": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n." }, diff --git a/homeassistant/components/netatmo/translations/hu.json b/homeassistant/components/netatmo/translations/hu.json index 5802e1a2679..ade7f8ce6f8 100644 --- a/homeassistant/components/netatmo/translations/hu.json +++ b/homeassistant/components/netatmo/translations/hu.json @@ -30,7 +30,7 @@ "outdoor": "{entity_name} k\u00fclt\u00e9ri esem\u00e9nyt \u00e9szlelt", "person": "{entity_name} szem\u00e9lyt \u00e9szlelt", "person_away": "{entity_name} \u00e9szlelte, hogy egy szem\u00e9ly t\u00e1vozott", - "set_point": "A(z) {entity_name} c\u00e9lh\u0151m\u00e9rs\u00e9klet manu\u00e1lisan lett be\u00e1ll\u00edtva", + "set_point": "{entity_name} c\u00e9lh\u0151m\u00e9rs\u00e9klete manu\u00e1lisan lett be\u00e1ll\u00edtva", "therm_mode": "{entity_name} \u00e1tv\u00e1ltott erre: \"{subtype}\"", "turned_off": "{entity_name} ki lett kapcsolva", "turned_on": "{entity_name} be lett kapcsolva", diff --git a/homeassistant/components/netgear/translations/bg.json b/homeassistant/components/netgear/translations/bg.json index 5ab9a57dd27..dd5e4fa2f38 100644 --- a/homeassistant/components/netgear/translations/bg.json +++ b/homeassistant/components/netgear/translations/bg.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" }, + "error": { + "config": "\u0413\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0438\u043b\u0438 \u0432\u043b\u0438\u0437\u0430\u043d\u0435: \u043c\u043e\u043b\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0432\u0430\u0448\u0430\u0442\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f" + }, "step": { "user": { "data": { @@ -11,6 +14,7 @@ "port": "\u041f\u043e\u0440\u0442 (\u043f\u043e \u0438\u0437\u0431\u043e\u0440)", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 (\u043f\u043e \u0438\u0437\u0431\u043e\u0440)" }, + "description": "\u0425\u043e\u0441\u0442 \u043f\u043e \u043f\u043e\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043d\u0435: {host}\n\u041f\u043e\u0440\u0442 \u043f\u043e \u043f\u043e\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043d\u0435: {port}\n\u041f\u043e\u0442\u0440. \u0438\u043c\u0435 \u043f\u043e \u043f\u043e\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043d\u0435: {username}", "title": "Netgear" } } diff --git a/homeassistant/components/notion/translations/hu.json b/homeassistant/components/notion/translations/hu.json index 1af2d5907fc..ef10a451a84 100644 --- a/homeassistant/components/notion/translations/hu.json +++ b/homeassistant/components/notion/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", diff --git a/homeassistant/components/nut/translations/bg.json b/homeassistant/components/nut/translations/bg.json index 4983c9a14b2..1413a7f101c 100644 --- a/homeassistant/components/nut/translations/bg.json +++ b/homeassistant/components/nut/translations/bg.json @@ -1,6 +1,16 @@ { "config": { "step": { + "resources": { + "data": { + "resources": "\u0420\u0435\u0441\u0443\u0440\u0441\u0438" + } + }, + "ups": { + "data": { + "resources": "\u0420\u0435\u0441\u0443\u0440\u0441\u0438" + } + }, "user": { "data": { "port": "\u041f\u043e\u0440\u0442" diff --git a/homeassistant/components/octoprint/translations/de.json b/homeassistant/components/octoprint/translations/de.json new file mode 100644 index 00000000000..9aa242be122 --- /dev/null +++ b/homeassistant/components/octoprint/translations/de.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "auth_failed": "Fehler beim Abrufen des Anwendungs-API-Schl\u00fcssels", + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "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\"." + }, + "step": { + "user": { + "data": { + "host": "Host", + "path": "Anwendungspfad", + "port": "Port-Nummer", + "ssl": "SSL verwenden", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/octoprint/translations/hu.json b/homeassistant/components/octoprint/translations/hu.json new file mode 100644 index 00000000000..d9de58e4012 --- /dev/null +++ b/homeassistant/components/octoprint/translations/hu.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "auth_failed": "Nem siker\u00fclt lek\u00e9rni az alkalmaz\u00e1s api kulcs\u00e1t", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "flow_title": "OctoPrint nyomtat\u00f3: {host}", + "progress": { + "get_api_key": "Nyissa meg az OctoPrint kezel\u0151 fel\u00fclet\u00e9t, \u00e9s kattintson az 'Allow' gombra a 'Home Assistant' hozz\u00e1f\u00e9r\u00e9si k\u00e9relemn\u00e9l." + }, + "step": { + "user": { + "data": { + "host": "C\u00edm", + "path": "Alkalmaz\u00e1s \u00fatvonala", + "port": "Port", + "ssl": "SSL haszn\u00e1lata", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/octoprint/translations/zh-Hant.json b/homeassistant/components/octoprint/translations/zh-Hant.json new file mode 100644 index 00000000000..870ff087588 --- /dev/null +++ b/homeassistant/components/octoprint/translations/zh-Hant.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "auth_failed": "\u63a5\u6536\u61c9\u7528\u7a0b\u5f0f API \u5bc6\u9470\u5931\u6557", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "OctoPrint \u5370\u8868\u6a5f\uff1a{host}", + "progress": { + "get_api_key": "\u958b\u555f OctoPrint UI \u4e26\u65bc 'Home Assistant' \u5b58\u53d6\u8acb\u6c42\u4e0a\u9ede\u9078 '\u5141\u8a31'\u3002" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "path": "\u61c9\u7528\u7a0b\u5f0f\u8def\u5f91", + "port": "\u901a\u8a0a\u57e0\u865f", + "ssl": "\u4f7f\u7528 SSL", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/hu.json b/homeassistant/components/onvif/translations/hu.json index f0df008f145..6b777b76ab0 100644 --- a/homeassistant/components/onvif/translations/hu.json +++ b/homeassistant/components/onvif/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "no_h264": "Nem voltak el\u00e9rhet\u0151 H264 streamek. Ellen\u0151rizze a profil konfigur\u00e1ci\u00f3j\u00e1t a k\u00e9sz\u00fcl\u00e9ken.", "no_mac": "Nem siker\u00fclt konfigur\u00e1lni az egyedi azonos\u00edt\u00f3t az ONVIF eszk\u00f6zh\u00f6z.", "onvif_error": "Hiba t\u00f6rt\u00e9nt az ONVIF eszk\u00f6z be\u00e1ll\u00edt\u00e1sakor. Tov\u00e1bbi inform\u00e1ci\u00f3k\u00e9rt ellen\u0151rizze a napl\u00f3kat." @@ -32,7 +32,7 @@ "data": { "include": "Hozzon l\u00e9tre kamera entit\u00e1st" }, - "description": "L\u00e9trehozza a(z) {profile} f\u00e9nyk\u00e9pez\u0151g\u00e9p entit\u00e1s\u00e1t {resolution} felbont\u00e1ssal?", + "description": "L\u00e9trehozza {profile} kamera entit\u00e1s\u00e1t {resolution} felbont\u00e1ssal?", "title": "Profilok konfigur\u00e1l\u00e1sa" }, "device": { diff --git a/homeassistant/components/ozw/translations/hu.json b/homeassistant/components/ozw/translations/hu.json index d38a8d55d7a..4201f4f2f06 100644 --- a/homeassistant/components/ozw/translations/hu.json +++ b/homeassistant/components/ozw/translations/hu.json @@ -5,7 +5,7 @@ "addon_install_failed": "Nem siker\u00fclt telep\u00edteni az OpenZWave b\u0151v\u00edtm\u00e9nyt.", "addon_set_config_failed": "Nem siker\u00fclt be\u00e1ll\u00edtani az OpenZWave konfigur\u00e1ci\u00f3t.", "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "mqtt_required": "Az MQTT integr\u00e1ci\u00f3 nincs be\u00e1ll\u00edtva", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, diff --git a/homeassistant/components/panasonic_viera/translations/bg.json b/homeassistant/components/panasonic_viera/translations/bg.json index 0ec64883f3e..9dc5d863d85 100644 --- a/homeassistant/components/panasonic_viera/translations/bg.json +++ b/homeassistant/components/panasonic_viera/translations/bg.json @@ -11,7 +11,8 @@ "pairing": { "data": { "pin": "\u041f\u0418\u041d \u043a\u043e\u0434" - } + }, + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u041f\u0418\u041d \u043a\u043e\u0434\u0430, \u043f\u043e\u043a\u0430\u0437\u0430\u043d \u043d\u0430 \u0432\u0430\u0448\u0438\u044f \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440" }, "user": { "data": { diff --git a/homeassistant/components/plex/translations/hu.json b/homeassistant/components/plex/translations/hu.json index cde11b9c7cc..d3aabedc58d 100644 --- a/homeassistant/components/plex/translations/hu.json +++ b/homeassistant/components/plex/translations/hu.json @@ -3,8 +3,8 @@ "abort": { "all_configured": "Az \u00f6sszes \u00f6sszekapcsolt szerver m\u00e1r konfigur\u00e1lva van", "already_configured": "Ez a Plex szerver m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", "token_request_timeout": "Token k\u00e9r\u00e9sre sz\u00e1nt id\u0151 lej\u00e1rt", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, diff --git a/homeassistant/components/powerwall/translations/hu.json b/homeassistant/components/powerwall/translations/hu.json index d5bc30e7d11..8975694ca95 100644 --- a/homeassistant/components/powerwall/translations/hu.json +++ b/homeassistant/components/powerwall/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", diff --git a/homeassistant/components/roku/translations/hu.json b/homeassistant/components/roku/translations/hu.json index a5d243dada4..256a8d45997 100644 --- a/homeassistant/components/roku/translations/hu.json +++ b/homeassistant/components/roku/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { diff --git a/homeassistant/components/roomba/translations/hu.json b/homeassistant/components/roomba/translations/hu.json index 34e36f55150..dd1c74cc0b6 100644 --- a/homeassistant/components/roomba/translations/hu.json +++ b/homeassistant/components/roomba/translations/hu.json @@ -19,7 +19,7 @@ "title": "Automatikus csatlakoz\u00e1s az eszk\u00f6zh\u00f6z" }, "link": { - "description": "Nyomja meg \u00e9s tartsa lenyomva a(z) {name} Kezd\u0151lap (Home) gombot, am\u00edg az eszk\u00f6z hangot ad (kb. K\u00e9t m\u00e1sodperc), majd engedje el 30 m\u00e1sodpercen bel\u00fcl.", + "description": "Nyomja meg \u00e9s tartsa lenyomva a {name} Home gombj\u00e1t, am\u00edg az eszk\u00f6z hangot ad (kb. k\u00e9t m\u00e1sodperc), majd engedje el 30 m\u00e1sodpercen bel\u00fcl.", "title": "Jelsz\u00f3 lek\u00e9r\u00e9se" }, "link_manual": { diff --git a/homeassistant/components/samsungtv/translations/hu.json b/homeassistant/components/samsungtv/translations/hu.json index efdf5f4810b..a85f295317d 100644 --- a/homeassistant/components/samsungtv/translations/hu.json +++ b/homeassistant/components/samsungtv/translations/hu.json @@ -2,13 +2,13 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "auth_missing": "Home Assistant nem jogosult csatlakozni ehhez a Samsung TV-hez. Ellen\u0151rizze a TV be\u00e1ll\u00edt\u00e1sait Home Assistant enged\u00e9lyez\u00e9s\u00e9hez.", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "id_missing": "Ennek a Samsung eszk\u00f6znek nincs sorsz\u00e1ma.", "missing_config_entry": "Ez a Samsung eszk\u00f6z nem rendelkezik konfigur\u00e1ci\u00f3s bejegyz\u00e9ssel.", "not_supported": "Ez a Samsung k\u00e9sz\u00fcl\u00e9k jelenleg nem t\u00e1mogatott.", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { @@ -21,7 +21,7 @@ "title": "Samsung TV" }, "reauth_confirm": { - "description": "A bek\u00fcld\u00e9s ut\u00e1n fogadja el a(z) {device} felugr\u00f3 ablakot, amely 30 m\u00e1sodpercen bel\u00fcl enged\u00e9lyt k\u00e9r." + "description": "A bek\u00fcld\u00e9s ut\u00e1n, fogadja el a {device} felugr\u00f3 ablak\u00e1ban l\u00e1that\u00f3 \u00fczenetet, mely 30 m\u00e1sodpercig \u00e1ll rendelkez\u00e9sre." }, "user": { "data": { diff --git a/homeassistant/components/select/translations/hu.json b/homeassistant/components/select/translations/hu.json index f93f0475c33..66a85b1adbd 100644 --- a/homeassistant/components/select/translations/hu.json +++ b/homeassistant/components/select/translations/hu.json @@ -1,7 +1,7 @@ { "device_automation": { "action_type": { - "select_option": "M\u00f3dos\u00edtsa a(z) {entity_name} be\u00e1ll\u00edt\u00e1st" + "select_option": "{entity_name} be\u00e1ll\u00edt\u00e1s\u00e1nak m\u00f3dos\u00edt\u00e1sa" }, "condition_type": { "selected_option": "{entity_name} aktu\u00e1lisan kiv\u00e1lasztott opci\u00f3" diff --git a/homeassistant/components/sharkiq/translations/hu.json b/homeassistant/components/sharkiq/translations/hu.json index b765ad68a3f..1951d23c664 100644 --- a/homeassistant/components/sharkiq/translations/hu.json +++ b/homeassistant/components/sharkiq/translations/hu.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { diff --git a/homeassistant/components/simplisafe/translations/hu.json b/homeassistant/components/simplisafe/translations/hu.json index 5bb3d73ae42..dc2b4ba7e1e 100644 --- a/homeassistant/components/simplisafe/translations/hu.json +++ b/homeassistant/components/simplisafe/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Ez a SimpliSafe-fi\u00f3k m\u00e1r haszn\u00e1latban van.", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", "wrong_account": "A megadott felhaszn\u00e1l\u00f3i hiteles\u00edt\u0151 adatok nem j\u00f3k ehhez a SimpliSafe fi\u00f3khoz." }, "error": { @@ -36,7 +36,7 @@ "password": "Jelsz\u00f3", "username": "E-mail" }, - "description": "2021-t\u0151l kezd\u0151d\u0151en a SimpliSafe egy \u00faj hiteles\u00edt\u00e9si mechanizmusra v\u00e1ltott a webalkalmaz\u00e1son kereszt\u00fcl. A technikai korl\u00e1toz\u00e1sok miatt a folyamat v\u00e9g\u00e9n van egy k\u00e9zi l\u00e9p\u00e9s; k\u00e9rj\u00fck, indul\u00e1s 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}) SimpliSafe webalkalmaz\u00e1s megnyit\u00e1s\u00e1hoz \u00e9s a hiteles\u00edt\u0151 adatok bevitel\u00e9hez. Amikor a folyamat befejez\u0151d\u00f6tt, t\u00e9rjen vissza ide, \u00e9s kattintson a K\u00fcld\u00e9s gombra.", + "description": "2021-t\u0151l kezd\u0151d\u0151en a SimpliSafe egy \u00faj hiteles\u00edt\u00e9si mechanizmusra v\u00e1ltott a webalkalmaz\u00e1son kereszt\u00fcl. A technikai korl\u00e1toz\u00e1sok miatt a folyamat v\u00e9g\u00e9n van egy k\u00e9zi l\u00e9p\u00e9s; k\u00e9rj\u00fck, indul\u00e1s 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 webalkalmaz\u00e1s megnyit\u00e1s\u00e1hoz \u00e9s a hiteles\u00edt\u0151 adatok bevitel\u00e9hez. Amikor a folyamat befejez\u0151d\u00f6tt, t\u00e9rjen vissza ide, \u00e9s kattintson a K\u00fcld\u00e9s gombra.", "title": "T\u00f6ltse ki az adatait" } } diff --git a/homeassistant/components/smarttub/translations/hu.json b/homeassistant/components/smarttub/translations/hu.json index 3764b27abd2..a63d4d91e9d 100644 --- a/homeassistant/components/smarttub/translations/hu.json +++ b/homeassistant/components/smarttub/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" diff --git a/homeassistant/components/somfy_mylink/translations/hu.json b/homeassistant/components/somfy_mylink/translations/hu.json index fa6620859f5..2b91ccd8676 100644 --- a/homeassistant/components/somfy_mylink/translations/hu.json +++ b/homeassistant/components/somfy_mylink/translations/hu.json @@ -44,7 +44,7 @@ "data": { "reverse": "A bor\u00edt\u00f3 megfordult" }, - "description": "A(z) `{target_name}` be\u00e1ll\u00edt\u00e1sainak konfigur\u00e1l\u00e1sa", + "description": "`{target_name}` be\u00e1ll\u00edt\u00e1sainak konfigur\u00e1l\u00e1sa", "title": "MyLink \u00e1rny\u00e9kol\u00f3 konfigur\u00e1l\u00e1sa" } } diff --git a/homeassistant/components/sonarr/translations/hu.json b/homeassistant/components/sonarr/translations/hu.json index 7ac0b621b13..f5286094b73 100644 --- a/homeassistant/components/sonarr/translations/hu.json +++ b/homeassistant/components/sonarr/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "A Sonarr integr\u00e1ci\u00f3t manu\u00e1lisan kell hiteles\u00edteni a(z) {host}", + "description": "A Sonarr integr\u00e1ci\u00f3t manu\u00e1lisan kell hiteles\u00edteni: {host}", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" }, "user": { diff --git a/homeassistant/components/sonos/translations/hu.json b/homeassistant/components/sonos/translations/hu.json index a521c1e9d75..d4fef8f5c06 100644 --- a/homeassistant/components/sonos/translations/hu.json +++ b/homeassistant/components/sonos/translations/hu.json @@ -2,12 +2,12 @@ "config": { "abort": { "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", - "not_sonos_device": "A felfedezett eszk\u00f6z nem Sonos-eszk\u00f6z", + "not_sonos_device": "A felfedezett eszk\u00f6z nem Sonos eszk\u00f6z", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "step": { "confirm": { - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani a Sonos-t?" + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani a Sonost?" } } } diff --git a/homeassistant/components/synology_dsm/translations/bg.json b/homeassistant/components/synology_dsm/translations/bg.json index 46e2b6d88f4..4b857bf5cf0 100644 --- a/homeassistant/components/synology_dsm/translations/bg.json +++ b/homeassistant/components/synology_dsm/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "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", "reconfigure_successful": "\u041f\u0440\u0435\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, @@ -17,8 +18,12 @@ }, "link": { "data": { - "port": "\u041f\u043e\u0440\u0442" - } + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + }, + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name} ({host})?", + "title": "Synology DSM" }, "reauth": { "data": { @@ -37,8 +42,10 @@ "data": { "host": "\u0425\u043e\u0441\u0442", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", - "port": "\u041f\u043e\u0440\u0442" - } + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + }, + "title": "Synology DSM" } } } diff --git a/homeassistant/components/system_bridge/translations/hu.json b/homeassistant/components/system_bridge/translations/hu.json index 31082202419..8fdacd8718f 100644 --- a/homeassistant/components/system_bridge/translations/hu.json +++ b/homeassistant/components/system_bridge/translations/hu.json @@ -16,7 +16,7 @@ "data": { "api_key": "API kulcs" }, - "description": "K\u00e9rj\u00fck, adja meg a(z) {name} konfigur\u00e1ci\u00f3j\u00e1ban be\u00e1ll\u00edtott API-kulcsot." + "description": "K\u00e9rem, adja meg a konfigur\u00e1ci\u00f3ban l\u00e9v\u0151, {name} API-kulcs\u00e1t." }, "user": { "data": { diff --git a/homeassistant/components/totalconnect/translations/hu.json b/homeassistant/components/totalconnect/translations/hu.json index 9b55278e9a8..18cbad6bc50 100644 --- a/homeassistant/components/totalconnect/translations/hu.json +++ b/homeassistant/components/totalconnect/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", diff --git a/homeassistant/components/tradfri/translations/hu.json b/homeassistant/components/tradfri/translations/hu.json index b1cb0ffd5ad..5e9c281fdd6 100644 --- a/homeassistant/components/tradfri/translations/hu.json +++ b/homeassistant/components/tradfri/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van." + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van" }, "error": { "cannot_authenticate": "Sikertelen azonos\u00edt\u00e1s. A Gateway egy m\u00e1sik eszk\u00f6zzel van p\u00e1ros\u00edtva, mint p\u00e9ld\u00e1ul a Homekittel?", diff --git a/homeassistant/components/tuya/translations/hu.json b/homeassistant/components/tuya/translations/hu.json index 9b19ea34d54..c01d8dbb2e7 100644 --- a/homeassistant/components/tuya/translations/hu.json +++ b/homeassistant/components/tuya/translations/hu.json @@ -65,7 +65,7 @@ "tuya_max_coltemp": "Az eszk\u00f6z \u00e1ltal megadott maxim\u00e1lis sz\u00ednh\u0151m\u00e9rs\u00e9klet", "unit_of_measurement": "Az eszk\u00f6z \u00e1ltal haszn\u00e1lt h\u0151m\u00e9rs\u00e9kleti egys\u00e9g" }, - "description": "Konfigur\u00e1l\u00e1si lehet\u0151s\u00e9gek a(z) {device_type} t\u00edpus\u00fa `{device_name}` eszk\u00f6z megjelen\u00edtett inform\u00e1ci\u00f3inak be\u00e1ll\u00edt\u00e1s\u00e1hoz", + "description": "Konfigur\u00e1l\u00e1si lehet\u0151s\u00e9gek {device_type} t\u00edpus\u00fa `{device_name}` eszk\u00f6z megjelen\u00edtett inform\u00e1ci\u00f3inak be\u00e1ll\u00edt\u00e1s\u00e1hoz", "title": "Tuya eszk\u00f6z konfigur\u00e1l\u00e1sa" }, "init": { diff --git a/homeassistant/components/unifi/translations/hu.json b/homeassistant/components/unifi/translations/hu.json index 9564b211043..7692342bf89 100644 --- a/homeassistant/components/unifi/translations/hu.json +++ b/homeassistant/components/unifi/translations/hu.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "A vez\u00e9rl\u0151 webhelye m\u00e1r konfigur\u00e1lva van", "configuration_updated": "A konfigur\u00e1ci\u00f3 friss\u00edtve.", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "faulty_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", diff --git a/homeassistant/components/verisure/translations/hu.json b/homeassistant/components/verisure/translations/hu.json index 89ff19bd1fe..91c0292c3b8 100644 --- a/homeassistant/components/verisure/translations/hu.json +++ b/homeassistant/components/verisure/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", diff --git a/homeassistant/components/vlc_telnet/translations/hu.json b/homeassistant/components/vlc_telnet/translations/hu.json index 1680d29b087..fff36c542d2 100644 --- a/homeassistant/components/vlc_telnet/translations/hu.json +++ b/homeassistant/components/vlc_telnet/translations/hu.json @@ -4,7 +4,7 @@ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { diff --git a/homeassistant/components/watttime/translations/hu.json b/homeassistant/components/watttime/translations/hu.json index 8ef371d2691..24741d82dbd 100644 --- a/homeassistant/components/watttime/translations/hu.json +++ b/homeassistant/components/watttime/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", diff --git a/homeassistant/components/wled/translations/hu.json b/homeassistant/components/wled/translations/hu.json index 1fa29cfee48..4c4223e7508 100644 --- a/homeassistant/components/wled/translations/hu.json +++ b/homeassistant/components/wled/translations/hu.json @@ -16,7 +16,7 @@ "description": "\u00c1ll\u00edtsa be a WLED-et Home Assistantba val\u00f3 integr\u00e1l\u00e1shoz." }, "zeroconf_confirm": { - "description": "Szeretn\u00e9 hozz\u00e1adni a(z) `{name}` WLED-et Home Assistanthoz?", + "description": "Szeretn\u00e9 hozz\u00e1adni `{name}` WLED-et Home Assistanthoz?", "title": "Felfedezett WLED eszk\u00f6z" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/hu.json b/homeassistant/components/xiaomi_aqara/translations/hu.json index a6139c8851b..e38bebefe19 100644 --- a/homeassistant/components/xiaomi_aqara/translations/hu.json +++ b/homeassistant/components/xiaomi_aqara/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "not_xiaomi_aqara": "Nem egy Xiaomi Aqara Gateway, a felfedezett eszk\u00f6z nem egyezett az ismert \u00e1tj\u00e1r\u00f3kkal" }, "error": { diff --git a/homeassistant/components/xiaomi_miio/translations/hu.json b/homeassistant/components/xiaomi_miio/translations/hu.json index 89515a62beb..8d2340988dd 100644 --- a/homeassistant/components/xiaomi_miio/translations/hu.json +++ b/homeassistant/components/xiaomi_miio/translations/hu.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "incomplete_info": "Az eszk\u00f6z be\u00e1ll\u00edt\u00e1s\u00e1hoz sz\u00fcks\u00e9ges inform\u00e1ci\u00f3k hi\u00e1nyosak, nincs megadva \u00e1llom\u00e1s vagy token.", "not_xiaomi_miio": "Az eszk\u00f6zt (m\u00e9g) nem t\u00e1mogatja a Xiaomi Miio integr\u00e1ci\u00f3.", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt" + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", diff --git a/homeassistant/components/zwave_js/translations/hu.json b/homeassistant/components/zwave_js/translations/hu.json index 16b219d1b7c..62afae94725 100644 --- a/homeassistant/components/zwave_js/translations/hu.json +++ b/homeassistant/components/zwave_js/translations/hu.json @@ -7,7 +7,7 @@ "addon_set_config_failed": "Nem siker\u00fclt be\u00e1ll\u00edtani a Z-Wave JS konfigur\u00e1ci\u00f3t.", "addon_start_failed": "Nem siker\u00fclt elind\u00edtani a Z-Wave JS b\u0151v\u00edtm\u00e9nyt.", "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van.", + "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "discovery_requires_supervisor": "A felfedez\u00e9shez a fel\u00fcgyel\u0151re van sz\u00fcks\u00e9g.", "not_zwave_device": "A felfedezett eszk\u00f6z nem Z-Wave eszk\u00f6z." @@ -57,7 +57,7 @@ "title": "Indul a Z-Wave JS b\u0151v\u00edtm\u00e9ny." }, "usb_confirm": { - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani a(z) {name} alkalmaz\u00e1st a Z-Wave JS b\u0151v\u00edtm\u00e9nnyel?" + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani {name} alkalmaz\u00e1st a Z-Wave JS b\u0151v\u00edtm\u00e9nnyel?" } } }, @@ -72,14 +72,14 @@ "set_value": "Z-Wave \u00e9rt\u00e9k be\u00e1ll\u00edt\u00e1sa" }, "condition_type": { - "config_parameter": "Konfigur\u00e1lja a(z) {subtype} param\u00e9ter \u00e9rt\u00e9k\u00e9t", + "config_parameter": "{subtype} param\u00e9ter \u00e9rt\u00e9k\u00e9nek be\u00e1ll\u00edt\u00e1sa", "node_status": "Csom\u00f3pont \u00e1llapota", "value": "A Z-Wave \u00e9rt\u00e9k aktu\u00e1lis \u00e9rt\u00e9ke" }, "trigger_type": { "event.notification.entry_control": "Bel\u00e9p\u00e9s-ellen\u0151rz\u00e9si \u00e9rtes\u00edt\u00e9st k\u00fcld\u00f6tt", "event.notification.notification": "\u00c9rtes\u00edt\u00e9s elk\u00fcldve", - "event.value_notification.basic": "Alapvet\u0151 CC esem\u00e9ny a(z) {subtype}", + "event.value_notification.basic": "Alapvet\u0151 {subtype} CC esem\u00e9ny", "event.value_notification.central_scene": "K\u00f6zponti jelenet m\u0171velet {subtype}", "event.value_notification.scene_activation": "Jelenetaktiv\u00e1l\u00e1s {subtype}", "state.node_status": "A csom\u00f3pont \u00e1llapota megv\u00e1ltozott", From da7e26c287ecd0658e5b44ac83bfac4a511461b2 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Sun, 24 Oct 2021 09:51:02 +0200 Subject: [PATCH 0756/1038] Filter by connections instead of identifiers for Shelly (#58305) * Fix connections * Switch to helper --- homeassistant/components/shelly/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index f181817e6f1..6b15e4e730d 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -132,7 +132,13 @@ async def async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> bo device_entry = None if entry.unique_id is not None: device_entry = dev_reg.async_get_device( - identifiers={(DOMAIN, entry.unique_id)}, connections=set() + identifiers=set(), + connections={ + ( + device_registry.CONNECTION_NETWORK_MAC, + device_registry.format_mac(entry.unique_id), + ) + }, ) if device_entry and entry.entry_id not in device_entry.config_entries: device_entry = None From 45a98aee10f26415705d8d50c57305d8b0e23102 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sun, 24 Oct 2021 11:24:05 +0200 Subject: [PATCH 0757/1038] Configurable mode for KNX number entity (#58268) --- homeassistant/components/knx/number.py | 9 ++++++++- homeassistant/components/knx/schema.py | 9 +++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/number.py b/homeassistant/components/knx/number.py index eb4f21de513..d1ef2d226b4 100644 --- a/homeassistant/components/knx/number.py +++ b/homeassistant/components/knx/number.py @@ -7,7 +7,13 @@ from xknx import XKNX from xknx.devices import NumericValue from homeassistant.components.number import NumberEntity -from homeassistant.const import CONF_NAME, CONF_TYPE, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.const import ( + CONF_MODE, + CONF_NAME, + CONF_TYPE, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity @@ -63,6 +69,7 @@ class KNXNumber(KnxEntity, NumberEntity, RestoreEntity): NumberSchema.CONF_MIN, self._device.sensor_value.dpt_class.value_min, ) + self._attr_mode = config[CONF_MODE] self._attr_step = config.get( NumberSchema.CONF_STEP, self._device.sensor_value.dpt_class.resolution, diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 89dab40958c..653084f3e3a 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import ABC from collections import OrderedDict -from typing import Any, ClassVar +from typing import Any, ClassVar, Final import voluptuous as vol from xknx import XKNX @@ -18,11 +18,13 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.components.climate.const import HVAC_MODE_HEAT, HVAC_MODES from homeassistant.components.cover import DEVICE_CLASSES as COVER_DEVICE_CLASSES +from homeassistant.components.number.const import MODE_AUTO, MODE_BOX, MODE_SLIDER from homeassistant.components.sensor import CONF_STATE_CLASS, STATE_CLASSES_SCHEMA from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_HOST, + CONF_MODE, CONF_NAME, CONF_PORT, CONF_TYPE, @@ -437,7 +439,7 @@ class ExposeSchema(KNXPlatformSchema): CONF_KNX_EXPOSE_ATTRIBUTE = "attribute" CONF_KNX_EXPOSE_BINARY = "binary" CONF_KNX_EXPOSE_DEFAULT = "default" - EXPOSE_TIME_TYPES = [ + EXPOSE_TIME_TYPES: Final = [ "time", "date", "datetime", @@ -653,11 +655,14 @@ class NumberSchema(KNXPlatformSchema): CONF_STEP = "step" DEFAULT_NAME = "KNX Number" + NUMBER_MODES: Final = [MODE_AUTO, MODE_BOX, MODE_SLIDER] + ENTITY_SCHEMA = vol.All( vol.Schema( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean, + vol.Optional(CONF_MODE, default=MODE_AUTO): vol.In(NUMBER_MODES), vol.Required(CONF_TYPE): numeric_type_validator, vol.Required(KNX_ADDRESS): ga_list_validator, vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, From cc0c79ac9ad0aaafb4933f43d6731e384eb00366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 24 Oct 2021 12:27:17 +0300 Subject: [PATCH 0758/1038] Use http.HTTPStatus in components/[tuv]* (#58325) --- .../components/tado/device_tracker.py | 5 ++-- .../components/thethingsnetwork/sensor.py | 9 +++--- .../components/tomato/device_tracker.py | 7 ++--- homeassistant/components/twitter/notify.py | 11 +++---- .../components/uk_transport/sensor.py | 5 ++-- .../components/verisure/coordinator.py | 5 ++-- .../components/viaggiatreno/sensor.py | 5 ++-- homeassistant/components/voicerss/tts.py | 5 ++-- tests/components/tado/test_config_flow.py | 5 ++-- tests/components/toon/test_config_flow.py | 3 +- tests/components/traccar/test_init.py | 30 ++++++++----------- tests/components/tts/test_init.py | 16 +++++----- tests/components/unifi/test_controller.py | 5 +++- tests/components/voicerss/test_tts.py | 15 +++++++--- 14 files changed, 69 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/tado/device_tracker.py b/homeassistant/components/tado/device_tracker.py index afa8bc6a604..e49a5be5a71 100644 --- a/homeassistant/components/tado/device_tracker.py +++ b/homeassistant/components/tado/device_tracker.py @@ -2,6 +2,7 @@ import asyncio from collections import namedtuple from datetime import timedelta +from http import HTTPStatus import logging import aiohttp @@ -13,7 +14,7 @@ from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA, DeviceScanner, ) -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, HTTP_OK +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers.aiohttp_client import async_create_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle @@ -114,7 +115,7 @@ class TadoDeviceScanner(DeviceScanner): response = await self.websession.get(url) - if response.status != HTTP_OK: + if response.status != HTTPStatus.OK: _LOGGER.warning("Error %d on %s", response.status, self.tadoapiurl) return False diff --git a/homeassistant/components/thethingsnetwork/sensor.py b/homeassistant/components/thethingsnetwork/sensor.py index 089d1eda2ee..792eaa0170c 100644 --- a/homeassistant/components/thethingsnetwork/sensor.py +++ b/homeassistant/components/thethingsnetwork/sensor.py @@ -1,5 +1,6 @@ """Support for The Things Network's Data storage integration.""" import asyncio +from http import HTTPStatus import logging import aiohttp @@ -13,8 +14,6 @@ from homeassistant.const import ( ATTR_TIME, CONF_DEVICE_ID, CONTENT_TYPE_JSON, - HTTP_NOT_FOUND, - HTTP_UNAUTHORIZED, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -134,15 +133,15 @@ class TtnDataStorage: status = response.status - if status == 204: + if status == HTTPStatus.NO_CONTENT: _LOGGER.error("The device is not available: %s", self._device_id) return None - if status == HTTP_UNAUTHORIZED: + if status == HTTPStatus.UNAUTHORIZED: _LOGGER.error("Not authorized for Application ID: %s", self._app_id) return None - if status == HTTP_NOT_FOUND: + if status == HTTPStatus.NOT_FOUND: _LOGGER.error("Application ID is not available: %s", self._app_id) return None diff --git a/homeassistant/components/tomato/device_tracker.py b/homeassistant/components/tomato/device_tracker.py index 4be3ae2470d..9799e1a4a87 100644 --- a/homeassistant/components/tomato/device_tracker.py +++ b/homeassistant/components/tomato/device_tracker.py @@ -1,4 +1,5 @@ """Support for Tomato routers.""" +from http import HTTPStatus import json import logging import re @@ -18,8 +19,6 @@ from homeassistant.const import ( CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, - HTTP_OK, - HTTP_UNAUTHORIZED, ) import homeassistant.helpers.config_validation as cv @@ -104,7 +103,7 @@ class TomatoDeviceScanner(DeviceScanner): # Calling and parsing the Tomato api here. We only need the # wldev and dhcpd_lease values. - if response.status_code == HTTP_OK: + if response.status_code == HTTPStatus.OK: for param, value in self.parse_api_pattern.findall(response.text): @@ -112,7 +111,7 @@ class TomatoDeviceScanner(DeviceScanner): self.last_results[param] = json.loads(value.replace("'", '"')) return True - if response.status_code == HTTP_UNAUTHORIZED: + if response.status_code == HTTPStatus.UNAUTHORIZED: # Authentication error _LOGGER.exception( "Failed to authenticate, please check your username and password" diff --git a/homeassistant/components/twitter/notify.py b/homeassistant/components/twitter/notify.py index ac7de89a61b..54aaf2142e5 100644 --- a/homeassistant/components/twitter/notify.py +++ b/homeassistant/components/twitter/notify.py @@ -1,6 +1,7 @@ """Twitter platform for notify component.""" from datetime import datetime, timedelta from functools import partial +from http import HTTPStatus import json import logging import mimetypes @@ -14,7 +15,7 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME, HTTP_OK +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_point_in_time @@ -88,7 +89,7 @@ class TwitterNotificationService(BaseNotificationService): if self.user: user_resp = self.api.request("users/lookup", {"screen_name": self.user}) user_id = user_resp.json()[0]["id"] - if user_resp.status_code != HTTP_OK: + if user_resp.status_code != HTTPStatus.OK: self.log_error_resp(user_resp) else: _LOGGER.debug("Message posted: %s", user_resp.json()) @@ -108,7 +109,7 @@ class TwitterNotificationService(BaseNotificationService): "statuses/update", {"status": message, "media_ids": media_id} ) - if resp.status_code != HTTP_OK: + if resp.status_code != HTTPStatus.OK: self.log_error_resp(resp) else: _LOGGER.debug("Message posted: %s", resp.json()) @@ -171,7 +172,7 @@ class TwitterNotificationService(BaseNotificationService): while bytes_sent < total_bytes: chunk = file.read(4 * 1024 * 1024) resp = self.upload_media_append(chunk, media_id, segment_id) - if resp.status_code not in range(HTTP_OK, 299): + if not HTTPStatus.OK <= resp.status_code < HTTPStatus.MULTIPLE_CHOICES: self.log_error_resp_append(resp) return None segment_id = segment_id + 1 @@ -200,7 +201,7 @@ class TwitterNotificationService(BaseNotificationService): {"command": "STATUS", "media_id": media_id}, method_override="GET", ) - if resp.status_code != HTTP_OK: + if resp.status_code != HTTPStatus.OK: _LOGGER.error("Media processing error: %s", resp.json()) processing_info = resp.json()["processing_info"] diff --git a/homeassistant/components/uk_transport/sensor.py b/homeassistant/components/uk_transport/sensor.py index 69e4f0df99b..567a6093c44 100644 --- a/homeassistant/components/uk_transport/sensor.py +++ b/homeassistant/components/uk_transport/sensor.py @@ -1,5 +1,6 @@ """Support for UK public transport data provided by transportapi.com.""" from datetime import datetime, timedelta +from http import HTTPStatus import logging import re @@ -7,7 +8,7 @@ import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import CONF_MODE, HTTP_OK, TIME_MINUTES +from homeassistant.const import CONF_MODE, TIME_MINUTES import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle import homeassistant.util.dt as dt_util @@ -121,7 +122,7 @@ class UkTransportSensor(SensorEntity): ) response = requests.get(self._url, params=request_params) - if response.status_code != HTTP_OK: + if response.status_code != HTTPStatus.OK: _LOGGER.warning("Invalid response from API") elif "error" in response.json(): if "exceeded" in response.json()["error"]: diff --git a/homeassistant/components/verisure/coordinator.py b/homeassistant/components/verisure/coordinator.py index b118979f586..ce7d5ea3bf9 100644 --- a/homeassistant/components/verisure/coordinator.py +++ b/homeassistant/components/verisure/coordinator.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import timedelta +from http import HTTPStatus from verisure import ( Error as VerisureError, @@ -10,7 +11,7 @@ from verisure import ( ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, HTTP_SERVICE_UNAVAILABLE +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import Event, HomeAssistant from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -68,7 +69,7 @@ class VerisureDataUpdateCoordinator(DataUpdateCoordinator): ) except VerisureResponseError as ex: LOGGER.error("Could not read overview, %s", ex) - if ex.status_code == HTTP_SERVICE_UNAVAILABLE: # Service unavailable + if ex.status_code == HTTPStatus.SERVICE_UNAVAILABLE: LOGGER.info("Trying to log in again") await self.async_login() return {} diff --git a/homeassistant/components/viaggiatreno/sensor.py b/homeassistant/components/viaggiatreno/sensor.py index ee02eaa63e1..0457572e066 100644 --- a/homeassistant/components/viaggiatreno/sensor.py +++ b/homeassistant/components/viaggiatreno/sensor.py @@ -1,5 +1,6 @@ """Support for the Italian train system using ViaggiaTreno API.""" import asyncio +from http import HTTPStatus import logging import time @@ -8,7 +9,7 @@ import async_timeout import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ATTR_ATTRIBUTION, HTTP_OK, TIME_MINUTES +from homeassistant.const import ATTR_ATTRIBUTION, TIME_MINUTES import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -71,7 +72,7 @@ async def async_http_request(hass, uri): session = hass.helpers.aiohttp_client.async_get_clientsession(hass) with async_timeout.timeout(REQUEST_TIMEOUT): req = await session.get(uri) - if req.status != HTTP_OK: + if req.status != HTTPStatus.OK: return {"error": req.status} json_response = await req.json() return json_response diff --git a/homeassistant/components/voicerss/tts.py b/homeassistant/components/voicerss/tts.py index 69029ea7031..3558179c4d1 100644 --- a/homeassistant/components/voicerss/tts.py +++ b/homeassistant/components/voicerss/tts.py @@ -1,5 +1,6 @@ """Support for the voicerss speech service.""" import asyncio +from http import HTTPStatus import logging import aiohttp @@ -7,7 +8,7 @@ import async_timeout import voluptuous as vol from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider -from homeassistant.const import CONF_API_KEY, HTTP_OK +from homeassistant.const import CONF_API_KEY from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -198,7 +199,7 @@ class VoiceRSSProvider(Provider): with async_timeout.timeout(10): request = await websession.post(VOICERSS_API_URL, data=form_data) - if request.status != HTTP_OK: + if request.status != HTTPStatus.OK: _LOGGER.error( "Error %d on load url %s", request.status, request.url ) diff --git a/tests/components/tado/test_config_flow.py b/tests/components/tado/test_config_flow.py index 6b0a7c62179..b181b78bf16 100644 --- a/tests/components/tado/test_config_flow.py +++ b/tests/components/tado/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Tado config flow.""" +from http import HTTPStatus from unittest.mock import MagicMock, patch import requests @@ -59,7 +60,7 @@ async def test_form_invalid_auth(hass): ) response_mock = MagicMock() - type(response_mock).status_code = 401 + type(response_mock).status_code = HTTPStatus.UNAUTHORIZED mock_tado_api = _get_mock_tado_api(getMe=requests.HTTPError(response=response_mock)) with patch( @@ -82,7 +83,7 @@ async def test_form_cannot_connect(hass): ) response_mock = MagicMock() - type(response_mock).status_code = 500 + type(response_mock).status_code = HTTPStatus.INTERNAL_SERVER_ERROR mock_tado_api = _get_mock_tado_api(getMe=requests.HTTPError(response=response_mock)) with patch( diff --git a/tests/components/toon/test_config_flow.py b/tests/components/toon/test_config_flow.py index a98db508bb4..826df81066b 100644 --- a/tests/components/toon/test_config_flow.py +++ b/tests/components/toon/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Toon config flow.""" +from http import HTTPStatus from unittest.mock import patch from toonapi import Agreement, ToonError @@ -77,7 +78,7 @@ async def test_full_flow_implementation( client = await hass_client_no_auth() resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert resp.headers["content-type"] == "text/html; charset=utf-8" aioclient_mock.post( diff --git a/tests/components/traccar/test_init.py b/tests/components/traccar/test_init.py index 53433baf9ae..2d0140db815 100644 --- a/tests/components/traccar/test_init.py +++ b/tests/components/traccar/test_init.py @@ -1,4 +1,5 @@ """The tests the for Traccar device tracker platform.""" +from http import HTTPStatus from unittest.mock import patch import pytest @@ -8,12 +9,7 @@ from homeassistant.components import traccar, zone from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.traccar import DOMAIN, TRACKER_UPDATE from homeassistant.config import async_process_ha_core_config -from homeassistant.const import ( - HTTP_OK, - HTTP_UNPROCESSABLE_ENTITY, - STATE_HOME, - STATE_NOT_HOME, -) +from homeassistant.const import STATE_HOME, STATE_NOT_HOME from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component @@ -84,21 +80,21 @@ async def test_missing_data(hass, client, webhook_id): # No data req = await client.post(url) await hass.async_block_till_done() - assert req.status == HTTP_UNPROCESSABLE_ENTITY + assert req.status == HTTPStatus.UNPROCESSABLE_ENTITY # No latitude copy = data.copy() del copy["lat"] req = await client.post(url, params=copy) await hass.async_block_till_done() - assert req.status == HTTP_UNPROCESSABLE_ENTITY + assert req.status == HTTPStatus.UNPROCESSABLE_ENTITY # No device copy = data.copy() del copy["id"] req = await client.post(url, params=copy) await hass.async_block_till_done() - assert req.status == HTTP_UNPROCESSABLE_ENTITY + assert req.status == HTTPStatus.UNPROCESSABLE_ENTITY async def test_enter_and_exit(hass, client, webhook_id): @@ -109,7 +105,7 @@ async def test_enter_and_exit(hass, client, webhook_id): # Enter the Home req = await client.post(url, params=data) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK state_name = hass.states.get( "{}.{}".format(DEVICE_TRACKER_DOMAIN, data["id"]) ).state @@ -118,7 +114,7 @@ async def test_enter_and_exit(hass, client, webhook_id): # Enter Home again req = await client.post(url, params=data) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK state_name = hass.states.get( "{}.{}".format(DEVICE_TRACKER_DOMAIN, data["id"]) ).state @@ -130,7 +126,7 @@ async def test_enter_and_exit(hass, client, webhook_id): # Enter Somewhere else req = await client.post(url, params=data) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK state_name = hass.states.get( "{}.{}".format(DEVICE_TRACKER_DOMAIN, data["id"]) ).state @@ -160,7 +156,7 @@ async def test_enter_with_attrs(hass, client, webhook_id): req = await client.post(url, params=data) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK state = hass.states.get("{}.{}".format(DEVICE_TRACKER_DOMAIN, data["id"])) assert state.state == STATE_NOT_HOME assert state.attributes["gps_accuracy"] == 10.5 @@ -182,7 +178,7 @@ async def test_enter_with_attrs(hass, client, webhook_id): req = await client.post(url, params=data) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK state = hass.states.get("{}.{}".format(DEVICE_TRACKER_DOMAIN, data["id"])) assert state.state == STATE_HOME assert state.attributes["gps_accuracy"] == 123 @@ -201,7 +197,7 @@ async def test_two_devices(hass, client, webhook_id): # Exit Home req = await client.post(url, params=data_device_1) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK state = hass.states.get("{}.{}".format(DEVICE_TRACKER_DOMAIN, data_device_1["id"])) assert state.state == "not_home" @@ -213,7 +209,7 @@ async def test_two_devices(hass, client, webhook_id): data_device_2["id"] = "device_2" req = await client.post(url, params=data_device_2) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK state = hass.states.get("{}.{}".format(DEVICE_TRACKER_DOMAIN, data_device_2["id"])) assert state.state == "home" @@ -232,7 +228,7 @@ async def test_load_unload_entry(hass, client, webhook_id): # Enter the Home req = await client.post(url, params=data) await hass.async_block_till_done() - assert req.status == HTTP_OK + assert req.status == HTTPStatus.OK state_name = hass.states.get( "{}.{}".format(DEVICE_TRACKER_DOMAIN, data["id"]) ).state diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 8cd1641caa0..3cbc1f0da00 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -1,4 +1,5 @@ """The tests for the TTS component.""" +from http import HTTPStatus from unittest.mock import PropertyMock, patch import pytest @@ -15,7 +16,6 @@ from homeassistant.components.media_player.const import ( import homeassistant.components.tts as tts from homeassistant.components.tts import _get_cache_files from homeassistant.config import async_process_ha_core_config -from homeassistant.const import HTTP_NOT_FOUND from homeassistant.setup import async_setup_component from tests.common import assert_setup_component, async_mock_service @@ -486,7 +486,7 @@ async def test_setup_component_and_test_service_with_receive_voice( "en", None, ) - assert req.status == 200 + assert req.status == HTTPStatus.OK assert await req.read() == demo_data @@ -523,7 +523,7 @@ async def test_setup_component_and_test_service_with_receive_voice_german( "de", None, ) - assert req.status == 200 + assert req.status == HTTPStatus.OK assert await req.read() == demo_data @@ -539,7 +539,7 @@ async def test_setup_component_and_web_view_wrong_file(hass, hass_client): url = "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" req = await client.get(url) - assert req.status == HTTP_NOT_FOUND + assert req.status == HTTPStatus.NOT_FOUND async def test_setup_component_and_web_view_wrong_filename(hass, hass_client): @@ -554,7 +554,7 @@ async def test_setup_component_and_web_view_wrong_filename(hass, hass_client): url = "/api/tts_proxy/265944dsk32c1b2a621be5930510bb2cd_en_-_demo.mp3" req = await client.get(url) - assert req.status == HTTP_NOT_FOUND + assert req.status == HTTPStatus.NOT_FOUND async def test_setup_component_test_without_cache(hass, empty_cache_dir): @@ -682,7 +682,7 @@ async def test_setup_component_load_cache_retrieve_without_mem_cache( url = "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" req = await client.get(url) - assert req.status == 200 + assert req.status == HTTPStatus.OK assert await req.read() == demo_data @@ -698,7 +698,7 @@ async def test_setup_component_and_web_get_url(hass, hass_client): data = {"platform": "demo", "message": "There is someone at the door."} req = await client.post(url, json=data) - assert req.status == 200 + assert req.status == HTTPStatus.OK response = await req.json() assert response == { "url": "http://example.local:8123/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", @@ -718,7 +718,7 @@ async def test_setup_component_and_web_get_url_bad_config(hass, hass_client): data = {"message": "There is someone at the door."} req = await client.post(url, json=data) - assert req.status == 400 + assert req.status == HTTPStatus.BAD_REQUEST async def test_tags_with_wave(hass, demo_provider): diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 869d33037bb..41864f5dc3e 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -3,6 +3,7 @@ import asyncio from copy import deepcopy from datetime import timedelta +from http import HTTPStatus from unittest.mock import Mock, patch import aiounifi @@ -404,7 +405,9 @@ async def test_reconnect_mechanism(hass, aioclient_mock, mock_unifi_websocket): await setup_unifi_integration(hass, aioclient_mock) aioclient_mock.clear_requests() - aioclient_mock.post(f"https://{DEFAULT_HOST}:1234/api/login", status=502) + aioclient_mock.post( + f"https://{DEFAULT_HOST}:1234/api/login", status=HTTPStatus.BAD_GATEWAY + ) mock_unifi_websocket(state=STATE_DISCONNECTED) await hass.async_block_till_done() diff --git a/tests/components/voicerss/test_tts.py b/tests/components/voicerss/test_tts.py index 6d7dfcf3d7f..222ed90f67a 100644 --- a/tests/components/voicerss/test_tts.py +++ b/tests/components/voicerss/test_tts.py @@ -1,5 +1,6 @@ """The tests for the VoiceRSS speech platform.""" import asyncio +from http import HTTPStatus import os import shutil @@ -65,7 +66,9 @@ class TestTTSVoiceRSSPlatform: """Test service call say.""" calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - aioclient_mock.post(self.url, data=self.form_data, status=200, content=b"test") + aioclient_mock.post( + self.url, data=self.form_data, status=HTTPStatus.OK, content=b"test" + ) config = {tts.DOMAIN: {"platform": "voicerss", "api_key": "1234567xx"}} @@ -92,7 +95,9 @@ class TestTTSVoiceRSSPlatform: calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) self.form_data["hl"] = "de-de" - aioclient_mock.post(self.url, data=self.form_data, status=200, content=b"test") + aioclient_mock.post( + self.url, data=self.form_data, status=HTTPStatus.OK, content=b"test" + ) config = { tts.DOMAIN: { @@ -124,7 +129,9 @@ class TestTTSVoiceRSSPlatform: calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) self.form_data["hl"] = "de-de" - aioclient_mock.post(self.url, data=self.form_data, status=200, content=b"test") + aioclient_mock.post( + self.url, data=self.form_data, status=HTTPStatus.OK, content=b"test" + ) config = {tts.DOMAIN: {"platform": "voicerss", "api_key": "1234567xx"}} @@ -203,7 +210,7 @@ class TestTTSVoiceRSSPlatform: aioclient_mock.post( self.url, data=self.form_data, - status=200, + status=HTTPStatus.OK, content=b"The subscription does not support SSML!", ) From 0c94fcecf6ce57657de41612d258aafa457d2e0e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 23 Oct 2021 23:32:45 -1000 Subject: [PATCH 0759/1038] Pull configuration_url from library in gogogate2 (#58318) --- homeassistant/components/gogogate2/common.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/gogogate2/common.py b/homeassistant/components/gogogate2/common.py index 42217910b81..c5d8c0f5137 100644 --- a/homeassistant/components/gogogate2/common.py +++ b/homeassistant/components/gogogate2/common.py @@ -96,11 +96,8 @@ class GoGoGate2Entity(CoordinatorEntity): def device_info(self) -> DeviceInfo: """Device info for the controller.""" data = self.coordinator.data - url = None - if data.model.startswith("ismartgate"): - url = f"https://{self._config_entry.unique_id}.isgaccess.com" return DeviceInfo( - configuration_url=url, + configuration_url=data.remoteaccess if data.remoteaccess else None, identifiers={(DOMAIN, str(self._config_entry.unique_id))}, name=self._config_entry.title, manufacturer=MANUFACTURER, From 2df13d01187a4fac2f9038facc180eb2c2543712 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 24 Oct 2021 05:34:45 -0400 Subject: [PATCH 0760/1038] Use DeviceInfo Class N-O (#58314) --- homeassistant/components/nanoleaf/light.py | 2 +- homeassistant/components/neato/camera.py | 2 +- homeassistant/components/neato/sensor.py | 2 +- homeassistant/components/neato/switch.py | 2 +- homeassistant/components/neato/vacuum.py | 17 +++++------ homeassistant/components/nest/device_info.py | 14 ++++----- .../components/nest/legacy/__init__.py | 16 +++++----- .../components/nest/legacy/camera.py | 15 +++++----- .../components/nest/legacy/climate.py | 17 ++++++----- homeassistant/components/nexia/entity.py | 22 +++++++------- .../components/nmap_tracker/device_tracker.py | 10 +++---- homeassistant/components/nut/sensor.py | 16 ++++------ homeassistant/components/nws/__init__.py | 15 +++++----- homeassistant/components/omnilogic/common.py | 22 +++++--------- homeassistant/components/ondilo_ico/sensor.py | 17 ++++++----- homeassistant/components/onvif/base.py | 29 +++++++++---------- .../components/opentherm_gw/binary_sensor.py | 18 ++++++------ .../components/opentherm_gw/climate.py | 18 ++++++------ .../components/opentherm_gw/sensor.py | 18 ++++++------ .../components/openweathermap/sensor.py | 13 +++++---- .../components/openweathermap/weather.py | 15 +++++----- .../components/ovo_energy/__init__.py | 12 ++++---- .../components/owntracks/device_tracker.py | 5 ++-- 23 files changed, 155 insertions(+), 162 deletions(-) diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py index af9faf21b79..6203431d609 100644 --- a/homeassistant/components/nanoleaf/light.py +++ b/homeassistant/components/nanoleaf/light.py @@ -84,9 +84,9 @@ class NanoleafLight(LightEntity): self._attr_name = self._nanoleaf.name self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self._nanoleaf.serial_no)}, - name=self._nanoleaf.name, manufacturer=self._nanoleaf.manufacturer, model=self._nanoleaf.model, + name=self._nanoleaf.name, sw_version=self._nanoleaf.firmware_version, ) self._attr_min_mireds = math.ceil( diff --git a/homeassistant/components/neato/camera.py b/homeassistant/components/neato/camera.py index ee70116a7d3..6d4de68b456 100644 --- a/homeassistant/components/neato/camera.py +++ b/homeassistant/components/neato/camera.py @@ -132,7 +132,7 @@ class NeatoCleaningMap(Camera): @property def device_info(self) -> DeviceInfo: """Device info for neato robot.""" - return {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}} + return DeviceInfo(identifiers={(NEATO_DOMAIN, self._robot_serial)}) @property def extra_state_attributes(self) -> dict[str, Any]: diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py index 2d54e89bb04..9a5da4c1950 100644 --- a/homeassistant/components/neato/sensor.py +++ b/homeassistant/components/neato/sensor.py @@ -103,4 +103,4 @@ class NeatoSensor(SensorEntity): @property def device_info(self) -> DeviceInfo: """Device info for neato robot.""" - return {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}} + return DeviceInfo(identifiers={(NEATO_DOMAIN, self._robot_serial)}) diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index 0e0d49f2b28..65d296bcb28 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -109,7 +109,7 @@ class NeatoConnectedSwitch(ToggleEntity): @property def device_info(self) -> DeviceInfo: """Device info for neato robot.""" - return {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}} + return DeviceInfo(identifiers={(NEATO_DOMAIN, self._robot_serial)}) def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index 527cd4dce23..0ebf7c70bf2 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -338,15 +338,14 @@ class NeatoConnectedVacuum(StateVacuumEntity): @property def device_info(self) -> DeviceInfo: """Device info for neato robot.""" - info: DeviceInfo = { - "identifiers": {(NEATO_DOMAIN, self._robot_serial)}, - "name": self._name, - } - if self._robot_stats: - info["manufacturer"] = self._robot_stats["battery"]["vendor"] - info["model"] = self._robot_stats["model"] - info["sw_version"] = self._robot_stats["firmware"] - return info + stats = self._robot_stats + return DeviceInfo( + identifiers={(NEATO_DOMAIN, self._robot_serial)}, + manufacturer=stats["battery"]["vendor"] if stats else None, + model=stats["model"] if stats else None, + name=self._name, + sw_version=stats["firmware"] if stats else None, + ) def start(self) -> None: """Start cleaning or resume cleaning.""" diff --git a/homeassistant/components/nest/device_info.py b/homeassistant/components/nest/device_info.py index 605f3c48446..714e9cb00da 100644 --- a/homeassistant/components/nest/device_info.py +++ b/homeassistant/components/nest/device_info.py @@ -30,13 +30,11 @@ class NestDeviceInfo: def device_info(self) -> DeviceInfo: """Return device specific attributes.""" return DeviceInfo( - { - # The API "name" field is a unique device identifier. - "identifiers": {(DOMAIN, self._device.name)}, - "name": self.device_name, - "manufacturer": self.device_brand, - "model": self.device_model, - } + # The API "name" field is a unique device identifier. + identifiers={(DOMAIN, self._device.name)}, + manufacturer=self.device_brand, + model=self.device_model, + name=self.device_name, ) @property @@ -45,7 +43,7 @@ class NestDeviceInfo: if InfoTrait.NAME in self._device.traits: trait: InfoTrait = self._device.traits[InfoTrait.NAME] if trait.custom_name: - return trait.custom_name + return str(trait.custom_name) # Build a name from the room/structure. Note: This room/structure name # is not associated with a home assistant Area. if parent_relations := self._device.parent_relations: diff --git a/homeassistant/components/nest/legacy/__init__.py b/homeassistant/components/nest/legacy/__init__.py index 76ecf16b67b..85ff98f6420 100644 --- a/homeassistant/components/nest/legacy/__init__.py +++ b/homeassistant/components/nest/legacy/__init__.py @@ -21,7 +21,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from . import local_auth from .const import DATA_NEST, DATA_NEST_CONFIG, DOMAIN, SIGNAL_NEST_UPDATE @@ -377,7 +377,7 @@ class NestSensorDevice(Entity): return f"{self.device.serial}-{self.variable}" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return information about the device.""" if not hasattr(self.device, "name_long"): name = self.structure.name @@ -393,12 +393,12 @@ class NestSensorDevice(Entity): else: model = None - return { - "identifiers": {(DOMAIN, self.device.serial)}, - "name": name, - "manufacturer": "Nest Labs", - "model": model, - } + return DeviceInfo( + identifiers={(DOMAIN, self.device.serial)}, + manufacturer="Nest Labs", + model=model, + name=name, + ) def update(self): """Do not use NestSensorDevice directly.""" diff --git a/homeassistant/components/nest/legacy/camera.py b/homeassistant/components/nest/legacy/camera.py index 3ef0089d2bc..0b6b7a649a6 100644 --- a/homeassistant/components/nest/legacy/camera.py +++ b/homeassistant/components/nest/legacy/camera.py @@ -7,6 +7,7 @@ import logging import requests from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_ON_OFF, Camera +from homeassistant.helpers.entity import DeviceInfo from homeassistant.util.dt import utcnow from .const import DATA_NEST, DOMAIN @@ -61,14 +62,14 @@ class NestCamera(Camera): return self.device.device_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return information about the device.""" - return { - "identifiers": {(DOMAIN, self.device.device_id)}, - "name": self.device.name_long, - "manufacturer": "Nest Labs", - "model": "Camera", - } + return DeviceInfo( + identifiers={(DOMAIN, self.device.device_id)}, + manufacturer="Nest Labs", + model="Camera", + name=self.device.name_long, + ) @property def should_poll(self): diff --git a/homeassistant/components/nest/legacy/climate.py b/homeassistant/components/nest/legacy/climate.py index 17448d9be8c..3e0eb5ac16b 100644 --- a/homeassistant/components/nest/legacy/climate.py +++ b/homeassistant/components/nest/legacy/climate.py @@ -32,6 +32,7 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from .const import DATA_NEST, DOMAIN, SIGNAL_NEST_UPDATE @@ -166,15 +167,15 @@ class NestThermostat(ClimateEntity): return self.device.serial @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return information about the device.""" - return { - "identifiers": {(DOMAIN, self.device.device_id)}, - "name": self.device.name_long, - "manufacturer": "Nest Labs", - "model": "Thermostat", - "sw_version": self.device.software_version, - } + return DeviceInfo( + identifiers={(DOMAIN, self.device.device_id)}, + manufacturer="Nest Labs", + model="Thermostat", + name=self.device.name_long, + sw_version=self.device.software_version, + ) @property def name(self): diff --git a/homeassistant/components/nexia/entity.py b/homeassistant/components/nexia/entity.py index 87e06fb05cf..06d3b90e048 100644 --- a/homeassistant/components/nexia/entity.py +++ b/homeassistant/components/nexia/entity.py @@ -1,7 +1,7 @@ """The nexia integration base entity.""" - from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -11,6 +11,7 @@ from .const import ( SIGNAL_THERMOSTAT_UPDATE, SIGNAL_ZONE_UPDATE, ) +from .coordinator import NexiaDataUpdateCoordinator class NexiaEntity(CoordinatorEntity): @@ -49,16 +50,17 @@ class NexiaThermostatEntity(NexiaEntity): self._thermostat = thermostat @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN, self._thermostat.thermostat_id)}, - "name": self._thermostat.get_name(), - "model": self._thermostat.get_model(), - "sw_version": self._thermostat.get_firmware(), - "manufacturer": MANUFACTURER, - "configuration_url": self.coordinator.nexia_home.root_url, - } + assert isinstance(self.coordinator, NexiaDataUpdateCoordinator) + return DeviceInfo( + configuration_url=self.coordinator.nexia_home.root_url, + identifiers={(DOMAIN, self._thermostat.thermostat_id)}, + manufacturer=MANUFACTURER, + model=self._thermostat.get_model(), + name=self._thermostat.get_name(), + sw_version=self._thermostat.get_firmware(), + ) async def async_added_to_hass(self): """Listen for signals for services.""" diff --git a/homeassistant/components/nmap_tracker/device_tracker.py b/homeassistant/components/nmap_tracker/device_tracker.py index b63280951a6..d1115fa8934 100644 --- a/homeassistant/components/nmap_tracker/device_tracker.py +++ b/homeassistant/components/nmap_tracker/device_tracker.py @@ -172,11 +172,11 @@ class NmapTrackerEntity(ScannerEntity): @property def device_info(self) -> DeviceInfo: """Return the device information.""" - return { - "connections": {(CONNECTION_NETWORK_MAC, self._mac_address)}, - "default_manufacturer": self._device.manufacturer, - "default_name": self.name, - } + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, self._mac_address)}, + default_manufacturer=self._device.manufacturer, + default_name=self.name, + ) @property def should_poll(self) -> bool: diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index c533a095270..7ec6e28401f 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -5,12 +5,8 @@ import logging from homeassistant.components.nut import PyNUTData from homeassistant.components.sensor import SensorEntity, SensorEntityDescription -from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_NAME, - CONF_RESOURCES, - STATE_UNKNOWN, -) +from homeassistant.const import CONF_RESOURCES, STATE_UNKNOWN +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -81,10 +77,10 @@ class NUTSensor(CoordinatorEntity, SensorEntity): self._attr_entity_registry_enabled_default = enabled_default self._attr_name = f"{device_name} {sensor_description.name}" self._attr_unique_id = f"{unique_id}_{sensor_description.key}" - self._attr_device_info = { - ATTR_IDENTIFIERS: {(DOMAIN, unique_id)}, - ATTR_NAME: device_name, - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + name=device_name, + ) self._attr_device_info.update(data.device_info) @property diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py index 0f0886c5f41..6f5bdd145c9 100644 --- a/homeassistant/components/nws/__init__.py +++ b/homeassistant/components/nws/__init__.py @@ -12,6 +12,7 @@ from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import debounce from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.dt import utcnow @@ -168,11 +169,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -def device_info(latitude, longitude): +def device_info(latitude, longitude) -> DeviceInfo: """Return device registry information.""" - return { - "identifiers": {(DOMAIN, base_unique_id(latitude, longitude))}, - "name": f"NWS: {latitude}, {longitude}", - "manufacturer": "National Weather Service", - "entry_type": "service", - } + return DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, base_unique_id(latitude, longitude))}, + manufacturer="National Weather Service", + name=f"NWS: {latitude}, {longitude}", + ) diff --git a/homeassistant/components/omnilogic/common.py b/homeassistant/components/omnilogic/common.py index 798c5abd69e..78685036e06 100644 --- a/homeassistant/components/omnilogic/common.py +++ b/homeassistant/components/omnilogic/common.py @@ -6,13 +6,8 @@ import logging from omnilogic import OmniLogic, OmniLogicException from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, -) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -152,15 +147,14 @@ class OmniLogicEntity(CoordinatorEntity): return self._attrs @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Define the device as back yard/MSP System.""" - - return { - ATTR_IDENTIFIERS: {(DOMAIN, self._msp_system_id)}, - ATTR_NAME: self._backyard_name, - ATTR_MANUFACTURER: "Hayward", - ATTR_MODEL: "OmniLogic", - } + return DeviceInfo( + identifiers={(DOMAIN, self._msp_system_id)}, + manufacturer="Hayward", + model="OmniLogic", + name=self._backyard_name, + ) def check_guard(state_key, item, entity_setting): diff --git a/homeassistant/components/ondilo_ico/sensor.py b/homeassistant/components/ondilo_ico/sensor.py index 693d685f77c..e02682ae298 100644 --- a/homeassistant/components/ondilo_ico/sensor.py +++ b/homeassistant/components/ondilo_ico/sensor.py @@ -16,6 +16,7 @@ from homeassistant.const import ( PERCENTAGE, TEMP_CELSIUS, ) +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -169,13 +170,13 @@ class OndiloICO(CoordinatorEntity, SensorEntity): return self._devdata()["value"] @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info for the sensor.""" pooldata = self._pooldata() - return { - "identifiers": {(DOMAIN, pooldata["ICO"]["serial_number"])}, - "name": self._device_name, - "manufacturer": "Ondilo", - "model": "ICO", - "sw_version": pooldata["ICO"]["sw_version"], - } + return DeviceInfo( + identifiers={(DOMAIN, pooldata["ICO"]["serial_number"])}, + manufacturer="Ondilo", + model="ICO", + name=self._device_name, + sw_version=pooldata["ICO"]["sw_version"], + ) diff --git a/homeassistant/components/onvif/base.py b/homeassistant/components/onvif/base.py index ab41fcc8f29..e4046f3c800 100644 --- a/homeassistant/components/onvif/base.py +++ b/homeassistant/components/onvif/base.py @@ -1,6 +1,6 @@ """Base classes for ONVIF entities.""" from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN from .device import ONVIFDevice @@ -21,14 +21,14 @@ class ONVIFBaseEntity(Entity): return self.device.available @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" - device_info = { - "manufacturer": self.device.info.manufacturer, - "model": self.device.info.model, - "name": self.device.name, - "sw_version": self.device.info.fw_version, - "identifiers": { + connections = None + if self.device.info.mac: + connections = {(CONNECTION_NETWORK_MAC, self.device.info.mac)} + return DeviceInfo( + connections=connections, + identifiers={ # MAC address is not always available, and given the number # of non-conformant ONVIF devices we have historically supported, # we can not guarantee serial number either. Due to this, we have @@ -37,11 +37,8 @@ class ONVIFBaseEntity(Entity): # See: https://github.com/home-assistant/core/issues/35883 (DOMAIN, self.device.info.mac or self.device.info.serial_number) }, - } - - if self.device.info.mac: - device_info["connections"] = { - (CONNECTION_NETWORK_MAC, self.device.info.mac) - } - - return device_info + manufacturer=self.device.info.manufacturer, + model=self.device.info.model, + name=self.device.name, + sw_version=self.device.info.fw_version, + ) diff --git a/homeassistant/components/opentherm_gw/binary_sensor.py b/homeassistant/components/opentherm_gw/binary_sensor.py index a896b37a26b..81160729c4b 100644 --- a/homeassistant/components/opentherm_gw/binary_sensor.py +++ b/homeassistant/components/opentherm_gw/binary_sensor.py @@ -6,7 +6,7 @@ from homeassistant.components.binary_sensor import ENTITY_ID_FORMAT, BinarySenso from homeassistant.const import CONF_ID from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.helpers.entity import DeviceInfo, async_generate_entity_id from homeassistant.helpers.entity_registry import async_get_registry from . import DOMAIN @@ -131,15 +131,15 @@ class OpenThermBinarySensor(BinarySensorEntity): return self._friendly_name @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info.""" - return { - "identifiers": {(DOMAIN, self._gateway.gw_id)}, - "name": self._gateway.name, - "manufacturer": "Schelte Bron", - "model": "OpenTherm Gateway", - "sw_version": self._gateway.gw_version, - } + return DeviceInfo( + identifiers={(DOMAIN, self._gateway.gw_id)}, + manufacturer="Schelte Bron", + model="OpenTherm Gateway", + name=self._gateway.name, + sw_version=self._gateway.gw_version, + ) @property def unique_id(self): diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index 294088ee608..269a154dd6c 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -25,7 +25,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.helpers.entity import DeviceInfo, async_generate_entity_id from . import DOMAIN from .const import ( @@ -171,15 +171,15 @@ class OpenThermClimate(ClimateEntity): return self.friendly_name @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info.""" - return { - "identifiers": {(DOMAIN, self._gateway.gw_id)}, - "name": self._gateway.name, - "manufacturer": "Schelte Bron", - "model": "OpenTherm Gateway", - "sw_version": self._gateway.gw_version, - } + return DeviceInfo( + identifiers={(DOMAIN, self._gateway.gw_id)}, + manufacturer="Schelte Bron", + model="OpenTherm Gateway", + name=self._gateway.name, + sw_version=self._gateway.gw_version, + ) @property def unique_id(self): diff --git a/homeassistant/components/opentherm_gw/sensor.py b/homeassistant/components/opentherm_gw/sensor.py index 28f139f188f..a29dd319176 100644 --- a/homeassistant/components/opentherm_gw/sensor.py +++ b/homeassistant/components/opentherm_gw/sensor.py @@ -6,7 +6,7 @@ from homeassistant.components.sensor import ENTITY_ID_FORMAT, SensorEntity from homeassistant.const import CONF_ID from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.helpers.entity import DeviceInfo, async_generate_entity_id from homeassistant.helpers.entity_registry import async_get_registry from . import DOMAIN @@ -135,15 +135,15 @@ class OpenThermSensor(SensorEntity): return self._friendly_name @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info.""" - return { - "identifiers": {(DOMAIN, self._gateway.gw_id)}, - "name": self._gateway.name, - "manufacturer": "Schelte Bron", - "model": "OpenTherm Gateway", - "sw_version": self._gateway.gw_version, - } + return DeviceInfo( + identifiers={(DOMAIN, self._gateway.gw_id)}, + manufacturer="Schelte Bron", + model="OpenTherm Gateway", + name=self._gateway.name, + sw_version=self._gateway.gw_version, + ) @property def unique_id(self): diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index 6da352abf0a..13b282b5ef6 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( @@ -70,12 +71,12 @@ class AbstractOpenWeatherMapSensor(SensorEntity): self._attr_name = f"{name} {description.name}" self._attr_unique_id = unique_id split_unique_id = unique_id.split("-") - self._attr_device_info = { - "identifiers": {(DOMAIN, f"{split_unique_id[0]}-{split_unique_id[1]}")}, - "name": DEFAULT_NAME, - "manufacturer": MANUFACTURER, - "entry_type": "service", - } + self._attr_device_info = DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, f"{split_unique_id[0]}-{split_unique_id[1]}")}, + manufacturer=MANUFACTURER, + name=DEFAULT_NAME, + ) @property def attribution(self): diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index ffd3e4b7269..f80566be329 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -1,6 +1,7 @@ """Support for the OpenWeatherMap (OWM) service.""" from homeassistant.components.weather import WeatherEntity from homeassistant.const import PRESSURE_HPA, PRESSURE_INHG, TEMP_CELSIUS +from homeassistant.helpers.entity import DeviceInfo from homeassistant.util.pressure import convert as pressure_convert from .const import ( @@ -58,14 +59,14 @@ class OpenWeatherMapWeather(WeatherEntity): return self._unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" - return { - "identifiers": {(DOMAIN, self._unique_id)}, - "name": DEFAULT_NAME, - "manufacturer": MANUFACTURER, - "entry_type": "service", - } + return DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, self._unique_id)}, + manufacturer=MANUFACTURER, + name=DEFAULT_NAME, + ) @property def should_poll(self): diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index f8c23b4f4f0..cb3fba80378 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -111,9 +111,9 @@ class OVOEnergyDeviceEntity(OVOEnergyEntity): @property def device_info(self) -> DeviceInfo: """Return device information about this OVO Energy instance.""" - return { - "identifiers": {(DOMAIN, self._client.account_id)}, - "manufacturer": "OVO Energy", - "name": self._client.username, - "entry_type": "service", - } + return DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, self._client.account_id)}, + manufacturer="OVO Energy", + name=self._client.username, + ) diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py index d50e5b9c414..7ba9346013f 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -13,6 +13,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers import device_registry +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.restore_state import RestoreEntity from . import DOMAIN as OT_DOMAIN @@ -117,9 +118,9 @@ class OwnTracksEntity(TrackerEntity, RestoreEntity): return self._data.get("source_type", SOURCE_TYPE_GPS) @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" - return {"name": self.name, "identifiers": {(OT_DOMAIN, self._dev_id)}} + return DeviceInfo(identifiers={(OT_DOMAIN, self._dev_id)}, name=self.name) async def async_added_to_hass(self): """Call when entity about to be added to Home Assistant.""" From 0aa06d22f10bc11009d879b583ddab93b240fb12 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 24 Oct 2021 11:35:46 +0200 Subject: [PATCH 0761/1038] Move `configuration_url` abbreviation to MQTT `DEVICE_ABBREVIATIONS` const (#58313) --- homeassistant/components/mqtt/abbreviations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index fa7344f82c5..155667b00cb 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -22,7 +22,6 @@ ABBREVIATIONS = { "clr_temp_cmd_tpl": "color_temp_command_template", "bat_lev_t": "battery_level_topic", "bat_lev_tpl": "battery_level_template", - "cu": "configuration_url", "chrg_t": "charging_topic", "chrg_tpl": "charging_template", "clrm": "color_mode", @@ -246,6 +245,7 @@ ABBREVIATIONS = { DEVICE_ABBREVIATIONS = { "cns": "connections", + "cu": "configuration_url", "ids": "identifiers", "name": "name", "mf": "manufacturer", From 31aa168bbb8c58b8af5297014e40dc8df17f9df9 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 24 Oct 2021 02:39:39 -0700 Subject: [PATCH 0762/1038] Fix bug in MediaSource definintion and enable strict type checking (#58321) --- .strict-typing | 1 + .../components/media_source/__init__.py | 18 +++++++--- .../components/media_source/local_source.py | 33 +++++++++++-------- .../components/media_source/models.py | 15 +++++---- .../components/netatmo/media_source.py | 6 +--- homeassistant/components/xbox/media_source.py | 5 +-- mypy.ini | 14 ++++++-- script/hassfest/mypy_config.py | 1 - 8 files changed, 55 insertions(+), 38 deletions(-) diff --git a/.strict-typing b/.strict-typing index 5fb4b1759ee..2dadf1a1d5b 100644 --- a/.strict-typing +++ b/.strict-typing @@ -72,6 +72,7 @@ homeassistant.components.mailbox.* homeassistant.components.media_player.* homeassistant.components.modbus.* homeassistant.components.modem_callerid.* +homeassistant.components.media_source.* homeassistant.components.mysensors.* homeassistant.components.nam.* homeassistant.components.nanoleaf.* diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py index cb485ac765f..e6cd86ef08c 100644 --- a/homeassistant/components/media_source/__init__.py +++ b/homeassistant/components/media_source/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import timedelta +from typing import Any from urllib.parse import quote import voluptuous as vol @@ -10,6 +11,7 @@ from homeassistant.components import websocket_api from homeassistant.components.http.auth import async_sign_path from homeassistant.components.media_player.const import ATTR_MEDIA_CONTENT_ID from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.websocket_api import ActiveConnection from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.integration_platform import ( async_process_integration_platforms, @@ -24,7 +26,7 @@ from .error import Unresolvable DEFAULT_EXPIRY_TIME = 3600 * 24 -def is_media_source_id(media_content_id: str): +def is_media_source_id(media_content_id: str) -> bool: """Test if identifier is a media source.""" return URI_SCHEME_REGEX.match(media_content_id) is not None @@ -52,7 +54,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def _process_media_source_platform(hass, domain, platform): +async def _process_media_source_platform( + hass: HomeAssistant, domain: str, platform: Any +) -> None: """Process a media source platform.""" hass.data[DOMAIN][domain] = await platform.async_get_media_source(hass) @@ -93,10 +97,12 @@ async def async_resolve_media( } ) @websocket_api.async_response -async def websocket_browse_media(hass, connection, msg): +async def websocket_browse_media( + hass: HomeAssistant, connection: ActiveConnection, msg: dict +) -> None: """Browse available media.""" try: - media = await async_browse_media(hass, msg.get("media_content_id")) + media = await async_browse_media(hass, msg.get("media_content_id", "")) connection.send_result( msg["id"], media.as_dict(), @@ -113,7 +119,9 @@ async def websocket_browse_media(hass, connection, msg): } ) @websocket_api.async_response -async def websocket_resolve_media(hass, connection, msg): +async def websocket_resolve_media( + hass: HomeAssistant, connection: ActiveConnection, msg: dict +) -> None: """Resolve media.""" try: media = await async_resolve_media(hass, msg["media_content_id"]) diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py index ab2e18183de..c7b25b4400c 100644 --- a/homeassistant/components/media_source/local_source.py +++ b/homeassistant/components/media_source/local_source.py @@ -18,7 +18,7 @@ from .models import BrowseMediaSource, MediaSource, MediaSourceItem, PlayMedia @callback -def async_setup(hass: HomeAssistant): +def async_setup(hass: HomeAssistant) -> None: """Set up local media source.""" source = LocalSource(hass) hass.data[DOMAIN][DOMAIN] = source @@ -36,7 +36,7 @@ class LocalSource(MediaSource): self.hass = hass @callback - def async_full_path(self, source_dir_id, location) -> Path: + def async_full_path(self, source_dir_id: str, location: str) -> Path: """Return full path.""" return Path(self.hass.config.media_dirs[source_dir_id], location) @@ -58,7 +58,7 @@ class LocalSource(MediaSource): return source_dir_id, location - async def async_resolve_media(self, item: MediaSourceItem) -> str: + async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve media to a url.""" source_dir_id, location = self.async_parse_identifier(item) if source_dir_id == "" or source_dir_id not in self.hass.config.media_dirs: @@ -67,22 +67,22 @@ class LocalSource(MediaSource): mime_type, _ = mimetypes.guess_type( str(self.async_full_path(source_dir_id, location)) ) + assert isinstance(mime_type, str) return PlayMedia(f"/media/{item.identifier}", mime_type) - async def async_browse_media( - self, item: MediaSourceItem, media_types: tuple[str] = MEDIA_MIME_TYPES - ) -> BrowseMediaSource: + async def async_browse_media(self, item: MediaSourceItem) -> BrowseMediaSource: """Return media.""" try: source_dir_id, location = self.async_parse_identifier(item) except Unresolvable as err: raise BrowseError(str(err)) from err - return await self.hass.async_add_executor_job( + result = await self.hass.async_add_executor_job( self._browse_media, source_dir_id, location ) + return result - def _browse_media(self, source_dir_id: str, location: Path): + def _browse_media(self, source_dir_id: str, location: str) -> BrowseMediaSource: """Browse media.""" # If only one media dir is configured, use that as the local media root @@ -122,9 +122,14 @@ class LocalSource(MediaSource): if not full_path.is_dir(): raise BrowseError("Path is not a directory.") - return self._build_item_response(source_dir_id, full_path) + result = self._build_item_response(source_dir_id, full_path) + if not result: + raise BrowseError("Unknown source directory.") + return result - def _build_item_response(self, source_dir_id: str, path: Path, is_child=False): + def _build_item_response( + self, source_dir_id: str, path: Path, is_child: bool = False + ) -> BrowseMediaSource | None: mime_type, _ = mimetypes.guess_type(str(path)) is_file = path.is_file() is_dir = path.is_dir() @@ -143,9 +148,11 @@ class LocalSource(MediaSource): if is_dir: title += "/" - media_class = MEDIA_CLASS_MAP.get( - mime_type and mime_type.split("/")[0], MEDIA_CLASS_DIRECTORY - ) + media_class = MEDIA_CLASS_DIRECTORY + if mime_type: + media_class = MEDIA_CLASS_MAP.get( + mime_type.split("/")[0], MEDIA_CLASS_DIRECTORY + ) media = BrowseMediaSource( domain=DOMAIN, diff --git a/homeassistant/components/media_source/models.py b/homeassistant/components/media_source/models.py index 247361296a1..32f0070176f 100644 --- a/homeassistant/components/media_source/models.py +++ b/homeassistant/components/media_source/models.py @@ -3,6 +3,7 @@ from __future__ import annotations from abc import ABC from dataclasses import dataclass +from typing import Any, cast from homeassistant.components.media_player import BrowseMedia from homeassistant.components.media_player.const import ( @@ -27,9 +28,11 @@ class PlayMedia: class BrowseMediaSource(BrowseMedia): """Represent a browsable media file.""" - children: list[BrowseMediaSource] | None + children: list[BrowseMediaSource | BrowseMedia] | None - def __init__(self, *, domain: str | None, identifier: str | None, **kwargs) -> None: + def __init__( + self, *, domain: str | None, identifier: str | None, **kwargs: Any + ) -> None: """Initialize media source browse media.""" media_content_id = f"{URI_SCHEME}{domain or ''}" if identifier: @@ -85,7 +88,7 @@ class MediaSourceItem: @callback def async_media_source(self) -> MediaSource: """Return media source that owns this item.""" - return self.hass.data[DOMAIN][self.domain] + return cast(MediaSource, self.hass.data[DOMAIN][self.domain]) @classmethod def from_uri(cls, hass: HomeAssistant, uri: str) -> MediaSourceItem: @@ -104,7 +107,7 @@ class MediaSourceItem: class MediaSource(ABC): """Represents a source of media files.""" - name: str = None + name: str | None = None def __init__(self, domain: str) -> None: """Initialize a media source.""" @@ -116,8 +119,6 @@ class MediaSource(ABC): """Resolve a media item to a playable item.""" raise NotImplementedError - async def async_browse_media( - self, item: MediaSourceItem, media_types: tuple[str] - ) -> BrowseMediaSource: + async def async_browse_media(self, item: MediaSourceItem) -> BrowseMediaSource: """Browse media.""" raise NotImplementedError diff --git a/homeassistant/components/netatmo/media_source.py b/homeassistant/components/netatmo/media_source.py index d80225c0368..f753a1163a5 100644 --- a/homeassistant/components/netatmo/media_source.py +++ b/homeassistant/components/netatmo/media_source.py @@ -52,11 +52,7 @@ class NetatmoSource(MediaSource): url = self.events[camera_id][event_id]["media_url"] return PlayMedia(url, MIME_TYPE) - async def async_browse_media( - self, - item: MediaSourceItem, - media_types: tuple[str] = ("video",), - ) -> BrowseMediaSource: + async def async_browse_media(self, item: MediaSourceItem) -> BrowseMediaSource: """Return media.""" try: source, camera_id, event_id = async_parse_identifier(item) diff --git a/homeassistant/components/xbox/media_source.py b/homeassistant/components/xbox/media_source.py index 06581088823..c6dae46a955 100644 --- a/homeassistant/components/xbox/media_source.py +++ b/homeassistant/components/xbox/media_source.py @@ -17,7 +17,6 @@ from homeassistant.components.media_player.const import ( MEDIA_CLASS_IMAGE, MEDIA_CLASS_VIDEO, ) -from homeassistant.components.media_source.const import MEDIA_MIME_TYPES from homeassistant.components.media_source.models import ( BrowseMediaSource, MediaSource, @@ -87,9 +86,7 @@ class XboxSource(MediaSource): kind = category.split("#", 1)[1] return PlayMedia(url, MIME_TYPE_MAP[kind]) - async def async_browse_media( - self, item: MediaSourceItem, media_types: tuple[str] = MEDIA_MIME_TYPES - ) -> BrowseMediaSource: + async def async_browse_media(self, item: MediaSourceItem) -> BrowseMediaSource: """Return media.""" title, category, _ = async_parse_identifier(item) diff --git a/mypy.ini b/mypy.ini index c338ebe5a31..9ea98222163 100644 --- a/mypy.ini +++ b/mypy.ini @@ -803,6 +803,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.media_source.*] +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.mysensors.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1692,9 +1703,6 @@ ignore_errors = true [mypy-homeassistant.components.lyric.*] ignore_errors = true -[mypy-homeassistant.components.media_source.*] -ignore_errors = true - [mypy-homeassistant.components.melcloud.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index d5542692e57..6ffd2e0da42 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -72,7 +72,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.luftdaten.*", "homeassistant.components.lutron_caseta.*", "homeassistant.components.lyric.*", - "homeassistant.components.media_source.*", "homeassistant.components.melcloud.*", "homeassistant.components.meteo_france.*", "homeassistant.components.metoffice.*", From 438ca73aba3f3d580145cc7a8ce8f614b8609ecd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 23 Oct 2021 23:46:35 -1000 Subject: [PATCH 0763/1038] Complete removal of auto_start, zeroconf_default_interface, and safe_mode from HomeKit (#58320) --- homeassistant/components/homekit/__init__.py | 14 +---- .../components/homekit/config_flow.py | 35 ++++------- homeassistant/components/homekit/const.py | 7 --- tests/components/homekit/test_config_flow.py | 23 +------ tests/components/homekit/test_homekit.py | 60 ------------------- 5 files changed, 17 insertions(+), 122 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 2c93f9db61e..9d8c3d04302 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -69,7 +69,6 @@ from .const import ( BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, CONF_ADVERTISE_IP, - CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_ENTRY_INDEX, CONF_EXCLUDE_ACCESSORY_MODE, @@ -80,14 +79,10 @@ from .const import ( CONF_LINKED_DOORBELL_SENSOR, CONF_LINKED_HUMIDITY_SENSOR, CONF_LINKED_MOTION_SENSOR, - CONF_SAFE_MODE, - CONF_ZEROCONF_DEFAULT_INTERFACE, CONFIG_OPTIONS, - DEFAULT_AUTO_START, DEFAULT_EXCLUDE_ACCESSORY_MODE, DEFAULT_HOMEKIT_MODE, DEFAULT_PORT, - DEFAULT_SAFE_MODE, DOMAIN, HOMEKIT, HOMEKIT_MODE_ACCESSORY, @@ -141,9 +136,6 @@ def _has_all_unique_names_and_ports(bridges): BRIDGE_SCHEMA = vol.All( - cv.deprecated(CONF_ZEROCONF_DEFAULT_INTERFACE), - cv.deprecated(CONF_SAFE_MODE), - cv.deprecated(CONF_AUTO_START), vol.Schema( { vol.Optional(CONF_HOMEKIT_MODE, default=DEFAULT_HOMEKIT_MODE): vol.In( @@ -155,11 +147,8 @@ BRIDGE_SCHEMA = vol.All( vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_IP_ADDRESS): vol.All(ipaddress.ip_address, cv.string), vol.Optional(CONF_ADVERTISE_IP): vol.All(ipaddress.ip_address, cv.string), - vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): cv.boolean, - vol.Optional(CONF_SAFE_MODE, default=DEFAULT_SAFE_MODE): cv.boolean, vol.Optional(CONF_FILTER, default={}): BASE_FILTER_SCHEMA, vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config, - vol.Optional(CONF_ZEROCONF_DEFAULT_INTERFACE): cv.boolean, vol.Optional(CONF_DEVICES): cv.ensure_list, }, extra=vol.ALLOW_EXTRA, @@ -279,7 +268,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) homekit_mode = options.get(CONF_HOMEKIT_MODE, DEFAULT_HOMEKIT_MODE) entity_config = options.get(CONF_ENTITY_CONFIG, {}).copy() - auto_start = options.get(CONF_AUTO_START, DEFAULT_AUTO_START) entity_filter = FILTER_SCHEMA(options.get(CONF_FILTER, {})) devices = options.get(CONF_DEVICES, []) @@ -307,7 +295,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if hass.state == CoreState.running: await homekit.async_start() - elif auto_start: + else: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, homekit.async_start) return True diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index 79fc43fde3a..a79db949ab0 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -36,14 +36,12 @@ from homeassistant.helpers.entityfilter import ( from homeassistant.loader import async_get_integration from .const import ( - CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_EXCLUDE_ACCESSORY_MODE, CONF_FILTER, CONF_HOMEKIT_MODE, CONF_SUPPORT_AUDIO, CONF_VIDEO_CODEC, - DEFAULT_AUTO_START, DEFAULT_CONFIG_FLOW_PORT, DEFAULT_HOMEKIT_MODE, DOMAIN, @@ -311,14 +309,14 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_advanced(self, user_input=None): """Choose advanced options.""" - if not self.show_advanced_options or user_input is not None: + if ( + not self.show_advanced_options + or user_input is not None + or self.hk_options[CONF_HOMEKIT_MODE] != HOMEKIT_MODE_BRIDGE + ): if user_input: self.hk_options.update(user_input) - self.hk_options[CONF_AUTO_START] = self.hk_options.get( - CONF_AUTO_START, DEFAULT_AUTO_START - ) - for key in (CONF_DOMAINS, CONF_ENTITIES): if key in self.hk_options: del self.hk_options[key] @@ -331,23 +329,16 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return self.async_create_entry(title="", data=self.hk_options) - data_schema = { - vol.Optional( - CONF_AUTO_START, - default=self.hk_options.get(CONF_AUTO_START, DEFAULT_AUTO_START), - ): bool - } - - if self.hk_options[CONF_HOMEKIT_MODE] == HOMEKIT_MODE_BRIDGE: - all_supported_devices = await _async_get_supported_devices(self.hass) - devices = self.hk_options.get(CONF_DEVICES, []) - data_schema[vol.Optional(CONF_DEVICES, default=devices)] = cv.multi_select( - all_supported_devices - ) - + all_supported_devices = await _async_get_supported_devices(self.hass) return self.async_show_form( step_id="advanced", - data_schema=vol.Schema(data_schema), + data_schema=vol.Schema( + { + vol.Optional( + CONF_DEVICES, default=self.hk_options.get(CONF_DEVICES, []) + ): cv.multi_select(all_supported_devices) + } + ), ) async def async_step_cameras(self, user_input=None): diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 4638c9f3b62..c77efb705da 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -34,7 +34,6 @@ CONF_ADVERTISE_IP = "advertise_ip" CONF_AUDIO_CODEC = "audio_codec" CONF_AUDIO_MAP = "audio_map" CONF_AUDIO_PACKET_SIZE = "audio_packet_size" -CONF_AUTO_START = "auto_start" CONF_ENTITY_CONFIG = "entity_config" CONF_FEATURE = "feature" CONF_FEATURE_LIST = "feature_list" @@ -50,8 +49,6 @@ CONF_LOW_BATTERY_THRESHOLD = "low_battery_threshold" CONF_MAX_FPS = "max_fps" CONF_MAX_HEIGHT = "max_height" CONF_MAX_WIDTH = "max_width" -CONF_SAFE_MODE = "safe_mode" -CONF_ZEROCONF_DEFAULT_INTERFACE = "zeroconf_default_interface" CONF_STREAM_ADDRESS = "stream_address" CONF_STREAM_SOURCE = "stream_source" CONF_SUPPORT_AUDIO = "support_audio" @@ -65,7 +62,6 @@ DEFAULT_SUPPORT_AUDIO = False DEFAULT_AUDIO_CODEC = AUDIO_CODEC_OPUS DEFAULT_AUDIO_MAP = "0:a:0" DEFAULT_AUDIO_PACKET_SIZE = 188 -DEFAULT_AUTO_START = True DEFAULT_EXCLUDE_ACCESSORY_MODE = False DEFAULT_LOW_BATTERY_THRESHOLD = 20 DEFAULT_MAX_FPS = 30 @@ -73,7 +69,6 @@ DEFAULT_MAX_HEIGHT = 1080 DEFAULT_MAX_WIDTH = 1920 DEFAULT_PORT = 21063 DEFAULT_CONFIG_FLOW_PORT = 21064 -DEFAULT_SAFE_MODE = False DEFAULT_VIDEO_CODEC = VIDEO_CODEC_LIBX264 DEFAULT_VIDEO_MAP = "0:v:0" DEFAULT_VIDEO_PACKET_SIZE = 1316 @@ -293,8 +288,6 @@ HK_NOT_CHARGABLE = 2 # ### Config Options ### CONFIG_OPTIONS = [ CONF_FILTER, - CONF_AUTO_START, - CONF_SAFE_MODE, CONF_ENTITY_CONFIG, CONF_HOMEKIT_MODE, CONF_DEVICES, diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index 7fa40b00f9c..e5d395a1f29 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -1,8 +1,6 @@ """Test the HomeKit config flow.""" from unittest.mock import patch -import pytest - from homeassistant import config_entries, data_entry_flow from homeassistant.components.homekit.const import DOMAIN, SHORT_BRIDGE_NAME from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_IMPORT @@ -273,8 +271,7 @@ async def test_import(hass, mock_get_source_ip): assert len(mock_setup_entry.mock_calls) == 2 -@pytest.mark.parametrize("auto_start", [True, False]) -async def test_options_flow_exclude_mode_advanced(auto_start, hass, mock_get_source_ip): +async def test_options_flow_exclude_mode_advanced(hass, mock_get_source_ip): """Test config flow options in exclude mode with advanced options.""" config_entry = _mock_config_entry_with_options_populated() @@ -308,12 +305,11 @@ async def test_options_flow_exclude_mode_advanced(auto_start, hass, mock_get_sou with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): result3 = await hass.config_entries.options.async_configure( result2["flow_id"], - user_input={"auto_start": auto_start}, + user_input={}, ) assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - "auto_start": auto_start, "devices": [], "mode": "bridge", "filter": { @@ -355,7 +351,6 @@ async def test_options_flow_exclude_mode_basic(hass, mock_get_source_ip): ) assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - "auto_start": True, "mode": "bridge", "filter": { "exclude_domains": [], @@ -412,12 +407,11 @@ async def test_options_flow_devices( with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): result3 = await hass.config_entries.options.async_configure( result2["flow_id"], - user_input={"auto_start": True, "devices": [device_id]}, + user_input={"devices": [device_id]}, ) assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - "auto_start": True, "devices": [device_id], "mode": "bridge", "filter": { @@ -486,7 +480,6 @@ async def test_options_flow_devices_preserved_when_advanced_off( assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - "auto_start": True, "devices": ["1fabcabcabcabcabcabcabcabcabc"], "mode": "bridge", "filter": { @@ -530,7 +523,6 @@ async def test_options_flow_include_mode_basic(hass, mock_get_source_ip): ) assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - "auto_start": True, "mode": "bridge", "filter": { "exclude_domains": [], @@ -586,7 +578,6 @@ async def test_options_flow_exclude_mode_with_cameras(hass, mock_get_source_ip): assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - "auto_start": True, "mode": "bridge", "filter": { "exclude_domains": [], @@ -632,7 +623,6 @@ async def test_options_flow_exclude_mode_with_cameras(hass, mock_get_source_ip): assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - "auto_start": True, "mode": "bridge", "filter": { "exclude_domains": [], @@ -689,7 +679,6 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip): assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - "auto_start": True, "mode": "bridge", "filter": { "exclude_domains": [], @@ -762,7 +751,6 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip): assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - "auto_start": True, "entity_config": {"camera.native_h264": {}}, "filter": { "exclude_domains": [], @@ -819,7 +807,6 @@ async def test_options_flow_with_camera_audio(hass, mock_get_source_ip): assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - "auto_start": True, "mode": "bridge", "filter": { "exclude_domains": [], @@ -892,7 +879,6 @@ async def test_options_flow_with_camera_audio(hass, mock_get_source_ip): assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - "auto_start": True, "entity_config": {"camera.audio": {}}, "filter": { "exclude_domains": [], @@ -911,7 +897,6 @@ async def test_options_flow_blocked_when_from_yaml(hass, mock_get_source_ip): domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345}, options={ - "auto_start": True, "devices": [], "filter": { "include_domains": [ @@ -988,7 +973,6 @@ async def test_options_flow_include_mode_basic_accessory(hass, mock_get_source_i ) assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - "auto_start": True, "mode": "accessory", "filter": { "exclude_domains": [], @@ -1093,7 +1077,6 @@ async def test_converting_bridge_to_accessory_mode(hass, hk_driver, mock_get_sou assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - "auto_start": True, "entity_config": {"camera.tv": {"video_codec": "copy"}}, "mode": "accessory", "filter": { diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index fbb715f1c39..e77980d4c2c 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -27,14 +27,12 @@ from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import ( BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, - CONF_AUTO_START, DEFAULT_PORT, DOMAIN, HOMEKIT, HOMEKIT_MODE_ACCESSORY, HOMEKIT_MODE_BRIDGE, SERVICE_HOMEKIT_RESET_ACCESSORY, - SERVICE_HOMEKIT_START, SERVICE_HOMEKIT_UNPAIR, ) from homeassistant.components.homekit.type_triggers import DeviceTriggerAccessory @@ -45,7 +43,6 @@ from homeassistant.const import ( ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, - CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, DEVICE_CLASS_BATTERY, @@ -184,63 +181,6 @@ async def test_setup_min(hass, mock_zeroconf): assert mock_homekit().async_start.called is True -async def test_setup_auto_start_disabled(hass, mock_zeroconf): - """Test async_setup with auto start disabled and test service calls.""" - - entry = MockConfigEntry( - domain=DOMAIN, - data={CONF_NAME: "Test Name", CONF_PORT: 11111, CONF_IP_ADDRESS: "172.0.0.0"}, - options={CONF_AUTO_START: False}, - ) - entry.add_to_hass(hass) - - with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit: - mock_homekit.return_value = homekit = Mock() - type(homekit).async_start = AsyncMock() - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - mock_homekit.assert_any_call( - hass, - "Test Name", - 11111, - "172.0.0.0", - ANY, - ANY, - {}, - HOMEKIT_MODE_BRIDGE, - None, - entry.entry_id, - entry.title, - devices=[], - ) - - # Test auto_start disabled - homekit.reset_mock() - homekit.async_start.reset_mock() - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - assert homekit.async_start.called is False - - # Test start call with driver is ready - homekit.reset_mock() - homekit.async_start.reset_mock() - homekit.status = STATUS_READY - - await hass.services.async_call(DOMAIN, SERVICE_HOMEKIT_START, blocking=True) - await hass.async_block_till_done() - assert homekit.async_start.called is True - - # Test start call with driver started - homekit.reset_mock() - homekit.async_start.reset_mock() - homekit.status = STATUS_STOPPED - - await hass.services.async_call(DOMAIN, SERVICE_HOMEKIT_START, blocking=True) - await hass.async_block_till_done() - assert homekit.async_start.called is False - - async def test_homekit_setup(hass, hk_driver, mock_zeroconf): """Test setup of bridge and driver.""" entry = MockConfigEntry( From 6f0c1d23451140db205065dc4bf88bc3e44e1998 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 24 Oct 2021 11:46:46 +0200 Subject: [PATCH 0764/1038] Complete Solar Light (tyndj) device support to Tuya (#58302) --- homeassistant/components/tuya/const.py | 1 + homeassistant/components/tuya/sensor.py | 3 +++ homeassistant/components/tuya/switch.py | 10 ++++++++++ 3 files changed, 14 insertions(+) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 0cf46390601..75589573042 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -269,6 +269,7 @@ class DPCode(str, Enum): SWITCH_LED_2 = "switch_led_2" SWITCH_LED_3 = "switch_led_3" SWITCH_NIGHT_LIGHT = "switch_night_light" + SWITCH_SAVE_ENERGY = "switch_save_energy" SWITCH_SPRAY = "switch_spray" # Spraying switch SWITCH_USB1 = "switch_usb1" # USB 1 SWITCH_USB2 = "switch_usb2" # USB 2 diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index f67d8f1c242..077b7803031 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -261,6 +261,9 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { state_class=STATE_CLASS_MEASUREMENT, ), ), + # Solar Light + # https://developer.tuya.com/en/docs/iot/tynd?id=Kaof8j02e1t98 + "tyndj": BATTERY_SENSORS, # Pressure Sensor # https://developer.tuya.com/en/docs/iot/categoryylcg?id=Kaiuz3kc2e4gm "ylcg": ( diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 95688f7dc78..1a37af30e2b 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -353,6 +353,16 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { entity_category=ENTITY_CATEGORY_CONFIG, ), ), + # Solar Light + # https://developer.tuya.com/en/docs/iot/tynd?id=Kaof8j02e1t98 + "tyndj": ( + SwitchEntityDescription( + key=DPCode.SWITCH_SAVE_ENERGY, + name="Energy Saving", + icon="mdi:leaf", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), # Ceiling Light # https://developer.tuya.com/en/docs/iot/ceiling-light?id=Kaiuz03xxfc4r "xdd": ( From 0f965c6b31fcb94f3bfcce69d89a570e3ed9861d Mon Sep 17 00:00:00 2001 From: TheNogl <19914064+TheNogl@users.noreply.github.com> Date: Sun, 24 Oct 2021 11:52:25 +0200 Subject: [PATCH 0765/1038] Add long-term statistics for Ondilo ICO (#58290) --- homeassistant/components/ondilo_ico/sensor.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ondilo_ico/sensor.py b/homeassistant/components/ondilo_ico/sensor.py index e02682ae298..e0a07c6fe26 100644 --- a/homeassistant/components/ondilo_ico/sensor.py +++ b/homeassistant/components/ondilo_ico/sensor.py @@ -6,7 +6,11 @@ import logging from ondilo import OndiloError -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, DEVICE_CLASS_BATTERY, @@ -32,6 +36,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=TEMP_CELSIUS, icon=None, device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="orp", @@ -39,6 +44,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=ELECTRIC_POTENTIAL_MILLIVOLT, icon="mdi:pool", device_class=None, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="ph", @@ -46,6 +52,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=None, icon="mdi:pool", device_class=None, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="tds", @@ -53,6 +60,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, icon="mdi:pool", device_class=None, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="battery", @@ -60,6 +68,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, icon=None, device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="rssi", @@ -67,6 +76,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, icon=None, device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, ), SensorEntityDescription( key="salt", @@ -74,6 +84,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement="mg/L", icon="mdi:pool", device_class=None, + state_class=STATE_CLASS_MEASUREMENT, ), ) From 539fdaad69ee6167b8c9908b8e8f95a99676eff4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 24 Oct 2021 15:58:11 +0200 Subject: [PATCH 0766/1038] Add VOC Sensor (voc) device support to Tuya (#58332) --- .../components/tuya/binary_sensor.py | 10 +++++ homeassistant/components/tuya/const.py | 1 + homeassistant/components/tuya/sensor.py | 40 +++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index a8627d17eae..7333070bd98 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -161,6 +161,16 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { ), TAMPER_BINARY_SENSOR, ), + # Volatile Organic Compound Sensor + # Note: Undocumented in cloud API docs, based on test device + "voc": ( + TuyaBinarySensorEntityDescription( + key=DPCode.VOC_STATE, + device_class=DEVICE_CLASS_SAFETY, + on_value="alarm", + ), + TAMPER_BINARY_SENSOR, + ), # Pressure Sensor # https://developer.tuya.com/en/docs/iot/categoryylcg?id=Kaiuz3kc2e4gm "ylcg": ( diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 75589573042..8cb501a3f92 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -291,6 +291,7 @@ class DPCode(str, Enum): UV = "uv" # UV sterilization VA_HUMIDITY = "va_humidity" VA_TEMPERATURE = "va_temperature" + VOC_STATE = "voc_state" VOC_VALUE = "voc_value" WARM = "warm" # Heat preservation WARM_TIME = "warm_time" # Heat preservation time diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 077b7803031..b5b9bc34d2f 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -264,6 +264,46 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { # Solar Light # https://developer.tuya.com/en/docs/iot/tynd?id=Kaof8j02e1t98 "tyndj": BATTERY_SENSORS, + # Volatile Organic Compound Sensor + # Note: Undocumented in cloud API docs, based on test device + "voc": ( + SensorEntityDescription( + key=DPCode.CO2_VALUE, + name="Carbon Dioxide", + device_class=DEVICE_CLASS_CO2, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.PM25_VALUE, + name="Particulate Matter 2.5 µm", + device_class=DEVICE_CLASS_PM25, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.CH2O_VALUE, + name="Formaldehyde", + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.HUMIDITY_VALUE, + name="Humidity", + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.TEMP_CURRENT, + name="Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.VOC_VALUE, + name="Volatile Organic Compound", + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + state_class=STATE_CLASS_MEASUREMENT, + ), + *BATTERY_SENSORS, + ), # Pressure Sensor # https://developer.tuya.com/en/docs/iot/categoryylcg?id=Kaiuz3kc2e4gm "ylcg": ( From a225d280892171e5894c7282df8415d09a1cfd37 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 24 Oct 2021 16:01:03 +0200 Subject: [PATCH 0767/1038] Add Methane Detector (jwbj) device support to Tuya (#58328) --- homeassistant/components/tuya/binary_sensor.py | 10 ++++++++++ homeassistant/components/tuya/const.py | 2 ++ homeassistant/components/tuya/sensor.py | 10 ++++++++++ 3 files changed, 22 insertions(+) diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index 7333070bd98..3c1a666ec79 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -97,6 +97,16 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { ), TAMPER_BINARY_SENSOR, ), + # Methane Detector + # https://developer.tuya.com/en/docs/iot/categoryjwbj?id=Kaiuz40u98lkm + "jwbj": ( + TuyaBinarySensorEntityDescription( + key=DPCode.CH4_SENSOR_STATE, + device_class=DEVICE_CLASS_GAS, + on_value="alarm", + ), + TAMPER_BINARY_SENSOR, + ), # Door Window Sensor # https://developer.tuya.com/en/docs/iot/s?id=K9gf48hm02l8m "mcs": ( diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 8cb501a3f92..5b8fd87242f 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -163,6 +163,8 @@ class DPCode(str, Enum): C_F = "c_f" # Temperature unit switching CH2O_STATE = "ch2o_state" CH2O_VALUE = "ch2o_value" + CH4_SENSOR_STATE = "ch4_sensor_state" + CH4_SENSOR_VALUE = "ch4_sensor_value" CHILD_LOCK = "child_lock" # Child lock CO_STATE = "co_state" CO_STATUS = "co_status" diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index b5b9bc34d2f..c74316f484f 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -172,6 +172,16 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { ), *BATTERY_SENSORS, ), + # Methane Detector + # https://developer.tuya.com/en/docs/iot/categoryjwbj?id=Kaiuz40u98lkm + "jwbj": ( + SensorEntityDescription( + key=DPCode.CH4_SENSOR_VALUE, + name="Methane", + state_class=STATE_CLASS_MEASUREMENT, + ), + *BATTERY_SENSORS, + ), # Luminance Sensor # https://developer.tuya.com/en/docs/iot/categoryldcg?id=Kaiuz3n7u69l8 "ldcg": ( From f2923d8a91fa1b9eb06a983228a60f0643d3482e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 24 Oct 2021 16:01:17 +0200 Subject: [PATCH 0768/1038] Add PM2.5 Sensor (pm25) device support to Tuya (#58329) --- .../components/tuya/binary_sensor.py | 10 ++++ homeassistant/components/tuya/const.py | 3 ++ homeassistant/components/tuya/sensor.py | 54 +++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index 3c1a666ec79..a1483447749 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -137,6 +137,16 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { ), TAMPER_BINARY_SENSOR, ), + # PM2.5 Sensor + # https://developer.tuya.com/en/docs/iot/categorypm25?id=Kaiuz3qof3yfu + "pm2.5": ( + TuyaBinarySensorEntityDescription( + key=DPCode.PM25_STATE, + device_class=DEVICE_CLASS_SAFETY, + on_value="alarm", + ), + TAMPER_BINARY_SENSOR, + ), # Gas Detector # https://developer.tuya.com/en/docs/iot/categoryrqbj?id=Kaiuz3d162ubw "rqbj": ( diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 5b8fd87242f..5da0bad19ac 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -227,6 +227,9 @@ class DPCode(str, Enum): PERCENT_STATE_2 = "percent_state_2" PERCENT_STATE_3 = "percent_state_3" PIR = "pir" # Motion sensor + PM1 = "pm1" + PM10 = "pm10" + PM25_STATE = "pm25_state" PM25_VALUE = "pm25_value" POWDER_SET = "powder_set" # Powder POWER_GO = "power_go" diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index c74316f484f..77ec9c1140d 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -19,6 +19,8 @@ from homeassistant.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, DEVICE_CLASS_PM25, DEVICE_CLASS_POWER, DEVICE_CLASS_PRESSURE, @@ -222,6 +224,58 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { # PIR Detector # https://developer.tuya.com/en/docs/iot/categorypir?id=Kaiuz3ss11b80 "pir": BATTERY_SENSORS, + # PM2.5 Sensor + # https://developer.tuya.com/en/docs/iot/categorypm25?id=Kaiuz3qof3yfu + "pm2.5": ( + SensorEntityDescription( + key=DPCode.PM25_VALUE, + name="Particulate Matter 2.5 µm", + device_class=DEVICE_CLASS_PM25, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.CH2O_VALUE, + name="Formaldehyde", + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.VOC_VALUE, + name="Volatile Organic Compound", + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.TEMP_CURRENT, + name="Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.CO2_VALUE, + name="Carbon Dioxide", + device_class=DEVICE_CLASS_CO2, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.HUMIDITY_VALUE, + name="Humidity", + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.PM1, + name="Particulate Matter 1.0 µm", + device_class=DEVICE_CLASS_PM1, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.PM10, + name="Particulate Matter 10.0 µm", + device_class=DEVICE_CLASS_PM10, + state_class=STATE_CLASS_MEASUREMENT, + ), + *BATTERY_SENSORS, + ), # Heater # https://developer.tuya.com/en/docs/iot/categoryqn?id=Kaiuz18kih0sm "qn": ( From 1a9bb47f78401340c80c4bfe52b93dcaee5d3625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 24 Oct 2021 17:01:50 +0300 Subject: [PATCH 0769/1038] Add more Huawei LTE sensor state classes (#57983) --- homeassistant/components/huawei_lte/sensor.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 568f7c31a53..4479f383524 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -249,9 +249,12 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { name="Battery", device_class=DEVICE_CLASS_BATTERY, unit=PERCENTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), (KEY_MONITORING_STATUS, "CurrentWifiUser"): SensorMeta( - name="WiFi clients connected", icon="mdi:wifi" + name="WiFi clients connected", + icon="mdi:wifi", + state_class=STATE_CLASS_MEASUREMENT, ), (KEY_MONITORING_STATUS, "PrimaryDns"): SensorMeta( name="Primary DNS server", icon="mdi:ip" @@ -296,7 +299,10 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { state_class=STATE_CLASS_MEASUREMENT, ), (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalConnectTime"): SensorMeta( - name="Total connected duration", unit=TIME_SECONDS, icon="mdi:timer-outline" + name="Total connected duration", + unit=TIME_SECONDS, + icon="mdi:timer-outline", + state_class=STATE_CLASS_TOTAL_INCREASING, ), (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalDownload"): SensorMeta( name="Total download", From 0de95610e3394b54d1eac400b3a738f3561a07f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 24 Oct 2021 18:25:01 +0300 Subject: [PATCH 0770/1038] Use http.HTTPStatus in components/[wxyz]* (#58330) --- homeassistant/components/wallbox/__init__.py | 3 ++- homeassistant/components/withings/common.py | 4 ++-- homeassistant/components/wsdot/sensor.py | 4 ++-- .../components/xiaomi/device_tracker.py | 7 ++++--- homeassistant/components/xmpp/notify.py | 4 ++-- homeassistant/components/yandextts/tts.py | 5 +++-- tests/components/wallbox/__init__.py | 5 +++-- tests/components/wallbox/test_config_flow.py | 11 +++++----- tests/components/webhook/test_init.py | 18 +++++++++------- tests/components/withings/common.py | 3 ++- tests/components/withings/test_common.py | 9 ++++---- tests/components/withings/test_config_flow.py | 4 +++- tests/components/xbox/test_config_flow.py | 3 ++- .../components/xiaomi/test_device_tracker.py | 5 +++-- tests/components/yandextts/test_tts.py | 21 +++++++++++-------- tests/components/zwave_js/test_api.py | 15 ++++++------- 16 files changed, 69 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py index 02c1cec668e..96ae5210d4c 100644 --- a/homeassistant/components/wallbox/__init__.py +++ b/homeassistant/components/wallbox/__init__.py @@ -1,5 +1,6 @@ """The Wallbox integration.""" from datetime import timedelta +from http import HTTPStatus import logging import requests @@ -45,7 +46,7 @@ class WallboxHub: self._wallbox.authenticate() return True except requests.exceptions.HTTPError as wallbox_connection_error: - if wallbox_connection_error.response.status_code == 403: + if wallbox_connection_error.response.status_code == HTTPStatus.FORBIDDEN: raise InvalidAuth from wallbox_connection_error raise ConnectionError from wallbox_connection_error diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index 83c5653afdf..608f20a4fb3 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -7,6 +7,7 @@ from dataclasses import dataclass import datetime from datetime import timedelta from enum import Enum, IntEnum +from http import HTTPStatus import logging import re from typing import Any, Dict @@ -32,7 +33,6 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.const import ( CONF_WEBHOOK_ID, - HTTP_UNAUTHORIZED, MASS_KILOGRAMS, PERCENTAGE, SPEED_METERS_PER_SECOND, @@ -58,7 +58,7 @@ from .const import Measurement _LOGGER = logging.getLogger(const.LOG_NAMESPACE) NOT_AUTHENTICATED_ERROR = re.compile( - f"^{HTTP_UNAUTHORIZED},.*", + f"^{HTTPStatus.UNAUTHORIZED},.*", re.IGNORECASE, ) DATA_UPDATED_SIGNAL = "withings_entity_state_updated" diff --git a/homeassistant/components/wsdot/sensor.py b/homeassistant/components/wsdot/sensor.py index bc0023ac54f..e0866f6f677 100644 --- a/homeassistant/components/wsdot/sensor.py +++ b/homeassistant/components/wsdot/sensor.py @@ -1,5 +1,6 @@ """Support for Washington State Department of Transportation (WSDOT) data.""" from datetime import datetime, timedelta, timezone +from http import HTTPStatus import logging import re @@ -13,7 +14,6 @@ from homeassistant.const import ( CONF_API_KEY, CONF_ID, CONF_NAME, - HTTP_OK, TIME_MINUTES, ) import homeassistant.helpers.config_validation as cv @@ -111,7 +111,7 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): } response = requests.get(RESOURCE, params, timeout=10) - if response.status_code != HTTP_OK: + if response.status_code != HTTPStatus.OK: _LOGGER.warning("Invalid response from WSDOT API") else: self._data = response.json() diff --git a/homeassistant/components/xiaomi/device_tracker.py b/homeassistant/components/xiaomi/device_tracker.py index 27c9aae89c9..c1e38f64c53 100644 --- a/homeassistant/components/xiaomi/device_tracker.py +++ b/homeassistant/components/xiaomi/device_tracker.py @@ -1,4 +1,5 @@ """Support for Xiaomi Mi routers.""" +from http import HTTPStatus import logging import requests @@ -9,7 +10,7 @@ from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, HTTP_OK +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -112,7 +113,7 @@ def _retrieve_list(host, token, **kwargs): except requests.exceptions.Timeout: _LOGGER.exception("Connection to the router timed out at URL %s", url) return - if res.status_code != HTTP_OK: + if res.status_code != HTTPStatus.OK: _LOGGER.exception("Connection failed with http code %s", res.status_code) return try: @@ -150,7 +151,7 @@ def _get_token(host, username, password): except requests.exceptions.Timeout: _LOGGER.exception("Connection to the router timed out") return - if res.status_code == HTTP_OK: + if res.status_code == HTTPStatus.OK: try: result = res.json() except ValueError: diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py index 1e022c9e72f..bef95faf1b2 100644 --- a/homeassistant/components/xmpp/notify.py +++ b/homeassistant/components/xmpp/notify.py @@ -1,5 +1,6 @@ """Jabber (XMPP) notification service.""" from concurrent.futures import TimeoutError as FutTimeoutError +from http import HTTPStatus import logging import mimetypes import pathlib @@ -29,7 +30,6 @@ from homeassistant.const import ( CONF_RESOURCE, CONF_ROOM, CONF_SENDER, - HTTP_BAD_REQUEST, ) import homeassistant.helpers.config_validation as cv import homeassistant.helpers.template as template_helper @@ -267,7 +267,7 @@ async def async_send_message( # noqa: C901 result = await hass.async_add_executor_job(get_url, url) - if result.status_code >= HTTP_BAD_REQUEST: + if result.status_code >= HTTPStatus.BAD_REQUEST: _LOGGER.error("Could not load file from %s", url) return None diff --git a/homeassistant/components/yandextts/tts.py b/homeassistant/components/yandextts/tts.py index 32b77e08df4..ec0868b2443 100644 --- a/homeassistant/components/yandextts/tts.py +++ b/homeassistant/components/yandextts/tts.py @@ -1,5 +1,6 @@ """Support for the yandex speechkit tts service.""" import asyncio +from http import HTTPStatus import logging import aiohttp @@ -7,7 +8,7 @@ import async_timeout import voluptuous as vol from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider -from homeassistant.const import CONF_API_KEY, HTTP_OK +from homeassistant.const import CONF_API_KEY from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -133,7 +134,7 @@ class YandexSpeechKitProvider(Provider): request = await websession.get(YANDEX_API_URL, params=url_param) - if request.status != HTTP_OK: + if request.status != HTTPStatus.OK: _LOGGER.error( "Error %d on load URL %s", request.status, request.url ) diff --git a/tests/components/wallbox/__init__.py b/tests/components/wallbox/__init__.py index 21554cc4456..c7d83665d94 100644 --- a/tests/components/wallbox/__init__.py +++ b/tests/components/wallbox/__init__.py @@ -1,5 +1,6 @@ """Tests for the Wallbox integration.""" +from http import HTTPStatus import json import requests_mock @@ -33,12 +34,12 @@ async def setup_integration(hass): mock_request.get( "https://api.wall-box.com/auth/token/user", text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', - status_code=200, + status_code=HTTPStatus.OK, ) mock_request.get( "https://api.wall-box.com/chargers/status/12345", json=test_response, - status_code=200, + status_code=HTTPStatus.OK, ) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/wallbox/test_config_flow.py b/tests/components/wallbox/test_config_flow.py index fba322182c9..acf62ee3fef 100644 --- a/tests/components/wallbox/test_config_flow.py +++ b/tests/components/wallbox/test_config_flow.py @@ -1,4 +1,5 @@ """Test the Wallbox config flow.""" +from http import HTTPStatus import json import requests_mock @@ -33,7 +34,7 @@ async def test_form_cannot_authenticate(hass): mock_request.get( "https://api.wall-box.com/auth/token/user", text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', - status_code=403, + status_code=HTTPStatus.FORBIDDEN, ) result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -58,12 +59,12 @@ async def test_form_cannot_connect(hass): mock_request.get( "https://api.wall-box.com/auth/token/user", text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', - status_code=200, + status_code=HTTPStatus.OK, ) mock_request.get( "https://api.wall-box.com/chargers/status/12345", text='{"Temperature": 100, "Location": "Toronto", "Datetime": "2020-07-23", "Units": "Celsius"}', - status_code=404, + status_code=HTTPStatus.NOT_FOUND, ) result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -88,12 +89,12 @@ async def test_form_validate_input(hass): mock_request.get( "https://api.wall-box.com/auth/token/user", text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', - status_code=200, + status_code=HTTPStatus.OK, ) mock_request.get( "https://api.wall-box.com/chargers/status/12345", text='{"Temperature": 100, "Location": "Toronto", "Datetime": "2020-07-23", "Units": "Celsius"}', - status_code=200, + status_code=HTTPStatus.OK, ) result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/webhook/test_init.py b/tests/components/webhook/test_init.py index 63d4a6e134d..f00c20af74a 100644 --- a/tests/components/webhook/test_init.py +++ b/tests/components/webhook/test_init.py @@ -1,4 +1,6 @@ """Test the webhook component.""" +from http import HTTPStatus + import pytest from homeassistant.config import async_process_ha_core_config @@ -24,13 +26,13 @@ async def test_unregistering_webhook(hass, mock_client): hass.components.webhook.async_register("test", "Test hook", webhook_id, handle) resp = await mock_client.post(f"/api/webhook/{webhook_id}") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert len(hooks) == 1 hass.components.webhook.async_unregister(webhook_id) resp = await mock_client.post(f"/api/webhook/{webhook_id}") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert len(hooks) == 1 @@ -54,14 +56,14 @@ async def test_async_generate_path(hass): async def test_posting_webhook_nonexisting(hass, mock_client): """Test posting to a nonexisting webhook.""" resp = await mock_client.post("/api/webhook/non-existing") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK async def test_posting_webhook_invalid_json(hass, mock_client): """Test posting to a nonexisting webhook.""" hass.components.webhook.async_register("test", "Test hook", "hello", None) resp = await mock_client.post("/api/webhook/hello", data="not-json") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK async def test_posting_webhook_json(hass, mock_client): @@ -76,7 +78,7 @@ async def test_posting_webhook_json(hass, mock_client): hass.components.webhook.async_register("test", "Test hook", webhook_id, handle) resp = await mock_client.post(f"/api/webhook/{webhook_id}", json={"data": True}) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert len(hooks) == 1 assert hooks[0][0] is hass assert hooks[0][1] == webhook_id @@ -95,7 +97,7 @@ async def test_posting_webhook_no_data(hass, mock_client): hass.components.webhook.async_register("test", "Test hook", webhook_id, handle) resp = await mock_client.post(f"/api/webhook/{webhook_id}") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert len(hooks) == 1 assert hooks[0][0] is hass assert hooks[0][1] == webhook_id @@ -115,7 +117,7 @@ async def test_webhook_put(hass, mock_client): hass.components.webhook.async_register("test", "Test hook", webhook_id, handle) resp = await mock_client.put(f"/api/webhook/{webhook_id}") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert len(hooks) == 1 assert hooks[0][0] is hass assert hooks[0][1] == webhook_id @@ -134,7 +136,7 @@ async def test_webhook_head(hass, mock_client): hass.components.webhook.async_register("test", "Test hook", webhook_id, handle) resp = await mock_client.head(f"/api/webhook/{webhook_id}") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert len(hooks) == 1 assert hooks[0][0] is hass assert hooks[0][1] == webhook_id diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index 847f482b6c5..71eb350410b 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import dataclass +from http import HTTPStatus from unittest.mock import MagicMock from urllib.parse import urlparse @@ -210,7 +211,7 @@ class ComponentFactory: # Simulate user being redirected from withings site. client: TestClient = await self._hass_client() resp = await client.get(f"{AUTH_CALLBACK_PATH}?code=abcd&state={state}") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert resp.headers["content-type"] == "text/html; charset=utf-8" self._aioclient_mock.clear_requests() diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index ef51f12398f..d65b80f256a 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -1,5 +1,6 @@ """Tests for the Withings component.""" import datetime +from http import HTTPStatus import re from typing import Any from unittest.mock import MagicMock @@ -43,7 +44,7 @@ async def test_config_entry_withings_api(hass: HomeAssistant) -> None: with requests_mock.mock() as rqmck: rqmck.get( re.compile(".*"), - status_code=200, + status_code=HTTPStatus.OK, json={"status": 0, "body": {"message": "success"}}, ) @@ -119,7 +120,7 @@ async def test_webhook_head( client: TestClient = await aiohttp_client(hass.http.app) resp = await client.head(urlparse(data_manager.webhook_config.url).path) - assert resp.status == 200 + assert resp.status == HTTPStatus.OK async def test_webhook_put( @@ -141,7 +142,7 @@ async def test_webhook_put( # Wait for remaining tasks to complete. await hass.async_block_till_done() - assert resp.status == 200 + assert resp.status == HTTPStatus.OK data = await resp.json() assert data assert data["code"] == 2 @@ -196,7 +197,7 @@ async def test_data_manager_webhook_subscription( aioclient_mock.request( "HEAD", data_manager.webhook_config.url, - status=200, + status=HTTPStatus.OK, ) # Test subscribing diff --git a/tests/components/withings/test_config_flow.py b/tests/components/withings/test_config_flow.py index 83368ed3fa1..210d1f669e9 100644 --- a/tests/components/withings/test_config_flow.py +++ b/tests/components/withings/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for config flow.""" +from http import HTTPStatus + from aiohttp.test_utils import TestClient from homeassistant import config_entries @@ -83,7 +85,7 @@ async def test_config_reauth_profile( client: TestClient = await hass_client_no_auth() resp = await client.get(f"{AUTH_CALLBACK_PATH}?code=abcd&state={state}") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert resp.headers["content-type"] == "text/html; charset=utf-8" aioclient_mock.clear_requests() diff --git a/tests/components/xbox/test_config_flow.py b/tests/components/xbox/test_config_flow.py index 794814c284f..f8c296dcbbe 100644 --- a/tests/components/xbox/test_config_flow.py +++ b/tests/components/xbox/test_config_flow.py @@ -1,4 +1,5 @@ """Test the xbox config flow.""" +from http import HTTPStatus from unittest.mock import patch from homeassistant import config_entries, data_entry_flow, setup @@ -56,7 +57,7 @@ async def test_full_flow( client = await hass_client_no_auth() resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert resp.headers["content-type"] == "text/html; charset=utf-8" aioclient_mock.post( diff --git a/tests/components/xiaomi/test_device_tracker.py b/tests/components/xiaomi/test_device_tracker.py index b5764fac089..3c42c6a2b05 100644 --- a/tests/components/xiaomi/test_device_tracker.py +++ b/tests/components/xiaomi/test_device_tracker.py @@ -1,4 +1,5 @@ """The tests for the Xiaomi router device tracker platform.""" +from http import HTTPStatus import logging from unittest.mock import MagicMock, call, patch @@ -40,8 +41,8 @@ def mocked_requests(*args, **kwargs): return self.json() def raise_for_status(self): - """Raise an HTTPError if status is not 200.""" - if self.status_code != 200: + """Raise an HTTPError if status is not OK.""" + if self.status_code != HTTPStatus.OK: raise requests.HTTPError(self.status_code) data = kwargs.get("data") diff --git a/tests/components/yandextts/test_tts.py b/tests/components/yandextts/test_tts.py index 3c5cc967c6b..beab0b396fd 100644 --- a/tests/components/yandextts/test_tts.py +++ b/tests/components/yandextts/test_tts.py @@ -69,7 +69,7 @@ class TestTTSYandexPlatform: "speed": 1, } aioclient_mock.get( - self._base_url, status=200, content=b"test", params=url_param + self._base_url, status=HTTPStatus.OK, content=b"test", params=url_param ) config = {tts.DOMAIN: {"platform": "yandextts", "api_key": "1234567xx"}} @@ -101,7 +101,7 @@ class TestTTSYandexPlatform: "speed": 1, } aioclient_mock.get( - self._base_url, status=200, content=b"test", params=url_param + self._base_url, status=HTTPStatus.OK, content=b"test", params=url_param ) config = { @@ -139,7 +139,7 @@ class TestTTSYandexPlatform: "speed": 1, } aioclient_mock.get( - self._base_url, status=200, content=b"test", params=url_param + self._base_url, status=HTTPStatus.OK, content=b"test", params=url_param ) config = {tts.DOMAIN: {"platform": "yandextts", "api_key": "1234567xx"}} @@ -175,7 +175,10 @@ class TestTTSYandexPlatform: "speed": 1, } aioclient_mock.get( - self._base_url, status=200, exc=asyncio.TimeoutError(), params=url_param + self._base_url, + status=HTTPStatus.OK, + exc=asyncio.TimeoutError(), + params=url_param, ) config = {tts.DOMAIN: {"platform": "yandextts", "api_key": "1234567xx"}} @@ -241,7 +244,7 @@ class TestTTSYandexPlatform: "speed": 1, } aioclient_mock.get( - self._base_url, status=200, content=b"test", params=url_param + self._base_url, status=HTTPStatus.OK, content=b"test", params=url_param ) config = { @@ -279,7 +282,7 @@ class TestTTSYandexPlatform: "speed": 1, } aioclient_mock.get( - self._base_url, status=200, content=b"test", params=url_param + self._base_url, status=HTTPStatus.OK, content=b"test", params=url_param ) config = { @@ -317,7 +320,7 @@ class TestTTSYandexPlatform: "speed": "0.1", } aioclient_mock.get( - self._base_url, status=200, content=b"test", params=url_param + self._base_url, status=HTTPStatus.OK, content=b"test", params=url_param ) config = { @@ -351,7 +354,7 @@ class TestTTSYandexPlatform: "speed": 2, } aioclient_mock.get( - self._base_url, status=200, content=b"test", params=url_param + self._base_url, status=HTTPStatus.OK, content=b"test", params=url_param ) config = { @@ -385,7 +388,7 @@ class TestTTSYandexPlatform: "speed": 2, } aioclient_mock.get( - self._base_url, status=200, content=b"test", params=url_param + self._base_url, status=HTTPStatus.OK, content=b"test", params=url_param ) config = {tts.DOMAIN: {"platform": "yandextts", "api_key": "1234567xx"}} diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index f41f5daefb3..e6bfbb45393 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -1,5 +1,6 @@ """Test the Z-Wave JS Websocket API.""" from copy import deepcopy +from http import HTTPStatus import json from unittest.mock import patch @@ -1976,7 +1977,7 @@ async def test_dump_view(integration, hass_client): return_value=[{"hello": "world"}, {"second": "msg"}], ): resp = await client.get(f"/api/zwave_js/dump/{integration.entry_id}") - assert resp.status == 200 + assert resp.status == HTTPStatus.OK assert json.loads(await resp.text()) == [{"hello": "world"}, {"second": "msg"}] @@ -2060,7 +2061,7 @@ async def test_firmware_upload_view_failed_command( f"/api/zwave_js/firmware/upload/{integration.entry_id}/{multisensor_6.node_id}", data={"file": firmware_file}, ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST async def test_firmware_upload_view_invalid_payload( @@ -2072,7 +2073,7 @@ async def test_firmware_upload_view_invalid_payload( f"/api/zwave_js/firmware/upload/{integration.entry_id}/{multisensor_6.node_id}", data={"wrong_key": bytes(10)}, ) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST @pytest.mark.parametrize( @@ -2087,7 +2088,7 @@ async def test_view_non_admin_user( # Verify we require admin user hass_admin_user.groups = [] resp = await client.request(method, url.format(integration.entry_id)) - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED @pytest.mark.parametrize( @@ -2104,7 +2105,7 @@ async def test_node_view_non_admin_user( resp = await client.request( method, url.format(integration.entry_id, multisensor_6.node_id) ) - assert resp.status == 401 + assert resp.status == HTTPStatus.UNAUTHORIZED @pytest.mark.parametrize( @@ -2118,7 +2119,7 @@ async def test_view_invalid_entry_id(integration, hass_client, method, url): """Test an invalid config entry id parameter.""" client = await hass_client() resp = await client.request(method, url) - assert resp.status == 400 + assert resp.status == HTTPStatus.BAD_REQUEST @pytest.mark.parametrize( @@ -2129,7 +2130,7 @@ async def test_view_invalid_node_id(integration, hass_client, method, url): """Test an invalid config entry id parameter.""" client = await hass_client() resp = await client.request(method, url.format(integration.entry_id)) - assert resp.status == 404 + assert resp.status == HTTPStatus.NOT_FOUND async def test_subscribe_log_updates(hass, integration, client, hass_ws_client): From 8c3b711c5edb99bf5f9205ab09192d9bcfaa0fd2 Mon Sep 17 00:00:00 2001 From: Ivan Belokobylskiy Date: Sun, 24 Oct 2021 18:36:35 +0300 Subject: [PATCH 0771/1038] Support suburban railways stations in yandex transport (#58281) * Add unique ids to yandex_transport entities * Add support for railway routes to yandex_transport component * Test suburban timetable parsed correctly * Remove redundant default value * Make unique_id unique and stable * Remove unique_id from yandex_transport component --- .../components/yandex_transport/sensor.py | 28 +- .../test_yandex_transport_sensor.py | 66 ++- ...y.json => yandex_transport_bus_reply.json} | 0 .../yandex_transport_suburban_reply.json | 547 ++++++++++++++++++ 4 files changed, 616 insertions(+), 25 deletions(-) rename tests/fixtures/{yandex_transport_reply.json => yandex_transport_bus_reply.json} (100%) create mode 100644 tests/fixtures/yandex_transport_suburban_reply.json diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py index 0acca753454..bd5d85d3ffe 100644 --- a/homeassistant/components/yandex_transport/sensor.py +++ b/homeassistant/components/yandex_transport/sensor.py @@ -61,7 +61,6 @@ class DiscoverYandexTransport(SensorEntity): """Initialize sensor.""" self.requester = requester self._stop_id = stop_id - self._routes = [] self._routes = routes self._state = None self._name = name @@ -96,22 +95,35 @@ class DiscoverYandexTransport(SensorEntity): stop_name = data["name"] transport_list = data["transports"] for transport in transport_list: - route = transport["name"] for thread in transport["threads"]: - if self._routes and route not in self._routes: - # skip unnecessary route info - continue if "Events" not in thread["BriefSchedule"]: continue + if thread.get("noBoarding") is True: + continue for event in thread["BriefSchedule"]["Events"]: - if "Estimated" not in event: + # Railway route depends on the essential stops and + # can vary over time. + # City transport has the fixed name for the route + if "railway" in transport["Types"]: + route = " - ".join( + [x["name"] for x in thread["EssentialStops"]] + ) + else: + route = transport["name"] + + if self._routes and route not in self._routes: + # skip unnecessary route info continue - posix_time_next = int(event["Estimated"]["value"]) + if "Estimated" not in event and "Scheduled" not in event: + continue + + departure = event.get("Estimated") or event["Scheduled"] + posix_time_next = int(departure["value"]) if closer_time is None or closer_time > posix_time_next: closer_time = posix_time_next if route not in attrs: attrs[route] = [] - attrs[route].append(event["Estimated"]["text"]) + attrs[route].append(departure["text"]) attrs[STOP_NAME] = stop_name attrs[ATTR_ATTRIBUTION] = ATTRIBUTION if closer_time is None: diff --git a/tests/components/yandex_transport/test_yandex_transport_sensor.py b/tests/components/yandex_transport/test_yandex_transport_sensor.py index f18bd34e671..c39ab979565 100644 --- a/tests/components/yandex_transport/test_yandex_transport_sensor.py +++ b/tests/components/yandex_transport/test_yandex_transport_sensor.py @@ -12,23 +12,38 @@ import homeassistant.util.dt as dt_util from tests.common import assert_setup_component, load_fixture -REPLY = json.loads(load_fixture("yandex_transport_reply.json")) +BUS_REPLY = json.loads(load_fixture("yandex_transport_bus_reply.json")) +SUBURBAN_TRAIN_REPLY = json.loads(load_fixture("yandex_transport_suburban_reply.json")) @pytest.fixture -def mock_requester(): +def mock_requester_bus(): """Create a mock for YandexMapsRequester.""" - with patch("aioymaps.YandexMapsRequester") as requester: + with patch( + "homeassistant.components.yandex_transport.sensor.YandexMapsRequester" + ) as requester: instance = requester.return_value instance.set_new_session = AsyncMock() - instance.get_stop_info = AsyncMock(return_value=REPLY) + instance.get_stop_info = AsyncMock(return_value=BUS_REPLY) + yield instance + + +@pytest.fixture +def mock_requester_suburban_train(): + """Create a mock for YandexMapsRequester.""" + with patch( + "homeassistant.components.yandex_transport.sensor.YandexMapsRequester" + ) as requester: + instance = requester.return_value + instance.set_new_session = AsyncMock() + instance.get_stop_info = AsyncMock(return_value=SUBURBAN_TRAIN_REPLY) yield instance STOP_ID = "stop__9639579" ROUTES = ["194", "т36", "т47", "м10"] NAME = "test_name" -TEST_CONFIG = { +TEST_BUS_CONFIG = { "sensor": { "platform": "yandex_transport", "stop_id": "stop__9639579", @@ -36,6 +51,13 @@ TEST_CONFIG = { "name": NAME, } } +TEST_SUBURBAN_CONFIG = { + "sensor": { + "platform": "yandex_transport", + "stop_id": "station__lh_9876336", + "name": NAME, + } +} FILTERED_ATTRS = { "т36": ["18:25", "18:42", "18:46"], @@ -45,7 +67,10 @@ FILTERED_ATTRS = { "attribution": "Data provided by maps.yandex.ru", } -RESULT_STATE = dt_util.utc_from_timestamp(1583421540).isoformat(timespec="seconds") +BUS_RESULT_STATE = dt_util.utc_from_timestamp(1583421540).isoformat(timespec="seconds") +SUBURBAN_RESULT_STATE = dt_util.utc_from_timestamp(1634984640).isoformat( + timespec="seconds" +) async def assert_setup_sensor(hass, config, count=1): @@ -55,35 +80,42 @@ async def assert_setup_sensor(hass, config, count=1): await hass.async_block_till_done() -async def test_setup_platform_valid_config(hass, mock_requester): +async def test_setup_platform_valid_config(hass, mock_requester_bus): """Test that sensor is set up properly with valid config.""" - await assert_setup_sensor(hass, TEST_CONFIG) + await assert_setup_sensor(hass, TEST_BUS_CONFIG) -async def test_setup_platform_invalid_config(hass, mock_requester): +async def test_setup_platform_invalid_config(hass, mock_requester_bus): """Check an invalid configuration.""" await assert_setup_sensor( hass, {"sensor": {"platform": "yandex_transport", "stopid": 1234}}, count=0 ) -async def test_name(hass, mock_requester): +async def test_name(hass, mock_requester_bus): """Return the name if set in the configuration.""" - await assert_setup_sensor(hass, TEST_CONFIG) + await assert_setup_sensor(hass, TEST_BUS_CONFIG) state = hass.states.get("sensor.test_name") - assert state.name == TEST_CONFIG["sensor"][CONF_NAME] + assert state.name == TEST_BUS_CONFIG["sensor"][CONF_NAME] -async def test_state(hass, mock_requester): +async def test_state(hass, mock_requester_bus): """Return the contents of _state.""" - await assert_setup_sensor(hass, TEST_CONFIG) + await assert_setup_sensor(hass, TEST_BUS_CONFIG) state = hass.states.get("sensor.test_name") - assert state.state == RESULT_STATE + assert state.state == BUS_RESULT_STATE -async def test_filtered_attributes(hass, mock_requester): +async def test_filtered_attributes(hass, mock_requester_bus): """Return the contents of attributes.""" - await assert_setup_sensor(hass, TEST_CONFIG) + await assert_setup_sensor(hass, TEST_BUS_CONFIG) state = hass.states.get("sensor.test_name") state_attrs = {key: state.attributes[key] for key in FILTERED_ATTRS} assert state_attrs == FILTERED_ATTRS + + +async def test_suburban_trains(hass, mock_requester_suburban_train): + """Return the contents of _state for suburban.""" + await assert_setup_sensor(hass, TEST_SUBURBAN_CONFIG) + state = hass.states.get("sensor.test_name") + assert state.state == SUBURBAN_RESULT_STATE diff --git a/tests/fixtures/yandex_transport_reply.json b/tests/fixtures/yandex_transport_bus_reply.json similarity index 100% rename from tests/fixtures/yandex_transport_reply.json rename to tests/fixtures/yandex_transport_bus_reply.json diff --git a/tests/fixtures/yandex_transport_suburban_reply.json b/tests/fixtures/yandex_transport_suburban_reply.json new file mode 100644 index 00000000000..e85f554afab --- /dev/null +++ b/tests/fixtures/yandex_transport_suburban_reply.json @@ -0,0 +1,547 @@ +{ + "data": { + "id": "station__lh_9876336", + "name": "Славянский Бульвар", + "coordinates": [ + 37.47243, + 55.730359 + ], + "currentTime": 1634984296770, + "tzOffset": 10800, + "type": "railway", + "region": { + "id": 213, + "capitalId": 0, + "hierarchy": [ + 225, + 1, + 213 + ], + "seoname": "moscow", + "bounds": [ + [ + 37.0402925, + 55.31141404514547 + ], + [ + 38.2047155, + 56.190068045145466 + ] + ], + "longitude": 37.622504, + "latitude": 55.753215, + "zoom": 10, + "names": { + "ablative": "", + "accusative": "Москву", + "dative": "Москве", + "directional": "", + "genitive": "Москвы", + "instrumental": "Москвой", + "locative": "", + "nominative": "Москва", + "preposition": "в", + "prepositional": "Москве" + } + }, + "transports": [ + { + "lineId": "lh_6031x7531_9600213_g21_4", + "name": "6031/7531", + "Types": [ + "suburban", + "railway" + ], + "type": "suburban", + "threads": [ + { + "threadId": "lh_6031x7531_0_9600213_g21_4", + "noBoarding": false, + "EssentialStops": [ + { + "id": "station__lh_9600213", + "name": "Аэропорт Шереметьево" + }, + { + "id": "station__lh_9600721", + "name": "Одинцово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1634984640", + "tzOffset": 10800, + "text": "13:24" + } + } + ], + "Frequencies": [], + "departureTime": "13:24" + } + } + ], + "uri": "ymapsbm1://transit/line?id=lh_6031x7531_9600213_g21_4&ll=37.349317%2C55.818167&name=6031%2F7531&r=16767&type=suburban", + "seoname": "6031_7531" + }, + { + "lineId": "lh_6183_9600781_g21_4", + "name": "6183", + "Types": [ + "suburban", + "railway" + ], + "type": "suburban", + "threads": [ + { + "threadId": "lh_6183_0_9600781_g21_4", + "noBoarding": false, + "EssentialStops": [ + { + "id": "station__lh_9600781", + "name": "Лобня" + }, + { + "id": "station__lh_9601006", + "name": "Можайск" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1634985180", + "tzOffset": 10800, + "text": "13:33" + } + } + ], + "Frequencies": [], + "departureTime": "13:33" + } + } + ], + "uri": "ymapsbm1://transit/line?id=lh_6183_9600781_g21_4&ll=36.753680%2C55.756043&name=6183&r=53892&type=suburban", + "seoname": "6183" + }, + { + "lineId": "lh_7268_9600721_g21_4", + "name": "7268", + "Types": [ + "suburban", + "railway" + ], + "type": "suburban", + "threads": [ + { + "threadId": "lh_7268_0_9600721_g21_4", + "noBoarding": false, + "EssentialStops": [ + { + "id": "station__lh_9600721", + "name": "Одинцово" + }, + { + "id": "station__lh_9600781", + "name": "Лобня" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1634985120", + "tzOffset": 10800, + "text": "13:32" + } + } + ], + "Frequencies": [], + "departureTime": "13:32" + } + } + ], + "uri": "ymapsbm1://transit/line?id=lh_7268_9600721_g21_4&ll=37.382971%2C55.843017&name=7268&r=20019&type=suburban", + "seoname": "7268" + } + ], + "links": [ + { + "type": "timetable", + "href": "https://rasp.yandex.ru/station/9876336?span=day&direction=all&type=suburban" + } + ], + "breadcrumbs": [ + { + "name": "Карты", + "type": "root", + "url": "https://yandex.ru/maps/" + }, + { + "name": "Москва", + "type": "region", + "url": "https://yandex.ru/maps/213/moscow/", + "region": { + "center": [ + 37.622504, + 55.753215 + ], + "zoom": 10 + } + }, + { + "name": "Общественный транспорт", + "type": "masstransit-home", + "url": "https://yandex.ru/maps/213/moscow/transport/" + }, + { + "name": "Славянский Бульвар", + "type": "search", + "url": "https://yandex.ru/maps/213/moscow/stops/station__lh_9876336/", + "currentPage": true + } + ], + "searchResult": { + "type": "business", + "requestId": "1634984296787090-1406171972-man2-7134-3a4-man-addrs-nmeta-new-8031", + "analyticsId": "1", + "title": "Станция метро Славянский бульвар", + "description": "Россия, Москва, Западный административный округ, район Фили-Давыдково", + "address": "Россия, Москва, Западный административный округ, район Фили-Давыдково", + "coordinates": [ + 37.47049, + 55.729442 + ], + "displayCoordinates": [ + 37.47049, + 55.729442 + ], + "bounds": [ + [ + 37.464149, + 55.725767 + ], + [ + 37.480606, + 55.735054 + ] + ], + "uri": "ymapsbm1://transit/stop?id=station__lh_9876336", + "logId": "dHlwZT1iaXpmaW5kZXI7aWQ9MjM2OTE0NjU0Nzcx", + "id": "236914654771", + "metro": [ + { + "id": "station__9858904", + "name": "Славянский бульвар", + "distance": "130 м", + "distanceValue": 131.128, + "coordinates": [ + 37.46880848, + 55.729018063 + ], + "type": "metro", + "color": "#003399" + }, + { + "id": "station__9858881", + "name": "Пионерская", + "distance": "1,1 км", + "distanceValue": 1145.75, + "coordinates": [ + 37.467484245, + 55.736075908 + ], + "type": "metro", + "color": "#0099cc" + }, + { + "id": "station__9858935", + "name": "Филёвский парк", + "distance": "1,9 км", + "distanceValue": 1940.38, + "coordinates": [ + 37.48289444, + 55.739425767 + ], + "type": "metro", + "color": "#0099cc" + } + ], + "stops": [ + { + "id": "2081245620", + "name": "Метро Славянский бульвар", + "distance": "300 м", + "distanceValue": 295.068, + "coordinates": [ + 37.473066913, + 55.728642545 + ], + "type": "common" + }, + { + "id": "stop__9641464", + "name": "Давыдково", + "distance": "680 м", + "distanceValue": 675.354, + "coordinates": [ + 37.469965263, + 55.727241055 + ], + "type": "common" + }, + { + "id": "stop__9650359", + "name": "Давыдковская улица", + "distance": "770 м", + "distanceValue": 774.503, + "coordinates": [ + 37.478631492, + 55.726709525 + ], + "type": "common" + }, + { + "id": "stop__9644821", + "name": "Метро Пионерская", + "distance": "790 м", + "distanceValue": 789.69, + "coordinates": [ + 37.470052901, + 55.733765425 + ], + "type": "common" + } + ], + "photos": { + "count": 7, + "urlTemplate": "https://avatars.mds.yandex.net/get-altay/2356223/2a00000173646464071921e9e0b575ac37ff/%s", + "items": [ + { + "urlTemplate": "https://avatars.mds.yandex.net/get-altay/2356223/2a00000173646464071921e9e0b575ac37ff/%s" + }, + { + "urlTemplate": "https://avatars.mds.yandex.net/get-altay/2838749/2a00000173646465a0be003e4d4d8f6eabe2/%s" + }, + { + "urlTemplate": "https://avatars.mds.yandex.net/get-altay/2701558/2a00000173646465fc6d7185d3d782d2d17f/%s" + }, + { + "urlTemplate": "https://avatars.mds.yandex.net/get-altay/2425845/2a00000173646464ebcd5b209af0606301c9/%s" + }, + { + "urlTemplate": "https://avatars.mds.yandex.net/get-altay/2408158/2a00000173646464915cb00f964811d0e948/%s" + }, + { + "urlTemplate": "https://avatars.mds.yandex.net/get-altay/2378041/2a00000173b976f3f703c0d895c481f303ca/%s" + }, + { + "urlTemplate": "https://avatars.mds.yandex.net/get-altay/2755030/2a0000017364646546b611057e56e3c4878c/%s" + } + ] + }, + "shortTitle": "Станция метро Славянский бульвар", + "fullAddress": "Россия, Москва, Западный административный округ, район Фили-Давыдково", + "country": "Россия", + "status": "open", + "businessLinks": [], + "ratingData": { + "ratingCount": 7, + "ratingValue": 0, + "reviewCount": 4 + }, + "sources": [ + { + "id": "yandex", + "name": "Яндекс", + "href": "https://www.yandex.ru" + } + ], + "categories": [ + { + "id": "244903403206", + "name": "Станция метро", + "class": "metro", + "seoname": "metro_station", + "pluralName": "Станции метро" + } + ], + "featureGroups": [], + "businessProperties": { + "has_verified_owner": false, + "hide_claim_organization": true, + "sensitive": true + }, + "modularSnippet": { + "snippet_show_title": true, + "snippet_show_rating": true, + "snippet_show_photo": "single_photo", + "snippet_show_eta": true, + "snippet_show_category": "single_category", + "snippet_show_work_hours": true, + "snippet_show_metro_line": true, + "snippet_show_metro_jams": true, + "snippet_show_subline": [ + "no_subline", + "closed_for_visitors", + "closed_for_without_qr" + ] + }, + "seoname": "stantsiya_metro_slavyanskiy_bulvar", + "geoId": 98561, + "compositeAddress": { + "locality": "Москва" + }, + "references": [ + { + "id": "4037957080", + "scope": "nyak" + } + ], + "subtitleItems": [ + { + "type": "rating", + "text": "6", + "property": [ + { + "key": "value", + "value": "6" + }, + { + "key": "value_5", + "value": "3" + } + ] + } + ], + "region": { + "id": 213, + "capitalId": 0, + "hierarchy": [ + 225, + 1, + 213 + ], + "seoname": "moscow", + "bounds": [ + [ + 37.0402925, + 55.31141404514547 + ], + [ + 38.2047155, + 56.190068045145466 + ] + ], + "longitude": 37.622504, + "latitude": 55.753215, + "zoom": 10, + "names": { + "ablative": "", + "accusative": "Москву", + "dative": "Москве", + "directional": "", + "genitive": "Москвы", + "instrumental": "Москвой", + "locative": "", + "nominative": "Москва", + "preposition": "в", + "prepositional": "Москве" + } + }, + "events": [], + "breadcrumbs": [ + { + "name": "Карты", + "type": "root", + "url": "https://yandex.ru/maps/" + }, + { + "name": "Москва", + "type": "region", + "url": "https://yandex.ru/maps/213/moscow/", + "region": { + "center": [ + 37.622504, + 55.753215 + ], + "zoom": 10 + } + }, + { + "type": "catalog", + "name": "Каталог организаций", + "url": "https://yandex.ru/maps/213/moscow/catalog/", + "region": { + "id": 213, + "capitalId": 0, + "hierarchy": [ + 225, + 1, + 213 + ], + "seoname": "moscow", + "bounds": [ + [ + 37.0402925, + 55.31141404514547 + ], + [ + 38.2047155, + 56.190068045145466 + ] + ], + "longitude": 37.622504, + "latitude": 55.753215, + "zoom": 10, + "names": { + "ablative": "", + "accusative": "Москву", + "dative": "Москве", + "directional": "", + "genitive": "Москвы", + "instrumental": "Москвой", + "locative": "", + "nominative": "Москва", + "preposition": "в", + "prepositional": "Москве" + } + } + }, + { + "category": { + "id": "244903403206", + "name": "Станция метро", + "class": "metro", + "seoname": "metro_station", + "pluralName": "Станции метро" + }, + "name": "Станции метро", + "type": "category", + "url": "https://yandex.ru/maps/213/moscow/category/metro_station/244903403206/" + }, + { + "name": "Станция метро Славянский бульвар", + "type": "search", + "url": "https://yandex.ru/maps/org/stantsiya_metro_slavyanskiy_bulvar/236914654771/", + "currentPage": true + } + ], + "geoWhere": { + "id": "53211697", + "seoname": "rayon_fili_davydkovo", + "kind": "district", + "coordinates": [ + 37.469359, + 55.727136 + ], + "displayCoordinates": [ + 37.469359, + 55.727136 + ], + "encodedCoordinates": "Z04YcgFpSkAOQFtvfXtzdn1gYg==" + } + } + } +} From f4c2c54e84b223a1d102fa56838d65e3d71823cb Mon Sep 17 00:00:00 2001 From: alexanv1 <44785744+alexanv1@users.noreply.github.com> Date: Sun, 24 Oct 2021 11:34:05 -0700 Subject: [PATCH 0772/1038] Extend Tuya Humidifier (#58260) --- homeassistant/components/tuya/const.py | 1 + homeassistant/components/tuya/humidifier.py | 1 + homeassistant/components/tuya/light.py | 10 ++++++++++ 3 files changed, 12 insertions(+) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 5da0bad19ac..96913c16aeb 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -172,6 +172,7 @@ class DPCode(str, Enum): CO2_STATE = "co2_state" CO2_VALUE = "co2_value" # CO2 concentration COLOR_DATA_V2 = "color_data_v2" + COLOUR_DATA_HSV = "colour_data_hsv" # Colored light mode COLOUR_DATA = "colour_data" # Colored light mode COLOUR_DATA_V2 = "colour_data_v2" # Colored light mode CONCENTRATION_SET = "concentration_set" # Concentration setting diff --git a/homeassistant/components/tuya/humidifier.py b/homeassistant/components/tuya/humidifier.py index 4152399daec..b9fc10790e3 100644 --- a/homeassistant/components/tuya/humidifier.py +++ b/homeassistant/components/tuya/humidifier.py @@ -45,6 +45,7 @@ HUMIDIFIERS: dict[str, TuyaHumidifierEntityDescription] = { # https://developer.tuya.com/en/docs/iot/categoryjsq?id=Kaiuz1smr440b "jsq": TuyaHumidifierEntityDescription( key=DPCode.SWITCH, + dpcode=(DPCode.SWITCH, DPCode.SWITCH_SPRAY), humidity=DPCode.HUMIDITY_SET, device_class=DEVICE_CLASS_HUMIDIFIER, ), diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 437c1a1262f..10aa8806a81 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -109,6 +109,16 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { color_data=DPCode.COLOUR_DATA, ), ), + # Humidifier Light + # https://developer.tuya.com/en/docs/iot/categoryjsq?id=Kaiuz1smr440b + "jsq": ( + TuyaLightEntityDescription( + key=DPCode.SWITCH_LED, + color_mode=DPCode.WORK_MODE, + brightness=DPCode.BRIGHT_VALUE, + color_data=DPCode.COLOUR_DATA_HSV, + ), + ), # Switch # https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s "kg": ( From c6a278a5447fdd9eb17bf1de9d888f5ddabb5dc4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 24 Oct 2021 21:22:17 +0200 Subject: [PATCH 0773/1038] Pin pytest-github-actions-annotate-failures to fix broken CI (#58351) --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9298400276f..aa5a6ddd3ca 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -682,7 +682,7 @@ jobs: # Ideally this should be part of our dependencies # However this plugin is fairly new and doesn't run correctly # on a non-GitHub environment. - pip install pytest-github-actions-annotate-failures + pip install pytest-github-actions-annotate-failures==0.1.3 - name: Run pytest run: | . venv/bin/activate From 137a346d15ee0db7d732d4e173d629416c515056 Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Sun, 24 Oct 2021 21:57:29 +0200 Subject: [PATCH 0774/1038] add BitronVideo AV2021 ZHA stick (#58337) --- homeassistant/components/zha/manifest.json | 3 ++- homeassistant/generated/usb.py | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 1a9fa64c453..4e169c20a48 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -19,7 +19,8 @@ {"vid":"10C4","pid":"EA60","description":"*tubeszb*","known_devices":["TubesZB Coordinator"]}, {"vid":"1A86","pid":"7523","description":"*tubeszb*","known_devices":["TubesZB Coordinator"]}, {"vid":"1CF1","pid":"0030","description":"*conbee*","known_devices":["Conbee II"]}, - {"vid":"10C4","pid":"8A2A","description":"*zigbee*","known_devices":["Nortek HUSBZB-1"]} + {"vid":"10C4","pid":"8A2A","description":"*zigbee*","known_devices":["Nortek HUSBZB-1"]}, + {"vid":"10C4","pid":"8B34","description":"*bv 2010/10*","known_devices":["Bitron Video AV2010/10"]} ], "codeowners": ["@dmulcahey", "@adminiuga"], "zeroconf": [ diff --git a/homeassistant/generated/usb.py b/homeassistant/generated/usb.py index bebeb393329..0fb3f52e1d7 100644 --- a/homeassistant/generated/usb.py +++ b/homeassistant/generated/usb.py @@ -41,6 +41,12 @@ USB = [ "pid": "8A2A", "description": "*zigbee*" }, + { + "domain": "zha", + "vid": "10C4", + "pid": "8B34", + "description": "*bv 2010/10*" + }, { "domain": "zwave_js", "vid": "0658", From 85ecb7ce3ad6243534e4ca8984eb20492c9770d6 Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Sun, 24 Oct 2021 22:16:19 +0200 Subject: [PATCH 0775/1038] feat: Add unit of measurement to KNX number platform (#58353) --- homeassistant/components/knx/number.py | 1 + tests/components/knx/test_number.py | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 tests/components/knx/test_number.py diff --git a/homeassistant/components/knx/number.py b/homeassistant/components/knx/number.py index d1ef2d226b4..c48c11fa998 100644 --- a/homeassistant/components/knx/number.py +++ b/homeassistant/components/knx/number.py @@ -75,6 +75,7 @@ class KNXNumber(KnxEntity, NumberEntity, RestoreEntity): self._device.sensor_value.dpt_class.resolution, ) self._attr_unique_id = str(self._device.sensor_value.group_address) + self._attr_unit_of_measurement = self._device.unit_of_measurement() self._device.sensor_value.value = max(0, self._attr_min_value) async def async_added_to_hass(self) -> None: diff --git a/tests/components/knx/test_number.py b/tests/components/knx/test_number.py new file mode 100644 index 00000000000..1a2044b166c --- /dev/null +++ b/tests/components/knx/test_number.py @@ -0,0 +1,23 @@ +"""Test KNX number.""" +from homeassistant.components.knx.const import KNX_ADDRESS +from homeassistant.components.knx.schema import NumberSchema +from homeassistant.const import CONF_NAME, CONF_TYPE +from homeassistant.core import HomeAssistant + +from .conftest import KNXTestKit + + +async def test_number_unit_of_measurement(hass: HomeAssistant, knx: KNXTestKit): + """Test simple KNX number.""" + test_address = "1/1/1" + await knx.setup_integration( + { + NumberSchema.PLATFORM_NAME: { + CONF_NAME: "test", + KNX_ADDRESS: test_address, + CONF_TYPE: "illuminance", + } + } + ) + assert len(hass.states.async_all()) == 1 + assert hass.states.get("number.test").attributes.get("unit_of_measurement") == "lx" From 02372cd65aed903ecc94f4573ec5580fbdab0b9a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Oct 2021 11:13:49 -1000 Subject: [PATCH 0776/1038] Fix lookin device validation in config flow (#58349) These need to be prefixed with http:// or validation will fail --- homeassistant/components/lookin/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/lookin/config_flow.py b/homeassistant/components/lookin/config_flow.py index f4fcece1303..14e4b517b5b 100644 --- a/homeassistant/components/lookin/config_flow.py +++ b/homeassistant/components/lookin/config_flow.py @@ -84,7 +84,7 @@ class LookinFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def _validate_device(self, host: str) -> Device: """Validate we can connect to the device.""" session = async_get_clientsession(self.hass) - lookin_protocol = LookInHttpProtocol(host, session) + lookin_protocol = LookInHttpProtocol(f"http://{host}", session) return await lookin_protocol.get_info() async def async_step_discovery_confirm( From 6c01ed8d9780daa87b88f1a93a2d67d5d2a47bdd Mon Sep 17 00:00:00 2001 From: Andre Richter Date: Sun, 24 Oct 2021 23:21:35 +0200 Subject: [PATCH 0777/1038] Use DataUpdateCoordinator in Vallox (#56966) --- homeassistant/components/vallox/__init__.py | 162 ++++++++++---------- homeassistant/components/vallox/const.py | 3 +- homeassistant/components/vallox/fan.py | 142 +++++++---------- homeassistant/components/vallox/sensor.py | 133 +++++----------- 4 files changed, 176 insertions(+), 264 deletions(-) diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py index 620114863a8..3f441054fb1 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -1,7 +1,7 @@ """Support for Vallox ventilation units.""" from __future__ import annotations -from datetime import datetime +from dataclasses import dataclass, field import ipaddress import logging from typing import Any @@ -11,13 +11,12 @@ from vallox_websocket_api.constants import vlxDevConstants from vallox_websocket_api.exceptions import ValloxApiException import voluptuous as vol -from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STARTED from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, StateType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( DEFAULT_FAN_SPEED_AWAY, @@ -28,8 +27,7 @@ from .const import ( METRIC_KEY_PROFILE_FAN_SPEED_AWAY, METRIC_KEY_PROFILE_FAN_SPEED_BOOST, METRIC_KEY_PROFILE_FAN_SPEED_HOME, - SIGNAL_VALLOX_STATE_UPDATE, - STATE_PROXY_SCAN_INTERVAL, + STATE_SCAN_INTERVAL, STR_TO_VALLOX_PROFILE_SETTABLE, ) @@ -91,6 +89,40 @@ SERVICE_TO_METHOD = { } +@dataclass +class ValloxState: + """Describes the current state of the unit.""" + + metric_cache: dict[str, Any] = field(default_factory=dict) + profile: VALLOX_PROFILE = VALLOX_PROFILE.NONE + + def get_metric(self, metric_key: str) -> StateType: + """Return cached state value.""" + _LOGGER.debug("Fetching metric key: %s", metric_key) + + if metric_key not in vlxDevConstants.__dict__: + _LOGGER.debug("Metric key invalid: %s", metric_key) + + if (value := self.metric_cache.get(metric_key)) is None: + return None + + if not isinstance(value, (str, int, float)): + _LOGGER.debug( + "Return value of metric %s has unexpected type %s", + metric_key, + type(value), + ) + return None + + return value + + +class ValloxDataUpdateCoordinator(DataUpdateCoordinator): + """The DataUpdateCoordinator for Vallox.""" + + data: ValloxState + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the client and boot the platforms.""" conf = config[DOMAIN] @@ -98,102 +130,74 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: name = conf.get(CONF_NAME) client = Vallox(host) - state_proxy = ValloxStateProxy(hass, client) - service_handler = ValloxServiceHandler(client, state_proxy) - hass.data[DOMAIN] = {"client": client, "state_proxy": state_proxy, "name": name} + async def async_update_data() -> ValloxState: + """Fetch state update.""" + _LOGGER.debug("Updating Vallox state cache") + try: + metric_cache = await client.fetch_metrics() + profile = await client.get_profile() + + except (OSError, ValloxApiException) as err: + raise UpdateFailed("Error during state cache update") from err + + return ValloxState(metric_cache, profile) + + coordinator = ValloxDataUpdateCoordinator( + hass, + _LOGGER, + name=f"{name} DataUpdateCoordinator", + update_interval=STATE_SCAN_INTERVAL, + update_method=async_update_data, + ) + + service_handler = ValloxServiceHandler(client, coordinator) for vallox_service, method in SERVICE_TO_METHOD.items(): schema = method["schema"] hass.services.async_register( DOMAIN, vallox_service, service_handler.async_handle, schema=schema ) - # The vallox hardware expects quite strict timings for websocket requests. Timings that machines - # with less processing power, like Raspberries, cannot live up to during the busy start phase of - # Home Asssistant. Hence, async_add_entities() for fan and sensor in respective code will be - # called with update_before_add=False to intentionally delay the first request, increasing - # chance that it is issued only when the machine is less busy again. - hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config)) - hass.async_create_task(async_load_platform(hass, "fan", DOMAIN, {}, config)) + hass.data[DOMAIN] = {"client": client, "coordinator": coordinator, "name": name} - async_track_time_interval(hass, state_proxy.async_update, STATE_PROXY_SCAN_INTERVAL) + async def _async_load_platform_delayed(*_: Any) -> None: + await coordinator.async_refresh() + hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config)) + hass.async_create_task(async_load_platform(hass, "fan", DOMAIN, {}, config)) + + # The Vallox hardware expects quite strict timings for websocket requests. Timings that machines + # with less processing power, like a Raspberry Pi, cannot live up to during the busy start phase + # of Home Asssistant. + # + # Hence, wait for the started event before doing a first data refresh and loading the platforms, + # because it usually means the system is less busy after the event and can now meet the + # websocket timing requirements. + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, _async_load_platform_delayed + ) return True -class ValloxStateProxy: - """Helper class to reduce websocket API calls.""" - - def __init__(self, hass: HomeAssistant, client: Vallox) -> None: - """Initialize the proxy.""" - self._hass = hass - self._client = client - self._metric_cache: dict[str, Any] = {} - self._profile = VALLOX_PROFILE.NONE - self._valid = False - - def fetch_metric(self, metric_key: str) -> StateType: - """Return cached state value.""" - _LOGGER.debug("Fetching metric key: %s", metric_key) - - if not self._valid: - raise OSError("Device state out of sync.") - - if metric_key not in vlxDevConstants.__dict__: - raise KeyError(f"Unknown metric key: {metric_key}") - - if (value := self._metric_cache[metric_key]) is None: - return None - - if not isinstance(value, (str, int, float)): - raise TypeError( - f"Return value of metric {metric_key} has unexpected type {type(value)}" - ) - - return value - - def get_profile(self) -> VALLOX_PROFILE: - """Return cached profile value.""" - _LOGGER.debug("Returning profile") - - if not self._valid: - raise OSError("Device state out of sync.") - - return self._profile - - async def async_update(self, time: datetime | None = None) -> None: - """Fetch state update.""" - _LOGGER.debug("Updating Vallox state cache") - - try: - self._metric_cache = await self._client.fetch_metrics() - self._profile = await self._client.get_profile() - - except (OSError, ValloxApiException) as err: - self._valid = False - _LOGGER.error("Error during state cache update: %s", err) - return - - self._valid = True - async_dispatcher_send(self._hass, SIGNAL_VALLOX_STATE_UPDATE) - - class ValloxServiceHandler: """Services implementation.""" - def __init__(self, client: Vallox, state_proxy: ValloxStateProxy) -> None: + def __init__( + self, client: Vallox, coordinator: DataUpdateCoordinator[ValloxState] + ) -> None: """Initialize the proxy.""" self._client = client - self._state_proxy = state_proxy + self._coordinator = coordinator async def async_set_profile(self, profile: str = "Home") -> bool: """Set the ventilation profile.""" _LOGGER.debug("Setting ventilation profile to: %s", profile) _LOGGER.warning( - "Attention: The service 'vallox.set_profile' is superseded by the 'fan.set_preset_mode' service." - "It will be removed in the future, please migrate to 'fan.set_preset_mode' to prevent breakage" + "Attention: The service 'vallox.set_profile' is superseded by the " + "'fan.set_preset_mode' service. It will be removed in the future, please migrate to " + "'fan.set_preset_mode' to prevent breakage" ) try: @@ -269,4 +273,4 @@ class ValloxServiceHandler: # This state change affects other entities like sensors. Force an immediate update that can # be observed by all parties involved. if result: - await self._state_proxy.async_update() + await self._coordinator.async_request_refresh() diff --git a/homeassistant/components/vallox/const.py b/homeassistant/components/vallox/const.py index 6a9c4ddc5f4..6d12d0bab8e 100644 --- a/homeassistant/components/vallox/const.py +++ b/homeassistant/components/vallox/const.py @@ -7,8 +7,7 @@ from vallox_websocket_api import PROFILE as VALLOX_PROFILE DOMAIN = "vallox" DEFAULT_NAME = "Vallox" -SIGNAL_VALLOX_STATE_UPDATE = "vallox_state_update" -STATE_PROXY_SCAN_INTERVAL = timedelta(seconds=60) +STATE_SCAN_INTERVAL = timedelta(seconds=60) # Common metric keys and (default) values. METRIC_KEY_MODE = "A_CYC_MODE" diff --git a/homeassistant/components/vallox/fan.py b/homeassistant/components/vallox/fan.py index 8ee1b8b471f..39242b01b4e 100644 --- a/homeassistant/components/vallox/fan.py +++ b/homeassistant/components/vallox/fan.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Mapping import logging -from typing import Any +from typing import Any, NamedTuple from vallox_websocket_api import Vallox from vallox_websocket_api.exceptions import ValloxApiException @@ -13,12 +13,12 @@ from homeassistant.components.fan import ( FanEntity, NotValidPresetModeError, ) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import ValloxStateProxy +from . import ValloxDataUpdateCoordinator from .const import ( DOMAIN, METRIC_KEY_MODE, @@ -27,25 +27,38 @@ from .const import ( METRIC_KEY_PROFILE_FAN_SPEED_HOME, MODE_OFF, MODE_ON, - SIGNAL_VALLOX_STATE_UPDATE, STR_TO_VALLOX_PROFILE_SETTABLE, VALLOX_PROFILE_TO_STR_SETTABLE, ) _LOGGER = logging.getLogger(__name__) -ATTR_PROFILE_FAN_SPEED_HOME = { - "description": "fan_speed_home", - "metric_key": METRIC_KEY_PROFILE_FAN_SPEED_HOME, -} -ATTR_PROFILE_FAN_SPEED_AWAY = { - "description": "fan_speed_away", - "metric_key": METRIC_KEY_PROFILE_FAN_SPEED_AWAY, -} -ATTR_PROFILE_FAN_SPEED_BOOST = { - "description": "fan_speed_boost", - "metric_key": METRIC_KEY_PROFILE_FAN_SPEED_BOOST, -} + +class ExtraStateAttributeDetails(NamedTuple): + """Extra state attribute properties.""" + + description: str + metric_key: str + + +EXTRA_STATE_ATTRIBUTES = ( + ExtraStateAttributeDetails( + description="fan_speed_home", metric_key=METRIC_KEY_PROFILE_FAN_SPEED_HOME + ), + ExtraStateAttributeDetails( + description="fan_speed_away", metric_key=METRIC_KEY_PROFILE_FAN_SPEED_AWAY + ), + ExtraStateAttributeDetails( + description="fan_speed_boost", metric_key=METRIC_KEY_PROFILE_FAN_SPEED_BOOST + ), +) + + +def _convert_fan_speed_value(value: StateType) -> int | None: + if isinstance(value, (int, float)): + return int(value) + + return None async def async_setup_platform( @@ -62,31 +75,29 @@ async def async_setup_platform( client.set_settable_address(METRIC_KEY_MODE, int) device = ValloxFan( - hass.data[DOMAIN]["name"], client, hass.data[DOMAIN]["state_proxy"] + hass.data[DOMAIN]["name"], client, hass.data[DOMAIN]["coordinator"] ) - async_add_entities([device], update_before_add=False) + async_add_entities([device]) -class ValloxFan(FanEntity): +class ValloxFan(CoordinatorEntity, FanEntity): """Representation of the fan.""" - _attr_should_poll = False + coordinator: ValloxDataUpdateCoordinator def __init__( - self, name: str, client: Vallox, state_proxy: ValloxStateProxy + self, + name: str, + client: Vallox, + coordinator: ValloxDataUpdateCoordinator, ) -> None: """Initialize the fan.""" + super().__init__(coordinator) + self._client = client - self._state_proxy = state_proxy - self._is_on = False - self._preset_mode: str | None = None - self._fan_speed_home: int | None = None - self._fan_speed_away: int | None = None - self._fan_speed_boost: int | None = None self._attr_name = name - self._attr_available = False @property def supported_features(self) -> int: @@ -102,73 +113,24 @@ class ValloxFan(FanEntity): @property def is_on(self) -> bool: """Return if device is on.""" - return self._is_on + return self.coordinator.data.get_metric(METRIC_KEY_MODE) == MODE_ON @property def preset_mode(self) -> str | None: """Return the current preset mode.""" - return self._preset_mode + vallox_profile = self.coordinator.data.profile + return VALLOX_PROFILE_TO_STR_SETTABLE.get(vallox_profile) @property def extra_state_attributes(self) -> Mapping[str, int | None]: """Return device specific state attributes.""" + data = self.coordinator.data + return { - ATTR_PROFILE_FAN_SPEED_HOME["description"]: self._fan_speed_home, - ATTR_PROFILE_FAN_SPEED_AWAY["description"]: self._fan_speed_away, - ATTR_PROFILE_FAN_SPEED_BOOST["description"]: self._fan_speed_boost, + attr.description: _convert_fan_speed_value(data.get_metric(attr.metric_key)) + for attr in EXTRA_STATE_ATTRIBUTES } - async def async_added_to_hass(self) -> None: - """Call to update.""" - self.async_on_remove( - async_dispatcher_connect( - self.hass, SIGNAL_VALLOX_STATE_UPDATE, self._update_callback - ) - ) - - @callback - def _update_callback(self) -> None: - """Call update method.""" - self.async_schedule_update_ha_state(True) - - async def async_update(self) -> None: - """Fetch state from the device.""" - try: - # Fetch if the whole device is in regular operation state. - self._is_on = self._state_proxy.fetch_metric(METRIC_KEY_MODE) == MODE_ON - - vallox_profile = self._state_proxy.get_profile() - - # Fetch the profile fan speeds. - fan_speed_home = self._state_proxy.fetch_metric( - ATTR_PROFILE_FAN_SPEED_HOME["metric_key"] - ) - fan_speed_away = self._state_proxy.fetch_metric( - ATTR_PROFILE_FAN_SPEED_AWAY["metric_key"] - ) - fan_speed_boost = self._state_proxy.fetch_metric( - ATTR_PROFILE_FAN_SPEED_BOOST["metric_key"] - ) - - except (OSError, KeyError, TypeError) as err: - self._attr_available = False - _LOGGER.error("Error updating fan: %s", err) - return - - self._preset_mode = VALLOX_PROFILE_TO_STR_SETTABLE.get(vallox_profile) - - self._fan_speed_home = ( - int(fan_speed_home) if isinstance(fan_speed_home, (int, float)) else None - ) - self._fan_speed_away = ( - int(fan_speed_away) if isinstance(fan_speed_away, (int, float)) else None - ) - self._fan_speed_boost = ( - int(fan_speed_boost) if isinstance(fan_speed_boost, (int, float)) else None - ) - - self._attr_available = True - async def _async_set_preset_mode_internal(self, preset_mode: str) -> bool: """ Set new preset mode. @@ -201,7 +163,7 @@ class ValloxFan(FanEntity): if update_needed: # This state change affects other entities like sensors. Force an immediate update that # can be observed by all parties involved. - await self._state_proxy.async_update() + await self.coordinator.async_request_refresh() async def async_turn_on( self, @@ -211,7 +173,7 @@ class ValloxFan(FanEntity): **kwargs: Any, ) -> None: """Turn the device on.""" - _LOGGER.debug("Turn on: %s", speed) + _LOGGER.debug("Turn on") update_needed = False @@ -231,7 +193,7 @@ class ValloxFan(FanEntity): if update_needed: # This state change affects other entities like sensors. Force an immediate update that # can be observed by all parties involved. - await self._state_proxy.async_update() + await self.coordinator.async_request_refresh() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" @@ -246,4 +208,4 @@ class ValloxFan(FanEntity): return # Same as for turn_on method. - await self._state_proxy.async_update() + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index 7bf9dca700f..2fdfd2cd472 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -19,143 +19,91 @@ from homeassistant.const import ( PERCENTAGE, TEMP_CELSIUS, ) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import ValloxStateProxy -from .const import ( - DOMAIN, - METRIC_KEY_MODE, - MODE_ON, - SIGNAL_VALLOX_STATE_UPDATE, - VALLOX_PROFILE_TO_STR_REPORTABLE, -) +from . import ValloxDataUpdateCoordinator +from .const import DOMAIN, METRIC_KEY_MODE, MODE_ON, VALLOX_PROFILE_TO_STR_REPORTABLE _LOGGER = logging.getLogger(__name__) -class ValloxSensor(SensorEntity): +class ValloxSensor(CoordinatorEntity, SensorEntity): """Representation of a Vallox sensor.""" - _attr_should_poll = False entity_description: ValloxSensorEntityDescription + coordinator: ValloxDataUpdateCoordinator def __init__( self, name: str, - state_proxy: ValloxStateProxy, + coordinator: ValloxDataUpdateCoordinator, description: ValloxSensorEntityDescription, ) -> None: """Initialize the Vallox sensor.""" - self._state_proxy = state_proxy + super().__init__(coordinator) self.entity_description = description self._attr_name = f"{name} {description.name}" - self._attr_available = False - async def async_added_to_hass(self) -> None: - """Call to update.""" - self.async_on_remove( - async_dispatcher_connect( - self.hass, SIGNAL_VALLOX_STATE_UPDATE, self._update_callback - ) - ) - - @callback - def _update_callback(self) -> None: - """Call update method.""" - self.async_schedule_update_ha_state(True) - - async def async_update(self) -> None: - """Fetch state from the ventilation unit.""" + @property + def native_value(self) -> StateType: + """Return the value reported by the sensor.""" if (metric_key := self.entity_description.metric_key) is None: - self._attr_available = False - _LOGGER.error("Error updating sensor. Empty metric key") - return + _LOGGER.debug("Error updating sensor. Empty metric key") + return None - try: - self._attr_native_value = self._state_proxy.fetch_metric(metric_key) - - except (OSError, KeyError, TypeError) as err: - self._attr_available = False - _LOGGER.error("Error updating sensor: %s", err) - return - - self._attr_available = True + return self.coordinator.data.get_metric(metric_key) class ValloxProfileSensor(ValloxSensor): """Child class for profile reporting.""" - async def async_update(self) -> None: - """Fetch state from the ventilation unit.""" - try: - vallox_profile = self._state_proxy.get_profile() - - except OSError as err: - self._attr_available = False - _LOGGER.error("Error updating sensor: %s", err) - return - - self._attr_native_value = VALLOX_PROFILE_TO_STR_REPORTABLE.get(vallox_profile) - self._attr_available = True + @property + def native_value(self) -> StateType: + """Return the value reported by the sensor.""" + vallox_profile = self.coordinator.data.profile + return VALLOX_PROFILE_TO_STR_REPORTABLE.get(vallox_profile) -# There seems to be a quirk with respect to the fan speed reporting. The device keeps on reporting -# the last valid fan speed from when the device was in regular operation mode, even if it left that -# state and has been shut off in the meantime. +# There is a quirk with respect to the fan speed reporting. The device keeps on reporting the last +# valid fan speed from when the device was in regular operation mode, even if it left that state and +# has been shut off in the meantime. # # Therefore, first query the overall state of the device, and report zero percent fan speed in case # it is not in regular operation mode. class ValloxFanSpeedSensor(ValloxSensor): """Child class for fan speed reporting.""" - async def async_update(self) -> None: - """Fetch state from the ventilation unit.""" - try: - fan_on = self._state_proxy.fetch_metric(METRIC_KEY_MODE) == MODE_ON - - except (OSError, KeyError, TypeError) as err: - self._attr_available = False - _LOGGER.error("Error updating sensor: %s", err) - return - - if fan_on: - await super().async_update() - else: - # Report zero percent otherwise. - self._attr_native_value = 0 - self._attr_available = True + @property + def native_value(self) -> StateType: + """Return the value reported by the sensor.""" + fan_is_on = self.coordinator.data.get_metric(METRIC_KEY_MODE) == MODE_ON + return super().native_value if fan_is_on else 0 class ValloxFilterRemainingSensor(ValloxSensor): """Child class for filter remaining time reporting.""" - async def async_update(self) -> None: - """Fetch state from the ventilation unit.""" - await super().async_update() + @property + def native_value(self) -> StateType: + """Return the value reported by the sensor.""" + super_native_value = super().native_value - # Check if the update in the super call was a success. - if not self._attr_available: - return - - if not isinstance(self._attr_native_value, (int, float)): - self._attr_available = False - _LOGGER.error( - "Value has unexpected type: %s", type(self._attr_native_value) - ) - return + if not isinstance(super_native_value, (int, float)): + _LOGGER.debug("Value has unexpected type: %s", type(super_native_value)) + return None # Since only a delta of days is received from the device, fix the time so the timestamp does # not change with every update. - days_remaining = float(self._attr_native_value) + days_remaining = float(super_native_value) days_remaining_delta = timedelta(days=days_remaining) now = datetime.utcnow().replace(hour=13, minute=0, second=0, microsecond=0) - self._attr_native_value = (now + days_remaining_delta).isoformat() + return (now + days_remaining_delta).isoformat() @dataclass @@ -259,12 +207,11 @@ async def async_setup_platform( return name = hass.data[DOMAIN]["name"] - state_proxy = hass.data[DOMAIN]["state_proxy"] + coordinator = hass.data[DOMAIN]["coordinator"] async_add_entities( [ - description.sensor_type(name, state_proxy, description) + description.sensor_type(name, coordinator, description) for description in SENSORS - ], - update_before_add=False, + ] ) From 0600a21e02d9ca08a0c3d7f36090dc3a655794f4 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 24 Oct 2021 16:22:16 -0500 Subject: [PATCH 0778/1038] Allow advanced Plex `play_media` search options (#56226) --- homeassistant/components/plex/errors.py | 4 - homeassistant/components/plex/media_player.py | 15 +- homeassistant/components/plex/media_search.py | 166 ++++------ homeassistant/components/plex/server.py | 38 +-- tests/components/plex/test_media_search.py | 303 ++++++++++++++++++ tests/components/plex/test_playback.py | 202 ++++++------ tests/components/plex/test_server.py | 225 ------------- tests/components/plex/test_services.py | 11 +- 8 files changed, 492 insertions(+), 472 deletions(-) create mode 100644 tests/components/plex/test_media_search.py diff --git a/homeassistant/components/plex/errors.py b/homeassistant/components/plex/errors.py index aacc340e2b1..534c553d45e 100644 --- a/homeassistant/components/plex/errors.py +++ b/homeassistant/components/plex/errors.py @@ -16,7 +16,3 @@ class ServerNotSpecified(PlexException): class ShouldUpdateConfigEntry(PlexException): """Config entry data is out of date and should be updated.""" - - -class MediaNotFound(PlexException): - """Media lookup failed for a given search query.""" diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 5a60c1e8b32..f210ebe8363 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -468,10 +468,10 @@ class PlexMediaPlayer(MediaPlayerEntity): def play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" if not (self.device and "playback" in self._device_protocol_capabilities): - _LOGGER.debug( - "Client is not currently accepting playback controls: %s", self.name + raise HomeAssistantError( + f"Client is not currently accepting playback controls: {self.name}" ) - return + if not self.plex_server.has_token: _LOGGER.warning( "Plex integration configured without a token, playback may fail" @@ -495,16 +495,17 @@ class PlexMediaPlayer(MediaPlayerEntity): media = self.plex_server.lookup_media(media_type, **src) if media is None: - _LOGGER.error("Media could not be found: %s", media_id) - return + raise HomeAssistantError(f"Media could not be found: {media_id}") _LOGGER.debug("Attempting to play %s on %s", media, self.name) playqueue = self.plex_server.create_playqueue(media, shuffle=shuffle) try: self.device.playMedia(playqueue) - except requests.exceptions.ConnectTimeout: - _LOGGER.error("Timed out playing on %s", self.name) + except requests.exceptions.ConnectTimeout as exc: + raise HomeAssistantError( + f"Request failed when playing on {self.name}" + ) from exc @property def extra_state_attributes(self): diff --git a/homeassistant/components/plex/media_search.py b/homeassistant/components/plex/media_search.py index 5992a49bf3b..abe32f7cf4c 100644 --- a/homeassistant/components/plex/media_search.py +++ b/homeassistant/components/plex/media_search.py @@ -3,114 +3,82 @@ import logging from plexapi.exceptions import BadRequest, NotFound -from .errors import MediaNotFound +LEGACY_PARAM_MAPPING = { + "show_name": "show.title", + "season_number": "season.index", + "episode_name": "episode.title", + "episode_number": "episode.index", + "artist_name": "artist.title", + "album_name": "album.title", + "track_name": "track.title", + "track_number": "track.index", + "video_name": "movie.title", +} + +PREFERRED_LIBTYPE_ORDER = ( + "episode", + "season", + "show", + "track", + "album", + "artist", +) + _LOGGER = logging.getLogger(__name__) -def lookup_movie(library_section, **kwargs): - """Find a specific movie and return a Plex media object.""" +def search_media(media_type, library_section, allow_multiple=False, **kwargs): + """Search for specified Plex media in the provided library section. + + Returns a single media item or None. + + If `allow_multiple` is `True`, return a list of matching items. + """ + search_query = {} + libtype = kwargs.pop("libtype", None) + + # Preserve legacy service parameters + for legacy_key, key in LEGACY_PARAM_MAPPING.items(): + if value := kwargs.pop(legacy_key, None): + _LOGGER.debug( + "Legacy parameter '%s' used, consider using '%s'", legacy_key, key + ) + search_query[key] = value + + search_query.update(**kwargs) + + if not libtype: + # Default to a sane libtype if not explicitly provided + for preferred_libtype in PREFERRED_LIBTYPE_ORDER: + if any(key.startswith(preferred_libtype) for key in search_query): + libtype = preferred_libtype + break + + search_query.update(libtype=libtype) + _LOGGER.debug("Processed search query: %s", search_query) + try: - title = kwargs["title"] - except KeyError: - _LOGGER.error("Must specify 'title' for this search") + results = library_section.search(**search_query) + except (BadRequest, NotFound) as exc: + _LOGGER.error("Problem in query %s: %s", search_query, exc) return None - try: - movies = library_section.search(**kwargs, libtype="movie", maxresults=3) - except BadRequest as err: - _LOGGER.error("Invalid search payload provided: %s", err) + if not results: return None - if not movies: - raise MediaNotFound(f"Movie {title}") from None + if len(results) > 1: + if allow_multiple: + return results - if len(movies) > 1: - exact_matches = [x for x in movies if x.title.lower() == title.lower()] - if len(exact_matches) == 1: - return exact_matches[0] - match_list = [f"{x.title} ({x.year})" for x in movies] - _LOGGER.warning("Multiple matches found during search: %s", match_list) + if title := search_query.get("title") or search_query.get("movie.title"): + exact_matches = [x for x in results if x.title.lower() == title.lower()] + if len(exact_matches) == 1: + return exact_matches[0] + _LOGGER.warning( + "Multiple matches, make content_id more specific or use `allow_multiple`: %s", + results, + ) return None - return movies[0] - - -def lookup_tv(library_section, **kwargs): - """Find TV media and return a Plex media object.""" - season_number = kwargs.get("season_number") - episode_number = kwargs.get("episode_number") - - try: - show_name = kwargs["show_name"] - show = library_section.get(show_name) - except KeyError: - _LOGGER.error("Must specify 'show_name' for this search") - return None - except NotFound as err: - raise MediaNotFound(f"Show {show_name}") from err - - if not season_number: - return show - - try: - season = show.season(int(season_number)) - except NotFound as err: - raise MediaNotFound(f"Season {season_number} of {show_name}") from err - - if not episode_number: - return season - - try: - return season.episode(episode=int(episode_number)) - except NotFound as err: - episode = f"S{str(season_number).zfill(2)}E{str(episode_number).zfill(2)}" - raise MediaNotFound(f"Episode {episode} of {show_name}") from err - - -def lookup_music(library_section, **kwargs): - """Search for music and return a Plex media object.""" - album_name = kwargs.get("album_name") - track_name = kwargs.get("track_name") - track_number = kwargs.get("track_number") - - try: - artist_name = kwargs["artist_name"] - artist = library_section.get(artist_name) - except KeyError: - _LOGGER.error("Must specify 'artist_name' for this search") - return None - except NotFound as err: - raise MediaNotFound(f"Artist {artist_name}") from err - - if album_name: - try: - album = artist.album(album_name) - except NotFound as err: - raise MediaNotFound(f"Album {album_name} by {artist_name}") from err - - if track_name: - try: - return album.track(track_name) - except NotFound as err: - raise MediaNotFound( - f"Track {track_name} on {album_name} by {artist_name}" - ) from err - - if track_number: - for track in album.tracks(): - if int(track.index) == int(track_number): - return track - - raise MediaNotFound( - f"Track {track_number} on {album_name} by {artist_name}" - ) from None - return album - - if track_name: - try: - return artist.get(track_name) - except NotFound as err: - raise MediaNotFound(f"Track {track_name} by {artist_name}") from err - - return artist + return results[0] diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 62fa095b50f..af8d96cce55 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -13,13 +13,7 @@ from requests import Session import requests.exceptions from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_EPISODE, - MEDIA_TYPE_MOVIE, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_PLAYLIST, - MEDIA_TYPE_VIDEO, -) +from homeassistant.components.media_player.const import MEDIA_TYPE_PLAYLIST from homeassistant.const import CONF_CLIENT_ID, CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import callback from homeassistant.helpers.debounce import Debouncer @@ -47,13 +41,8 @@ from .const import ( X_PLEX_PRODUCT, X_PLEX_VERSION, ) -from .errors import ( - MediaNotFound, - NoServersFound, - ServerNotSpecified, - ShouldUpdateConfigEntry, -) -from .media_search import lookup_movie, lookup_music, lookup_tv +from .errors import NoServersFound, ServerNotSpecified, ShouldUpdateConfigEntry +from .media_search import search_media from .models import PlexSession _LOGGER = logging.getLogger(__name__) @@ -652,26 +641,7 @@ class PlexServer: _LOGGER.error("Library '%s' not found", library_name) return None - try: - if media_type == MEDIA_TYPE_EPISODE: - return lookup_tv(library_section, **kwargs) - if media_type == MEDIA_TYPE_MOVIE: - return lookup_movie(library_section, **kwargs) - if media_type == MEDIA_TYPE_MUSIC: - return lookup_music(library_section, **kwargs) - if media_type == MEDIA_TYPE_VIDEO: - # Legacy method for compatibility - try: - video_name = kwargs["video_name"] - return library_section.get(video_name) - except KeyError: - _LOGGER.error("Must specify 'video_name' for this search") - return None - except NotFound as err: - raise MediaNotFound(f"Video {video_name}") from err - except MediaNotFound as failed_item: - _LOGGER.error("%s not found in %s", failed_item, library_name) - return None + return search_media(media_type, library_section, **kwargs) @property def sensor_attributes(self): diff --git a/tests/components/plex/test_media_search.py b/tests/components/plex/test_media_search.py new file mode 100644 index 00000000000..467ab3555f5 --- /dev/null +++ b/tests/components/plex/test_media_search.py @@ -0,0 +1,303 @@ +"""Tests for Plex server.""" +from unittest.mock import patch + +from plexapi.exceptions import BadRequest, NotFound +import pytest + +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN +from homeassistant.components.media_player.const import ( + ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_CONTENT_TYPE, + MEDIA_TYPE_EPISODE, + MEDIA_TYPE_MOVIE, + MEDIA_TYPE_MUSIC, + MEDIA_TYPE_PLAYLIST, + MEDIA_TYPE_VIDEO, + SERVICE_PLAY_MEDIA, +) +from homeassistant.components.plex.const import DOMAIN +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.exceptions import HomeAssistantError + + +async def test_media_lookups( + hass, mock_plex_server, requests_mock, playqueue_created, caplog +): + """Test media lookups to Plex server.""" + # Plex Key searches + media_player_id = hass.states.async_entity_ids("media_player")[0] + requests_mock.post("/playqueues", text=playqueue_created) + requests_mock.get("/player/playback/playMedia", status_code=200) + + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player_id, + ATTR_MEDIA_CONTENT_TYPE: DOMAIN, + ATTR_MEDIA_CONTENT_ID: 1, + }, + True, + ) + with pytest.raises(HomeAssistantError) as excinfo: + with patch("plexapi.server.PlexServer.fetchItem", side_effect=NotFound): + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player_id, + ATTR_MEDIA_CONTENT_TYPE: DOMAIN, + ATTR_MEDIA_CONTENT_ID: 123, + }, + True, + ) + assert "Media could not be found: 123" in str(excinfo.value) + + # TV show searches + with pytest.raises(HomeAssistantError) as excinfo: + payload = '{"library_name": "Not a Library", "show_name": "TV Show"}' + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player_id, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_EPISODE, + ATTR_MEDIA_CONTENT_ID: payload, + }, + True, + ) + assert f"Media could not be found: {payload}" in str(excinfo.value) + + with patch("plexapi.library.LibrarySection.search") as search: + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player_id, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_EPISODE, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "TV Shows", "show_name": "TV Show"}', + }, + True, + ) + search.assert_called_with(**{"show.title": "TV Show", "libtype": "show"}) + + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player_id, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_EPISODE, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "TV Shows", "episode_name": "An Episode"}', + }, + True, + ) + search.assert_called_with( + **{"episode.title": "An Episode", "libtype": "episode"} + ) + + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player_id, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_EPISODE, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "TV Shows", "show_name": "TV Show", "season_number": 1}', + }, + True, + ) + search.assert_called_with( + **{"show.title": "TV Show", "season.index": 1, "libtype": "season"} + ) + + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player_id, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_EPISODE, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "TV Shows", "show_name": "TV Show", "season_number": 1, "episode_number": 3}', + }, + True, + ) + search.assert_called_with( + **{ + "show.title": "TV Show", + "season.index": 1, + "episode.index": 3, + "libtype": "episode", + } + ) + + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player_id, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist"}', + }, + True, + ) + search.assert_called_with(**{"artist.title": "Artist", "libtype": "artist"}) + + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player_id, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "album_name": "Album"}', + }, + True, + ) + search.assert_called_with(**{"album.title": "Album", "libtype": "album"}) + + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player_id, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "track_name": "Track 3"}', + }, + True, + ) + search.assert_called_with( + **{"artist.title": "Artist", "track.title": "Track 3", "libtype": "track"} + ) + + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player_id, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album"}', + }, + True, + ) + search.assert_called_with( + **{"artist.title": "Artist", "album.title": "Album", "libtype": "album"} + ) + + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player_id, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album", "track_number": 3}', + }, + True, + ) + search.assert_called_with( + **{ + "artist.title": "Artist", + "album.title": "Album", + "track.index": 3, + "libtype": "track", + } + ) + + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player_id, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album", "track_name": "Track 3"}', + }, + True, + ) + search.assert_called_with( + **{ + "artist.title": "Artist", + "album.title": "Album", + "track.title": "Track 3", + "libtype": "track", + } + ) + + # Movie searches + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player_id, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_VIDEO, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "video_name": "Movie 1"}', + }, + True, + ) + search.assert_called_with(**{"movie.title": "Movie 1", "libtype": None}) + + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player_id, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "title": "Movie 1"}', + }, + True, + ) + search.assert_called_with(**{"title": "Movie 1", "libtype": None}) + + # TV show searches + with pytest.raises(HomeAssistantError) as excinfo: + payload = '{"library_name": "Movies", "title": "Not a Movie"}' + with patch("plexapi.library.LibrarySection.search", side_effect=BadRequest): + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player_id, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_VIDEO, + ATTR_MEDIA_CONTENT_ID: payload, + }, + True, + ) + assert "Problem in query" in caplog.text + assert f"Media could not be found: {payload}" in str(excinfo.value) + + # Playlist searches + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player_id, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_PLAYLIST, + ATTR_MEDIA_CONTENT_ID: '{"playlist_name": "Playlist 1"}', + }, + True, + ) + + with pytest.raises(HomeAssistantError) as excinfo: + payload = '{"playlist_name": "Not a Playlist"}' + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player_id, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_PLAYLIST, + ATTR_MEDIA_CONTENT_ID: payload, + }, + True, + ) + assert "Playlist 'Not a Playlist' not found" in caplog.text + assert f"Media could not be found: {payload}" in str(excinfo.value) + + with pytest.raises(HomeAssistantError) as excinfo: + payload = "{}" + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player_id, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_PLAYLIST, + ATTR_MEDIA_CONTENT_ID: payload, + }, + True, + ) + assert "Must specify 'playlist_name' for this search" in caplog.text + assert f"Media could not be found: {payload}" in str(excinfo.value) diff --git a/tests/components/plex/test_playback.py b/tests/components/plex/test_playback.py index 7ab0fc0f434..2db0323bdcd 100644 --- a/tests/components/plex/test_playback.py +++ b/tests/components/plex/test_playback.py @@ -2,20 +2,48 @@ from http import HTTPStatus from unittest.mock import patch +import pytest + from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, DOMAIN as MP_DOMAIN, - MEDIA_TYPE_EPISODE, MEDIA_TYPE_MOVIE, - MEDIA_TYPE_MUSIC, SERVICE_PLAY_MEDIA, ) from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.exceptions import HomeAssistantError + + +class MockPlexMedia: + """Minimal mock of plexapi media object.""" + + key = "key" + + def __init__(self, title, mediatype): + """Initialize the instance.""" + self.listType = mediatype + self.title = title + self.type = mediatype + + def section(self): + """Return the LibrarySection.""" + return MockPlexLibrarySection() + + +class MockPlexLibrarySection: + """Minimal mock of plexapi LibrarySection.""" + + uuid = "00000000-0000-0000-0000-000000000000" async def test_media_player_playback( - hass, setup_plex_server, requests_mock, playqueue_created, player_plexweb_resources + hass, + setup_plex_server, + requests_mock, + playqueue_created, + player_plexweb_resources, + caplog, ): """Test playing media on a Plex media_player.""" requests_mock.get("http://1.2.3.5:32400/resources", text=player_plexweb_resources) @@ -26,112 +54,88 @@ async def test_media_player_playback( requests_mock.post("/playqueues", text=playqueue_created) requests_mock.get("/player/playback/playMedia", status_code=HTTPStatus.OK) - # Test movie success - assert await hass.services.async_call( - MP_DOMAIN, - SERVICE_PLAY_MEDIA, - { - ATTR_ENTITY_ID: media_player, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, - ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "title": "Movie 1" }', - }, - True, - ) - - # Test movie incomplete dict - assert await hass.services.async_call( - MP_DOMAIN, - SERVICE_PLAY_MEDIA, - { - ATTR_ENTITY_ID: media_player, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, - ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies"}', - }, - True, - ) - - # Test movie failure with options - assert await hass.services.async_call( - MP_DOMAIN, - SERVICE_PLAY_MEDIA, - { - ATTR_ENTITY_ID: media_player, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, - ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "title": "Does not exist" }', - }, - True, - ) - - # Test movie failure with nothing found + # Test media lookup failure + payload = '{"library_name": "Movies", "title": "Movie 1" }' with patch("plexapi.library.LibrarySection.search", return_value=None): + with pytest.raises(HomeAssistantError) as excinfo: + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, + ATTR_MEDIA_CONTENT_ID: payload, + }, + True, + ) + assert f"Media could not be found: {payload}" in str(excinfo.value) + + movie1 = MockPlexMedia("Movie", "movie") + movie2 = MockPlexMedia("Movie II", "movie") + movie3 = MockPlexMedia("Movie III", "movie") + + # Test movie success + movies = [movie1] + with patch("plexapi.library.LibrarySection.search", return_value=movies): assert await hass.services.async_call( MP_DOMAIN, SERVICE_PLAY_MEDIA, { ATTR_ENTITY_ID: media_player, ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, - ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "title": "Does not exist" }', + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "title": "Movie 1" }', }, True, ) - # Test movie success with dict - assert await hass.services.async_call( - MP_DOMAIN, - SERVICE_PLAY_MEDIA, - { - ATTR_ENTITY_ID: media_player, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album"}', - }, - True, - ) + # Test multiple choices with exact match + movies = [movie1, movie2] + with patch("plexapi.library.LibrarySection.search", return_value=movies), patch( + "homeassistant.components.plex.server.PlexServer.create_playqueue" + ) as mock_create_playqueue: + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "title": "Movie" }', + }, + True, + ) + assert mock_create_playqueue.call_args.args == (movie1,) - # Test TV show episoe lookup failure - assert await hass.services.async_call( - MP_DOMAIN, - SERVICE_PLAY_MEDIA, - { - ATTR_ENTITY_ID: media_player, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_EPISODE, - ATTR_MEDIA_CONTENT_ID: '{"library_name": "TV Shows", "show_name": "TV Show", "season_number": 1, "episode_number": 99}', - }, - True, - ) + # Test multiple choices without exact match + movies = [movie2, movie3] + with pytest.raises(HomeAssistantError) as excinfo: + payload = '{"library_name": "Movies", "title": "Movie" }' + with patch("plexapi.library.LibrarySection.search", return_value=movies): + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, + ATTR_MEDIA_CONTENT_ID: payload, + }, + True, + ) + assert f"Media could not be found: {payload}" in str(excinfo.value) + assert "Multiple matches, make content_id more specific" in caplog.text - # Test track name lookup failure - assert await hass.services.async_call( - MP_DOMAIN, - SERVICE_PLAY_MEDIA, - { - ATTR_ENTITY_ID: media_player, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album", "track_name": "Not a track"}', - }, - True, - ) - - # Test media lookup failure by key - requests_mock.get("/library/metadata/999", status_code=HTTPStatus.NOT_FOUND) - assert await hass.services.async_call( - MP_DOMAIN, - SERVICE_PLAY_MEDIA, - { - ATTR_ENTITY_ID: media_player, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: "999", - }, - True, - ) - - # Test invalid Plex server requested - assert await hass.services.async_call( - MP_DOMAIN, - SERVICE_PLAY_MEDIA, - { - ATTR_ENTITY_ID: media_player, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: '{"plex_server": "unknown_plex_server", "library_name": "Music", "artist_name": "Artist", "album_name": "Album"}', - }, - True, - ) + # Test multiple choices with allow_multiple + movies = [movie1, movie2, movie3] + with patch("plexapi.library.LibrarySection.search", return_value=movies), patch( + "homeassistant.components.plex.server.PlexServer.create_playqueue" + ) as mock_create_playqueue: + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "title": "Movie", "allow_multiple": true }', + }, + True, + ) + assert mock_create_playqueue.call_args.args == (movies,) diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py index 5a8a9869f59..724b34cf729 100644 --- a/tests/components/plex/test_server.py +++ b/tests/components/plex/test_server.py @@ -1,22 +1,10 @@ """Tests for Plex server.""" import copy -from http import HTTPStatus from unittest.mock import patch -from plexapi.exceptions import BadRequest, NotFound from requests.exceptions import ConnectionError, RequestException from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.components.media_player.const import ( - ATTR_MEDIA_CONTENT_ID, - ATTR_MEDIA_CONTENT_TYPE, - MEDIA_TYPE_EPISODE, - MEDIA_TYPE_MOVIE, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_PLAYLIST, - MEDIA_TYPE_VIDEO, - SERVICE_PLAY_MEDIA, -) from homeassistant.components.plex.const import ( CONF_IGNORE_NEW_SHARED_USERS, CONF_IGNORE_PLEX_WEB_CLIENTS, @@ -25,7 +13,6 @@ from homeassistant.components.plex.const import ( DOMAIN, SERVERS, ) -from homeassistant.const import ATTR_ENTITY_ID from .const import DEFAULT_DATA, DEFAULT_OPTIONS from .helpers import trigger_plex_update, wait_for_debouncer @@ -179,215 +166,3 @@ async def test_ignore_plex_web_client(hass, entry, setup_plex_server): media_players = hass.states.async_entity_ids("media_player") assert len(media_players) == int(sensor.state) - 1 - - -async def test_media_lookups(hass, mock_plex_server, requests_mock, playqueue_created): - """Test media lookups to Plex server.""" - server_id = mock_plex_server.machine_identifier - loaded_server = hass.data[DOMAIN][SERVERS][server_id] - - # Plex Key searches - media_player_id = hass.states.async_entity_ids("media_player")[0] - requests_mock.post("/playqueues", text=playqueue_created) - requests_mock.get("/player/playback/playMedia", status_code=HTTPStatus.OK) - assert await hass.services.async_call( - MP_DOMAIN, - SERVICE_PLAY_MEDIA, - { - ATTR_ENTITY_ID: media_player_id, - ATTR_MEDIA_CONTENT_TYPE: DOMAIN, - ATTR_MEDIA_CONTENT_ID: 1, - }, - True, - ) - with patch("plexapi.server.PlexServer.fetchItem", side_effect=NotFound): - assert await hass.services.async_call( - MP_DOMAIN, - SERVICE_PLAY_MEDIA, - { - ATTR_ENTITY_ID: media_player_id, - ATTR_MEDIA_CONTENT_TYPE: DOMAIN, - ATTR_MEDIA_CONTENT_ID: 123, - }, - True, - ) - - # TV show searches - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_EPISODE, library_name="Not a Library", show_name="TV Show" - ) - is None - ) - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_EPISODE, library_name="TV Shows", show_name="Not a TV Show" - ) - is None - ) - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_EPISODE, library_name="TV Shows", episode_name="An Episode" - ) - is None - ) - assert loaded_server.lookup_media( - MEDIA_TYPE_EPISODE, library_name="TV Shows", show_name="TV Show" - ) - assert loaded_server.lookup_media( - MEDIA_TYPE_EPISODE, - library_name="TV Shows", - show_name="TV Show", - season_number=1, - ) - assert loaded_server.lookup_media( - MEDIA_TYPE_EPISODE, - library_name="TV Shows", - show_name="TV Show", - season_number=1, - episode_number=3, - ) - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_EPISODE, - library_name="TV Shows", - show_name="TV Show", - season_number=2, - ) - is None - ) - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_EPISODE, - library_name="TV Shows", - show_name="TV Show", - season_number=2, - episode_number=1, - ) - is None - ) - - # Music searches - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_MUSIC, library_name="Music", album_name="Album" - ) - is None - ) - assert loaded_server.lookup_media( - MEDIA_TYPE_MUSIC, library_name="Music", artist_name="Artist" - ) - assert loaded_server.lookup_media( - MEDIA_TYPE_MUSIC, - library_name="Music", - artist_name="Artist", - track_name="Track 3", - ) - assert loaded_server.lookup_media( - MEDIA_TYPE_MUSIC, - library_name="Music", - artist_name="Artist", - album_name="Album", - ) - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_MUSIC, - library_name="Music", - artist_name="Not an Artist", - album_name="Album", - ) - is None - ) - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_MUSIC, - library_name="Music", - artist_name="Artist", - album_name="Not an Album", - ) - is None - ) - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_MUSIC, - library_name="Music", - artist_name="Artist", - album_name=" Album", - track_name="Not a Track", - ) - is None - ) - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_MUSIC, - library_name="Music", - artist_name="Artist", - track_name="Not a Track", - ) - is None - ) - assert loaded_server.lookup_media( - MEDIA_TYPE_MUSIC, - library_name="Music", - artist_name="Artist", - album_name="Album", - track_number=3, - ) - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_MUSIC, - library_name="Music", - artist_name="Artist", - album_name="Album", - track_number=30, - ) - is None - ) - assert loaded_server.lookup_media( - MEDIA_TYPE_MUSIC, - library_name="Music", - artist_name="Artist", - album_name="Album", - track_name="Track 3", - ) - - # Playlist searches - assert loaded_server.lookup_media(MEDIA_TYPE_PLAYLIST, playlist_name="Playlist 1") - assert loaded_server.lookup_media(MEDIA_TYPE_PLAYLIST) is None - assert ( - loaded_server.lookup_media(MEDIA_TYPE_PLAYLIST, playlist_name="Not a Playlist") - is None - ) - - # Legacy Movie searches - assert loaded_server.lookup_media(MEDIA_TYPE_VIDEO, video_name="Movie") is None - assert loaded_server.lookup_media(MEDIA_TYPE_VIDEO, library_name="Movies") is None - assert loaded_server.lookup_media( - MEDIA_TYPE_VIDEO, library_name="Movies", video_name="Movie 1" - ) - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_VIDEO, library_name="Movies", video_name="Not a Movie" - ) - is None - ) - - # Movie searches - assert loaded_server.lookup_media(MEDIA_TYPE_MOVIE, title="Movie") is None - assert loaded_server.lookup_media(MEDIA_TYPE_MOVIE, library_name="Movies") is None - assert loaded_server.lookup_media( - MEDIA_TYPE_MOVIE, library_name="Movies", title="Movie 1" - ) - with patch("plexapi.library.LibrarySection.search", side_effect=BadRequest): - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_MOVIE, library_name="Movies", title="Not a Movie" - ) - is None - ) - - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_MOVIE, library_name="Movies", title="Movie" - ) - ) is None diff --git a/tests/components/plex/test_services.py b/tests/components/plex/test_services.py index 7ad7b033caa..778d9cbde63 100644 --- a/tests/components/plex/test_services.py +++ b/tests/components/plex/test_services.py @@ -180,10 +180,13 @@ async def test_sonos_play_media( # Test with speakers available but media not found content_id_bad_media = '{"library_name": "Music", "artist_name": "Not an Artist"}' - with pytest.raises(HomeAssistantError) as excinfo: - play_on_sonos(hass, MEDIA_TYPE_MUSIC, content_id_bad_media, sonos_speaker_name) - assert "Plex media not found" in str(excinfo.value) - assert playback_mock.call_count == 3 + with patch("plexapi.library.LibrarySection.search", return_value=None): + with pytest.raises(HomeAssistantError) as excinfo: + play_on_sonos( + hass, MEDIA_TYPE_MUSIC, content_id_bad_media, sonos_speaker_name + ) + assert "Plex media not found" in str(excinfo.value) + assert playback_mock.call_count == 3 # Test with speakers available and playqueue requests_mock.get("https://1.2.3.4:32400/playQueues/1234", text=playqueue_1234) From 2a6247cf209505bd6b114bc427d81f9082da5733 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Oct 2021 11:53:11 -1000 Subject: [PATCH 0779/1038] Fix lookin push updates when sensor entities disabled (#58346) --- homeassistant/components/lookin/__init__.py | 25 +++++++++++++++++++-- homeassistant/components/lookin/sensor.py | 23 +------------------ 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/lookin/__init__.py b/homeassistant/components/lookin/__init__.py index a096c08dfeb..f749621beaf 100644 --- a/homeassistant/components/lookin/__init__.py +++ b/homeassistant/components/lookin/__init__.py @@ -5,11 +5,17 @@ from datetime import timedelta import logging import aiohttp -from aiolookin import LookInHttpProtocol, LookinUDPSubscriptions, start_lookin_udp +from aiolookin import ( + LookInHttpProtocol, + LookinUDPSubscriptions, + MeteoSensor, + SensorID, + start_lookin_udp, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -45,7 +51,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await meteo_coordinator.async_config_entry_first_refresh() + @callback + def _async_meteo_push_update(msg: dict[str, str]) -> None: + """Process an update pushed via UDP.""" + if int(msg["event_id"]): + return + LOGGER.debug("Processing push message for meteo sensor: %s", msg) + meteo: MeteoSensor = meteo_coordinator.data + meteo.update_from_value(msg["value"]) + meteo_coordinator.async_set_updated_data(meteo) + lookin_udp_subs = LookinUDPSubscriptions() + entry.async_on_unload( + lookin_udp_subs.subscribe_sensor( + lookin_device.id, SensorID.Meteo, None, _async_meteo_push_update + ) + ) entry.async_on_unload(await start_lookin_udp(lookin_udp_subs)) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = LookinData( diff --git a/homeassistant/components/lookin/sensor.py b/homeassistant/components/lookin/sensor.py index d7d4a7a937a..b320f5d537a 100644 --- a/homeassistant/components/lookin/sensor.py +++ b/homeassistant/components/lookin/sensor.py @@ -3,8 +3,6 @@ from __future__ import annotations import logging -from aiolookin import MeteoSensor, SensorID - from homeassistant.components.sensor import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, @@ -14,7 +12,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, TEMP_CELSIUS -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN @@ -74,22 +72,3 @@ class LookinSensorEntity(LookinDeviceCoordinatorEntity, SensorEntity): self.coordinator.data, self.entity_description.key ) super()._handle_coordinator_update() - - @callback - def _async_push_update(self, msg: dict[str, str]) -> None: - """Process an update pushed via UDP.""" - if int(msg["event_id"]): - return - LOGGER.debug("Processing push message for meteo sensor: %s", msg) - meteo: MeteoSensor = self.coordinator.data - meteo.update_from_value(msg["value"]) - self.coordinator.async_set_updated_data(meteo) - - async def async_added_to_hass(self) -> None: - """Call when the entity is added to hass.""" - self.async_on_remove( - self._lookin_udp_subs.subscribe_sensor( - self._lookin_device.id, SensorID.Meteo, None, self._async_push_update - ) - ) - return await super().async_added_to_hass() From 3672889609c79704bd1e8198dc0a43b0cdd5bce6 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 24 Oct 2021 17:51:45 -0500 Subject: [PATCH 0780/1038] Add warning if Sonos not linked to Plex (#58150) --- homeassistant/components/plex/services.py | 10 ++++++++-- tests/components/plex/test_services.py | 7 +++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/plex/services.py b/homeassistant/components/plex/services.py index a5faa56a8bb..32af3c429dc 100644 --- a/homeassistant/components/plex/services.py +++ b/homeassistant/components/plex/services.py @@ -2,7 +2,7 @@ import json import logging -from plexapi.exceptions import NotFound +from plexapi.exceptions import BadRequest, NotFound import voluptuous as vol from homeassistant.exceptions import HomeAssistantError @@ -133,7 +133,13 @@ def play_on_sonos(hass, content_type, content_id, speaker_name): Called by Sonos 'media_player.play_media' service. """ media, plex_server = lookup_plex_media(hass, content_type, content_id) - sonos_speaker = plex_server.account.sonos_speaker(speaker_name) + try: + sonos_speaker = plex_server.account.sonos_speaker(speaker_name) + except BadRequest as exc: + raise HomeAssistantError( + "Sonos speakers not linked to Plex account, complete this step in the Plex app" + ) from exc + if sonos_speaker is None: message = f"Sonos speaker '{speaker_name}' is not associated with '{plex_server.friendly_name}'" _LOGGER.error(message) diff --git a/tests/components/plex/test_services.py b/tests/components/plex/test_services.py index 778d9cbde63..161755c9703 100644 --- a/tests/components/plex/test_services.py +++ b/tests/components/plex/test_services.py @@ -154,6 +154,13 @@ async def test_sonos_play_media( await hass.config_entries.async_unload(entry.entry_id) mock_plex_server = await setup_plex_server() + # Test with unlinked Plex/Sonos accounts + requests_mock.get("https://sonos.plex.tv/resources", status_code=403) + with pytest.raises(HomeAssistantError) as excinfo: + play_on_sonos(hass, MEDIA_TYPE_MUSIC, media_content_id, sonos_speaker_name) + assert "Sonos speakers not linked to Plex account" in str(excinfo.value) + assert playback_mock.call_count == 0 + # Test with no speakers available requests_mock.get("https://sonos.plex.tv/resources", text=empty_payload) with pytest.raises(HomeAssistantError) as excinfo: From 5a20d9fce397d16f58770df090ab167140861d78 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 24 Oct 2021 17:52:15 -0500 Subject: [PATCH 0781/1038] Set Sonos alarm and battery entity categories (#58340) --- homeassistant/components/sonos/binary_sensor.py | 3 +++ homeassistant/components/sonos/sensor.py | 8 +++++++- homeassistant/components/sonos/switch.py | 4 +++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/binary_sensor.py b/homeassistant/components/sonos/binary_sensor.py index 9e35fc59616..488f29a7be8 100644 --- a/homeassistant/components/sonos/binary_sensor.py +++ b/homeassistant/components/sonos/binary_sensor.py @@ -7,6 +7,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_BATTERY_CHARGING, BinarySensorEntity, ) +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import SONOS_CREATE_BATTERY @@ -31,6 +32,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class SonosPowerEntity(SonosEntity, BinarySensorEntity): """Representation of a Sonos power entity.""" + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + @property def unique_id(self) -> str: """Return the unique ID of the sensor.""" diff --git a/homeassistant/components/sonos/sensor.py b/homeassistant/components/sonos/sensor.py index f8e5142c123..599e5434fb4 100644 --- a/homeassistant/components/sonos/sensor.py +++ b/homeassistant/components/sonos/sensor.py @@ -2,7 +2,11 @@ from __future__ import annotations from homeassistant.components.sensor import SensorEntity -from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE +from homeassistant.const import ( + DEVICE_CLASS_BATTERY, + ENTITY_CATEGORY_DIAGNOSTIC, + PERCENTAGE, +) from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import SONOS_CREATE_BATTERY @@ -25,6 +29,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class SonosBatteryEntity(SonosEntity, SensorEntity): """Representation of a Sonos Battery entity.""" + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + @property def unique_id(self) -> str: """Return the unique ID of the sensor.""" diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 3e9d5484784..9af6c1eebec 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -178,6 +178,8 @@ class SonosSwitchEntity(SonosEntity, SwitchEntity): class SonosAlarmEntity(SonosEntity, SwitchEntity): """Representation of a Sonos Alarm entity.""" + _attr_entity_category = ENTITY_CATEGORY_CONFIG + def __init__(self, alarm_id: str, speaker: SonosSpeaker) -> None: """Initialize the switch.""" super().__init__(speaker) @@ -215,7 +217,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): @property def name(self) -> str: """Return the name of the sensor.""" - return "Sonos Alarm {} {} {}".format( + return "{} {} Alarm {}".format( self.speaker.zone_name, self.alarm.recurrence.title(), str(self.alarm.start_time)[0:5], From 2fe758edd4e14144d3f1861c7bb494d3c2d857e1 Mon Sep 17 00:00:00 2001 From: Andre Richter Date: Mon, 25 Oct 2021 01:30:09 +0200 Subject: [PATCH 0782/1038] Add Cell State sensor to Vallox (#58358) --- homeassistant/components/vallox/const.py | 7 ++++++ homeassistant/components/vallox/sensor.py | 30 ++++++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/vallox/const.py b/homeassistant/components/vallox/const.py index 6d12d0bab8e..aba10188bde 100644 --- a/homeassistant/components/vallox/const.py +++ b/homeassistant/components/vallox/const.py @@ -37,3 +37,10 @@ VALLOX_PROFILE_TO_STR_REPORTABLE = { STR_TO_VALLOX_PROFILE_SETTABLE = { value: key for (key, value) in VALLOX_PROFILE_TO_STR_SETTABLE.items() } + +VALLOX_CELL_STATE_TO_STR = { + 0: "Heat Recovery", + 1: "Cool Recovery", + 2: "Bypass", + 3: "Defrosting", +} diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index 2fdfd2cd472..0341153a9ff 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -25,7 +25,13 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateTyp from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import ValloxDataUpdateCoordinator -from .const import DOMAIN, METRIC_KEY_MODE, MODE_ON, VALLOX_PROFILE_TO_STR_REPORTABLE +from .const import ( + DOMAIN, + METRIC_KEY_MODE, + MODE_ON, + VALLOX_CELL_STATE_TO_STR, + VALLOX_PROFILE_TO_STR_REPORTABLE, +) _LOGGER = logging.getLogger(__name__) @@ -106,6 +112,21 @@ class ValloxFilterRemainingSensor(ValloxSensor): return (now + days_remaining_delta).isoformat() +class ValloxCellStateSensor(ValloxSensor): + """Child class for cell state reporting.""" + + @property + def native_value(self) -> StateType: + """Return the value reported by the sensor.""" + super_native_value = super().native_value + + if not isinstance(super_native_value, (int, float)): + _LOGGER.debug("Value has unexpected type: %s", type(super_native_value)) + return None + + return VALLOX_CELL_STATE_TO_STR.get(int(super_native_value)) + + @dataclass class ValloxSensorEntityDescription(SensorEntityDescription): """Describes Vallox sensor entity.""" @@ -137,6 +158,13 @@ SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( device_class=DEVICE_CLASS_TIMESTAMP, sensor_type=ValloxFilterRemainingSensor, ), + ValloxSensorEntityDescription( + key="cell_state", + name="Cell State", + icon="mdi:swap-horizontal-bold", + metric_key="A_CYC_CELL_STATE", + sensor_type=ValloxCellStateSensor, + ), ValloxSensorEntityDescription( key="extract_air", name="Extract Air", From f3a1c81e22b6bf9c735778a3932e9dd8a5491df3 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 25 Oct 2021 00:12:33 +0000 Subject: [PATCH 0783/1038] [ci skip] Translation update --- .../tuya/translations/select.ca.json | 27 +++++++++++++++++++ .../tuya/translations/select.de.json | 27 +++++++++++++++++++ .../tuya/translations/select.et.json | 27 +++++++++++++++++++ .../tuya/translations/select.hu.json | 27 +++++++++++++++++++ .../tuya/translations/select.ru.json | 27 +++++++++++++++++++ 5 files changed, 135 insertions(+) diff --git a/homeassistant/components/tuya/translations/select.ca.json b/homeassistant/components/tuya/translations/select.ca.json index c5eec67dd3a..8aa03aa4bfa 100644 --- a/homeassistant/components/tuya/translations/select.ca.json +++ b/homeassistant/components/tuya/translations/select.ca.json @@ -1,5 +1,23 @@ { "state": { + "tuya__basic_anti_flickr": { + "0": "Desactivat", + "1": "50 Hz", + "2": "60 Hz" + }, + "tuya__basic_nightvision": { + "0": "Autom\u00e0tic", + "1": "off", + "2": "on" + }, + "tuya__decibel_sensitivity": { + "0": "Sensibilitat baixa", + "1": "Sensibilitat alta" + }, + "tuya__ipc_work_mode": { + "0": "Mode de baix consum", + "1": "Mode de funcionament continu" + }, "tuya__led_type": { "halogen": "Halogen", "incandescent": "Incandescent", @@ -10,6 +28,15 @@ "pos": "Indica la ubicaci\u00f3 de l'interruptor", "relay": "Indiqueu l'estat, activat/desactivat" }, + "tuya__motion_sensitivity": { + "0": "Sensibilitat baixa", + "1": "Sensibilitat mitjana", + "2": "Sensibilitat alta" + }, + "tuya__record_mode": { + "1": "Nom\u00e9s enregistra esdeveniments", + "2": "Enregistrament continu" + }, "tuya__relay_status": { "last": "Recorda l'\u00faltim estat", "memory": "Recorda l'\u00faltim estat", diff --git a/homeassistant/components/tuya/translations/select.de.json b/homeassistant/components/tuya/translations/select.de.json index bebed009b54..f068b8b4425 100644 --- a/homeassistant/components/tuya/translations/select.de.json +++ b/homeassistant/components/tuya/translations/select.de.json @@ -1,5 +1,23 @@ { "state": { + "tuya__basic_anti_flickr": { + "0": "Aus", + "1": "50 Hz", + "2": "60 Hz" + }, + "tuya__basic_nightvision": { + "0": "Automatisch", + "1": "Aus", + "2": "An" + }, + "tuya__decibel_sensitivity": { + "0": "Geringe Empfindlichkeit", + "1": "Hohe Empfindlichkeit" + }, + "tuya__ipc_work_mode": { + "0": "Energiesparmodus", + "1": "Kontinuierlicher Arbeitsmodus" + }, "tuya__led_type": { "halogen": "Halogen", "incandescent": "Gl\u00fchlampe", @@ -10,6 +28,15 @@ "pos": "Schalterposition anzeigen", "relay": "Ein-/Ausschaltzustand anzeigen" }, + "tuya__motion_sensitivity": { + "0": "Geringe Empfindlichkeit", + "1": "Mittlere Empfindlichkeit", + "2": "Hohe Empfindlichkeit" + }, + "tuya__record_mode": { + "1": "Nur Ereignisse aufzeichnen", + "2": "Kontinuierliche Aufnahme" + }, "tuya__relay_status": { "last": "Letzten Zustand merken", "memory": "Letzten Zustand merken", diff --git a/homeassistant/components/tuya/translations/select.et.json b/homeassistant/components/tuya/translations/select.et.json index d2556dc8277..58152555b56 100644 --- a/homeassistant/components/tuya/translations/select.et.json +++ b/homeassistant/components/tuya/translations/select.et.json @@ -1,5 +1,23 @@ { "state": { + "tuya__basic_anti_flickr": { + "0": "Keelatud", + "1": "50 Hz", + "2": "60 Hz" + }, + "tuya__basic_nightvision": { + "0": "Automaatne", + "1": "V\u00e4ljas", + "2": "Sees" + }, + "tuya__decibel_sensitivity": { + "0": "Madal tundlikkus", + "1": "K\u00f5rge tundlikkus" + }, + "tuya__ipc_work_mode": { + "0": "Madala energiatarbega re\u017eiim", + "1": "Pidev t\u00f6\u00f6re\u017eiim" + }, "tuya__led_type": { "halogen": "Halogeenlamp", "incandescent": "H\u00f5\u00f5glamp", @@ -10,6 +28,15 @@ "pos": "Kuva l\u00fcliti olekut", "relay": "Kuva l\u00fcliti sees/v\u00e4ljas olekut" }, + "tuya__motion_sensitivity": { + "0": "Madal tundlikkus", + "1": "Keskmine tundlikkus", + "2": "K\u00f5rge tundlikkus" + }, + "tuya__record_mode": { + "1": "Salvesta ainult s\u00fcndmused", + "2": "Pidev salvestamine" + }, "tuya__relay_status": { "last": "J\u00e4ta viimane olek meelde", "memory": "J\u00e4ta viimane olek meelde", diff --git a/homeassistant/components/tuya/translations/select.hu.json b/homeassistant/components/tuya/translations/select.hu.json index 0a76428404b..6d9b4846e64 100644 --- a/homeassistant/components/tuya/translations/select.hu.json +++ b/homeassistant/components/tuya/translations/select.hu.json @@ -1,5 +1,23 @@ { "state": { + "tuya__basic_anti_flickr": { + "0": "Letiltva", + "1": "50 Hz", + "2": "60 Hz" + }, + "tuya__basic_nightvision": { + "0": "Automatikus", + "1": "Ki", + "2": "Be" + }, + "tuya__decibel_sensitivity": { + "0": "Alacsony \u00e9rz\u00e9kenys\u00e9g", + "1": "Magas \u00e9rz\u00e9kenys\u00e9g" + }, + "tuya__ipc_work_mode": { + "0": "Alacsony fogyaszt\u00e1s\u00fa m\u00f3d", + "1": "Folyamatos \u00fczemm\u00f3d" + }, "tuya__led_type": { "halogen": "Halog\u00e9n", "incandescent": "Izz\u00f3", @@ -10,6 +28,15 @@ "pos": "A kapcsol\u00f3 hely\u00e9nek jelz\u00e9se", "relay": "Be-/kikapcsolt \u00e1llapot jelz\u00e9se" }, + "tuya__motion_sensitivity": { + "0": "Alacsony \u00e9rz\u00e9kenys\u00e9g", + "1": "K\u00f6zepes \u00e9rz\u00e9kenys\u00e9g", + "2": "Magas \u00e9rz\u00e9kenys\u00e9g" + }, + "tuya__record_mode": { + "1": "Csak esem\u00e9nyek r\u00f6gz\u00edt\u00e9se", + "2": "Folyamatos felv\u00e9tel" + }, "tuya__relay_status": { "last": "Utols\u00f3 \u00e1llapot megjegyz\u00e9se", "memory": "Utols\u00f3 \u00e1llapot megjegyz\u00e9se", diff --git a/homeassistant/components/tuya/translations/select.ru.json b/homeassistant/components/tuya/translations/select.ru.json index b2a31d128d6..3c9401e4249 100644 --- a/homeassistant/components/tuya/translations/select.ru.json +++ b/homeassistant/components/tuya/translations/select.ru.json @@ -1,5 +1,23 @@ { "state": { + "tuya__basic_anti_flickr": { + "0": "\u041e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "1": "50 \u0413\u0446", + "2": "60 \u0413\u0446" + }, + "tuya__basic_nightvision": { + "0": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438", + "1": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "2": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + }, + "tuya__decibel_sensitivity": { + "0": "\u041d\u0438\u0437\u043a\u0430\u044f \u0447\u0443\u0432\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c", + "1": "\u0412\u044b\u0441\u043e\u043a\u0430\u044f \u0447\u0443\u0432\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c" + }, + "tuya__ipc_work_mode": { + "0": "\u0420\u0435\u0436\u0438\u043c \u043d\u0438\u0437\u043a\u043e\u0433\u043e \u044d\u043d\u0435\u0440\u0433\u043e\u043f\u043e\u0442\u0440\u0435\u0431\u043b\u0435\u043d\u0438\u044f", + "1": "\u041d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c \u0440\u0430\u0431\u043e\u0442\u044b" + }, "tuya__led_type": { "halogen": "\u0413\u0430\u043b\u043e\u0433\u0435\u043d", "incandescent": "\u041b\u0430\u043c\u043f\u0430 \u043d\u0430\u043a\u0430\u043b\u0438\u0432\u0430\u043d\u0438\u044f", @@ -10,6 +28,15 @@ "pos": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044f", "relay": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f/\u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" }, + "tuya__motion_sensitivity": { + "0": "\u041d\u0438\u0437\u043a\u0430\u044f \u0447\u0443\u0432\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c", + "1": "\u0421\u0440\u0435\u0434\u043d\u044f\u044f \u0447\u0443\u0432\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c", + "2": "\u0412\u044b\u0441\u043e\u043a\u0430\u044f \u0447\u0443\u0432\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c" + }, + "tuya__record_mode": { + "1": "\u0417\u0430\u043f\u0438\u0441\u044b\u0432\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u0441\u043e\u0431\u044b\u0442\u0438\u044f", + "2": "\u041d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c" + }, "tuya__relay_status": { "last": "\u0417\u0430\u043f\u043e\u043c\u043d\u0438\u0442\u044c \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435", "memory": "\u0417\u0430\u043f\u043e\u043c\u043d\u0438\u0442\u044c \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435", From e9ca511327fefb127f6f41b52b1b62faf2f75f7c Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Mon, 25 Oct 2021 05:12:26 +0200 Subject: [PATCH 0784/1038] Add support for entity category for necessary KNX platforms (#58357) --- homeassistant/components/knx/binary_sensor.py | 2 ++ homeassistant/components/knx/climate.py | 8 ++++- homeassistant/components/knx/cover.py | 3 +- homeassistant/components/knx/fan.py | 3 +- homeassistant/components/knx/light.py | 3 +- homeassistant/components/knx/number.py | 2 ++ homeassistant/components/knx/scene.py | 3 +- homeassistant/components/knx/schema.py | 13 +++++++ homeassistant/components/knx/select.py | 8 ++++- homeassistant/components/knx/sensor.py | 3 +- homeassistant/components/knx/switch.py | 9 ++++- homeassistant/components/knx/weather.py | 3 +- tests/components/knx/test_binary_sensor.py | 34 ++++++++++++++++++- 13 files changed, 84 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index 7a3de7e1ec0..5ed0e55765d 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -9,6 +9,7 @@ from xknx.devices import BinarySensor as XknxBinarySensor from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import ( CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, CONF_NAME, STATE_ON, STATE_UNAVAILABLE, @@ -64,6 +65,7 @@ class KNXBinarySensor(KnxEntity, BinarySensorEntity, RestoreEntity): reset_after=config.get(BinarySensorSchema.CONF_RESET_AFTER), ) ) + self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_device_class = config.get(CONF_DEVICE_CLASS) self._attr_force_update = self._device.ignore_internal_state self._attr_unique_id = str(self._device.remote_value.group_address_state) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 22289738711..2099270f036 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -17,7 +17,12 @@ from homeassistant.components.climate.const import ( SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.const import ATTR_TEMPERATURE, CONF_NAME, TEMP_CELSIUS +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_ENTITY_CATEGORY, + CONF_NAME, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -176,6 +181,7 @@ class KNXClimate(KnxEntity, ClimateEntity): def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize of a KNX climate device.""" super().__init__(_create_climate(xknx, config)) + self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_supported_features = SUPPORT_TARGET_TEMPERATURE if self.preset_modes: self._attr_supported_features |= SUPPORT_PRESET_MODE diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 85cd6c9a60f..8eb906d1ba0 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -23,7 +23,7 @@ from homeassistant.components.cover import ( SUPPORT_STOP_TILT, CoverEntity, ) -from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME +from homeassistant.const import CONF_DEVICE_CLASS, CONF_ENTITY_CATEGORY, CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -109,6 +109,7 @@ class KNXCover(KnxEntity, CoverEntity): ) ) self._unsubscribe_auto_updater: Callable[[], None] | None = None + self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_device_class = config.get(CONF_DEVICE_CLASS) or ( DEVICE_CLASS_BLIND if self._device.supports_angle else None diff --git a/homeassistant/components/knx/fan.py b/homeassistant/components/knx/fan.py index 21ede700fdd..a000cdec973 100644 --- a/homeassistant/components/knx/fan.py +++ b/homeassistant/components/knx/fan.py @@ -8,7 +8,7 @@ from xknx import XKNX from xknx.devices import Fan as XknxFan from homeassistant.components.fan import SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -65,6 +65,7 @@ class KNXFan(KnxEntity, FanEntity): ) # FanSpeedMode.STEP if max_step is set self._step_range: tuple[int, int] | None = (1, max_step) if max_step else None + self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = str(self._device.speed.group_address) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index b807ad1335d..bee96270c36 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -23,7 +23,7 @@ from homeassistant.components.light import ( COLOR_MODE_XY, LightEntity, ) -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -242,6 +242,7 @@ class KNXLight(KnxEntity, LightEntity): self._attr_min_mireds = color_util.color_temperature_kelvin_to_mired( self._max_kelvin ) + self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = self._device_unique_id() def _device_unique_id(self) -> str: diff --git a/homeassistant/components/knx/number.py b/homeassistant/components/knx/number.py index c48c11fa998..2fc8e1af244 100644 --- a/homeassistant/components/knx/number.py +++ b/homeassistant/components/knx/number.py @@ -8,6 +8,7 @@ from xknx.devices import NumericValue from homeassistant.components.number import NumberEntity from homeassistant.const import ( + CONF_ENTITY_CATEGORY, CONF_MODE, CONF_NAME, CONF_TYPE, @@ -74,6 +75,7 @@ class KNXNumber(KnxEntity, NumberEntity, RestoreEntity): NumberSchema.CONF_STEP, self._device.sensor_value.dpt_class.resolution, ) + self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = str(self._device.sensor_value.group_address) self._attr_unit_of_measurement = self._device.unit_of_measurement() self._device.sensor_value.value = max(0, self._attr_min_value) diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py index 9bd32c99e41..b09dc678be3 100644 --- a/homeassistant/components/knx/scene.py +++ b/homeassistant/components/knx/scene.py @@ -7,7 +7,7 @@ from xknx import XKNX from xknx.devices import Scene as XknxScene from homeassistant.components.scene import Scene -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -49,6 +49,7 @@ class KNXScene(KnxEntity, Scene): scene_number=config[SceneSchema.CONF_SCENE_NUMBER], ) ) + self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = ( f"{self._device.scene_value.group_address}_{self._device.scene_number}" ) diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 653084f3e3a..0e54a9abbc5 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -22,6 +22,7 @@ from homeassistant.components.number.const import MODE_AUTO, MODE_BOX, MODE_SLID from homeassistant.components.sensor import CONF_STATE_CLASS, STATE_CLASSES_SCHEMA from homeassistant.const import ( CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, CONF_ENTITY_ID, CONF_HOST, CONF_MODE, @@ -30,6 +31,7 @@ from homeassistant.const import ( CONF_TYPE, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA from .const import ( CONF_INVERT, @@ -253,6 +255,7 @@ class BinarySensorSchema(KNXPlatformSchema): ), vol.Optional(CONF_DEVICE_CLASS): vol.In(BINARY_SENSOR_DEVICE_CLASSES), vol.Optional(CONF_RESET_AFTER): cv.positive_float, + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } ), ) @@ -371,6 +374,7 @@ class ClimateSchema(KNXPlatformSchema): ): vol.In(HVAC_MODES), vol.Optional(CONF_MIN_TEMP): vol.Coerce(float), vol.Optional(CONF_MAX_TEMP): vol.Coerce(float), + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } ), ) @@ -425,6 +429,7 @@ class CoverSchema(KNXPlatformSchema): vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean, vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean, vol.Optional(CONF_DEVICE_CLASS): vol.In(COVER_DEVICE_CLASSES), + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } ), ) @@ -487,6 +492,7 @@ class FanSchema(KNXPlatformSchema): vol.Optional(CONF_OSCILLATION_ADDRESS): ga_list_validator, vol.Optional(CONF_OSCILLATION_STATE_ADDRESS): ga_list_validator, vol.Optional(CONF_MAX_STEP): cv.byte, + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } ) @@ -590,6 +596,7 @@ class LightSchema(KNXPlatformSchema): vol.Optional(CONF_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): vol.All( vol.Coerce(int), vol.Range(min=1) ), + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } ), vol.Any( @@ -669,6 +676,7 @@ class NumberSchema(KNXPlatformSchema): vol.Optional(CONF_MAX): vol.Coerce(float), vol.Optional(CONF_MIN): vol.Coerce(float), vol.Optional(CONF_STEP): cv.positive_float, + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } ), number_limit_sub_validator, @@ -690,6 +698,7 @@ class SceneSchema(KNXPlatformSchema): vol.Required(CONF_SCENE_NUMBER): vol.All( vol.Coerce(int), vol.Range(min=1, max=64) ), + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } ) @@ -722,6 +731,7 @@ class SelectSchema(KNXPlatformSchema): ], vol.Required(KNX_ADDRESS): ga_list_validator, vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } ), select_options_sub_validator, @@ -746,6 +756,7 @@ class SensorSchema(KNXPlatformSchema): vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, vol.Required(CONF_TYPE): sensor_type_validator, vol.Required(CONF_STATE_ADDRESS): ga_list_validator, + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } ) @@ -766,6 +777,7 @@ class SwitchSchema(KNXPlatformSchema): vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean, vol.Required(KNX_ADDRESS): ga_list_validator, vol.Optional(CONF_STATE_ADDRESS): ga_list_validator, + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } ) @@ -812,6 +824,7 @@ class WeatherSchema(KNXPlatformSchema): vol.Optional(CONF_KNX_DAY_NIGHT_ADDRESS): ga_list_validator, vol.Optional(CONF_KNX_AIR_PRESSURE_ADDRESS): ga_list_validator, vol.Optional(CONF_KNX_HUMIDITY_ADDRESS): ga_list_validator, + vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } ), ) diff --git a/homeassistant/components/knx/select.py b/homeassistant/components/knx/select.py index 90e0203a8be..e548ad27c8a 100644 --- a/homeassistant/components/knx/select.py +++ b/homeassistant/components/knx/select.py @@ -5,7 +5,12 @@ from xknx import XKNX from xknx.devices import Device as XknxDevice, RawValue from homeassistant.components.select import SelectEntity -from homeassistant.const import CONF_NAME, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.const import ( + CONF_ENTITY_CATEGORY, + CONF_NAME, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity @@ -66,6 +71,7 @@ class KNXSelect(KnxEntity, SelectEntity, RestoreEntity): } self._attr_options = list(self._option_payloads) self._attr_current_option = None + self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = str(self._device.remote_value.group_address) async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index c1f68e4c376..fb64f65968b 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -11,7 +11,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASSES, SensorEntity, ) -from homeassistant.const import CONF_NAME, CONF_TYPE +from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType @@ -65,6 +65,7 @@ class KNXSensor(KnxEntity, SensorEntity): else None ) self._attr_force_update = self._device.always_callback + self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = str(self._device.sensor_value.group_address_state) self._attr_native_unit_of_measurement = self._device.unit_of_measurement() self._attr_state_class = config.get(CONF_STATE_CLASS) diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index cca0ffde853..c775ce70d32 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -7,7 +7,13 @@ from xknx import XKNX from xknx.devices import Switch as XknxSwitch from homeassistant.components.switch import SwitchEntity -from homeassistant.const import CONF_NAME, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.const import ( + CONF_ENTITY_CATEGORY, + CONF_NAME, + STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity @@ -52,6 +58,7 @@ class KNXSwitch(KnxEntity, SwitchEntity, RestoreEntity): invert=config[SwitchSchema.CONF_INVERT], ) ) + self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = str(self._device.switch.group_address) async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py index 4a55f81ff72..13ebd2480e3 100644 --- a/homeassistant/components/knx/weather.py +++ b/homeassistant/components/knx/weather.py @@ -5,7 +5,7 @@ from xknx import XKNX from xknx.devices import Weather as XknxWeather from homeassistant.components.weather import WeatherEntity -from homeassistant.const import CONF_NAME, TEMP_CELSIUS +from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -78,6 +78,7 @@ class KNXWeather(KnxEntity, WeatherEntity): """Initialize of a KNX sensor.""" super().__init__(_create_weather(xknx, config)) self._attr_unique_id = str(self._device._temperature.group_address_state) + self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) @property def temperature(self) -> float | None: diff --git a/tests/components/knx/test_binary_sensor.py b/tests/components/knx/test_binary_sensor.py index 811c8ab1341..9adc7205543 100644 --- a/tests/components/knx/test_binary_sensor.py +++ b/tests/components/knx/test_binary_sensor.py @@ -4,8 +4,17 @@ from unittest.mock import patch from homeassistant.components.knx.const import CONF_STATE_ADDRESS, CONF_SYNC_STATE from homeassistant.components.knx.schema import BinarySensorSchema -from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON +from homeassistant.const import ( + CONF_ENTITY_CATEGORY, + CONF_NAME, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_OFF, + STATE_ON, +) from homeassistant.core import HomeAssistant, State +from homeassistant.helpers.entity_registry import ( + async_get_registry as async_get_entity_registry, +) from homeassistant.util import dt from .conftest import KNXTestKit @@ -13,6 +22,29 @@ from .conftest import KNXTestKit from tests.common import async_capture_events, async_fire_time_changed +async def test_binary_sensor_entity_category(hass: HomeAssistant, knx: KNXTestKit): + """Test KNX binary sensor entity category.""" + await knx.setup_integration( + { + BinarySensorSchema.PLATFORM_NAME: [ + { + CONF_NAME: "test_normal", + CONF_STATE_ADDRESS: "1/1/1", + CONF_ENTITY_CATEGORY: ENTITY_CATEGORY_DIAGNOSTIC, + }, + ] + } + ) + assert len(hass.states.async_all()) == 1 + + await knx.assert_read("1/1/1") + await knx.receive_response("1/1/1", True) + + registry = await async_get_entity_registry(hass) + entity = registry.async_get("binary_sensor.test_normal") + assert entity.entity_category == ENTITY_CATEGORY_DIAGNOSTIC + + async def test_binary_sensor(hass: HomeAssistant, knx: KNXTestKit): """Test KNX binary sensor and inverted binary_sensor.""" await knx.setup_integration( From 06008bc343743de3087ccc77f9f3da65d356f79a Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Mon, 25 Oct 2021 11:33:41 +0800 Subject: [PATCH 0785/1038] Fix EXT-X-PROGRAM-DATE-TIME in stream (#58036) * Fix EXT-X-PROGRAM-DATE-TIME in stream * Update fragment duration comments in worker * Update duration test in worker * Augment test on low latency playlists * Reset start_time on discontinuity --- homeassistant/components/stream/worker.py | 25 +++++--- tests/components/stream/test_hls.py | 7 ++- tests/components/stream/test_ll_hls.py | 71 +++++++++++++++++------ tests/components/stream/test_worker.py | 21 +++++-- 4 files changed, 92 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index a576ff6d02b..398d45595d3 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -93,11 +93,17 @@ class SegmentBuffer: # Create a fragment every TARGET_PART_DURATION. The data from each fragment is stored in # a "Part" that can be combined with the data from all the other "Part"s, plus an init # section, to reconstitute the data in a "Segment". - # frag_duration seems to be a minimum threshold for determining part boundaries, so some - # parts may have a higher duration. Since Part Target Duration is used in LL-HLS as a - # maximum threshold for part durations, we scale that number down here by .85 and hope - # that the output part durations stay below the maximum Part Target Duration threshold. - # See https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-4.4.4.9 + # frag_duration is the threshold for determining part boundaries, and the dts of the last + # packet in the part should correspond to a duration that is smaller than this value. + # However, as the part duration includes the duration of the last frame, the part duration + # will be equal to or greater than this value. + # We previously scaled this number down by .85 to account for this while keeping within + # the 15% variance allowed in part duration. However, this did not work when inputs had + # an audio stream - sometimes the fragment would get cut on the audio packet, causing + # the durations to actually be to short. + # The current approach is to use this frag_duration for creating the media while + # adjusting the metadata duration to keep the durations in the metadata below the + # part_target_duration threshold. "frag_duration": str( self._stream_settings.part_target_duration * 1e6 ), @@ -153,8 +159,6 @@ class SegmentBuffer: ): # Flush segment (also flushes the stub part segment) self.flush(packet, last_part=True) - # Reinitialize - self.reset(packet.dts) # Mux the packet packet.stream = self._output_video_stream @@ -201,6 +205,10 @@ class SegmentBuffer: # value which exceeds the part_target_duration. This can muck up the # duration of both this part and the next part. An easy fix is to just # use the current packet dts and cap it by the part target duration. + # The adjustment may cause a drift between this adjusted duration + # (used in the metadata) and the media duration, but the drift should be + # automatically corrected when the part duration cleanly divides the + # framerate. current_dts = min( packet.dts, self._part_start_dts @@ -226,6 +234,8 @@ class SegmentBuffer: if last_part: # If we've written the last part, we can close the memory_file. self._memory_file.close() # We don't need the BytesIO object anymore + # Reinitialize + self.reset(current_dts) else: # For the last part, these will get set again elsewhere so we can skip # setting them here. @@ -239,6 +249,7 @@ class SegmentBuffer: # simple to check for discontinuity at output time, and to determine # the discontinuity sequence number. self._stream_id += 1 + self._start_time = datetime.datetime.utcnow() def close(self) -> None: """Close stream buffer.""" diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index c3c4779a948..07c8cc88a65 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -83,15 +83,18 @@ def make_playlist( discontinuity_sequence=0, segments=None, hint=None, + segment_duration=None, part_target_duration=None, ): """Create a an hls playlist response for tests to assert on.""" + if not segment_duration: + segment_duration = SEGMENT_DURATION response = [ "#EXTM3U", "#EXT-X-VERSION:6", "#EXT-X-INDEPENDENT-SEGMENTS", '#EXT-X-MAP:URI="init.mp4"', - f"#EXT-X-TARGETDURATION:{SEGMENT_DURATION}", + f"#EXT-X-TARGETDURATION:{segment_duration}", f"#EXT-X-MEDIA-SEQUENCE:{sequence}", f"#EXT-X-DISCONTINUITY-SEQUENCE:{discontinuity_sequence}", ] @@ -105,7 +108,7 @@ def make_playlist( ) else: response.append( - f"#EXT-X-START:TIME-OFFSET=-{EXT_X_START_NON_LL_HLS*SEGMENT_DURATION:.3f},PRECISE=YES", + f"#EXT-X-START:TIME-OFFSET=-{EXT_X_START_NON_LL_HLS*segment_duration:.3f},PRECISE=YES", ) if segments: response.extend(segments) diff --git a/tests/components/stream/test_ll_hls.py b/tests/components/stream/test_ll_hls.py index 324b1435110..1156832ada9 100644 --- a/tests/components/stream/test_ll_hls.py +++ b/tests/components/stream/test_ll_hls.py @@ -1,10 +1,13 @@ """The tests for hls streams.""" import asyncio +from collections import deque from http import HTTPStatus import itertools +import math import re from urllib.parse import urlparse +from dateutil import parser import pytest from homeassistant.components.stream import create_stream @@ -19,7 +22,7 @@ from homeassistant.components.stream.const import ( from homeassistant.components.stream.core import Part from homeassistant.setup import async_setup_component -from .test_hls import SEGMENT_DURATION, STREAM_SOURCE, HlsClient, make_playlist +from .test_hls import STREAM_SOURCE, HlsClient, make_playlist from tests.components.stream.common import ( FAKE_TIME, @@ -27,7 +30,8 @@ from tests.components.stream.common import ( generate_h264_video, ) -TEST_PART_DURATION = 1 +SEGMENT_DURATION = 6 +TEST_PART_DURATION = 0.75 NUM_PART_SEGMENTS = int(-(-SEGMENT_DURATION // TEST_PART_DURATION)) PART_INDEPENDENT_PERIOD = int(1 / TEST_PART_DURATION) or 1 BYTERANGE_LENGTH = 1 @@ -98,7 +102,7 @@ def make_segment_with_parts( "#EXT-X-PROGRAM-DATE-TIME:" + FAKE_TIME.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z", - f"#EXTINF:{SEGMENT_DURATION:.3f},", + f"#EXTINF:{math.ceil(SEGMENT_DURATION/TEST_PART_DURATION)*TEST_PART_DURATION:.3f},", f"./segment/{segment}.m4s", ] ) @@ -124,15 +128,18 @@ async def test_ll_hls_stream(hass, hls_stream, stream_worker_sync): "stream": { CONF_LL_HLS: True, CONF_SEGMENT_DURATION: SEGMENT_DURATION, - CONF_PART_DURATION: TEST_PART_DURATION, + # Use a slight mismatch in PART_DURATION to mimic + # misalignments with source DTSs + CONF_PART_DURATION: TEST_PART_DURATION - 0.01, } }, ) stream_worker_sync.pause() + num_playlist_segments = 3 # Setup demo HLS track - source = generate_h264_video(duration=SEGMENT_DURATION + 1) + source = generate_h264_video(duration=num_playlist_segments * SEGMENT_DURATION + 2) stream = create_stream(hass, source, {}) # Request stream @@ -152,7 +159,9 @@ async def test_ll_hls_stream(hass, hls_stream, stream_worker_sync): # Fetch playlist playlist_url = "/" + master_playlist.splitlines()[-1] - playlist_response = await hls_client.get(playlist_url) + playlist_response = await hls_client.get( + playlist_url + f"?_HLS_msn={num_playlist_segments-1}" + ) assert playlist_response.status == HTTPStatus.OK # Fetch segments @@ -181,27 +190,53 @@ async def test_ll_hls_stream(hass, hls_stream, stream_worker_sync): return False return True - # Fetch all completed part segments + # Parse playlist part_re = re.compile( - r'#EXT-X-PART:DURATION=[0-9].[0-9]{5,5},URI="(?P.+?)",BYTERANGE="(?P[0-9]+?)@(?P[0-9]+?)"(,INDEPENDENT=YES)?' + r'#EXT-X-PART:DURATION=(?P[0-9]{1,}.[0-9]{3,}),URI="(?P.+?)"(,INDEPENDENT=YES)?' ) + datetime_re = re.compile(r"#EXT-X-PROGRAM-DATE-TIME:(?P.+)") + inf_re = re.compile(r"#EXTINF:(?P[0-9]{1,}.[0-9]{3,}),") + # keep track of which tests were done (indexed by re) + tested = {regex: False for regex in (part_re, datetime_re, inf_re)} + # keep track of times and durations along playlist for checking consistency + part_durations = [] + segment_duration = 0 + datetimes = deque() for line in playlist.splitlines(): match = part_re.match(line) if match: + # Fetch all completed part segments + part_durations.append(float(match.group("part_duration"))) part_segment_url = "/" + match.group("part_url") - byterange_end = ( - int(match.group("byterange_length")) - + int(match.group("byterange_start")) - - 1 - ) part_segment_response = await hls_client.get( part_segment_url, - headers={ - "Range": f'bytes={match.group("byterange_start")}-{byterange_end}' - }, ) - assert part_segment_response.status == HTTPStatus.PARTIAL_CONTENT + assert part_segment_response.status == HTTPStatus.OK assert check_part_is_moof_mdat(await part_segment_response.read()) + tested[part_re] = True + continue + match = datetime_re.match(line) + if match: + datetimes.append(parser.parse(match.group("datetime"))) + # Check that segment durations are consistent with PROGRAM-DATE-TIME + if len(datetimes) > 1: + datetime_duration = ( + datetimes[-1] - datetimes.popleft() + ).total_seconds() + if segment_duration: + assert datetime_duration == segment_duration + tested[datetime_re] = True + continue + match = inf_re.match(line) + if match: + segment_duration = float(match.group("segment_duration")) + # Check that segment durations are consistent with part durations + if len(part_durations) > 1: + assert math.isclose(sum(part_durations), segment_duration) + tested[inf_re] = True + part_durations.clear() + # make sure all playlist tests were performed + assert all(tested.values()) stream_worker_sync.resume() @@ -252,6 +287,7 @@ async def test_ll_hls_playlist_view(hass, hls_stream, stream_worker_sync): for i in range(2) ], hint=make_hint(2, 0), + segment_duration=SEGMENT_DURATION, part_target_duration=hls.stream_settings.part_target_duration, ) @@ -273,6 +309,7 @@ async def test_ll_hls_playlist_view(hass, hls_stream, stream_worker_sync): for i in range(3) ], hint=make_hint(3, 0), + segment_duration=SEGMENT_DURATION, part_target_duration=hls.stream_settings.part_target_duration, ) diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index e353f950aea..12f859b203b 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -702,12 +702,21 @@ async def test_durations(hass, record_worker_sync): for part in segment.parts: av_part = av.open(io.BytesIO(segment.init + part.data)) running_metadata_duration += part.duration - # av_part.duration will just return the largest dts in av_part. - # When we normalize by av.time_base this should equal the running duration - assert math.isclose( - running_metadata_duration, - av_part.duration / av.time_base, - abs_tol=1e-6, + # av_part.duration actually returns the dts of the first packet of + # the next av_part. When we normalize this by av.time_base we get + # the running duration of the media. + # The metadata duration is slightly different. The worker has + # some flexibility of where to set each metadata boundary, and + # when the media's duration is slightly too long, the metadata + # duration is adjusted down. This means that the running metadata + # duration may be up to one video frame duration smaller than the + # part duration. + assert running_metadata_duration < av_part.duration / av.time_base + 1e-6 + assert ( + running_metadata_duration + > av_part.duration / av.time_base + - 1 / av_part.streams.video[0].rate + - 1e-6 ) av_part.close() # check that the Part durations are consistent with the Segment durations From a69416521e89ffb73a14b6be223ecd0d06f13d1c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 24 Oct 2021 22:21:20 -0700 Subject: [PATCH 0786/1038] Add entity category to MyQ (#58377) Co-authored-by: J. Nick Koston --- homeassistant/components/myq/binary_sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/myq/binary_sensor.py b/homeassistant/components/myq/binary_sensor.py index 9f2d766fcc4..2cb35d351c4 100644 --- a/homeassistant/components/myq/binary_sensor.py +++ b/homeassistant/components/myq/binary_sensor.py @@ -3,6 +3,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, BinarySensorEntity, ) +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from . import MyQEntity from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY @@ -26,6 +27,7 @@ class MyQBinarySensorEntity(MyQEntity, BinarySensorEntity): """Representation of a MyQ gateway.""" _attr_device_class = DEVICE_CLASS_CONNECTIVITY + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC @property def name(self): From c950f1ccfa600972d3fe469737f47494616f78a2 Mon Sep 17 00:00:00 2001 From: Patrik Lindgren <21142447+ggravlingen@users.noreply.github.com> Date: Mon, 25 Oct 2021 08:15:46 +0200 Subject: [PATCH 0787/1038] Initial support for Tradfri STARKVIND Air purifier (#58295) * Initial commit * Update homeassistant/components/tradfri/const.py Co-authored-by: Martin Hjelmare * Feedback * Updates * Remove logger * Fix codestring * Update homeassistant/components/tradfri/fan.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/tradfri/fan.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/tradfri/fan.py Co-authored-by: Martin Hjelmare * Percent value * Add tests * Typo * Update tests * Fix util function * Update homeassistant/components/tradfri/fan.py Co-authored-by: Martin Hjelmare * Update after review * Review * Coverage * Fix test Co-authored-by: Martin Hjelmare --- .coveragerc | 1 + .../components/tradfri/base_class.py | 6 +- homeassistant/components/tradfri/const.py | 3 +- homeassistant/components/tradfri/fan.py | 175 ++++++++++++++++++ homeassistant/components/tradfri/sensor.py | 1 + tests/components/tradfri/test_light.py | 1 + tests/components/tradfri/test_util.py | 22 +++ 7 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/tradfri/fan.py create mode 100644 tests/components/tradfri/test_util.py diff --git a/.coveragerc b/.coveragerc index 8f6db3b3935..a4a221db69a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1110,6 +1110,7 @@ omit = homeassistant/components/tradfri/base_class.py homeassistant/components/tradfri/config_flow.py homeassistant/components/tradfri/cover.py + homeassistant/components/tradfri/fan.py homeassistant/components/tradfri/light.py homeassistant/components/tradfri/sensor.py homeassistant/components/tradfri/switch.py diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py index 1222e9b7792..a03f16a1f09 100644 --- a/homeassistant/components/tradfri/base_class.py +++ b/homeassistant/components/tradfri/base_class.py @@ -8,6 +8,8 @@ from typing import Any from pytradfri.command import Command from pytradfri.device import Device +from pytradfri.device.air_purifier import AirPurifier +from pytradfri.device.air_purifier_control import AirPurifierControl from pytradfri.device.blind import Blind from pytradfri.device.blind_control import BlindControl from pytradfri.device.light import Light @@ -58,10 +60,10 @@ class TradfriBaseClass(Entity): """Initialize a device.""" self._api = handle_error(api) self._device: Device = device - self._device_control: BlindControl | LightControl | SocketControl | SignalRepeaterControl | None = ( + self._device_control: BlindControl | LightControl | SocketControl | SignalRepeaterControl | AirPurifierControl | None = ( None ) - self._device_data: Socket | Light | Blind | None = None + self._device_data: Socket | Light | Blind | AirPurifier | None = None self._gateway_id = gateway_id self._refresh(device) diff --git a/homeassistant/components/tradfri/const.py b/homeassistant/components/tradfri/const.py index 1f382548263..8efb1837ae4 100644 --- a/homeassistant/components/tradfri/const.py +++ b/homeassistant/components/tradfri/const.py @@ -2,6 +2,7 @@ from homeassistant.components.light import SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION from homeassistant.const import CONF_HOST # noqa: F401 pylint: disable=unused-import +ATTR_AUTO = "Auto" ATTR_DIMMER = "dimmer" ATTR_HUE = "hue" ATTR_SAT = "saturation" @@ -23,4 +24,4 @@ GROUPS = "tradfri_groups" KEY_SECURITY_CODE = "security_code" SUPPORTED_GROUP_FEATURES = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION SUPPORTED_LIGHT_FEATURES = SUPPORT_TRANSITION -PLATFORMS = ["cover", "light", "sensor", "switch"] +PLATFORMS = ["cover", "fan", "light", "sensor", "switch"] diff --git a/homeassistant/components/tradfri/fan.py b/homeassistant/components/tradfri/fan.py new file mode 100644 index 00000000000..b2f8641addb --- /dev/null +++ b/homeassistant/components/tradfri/fan.py @@ -0,0 +1,175 @@ +"""Represent an air purifier.""" + +from __future__ import annotations + +from collections.abc import Callable +from typing import Any, cast + +from pytradfri.command import Command + +from homeassistant.components.fan import ( + SUPPORT_PRESET_MODE, + SUPPORT_SET_SPEED, + FanEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .base_class import TradfriBaseDevice +from .const import ATTR_AUTO, CONF_GATEWAY_ID, DEVICES, DOMAIN, KEY_API + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Load Tradfri switches based on a config entry.""" + gateway_id = config_entry.data[CONF_GATEWAY_ID] + tradfri_data = hass.data[DOMAIN][config_entry.entry_id] + api = tradfri_data[KEY_API] + devices = tradfri_data[DEVICES] + + purifiers = [dev for dev in devices if dev.has_air_purifier_control] + if purifiers: + async_add_entities( + TradfriAirPurifierFan(purifier, api, gateway_id) for purifier in purifiers + ) + + +def _from_percentage(percentage: int) -> int: + """Convert percent to a value that the Tradfri API understands.""" + if percentage < 20: + # The device cannot be set to speed 5 (10%), so we should turn off the device + # for any value below 20 + return 0 + + nearest_10: int = round(percentage / 10) * 10 # Round to nearest multiple of 10 + return round(nearest_10 / 100 * 50) + + +def _from_fan_speed(fan_speed: int) -> int: + """Convert the Tradfri API fan speed to a percentage value.""" + nearest_10: int = round(fan_speed / 10) * 10 # Round to nearest multiple of 10 + return round(nearest_10 / 50 * 100) + + +class TradfriAirPurifierFan(TradfriBaseDevice, FanEntity): + """The platform class required by Home Assistant.""" + + def __init__( + self, + device: Command, + api: Callable[[Command | list[Command]], Any], + gateway_id: str, + ) -> None: + """Initialize a switch.""" + super().__init__(device, api, gateway_id) + self._attr_unique_id = f"{gateway_id}-{device.id}" + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return SUPPORT_PRESET_MODE + SUPPORT_SET_SPEED + + @property + def speed_count(self) -> int: + """ + Return the number of speeds the fan supports. + + These are the steps: + 0 = Off + 10 = Min + 15 + 20 + 25 + 30 + 35 + 40 + 45 + 50 = Max + """ + return 10 + + @property + def is_on(self) -> bool: + """Return true if switch is on.""" + if not self._device_data: + return False + return cast(bool, self._device_data.mode) + + @property + def preset_modes(self) -> list[str] | None: + """Return a list of available preset modes.""" + return [ATTR_AUTO] + + @property + def percentage(self) -> int | None: + """Return the current speed percentage.""" + if not self._device_data: + return None + + if self._device_data.fan_speed: + return _from_fan_speed(self._device_data.fan_speed) + + return None + + @property + def preset_mode(self) -> str | None: + """Return the current preset mode.""" + if not self._device_data: + return None + + if self._device_data.mode == ATTR_AUTO: + return ATTR_AUTO + + return None + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode of the fan.""" + if not self._device_control: + return + + if not preset_mode == ATTR_AUTO: + raise ValueError("Preset must be 'Auto'.") + await self._api(self._device_control.set_mode(1)) + + async def async_turn_on( + self, + speed: str | None = None, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, + ) -> None: + """Turn on the fan.""" + if not self._device_control: + return + + if percentage is not None: + await self._api(self._device_control.set_mode(_from_percentage(percentage))) + return + + if preset_mode: + await self.async_set_preset_mode(preset_mode) + + async def async_set_percentage(self, percentage: int) -> None: + """Set the speed percentage of the fan.""" + if not self._device_control: + return + + await self._api(self._device_control.set_mode(_from_percentage(percentage))) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the fan.""" + if not self._device_control: + return + await self._api(self._device_control.set_mode(0)) + + def _refresh(self, device: Command) -> None: + """Refresh the purifier data.""" + super()._refresh(device) + + # Caching of air purifier control and purifier object + self._device_control = device.air_purifier_control + self._device_data = device.air_purifier_control.air_purifiers[0] diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index f761aba5ddd..c6214773b97 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -34,6 +34,7 @@ async def async_setup_entry( and not dev.has_socket_control and not dev.has_blind_control and not dev.has_signal_repeater_control + and not dev.has_air_purifier_control ) if sensors: async_add_entities(TradfriSensor(sensor, api, gateway_id) for sensor in sensors) diff --git a/tests/components/tradfri/test_light.py b/tests/components/tradfri/test_light.py index 370fba42fba..85e7f8dc037 100644 --- a/tests/components/tradfri/test_light.py +++ b/tests/components/tradfri/test_light.py @@ -139,6 +139,7 @@ def mock_light(test_features=None, test_state=None, light_number=0): has_socket_control=False, has_blind_control=False, has_signal_repeater_control=False, + has_air_purifier_control=False, ) _mock_light.name = f"tradfri_light_{light_number}" diff --git a/tests/components/tradfri/test_util.py b/tests/components/tradfri/test_util.py new file mode 100644 index 00000000000..3dbdf801f89 --- /dev/null +++ b/tests/components/tradfri/test_util.py @@ -0,0 +1,22 @@ +"""Tradfri utility function tests.""" + +from homeassistant.components.tradfri.fan import _from_fan_speed, _from_percentage + + +def test_from_fan_speed(): + """Test that we can convert fan speed to percentage value.""" + assert _from_fan_speed(41) == 80 + + +def test_from_percentage(): + """Test that we can convert percentage value to fan speed.""" + assert _from_percentage(84) == 40 + + +def test_from_percentage_limit(): + """ + Test that we can convert percentage value to fan speed. + + Handle special case of percent value being below 20. + """ + assert _from_percentage(10) == 0 From 9885b3de475a788967054e8163360651c17ce270 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Oct 2021 20:16:15 -1000 Subject: [PATCH 0788/1038] Fix configuration url in gogogate2 (#58365) - I missed that https:// needed to be prepended because the existing device already had the url from previous testing --- homeassistant/components/gogogate2/common.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/gogogate2/common.py b/homeassistant/components/gogogate2/common.py index c5d8c0f5137..a70a1b6bf81 100644 --- a/homeassistant/components/gogogate2/common.py +++ b/homeassistant/components/gogogate2/common.py @@ -96,8 +96,11 @@ class GoGoGate2Entity(CoordinatorEntity): def device_info(self) -> DeviceInfo: """Device info for the controller.""" data = self.coordinator.data + configuration_url = ( + f"https://{data.remoteaccess}" if data.remoteaccess else None + ) return DeviceInfo( - configuration_url=data.remoteaccess if data.remoteaccess else None, + configuration_url=configuration_url, identifiers={(DOMAIN, str(self._config_entry.unique_id))}, name=self._config_entry.title, manufacturer=MANUFACTURER, From 79f68b050a7d7022018a82b691b735c7cbf8f3e6 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 25 Oct 2021 08:55:50 +0200 Subject: [PATCH 0789/1038] Bump pytradfri to 7.1.1 (#58379) --- homeassistant/components/tradfri/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index 0393cda0c5b..3579d9a9bd8 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -3,7 +3,7 @@ "name": "IKEA TR\u00c5DFRI", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tradfri", - "requirements": ["pytradfri[async]==7.1.0"], + "requirements": ["pytradfri[async]==7.1.1"], "homekit": { "models": ["TRADFRI"] }, diff --git a/requirements_all.txt b/requirements_all.txt index 333a57d585b..bd3f61eba77 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1964,7 +1964,7 @@ pytouchline==0.7 pytraccar==0.9.0 # homeassistant.components.tradfri -pytradfri[async]==7.1.0 +pytradfri[async]==7.1.1 # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 65f13eff966..2008df63017 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1148,7 +1148,7 @@ pytile==5.2.4 pytraccar==0.9.0 # homeassistant.components.tradfri -pytradfri[async]==7.1.0 +pytradfri[async]==7.1.1 # homeassistant.components.usb pyudev==0.22.0 From d05127cb711e8f0db243b8fb3f1da2e84b5364f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Oct 2021 20:56:15 -1000 Subject: [PATCH 0790/1038] Add entity category to august (#58359) --- homeassistant/components/august/binary_sensor.py | 2 ++ homeassistant/components/august/sensor.py | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index 9a38cd1e301..cf34952309b 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -26,6 +26,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import callback from homeassistant.helpers.event import async_call_later @@ -129,6 +130,7 @@ SENSOR_TYPES_DOORBELL: tuple[AugustBinarySensorEntityDescription, ...] = ( key="doorbell_online", name="Online", device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, value_fn=_retrieve_online_state, is_time_based=False, ), diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index b6fa767edb7..744177cbef3 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -16,7 +16,12 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) -from homeassistant.const import ATTR_ENTITY_PICTURE, PERCENTAGE, STATE_UNAVAILABLE +from homeassistant.const import ( + ATTR_ENTITY_PICTURE, + ENTITY_CATEGORY_DIAGNOSTIC, + PERCENTAGE, + STATE_UNAVAILABLE, +) from homeassistant.core import callback from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.helpers.restore_state import RestoreEntity @@ -68,12 +73,14 @@ class AugustSensorEntityDescription( SENSOR_TYPE_DEVICE_BATTERY = AugustSensorEntityDescription[LockDetail]( key="device_battery", name="Battery", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, value_fn=_retrieve_device_battery_state, ) SENSOR_TYPE_KEYPAD_BATTERY = AugustSensorEntityDescription[KeypadDetail]( key="linked_keypad_battery", name="Battery", + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, value_fn=_retrieve_linked_keypad_battery_state, ) From 1a261f7802d4d5f0d31b2f265efb0d333496e986 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Oct 2021 20:56:51 -1000 Subject: [PATCH 0791/1038] Add entity category to roomba (#58362) --- homeassistant/components/roomba/sensor.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/roomba/sensor.py b/homeassistant/components/roomba/sensor.py index bc20b4397e2..4552159677c 100644 --- a/homeassistant/components/roomba/sensor.py +++ b/homeassistant/components/roomba/sensor.py @@ -1,7 +1,11 @@ """Sensor for checking the battery level of Roomba.""" from homeassistant.components.sensor import SensorEntity from homeassistant.components.vacuum import STATE_DOCKED -from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE +from homeassistant.const import ( + DEVICE_CLASS_BATTERY, + ENTITY_CATEGORY_DIAGNOSTIC, + PERCENTAGE, +) from homeassistant.helpers.icon import icon_for_battery_level from .const import BLID, DOMAIN, ROOMBA_SESSION @@ -20,6 +24,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class RoombaBattery(IRobotEntity, SensorEntity): """Class to hold Roomba Sensor basic info.""" + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + @property def name(self): """Return the name of the sensor.""" From 837e343c5625eea0322bec9ce0403214c2249a03 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Oct 2021 20:59:06 -1000 Subject: [PATCH 0792/1038] Add entity category to gogogate2 (#58366) --- homeassistant/components/gogogate2/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/gogogate2/sensor.py b/homeassistant/components/gogogate2/sensor.py index 8788a69908e..6eb3d823c22 100644 --- a/homeassistant/components/gogogate2/sensor.py +++ b/homeassistant/components/gogogate2/sensor.py @@ -10,6 +10,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant @@ -51,6 +52,8 @@ async def async_setup_entry( class DoorSensorBattery(GoGoGate2Entity, SensorEntity): """Battery sensor entity for gogogate2 door sensor.""" + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + def __init__( self, config_entry: ConfigEntry, From 80b12346d8efc19c9d81346461cda41cf96cbba0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Oct 2021 21:00:06 -1000 Subject: [PATCH 0793/1038] Add entity category to elkm1 (#58364) --- homeassistant/components/elkm1/sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/elkm1/sensor.py b/homeassistant/components/elkm1/sensor.py index 30fe87103c7..b5bab021bca 100644 --- a/homeassistant/components/elkm1/sensor.py +++ b/homeassistant/components/elkm1/sensor.py @@ -9,7 +9,7 @@ from elkm1_lib.util import pretty_const, username import voluptuous as vol from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ELECTRIC_POTENTIAL_VOLT +from homeassistant.const import ELECTRIC_POTENTIAL_VOLT, ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform @@ -158,6 +158,8 @@ class ElkKeypad(ElkSensor): class ElkPanel(ElkSensor): """Representation of an Elk-M1 Panel.""" + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + @property def icon(self): """Icon to use in the frontend.""" From 35872a212b2340d3906557e5f644d99f7360ff15 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 25 Oct 2021 09:19:00 +0200 Subject: [PATCH 0794/1038] Add Temperature and Humidity Sensor (wsdcg) device support to Tuya (#58335) --- .../components/tuya/binary_sensor.py | 3 ++ homeassistant/components/tuya/const.py | 1 + homeassistant/components/tuya/sensor.py | 42 +++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index a1483447749..12fbc963942 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -191,6 +191,9 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { ), TAMPER_BINARY_SENSOR, ), + # Temperature and Humidity Sensor + # https://developer.tuya.com/en/docs/iot/categorywsdcg?id=Kaiuz3hinij34 + "wsdcg": (TAMPER_BINARY_SENSOR,), # Pressure Sensor # https://developer.tuya.com/en/docs/iot/categoryylcg?id=Kaiuz3kc2e4gm "ylcg": ( diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 96913c16aeb..2b598a83277 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -295,6 +295,7 @@ class DPCode(str, Enum): TEMP_VALUE_V2 = "temp_value_v2" TEMPER_ALARM = "temper_alarm" # Tamper alarm UV = "uv" # UV sterilization + VA_BATTERY = "va_battery" VA_HUMIDITY = "va_humidity" VA_TEMPERATURE = "va_temperature" VOC_STATE = "voc_state" diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 77ec9c1140d..a69ce86d0e3 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -68,6 +68,13 @@ BATTERY_SENSORS: tuple[SensorEntityDescription, ...] = ( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, state_class=STATE_CLASS_MEASUREMENT, ), + SensorEntityDescription( + key=DPCode.VA_BATTERY, + name="Battery", + device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + state_class=STATE_CLASS_MEASUREMENT, + ), ) # All descriptions can be found here. Mostly the Integer data types in the @@ -368,6 +375,41 @@ SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { ), *BATTERY_SENSORS, ), + # Temperature and Humidity Sensor + # https://developer.tuya.com/en/docs/iot/categorywsdcg?id=Kaiuz3hinij34 + "wsdcg": ( + SensorEntityDescription( + key=DPCode.VA_TEMPERATURE, + name="Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.TEMP_CURRENT, + name="Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.VA_HUMIDITY, + name="Humidity", + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.HUMIDITY_VALUE, + name="Humidity", + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.BRIGHT_VALUE, + name="Luminosity", + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + *BATTERY_SENSORS, + ), # Pressure Sensor # https://developer.tuya.com/en/docs/iot/categoryylcg?id=Kaiuz3kc2e4gm "ylcg": ( From 3abb4bd3e5bae108d0819c88fdcc80e0a1a9d3a3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Oct 2021 21:37:37 -1000 Subject: [PATCH 0795/1038] Add configuration_url to isy994 (#58372) --- homeassistant/components/isy994/__init__.py | 11 ++++++++++- homeassistant/components/isy994/entity.py | 7 ++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index a5ceeaea1d8..65be6a74c19 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -251,11 +251,19 @@ def _async_import_options_from_data_if_missing( hass.config_entries.async_update_entry(entry, options=options) +@callback +def _async_isy_to_configuration_url(isy: ISY) -> str: + """Extract the configuration url from the isy.""" + connection_info = isy.conn.connection_info + proto = "https" if "tls" in connection_info else "http" + return f"{proto}://{connection_info['addr']}:{connection_info['port']}" + + async def _async_get_or_create_isy_device_in_registry( hass: HomeAssistant, entry: config_entries.ConfigEntry, isy ) -> None: device_registry = await dr.async_get_registry(hass) - + url = _async_isy_to_configuration_url(isy) device_registry.async_get_or_create( config_entry_id=entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, isy.configuration["uuid"])}, @@ -264,6 +272,7 @@ async def _async_get_or_create_isy_device_in_registry( name=isy.configuration["name"], model=isy.configuration["model"], sw_version=isy.configuration["firmware"], + configuration_url=url, ) diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index a0a898206f5..aca0dbf5d3d 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -23,6 +23,7 @@ from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import DeviceInfo, Entity +from . import _async_isy_to_configuration_url from .const import DOMAIN @@ -76,8 +77,11 @@ class ISYEntity(Entity): if hasattr(self._node, "protocol") and self._node.protocol == PROTO_GROUP: # not a device return None - uuid = self._node.isy.configuration["uuid"] + isy = self._node.isy + uuid = isy.configuration["uuid"] node = self._node + url = _async_isy_to_configuration_url(isy) + basename = self.name if hasattr(self._node, "parent_node") and self._node.parent_node is not None: @@ -91,6 +95,7 @@ class ISYEntity(Entity): model="Unknown", name=basename, via_device=(DOMAIN, uuid), + configuration_url=url, ) if hasattr(node, "address"): From 34b936da9855e019b75fec345ab8dc82d795dbe3 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 25 Oct 2021 09:44:15 +0200 Subject: [PATCH 0796/1038] Add entity category to Neato (#58367) --- homeassistant/components/neato/sensor.py | 7 ++++++- homeassistant/components/neato/switch.py | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py index 9a5da4c1950..1fd6547d926 100644 --- a/homeassistant/components/neato/sensor.py +++ b/homeassistant/components/neato/sensor.py @@ -11,7 +11,7 @@ from pybotvac.robot import Robot from homeassistant.components.neato import NeatoHub from homeassistant.components.sensor import DEVICE_CLASS_BATTERY, SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -83,6 +83,11 @@ class NeatoSensor(SensorEntity): """Return the device class.""" return DEVICE_CLASS_BATTERY + @property + def entity_category(self) -> str: + """Device entity category.""" + return ENTITY_CATEGORY_DIAGNOSTIC + @property def available(self) -> bool: """Return availability.""" diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index 65d296bcb28..b6a1302bda7 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -10,7 +10,7 @@ from pybotvac.robot import Robot from homeassistant.components.neato import NeatoHub from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.const import ENTITY_CATEGORY_CONFIG, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo, ToggleEntity from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -106,6 +106,11 @@ class NeatoConnectedSwitch(ToggleEntity): self.type == SWITCH_TYPE_SCHEDULE and self._schedule_state == STATE_ON ) + @property + def entity_category(self) -> str: + """Device entity category.""" + return ENTITY_CATEGORY_CONFIG + @property def device_info(self) -> DeviceInfo: """Device info for neato robot.""" From 027e167d95dd9504e268d2e94b4dd3c5b61057bb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 25 Oct 2021 09:44:43 +0200 Subject: [PATCH 0797/1038] Add support for unit of measurement in MQTT number platform (#58343) --- homeassistant/components/mqtt/number.py | 15 ++++++++++++++- tests/components/mqtt/test_number.py | 10 +++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index a54fd55adb8..f2a472709b3 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -1,4 +1,6 @@ """Configure number in a device through MQTT topic.""" +from __future__ import annotations + import functools import logging @@ -11,7 +13,12 @@ from homeassistant.components.number import ( DEFAULT_STEP, NumberEntity, ) -from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE +from homeassistant.const import ( + CONF_NAME, + CONF_OPTIMISTIC, + CONF_UNIT_OF_MEASUREMENT, + CONF_VALUE_TEMPLATE, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.reload import async_setup_reload_service @@ -64,6 +71,7 @@ PLATFORM_SCHEMA = vol.All( vol.Coerce(float), vol.Range(min=1e-3) ), vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, }, ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema), validate_config, @@ -195,6 +203,11 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): """Return the increment/decrement step.""" return self._config[CONF_STEP] + @property + def unit_of_measurement(self) -> str | None: + """Return the unit of measurement.""" + return self._config.get(CONF_UNIT_OF_MEASUREMENT) + @property def value(self): """Return the current value.""" diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index b0989c59ca2..e1e961aa84a 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -18,7 +18,11 @@ from homeassistant.components.number import ( DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE, ) -from homeassistant.const import ATTR_ASSUMED_STATE, ATTR_ENTITY_ID +from homeassistant.const import ( + ATTR_ASSUMED_STATE, + ATTR_ENTITY_ID, + ATTR_UNIT_OF_MEASUREMENT, +) import homeassistant.core as ha from homeassistant.setup import async_setup_component @@ -66,6 +70,7 @@ async def test_run_number_setup(hass, mqtt_mock): "state_topic": topic, "command_topic": topic, "name": "Test Number", + "unit_of_measurement": "my unit", "payload_reset": "reset!", } }, @@ -78,6 +83,7 @@ async def test_run_number_setup(hass, mqtt_mock): state = hass.states.get("number.test_number") assert state.state == "10" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "my unit" async_fire_mqtt_message(hass, topic, "20.5") @@ -85,6 +91,7 @@ async def test_run_number_setup(hass, mqtt_mock): state = hass.states.get("number.test_number") assert state.state == "20.5" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "my unit" async_fire_mqtt_message(hass, topic, "reset!") @@ -92,6 +99,7 @@ async def test_run_number_setup(hass, mqtt_mock): state = hass.states.get("number.test_number") assert state.state == "unknown" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "my unit" async def test_value_template(hass, mqtt_mock): From f30963e15b20db5d4a9134eb3a1406d46056a30d Mon Sep 17 00:00:00 2001 From: gjong Date: Mon, 25 Oct 2021 09:46:08 +0200 Subject: [PATCH 0798/1038] Upgrade youless library to fix LS110 power total is not a number (#58333) --- homeassistant/components/youless/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/youless/manifest.json b/homeassistant/components/youless/manifest.json index 514c73fbd2c..49a5b6187e6 100644 --- a/homeassistant/components/youless/manifest.json +++ b/homeassistant/components/youless/manifest.json @@ -3,7 +3,7 @@ "name": "YouLess", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/youless", - "requirements": ["youless-api==0.14"], + "requirements": ["youless-api==0.15"], "codeowners": ["@gjong"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index bd3f61eba77..73b7e9f418f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2456,7 +2456,7 @@ yeelight==0.7.8 yeelightsunflower==0.0.10 # homeassistant.components.youless -youless-api==0.14 +youless-api==0.15 # homeassistant.components.media_extractor youtube_dl==2021.06.06 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2008df63017..758125c5a0a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1424,7 +1424,7 @@ yalexs==1.1.13 yeelight==0.7.8 # homeassistant.components.youless -youless-api==0.14 +youless-api==0.15 # homeassistant.components.zeroconf zeroconf==0.36.9 From b3f6be0cec0570ba42f3d2d54138095fd31c69ed Mon Sep 17 00:00:00 2001 From: Andre Richter Date: Mon, 25 Oct 2021 10:15:18 +0200 Subject: [PATCH 0799/1038] Minor cleanups for Vallox (#58384) --- homeassistant/components/vallox/__init__.py | 14 ++------------ homeassistant/components/vallox/fan.py | 2 +- homeassistant/components/vallox/sensor.py | 10 ++-------- 3 files changed, 5 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py index 3f441054fb1..cb9a3478839 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -7,7 +7,6 @@ import logging from typing import Any from vallox_websocket_api import PROFILE as VALLOX_PROFILE, Vallox -from vallox_websocket_api.constants import vlxDevConstants from vallox_websocket_api.exceptions import ValloxApiException import voluptuous as vol @@ -98,20 +97,11 @@ class ValloxState: def get_metric(self, metric_key: str) -> StateType: """Return cached state value.""" - _LOGGER.debug("Fetching metric key: %s", metric_key) - - if metric_key not in vlxDevConstants.__dict__: - _LOGGER.debug("Metric key invalid: %s", metric_key) if (value := self.metric_cache.get(metric_key)) is None: return None if not isinstance(value, (str, int, float)): - _LOGGER.debug( - "Return value of metric %s has unexpected type %s", - metric_key, - type(value), - ) return None return value @@ -126,8 +116,8 @@ class ValloxDataUpdateCoordinator(DataUpdateCoordinator): async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the client and boot the platforms.""" conf = config[DOMAIN] - host = conf.get(CONF_HOST) - name = conf.get(CONF_NAME) + host = conf[CONF_HOST] + name = conf[CONF_NAME] client = Vallox(host) diff --git a/homeassistant/components/vallox/fan.py b/homeassistant/components/vallox/fan.py index 39242b01b4e..4d621615aef 100644 --- a/homeassistant/components/vallox/fan.py +++ b/homeassistant/components/vallox/fan.py @@ -35,7 +35,7 @@ _LOGGER = logging.getLogger(__name__) class ExtraStateAttributeDetails(NamedTuple): - """Extra state attribute properties.""" + """Extra state attribute details.""" description: str metric_key: str diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index 0341153a9ff..0b96316b766 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -3,7 +3,6 @@ from __future__ import annotations from dataclasses import dataclass from datetime import datetime, timedelta -import logging from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, @@ -33,8 +32,6 @@ from .const import ( VALLOX_PROFILE_TO_STR_REPORTABLE, ) -_LOGGER = logging.getLogger(__name__) - class ValloxSensor(CoordinatorEntity, SensorEntity): """Representation of a Vallox sensor.""" @@ -59,7 +56,6 @@ class ValloxSensor(CoordinatorEntity, SensorEntity): def native_value(self) -> StateType: """Return the value reported by the sensor.""" if (metric_key := self.entity_description.metric_key) is None: - _LOGGER.debug("Error updating sensor. Empty metric key") return None return self.coordinator.data.get_metric(metric_key) @@ -100,7 +96,6 @@ class ValloxFilterRemainingSensor(ValloxSensor): super_native_value = super().native_value if not isinstance(super_native_value, (int, float)): - _LOGGER.debug("Value has unexpected type: %s", type(super_native_value)) return None # Since only a delta of days is received from the device, fix the time so the timestamp does @@ -120,11 +115,10 @@ class ValloxCellStateSensor(ValloxSensor): """Return the value reported by the sensor.""" super_native_value = super().native_value - if not isinstance(super_native_value, (int, float)): - _LOGGER.debug("Value has unexpected type: %s", type(super_native_value)) + if not isinstance(super_native_value, int): return None - return VALLOX_CELL_STATE_TO_STR.get(int(super_native_value)) + return VALLOX_CELL_STATE_TO_STR.get(super_native_value) @dataclass From ef48238ac3137316e7b983e2532afc2f87610f27 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 25 Oct 2021 12:05:45 +0200 Subject: [PATCH 0800/1038] Adjust DeviceInfo registration on zwave_js (#58391) Co-authored-by: epenet --- homeassistant/components/zwave_js/__init__.py | 4 +--- homeassistant/const.py | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 99b7684a9ad..7d2af4af126 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -18,7 +18,6 @@ from zwave_js_server.model.value import Value, ValueNotification from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_CONFIG_ENTRY_ID, ATTR_DEVICE_ID, ATTR_DOMAIN, ATTR_ENTITY_ID, @@ -127,7 +126,6 @@ def register_node_in_dev_reg( ): remove_device_func(device) params = { - ATTR_CONFIG_ENTRY_ID: entry.entry_id, ATTR_IDENTIFIERS: {device_id}, ATTR_SW_VERSION: node.firmware_version, ATTR_NAME: node.name @@ -138,7 +136,7 @@ def register_node_in_dev_reg( } if node.location: params[ATTR_SUGGESTED_AREA] = node.location - device = dev_reg.async_get_or_create(**params) + device = dev_reg.async_get_or_create(config_entry_id=entry.entry_id, **params) async_dispatcher_send(hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device) diff --git a/homeassistant/const.py b/homeassistant/const.py index eedfc13067e..50a26bd208e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -352,7 +352,6 @@ ATTR_LOCATION: Final = "location" ATTR_MODE: Final = "mode" -ATTR_CONFIG_ENTRY_ID: Final = "config_entry_id" ATTR_CONFIGURATION_URL: Final = "configuration_url" ATTR_CONNECTIONS: Final = "connections" ATTR_MANUFACTURER: Final = "manufacturer" From 21709e71704fc52f861095443202041339e41d50 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 25 Oct 2021 12:09:54 +0200 Subject: [PATCH 0801/1038] Upgrade Tuya IoT Python SDK to 0.6.3 (#58240) --- homeassistant/components/tuya/camera.py | 25 +++++---------------- homeassistant/components/tuya/manifest.json | 24 ++++++++++---------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 19 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/tuya/camera.py b/homeassistant/components/tuya/camera.py index 78c38725d7e..4f01bce18e9 100644 --- a/homeassistant/components/tuya/camera.py +++ b/homeassistant/components/tuya/camera.py @@ -81,26 +81,11 @@ class TuyaCameraEntity(TuyaEntity, CameraEntity): async def stream_source(self) -> str | None: """Return the source of the stream.""" - - def _stream_source() -> str | None: - # This method can be replaced by the following snippet, once - # upstream changes have been merged. - # - # return self.device_manager.get_device_stream_allocate( - # self.device.id, stream_type="rtsp" - # ) - # - # https://github.com/tuya/tuya-iot-python-sdk/pull/28 - - response = self.device_manager.api.post( - f"/v1.0/devices/{self.device.id}/stream/actions/allocate", - {"type": "rtsp"}, - ) - if response["success"]: - return response["result"]["url"] - return None - - return await self.hass.async_add_executor_job(_stream_source) + return await self.hass.async_add_executor_job( + self.device_manager.get_device_stream_allocate, + self.device.id, + "rtsp", + ) async def async_camera_image( self, width: int | None = None, height: int | None = None diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index 28b5633a633..c48771b85be 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -2,22 +2,22 @@ "domain": "tuya", "name": "Tuya", "documentation": "https://www.home-assistant.io/integrations/tuya", - "requirements": ["tuya-iot-py-sdk==0.5.0"], + "requirements": ["tuya-iot-py-sdk==0.6.3"], "dependencies": ["ffmpeg"], "codeowners": ["@Tuya", "@zlinoliver", "@METISU", "@frenck"], "config_flow": true, "iot_class": "cloud_push", "dhcp": [ - {"macaddress": "105A17*"}, - {"macaddress": "10D561*"}, - {"macaddress": "1869D8*"}, - {"macaddress": "381F8D*"}, - {"macaddress": "508A06*"}, - {"macaddress": "68572D*"}, - {"macaddress": "708976*"}, - {"macaddress": "7CF666*"}, - {"macaddress": "84E342*"}, - {"macaddress": "D4A651*"}, - {"macaddress": "D81F12*"} + { "macaddress": "105A17*" }, + { "macaddress": "10D561*" }, + { "macaddress": "1869D8*" }, + { "macaddress": "381F8D*" }, + { "macaddress": "508A06*" }, + { "macaddress": "68572D*" }, + { "macaddress": "708976*" }, + { "macaddress": "7CF666*" }, + { "macaddress": "84E342*" }, + { "macaddress": "D4A651*" }, + { "macaddress": "D81F12*" } ] } diff --git a/requirements_all.txt b/requirements_all.txt index 73b7e9f418f..e7b8f78bd44 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2323,7 +2323,7 @@ tp-connected==0.0.4 transmissionrpc==0.11 # homeassistant.components.tuya -tuya-iot-py-sdk==0.5.0 +tuya-iot-py-sdk==0.6.3 # homeassistant.components.twentemilieu twentemilieu==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 758125c5a0a..9d494ad3fee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1333,7 +1333,7 @@ total_connect_client==0.57 transmissionrpc==0.11 # homeassistant.components.tuya -tuya-iot-py-sdk==0.5.0 +tuya-iot-py-sdk==0.6.3 # homeassistant.components.twentemilieu twentemilieu==0.3.0 From e5e38ace6cd6d679a75ec52ed09259d59d863fb4 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Mon, 25 Oct 2021 12:22:19 +0200 Subject: [PATCH 0802/1038] Bump async-upnp-client to 0.22.10 (#58387) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index a7c0a674853..2c87260834f 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Renderer", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.22.9"], + "requirements": ["async-upnp-client==0.22.10"], "dependencies": ["ssdp"], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index facb93efdad..2017dd9e75c 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,7 +2,7 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", - "requirements": ["async-upnp-client==0.22.9"], + "requirements": ["async-upnp-client==0.22.10"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 20e90fec751..05464c54914 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -3,7 +3,7 @@ "name": "UPnP/IGD", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": ["async-upnp-client==0.22.9"], + "requirements": ["async-upnp-client==0.22.10"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman","@ehendrix23"], "ssdp": [ diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 944ca80b105..0b7718074a7 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,7 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.7.8", "async-upnp-client==0.22.9"], + "requirements": ["yeelight==0.7.8", "async-upnp-client==0.22.10"], "codeowners": ["@rytilahti", "@zewelor", "@shenxn", "@starkillerOG"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b4fd1d08e40..9db59de1fc3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.5 aiohttp==3.7.4.post0 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.22.9 +async-upnp-client==0.22.10 async_timeout==3.0.1 attrs==21.2.0 awesomeversion==21.10.1 diff --git a/requirements_all.txt b/requirements_all.txt index e7b8f78bd44..a1af8fda1b8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -333,7 +333,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.22.9 +async-upnp-client==0.22.10 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9d494ad3fee..66334d02fad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -230,7 +230,7 @@ arcam-fmj==0.12.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.22.9 +async-upnp-client==0.22.10 # homeassistant.components.aurora auroranoaa==0.0.2 From 9d952d024273030245eb282b74e250df711385e4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 25 Oct 2021 12:23:11 +0200 Subject: [PATCH 0803/1038] Use DeviceInfo in control4 (#58388) Co-authored-by: epenet --- homeassistant/components/control4/__init__.py | 33 +++++++------------ homeassistant/components/control4/light.py | 3 -- 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/control4/__init__.py b/homeassistant/components/control4/__init__.py index e48f7fc0ccb..e57abfa3b73 100644 --- a/homeassistant/components/control4/__init__.py +++ b/homeassistant/components/control4/__init__.py @@ -20,6 +20,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, device_registry as dr +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -147,7 +148,6 @@ class Control4Entity(CoordinatorEntity): def __init__( self, entry_data: dict, - entry: ConfigEntry, coordinator: DataUpdateCoordinator, name: str, idx: int, @@ -158,9 +158,9 @@ class Control4Entity(CoordinatorEntity): ) -> None: """Initialize a Control4 entity.""" super().__init__(coordinator) - self.entry = entry self.entry_data = entry_data - self._name = name + self._attr_name = name + self._attr_unique_id = str(idx) self._idx = idx self._controller_unique_id = entry_data[CONF_CONTROLLER_UNIQUE_ID] self._device_name = device_name @@ -169,23 +169,12 @@ class Control4Entity(CoordinatorEntity): self._device_id = device_id @property - def name(self): - """Return name of entity.""" - return self._name - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return str(self._idx) - - @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return info of parent Control4 device of entity.""" - return { - "config_entry_id": self.entry.entry_id, - "identifiers": {(DOMAIN, self._device_id)}, - "name": self._device_name, - "manufacturer": self._device_manufacturer, - "model": self._device_model, - "via_device": (DOMAIN, self._controller_unique_id), - } + return DeviceInfo( + identifiers={(DOMAIN, str(self._device_id))}, + manufacturer=self._device_manufacturer, + model=self._device_model, + name=self._device_name, + via_device=(DOMAIN, self._controller_unique_id), + ) diff --git a/homeassistant/components/control4/light.py b/homeassistant/components/control4/light.py index 38eca233f27..b2e5f6b43cf 100644 --- a/homeassistant/components/control4/light.py +++ b/homeassistant/components/control4/light.py @@ -122,7 +122,6 @@ async def async_setup_entry( entity_list.append( Control4Light( entry_data, - entry, item_coordinator, item_name, item_id, @@ -143,7 +142,6 @@ class Control4Light(Control4Entity, LightEntity): def __init__( self, entry_data: dict, - entry: ConfigEntry, coordinator: DataUpdateCoordinator, name: str, idx: int, @@ -156,7 +154,6 @@ class Control4Light(Control4Entity, LightEntity): """Initialize Control4 light entity.""" super().__init__( entry_data, - entry, coordinator, name, idx, From c47ac1d9d6e86d662605dd31af921b725649d8dc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 25 Oct 2021 12:38:38 +0200 Subject: [PATCH 0804/1038] Use DeviceInfo on accuweather (#58394) Co-authored-by: epenet --- homeassistant/components/accuweather/weather.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index cd8d64cc80f..af2d1c15b2b 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -19,6 +19,7 @@ from homeassistant.components.weather import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT 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.dt import utc_from_timestamp @@ -66,12 +67,12 @@ class AccuWeatherEntity(CoordinatorEntity, WeatherEntity): TEMP_CELSIUS if coordinator.is_metric else TEMP_FAHRENHEIT ) self._attr_attribution = ATTRIBUTION - self._attr_device_info = { - "identifiers": {(DOMAIN, coordinator.location_key)}, - "name": NAME, - "manufacturer": MANUFACTURER, - "entry_type": "service", - } + self._attr_device_info = DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, coordinator.location_key)}, + manufacturer=MANUFACTURER, + name=NAME, + ) @property def condition(self) -> str | None: From ab66efba0ed6d6e517fb30d562a8d90ccc99b25d Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 25 Oct 2021 12:38:59 +0200 Subject: [PATCH 0805/1038] Add entity categories to Netatmo (#58383) --- homeassistant/components/netatmo/sensor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 108ed3b2cea..995cd6fa9e0 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -25,6 +25,7 @@ from homeassistant.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, + ENTITY_CATEGORY_DIAGNOSTIC, LENGTH_MILLIMETERS, PERCENTAGE, PRESSURE_MBAR, @@ -177,6 +178,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Battery Percent", netatmo_name="battery_percent", entity_registry_enabled_default=True, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_BATTERY, state_class=STATE_CLASS_MEASUREMENT, @@ -236,6 +238,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Reachability", netatmo_name="reachable", entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon="mdi:signal", ), NetatmoSensorEntityDescription( @@ -243,6 +246,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Radio", netatmo_name="rf_status", entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon="mdi:signal", ), NetatmoSensorEntityDescription( @@ -250,6 +254,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Radio Level", netatmo_name="rf_status", entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=DEVICE_CLASS_SIGNAL_STRENGTH, state_class=STATE_CLASS_MEASUREMENT, @@ -259,6 +264,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Wifi", netatmo_name="wifi_status", entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon="mdi:wifi", ), NetatmoSensorEntityDescription( @@ -266,6 +272,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( name="Wifi Level", netatmo_name="wifi_status", entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=DEVICE_CLASS_SIGNAL_STRENGTH, state_class=STATE_CLASS_MEASUREMENT, From 07f268de1f1c20e440228ad247c65392576c2b56 Mon Sep 17 00:00:00 2001 From: Sagi Bernstein Date: Mon, 25 Oct 2021 13:41:32 +0300 Subject: [PATCH 0806/1038] Run nuki bidge.info() on executor (#58345) Co-authored-by: Franck Nijhof --- homeassistant/components/nuki/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nuki/config_flow.py b/homeassistant/components/nuki/config_flow.py index 7a98ad2f00d..330f38c96bd 100644 --- a/homeassistant/components/nuki/config_flow.py +++ b/homeassistant/components/nuki/config_flow.py @@ -41,7 +41,7 @@ async def validate_input(hass, data): DEFAULT_TIMEOUT, ) - info = bridge.info() + info = await hass.async_add_executor_job(bridge.info) except InvalidCredentialsException as err: raise InvalidAuth from err except RequestException as err: From b09f6620ebbe752bb03a6d44fe4242cfb44cfe59 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 25 Oct 2021 00:42:15 -1000 Subject: [PATCH 0807/1038] Add entity category to nut battery (#58363) --- homeassistant/components/nut/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/nut/const.py b/homeassistant/components/nut/const.py index 74d9614c29b..59d5cd484af 100644 --- a/homeassistant/components/nut/const.py +++ b/homeassistant/components/nut/const.py @@ -231,6 +231,7 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=PERCENTAGE, device_class=DEVICE_CLASS_BATTERY, state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), "battery.charge.low": SensorEntityDescription( key="battery.charge.low", From a36ac11d57616a714882c85627dba1de3de035f2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 25 Oct 2021 00:43:00 -1000 Subject: [PATCH 0808/1038] Add entity category to hunterdouglas_powerview (#58368) --- .../components/hunterdouglas_powerview/sensor.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hunterdouglas_powerview/sensor.py b/homeassistant/components/hunterdouglas_powerview/sensor.py index 14501a9c528..c789dd150da 100644 --- a/homeassistant/components/hunterdouglas_powerview/sensor.py +++ b/homeassistant/components/hunterdouglas_powerview/sensor.py @@ -2,7 +2,11 @@ from aiopvapi.resources.shade import factory as PvShade from homeassistant.components.sensor import SensorEntity -from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE +from homeassistant.const import ( + DEVICE_CLASS_BATTERY, + ENTITY_CATEGORY_DIAGNOSTIC, + PERCENTAGE, +) from homeassistant.core import callback from .const import ( @@ -49,6 +53,8 @@ async def async_setup_entry(hass, entry, async_add_entities): class PowerViewShadeBatterySensor(ShadeEntity, SensorEntity): """Representation of an shade battery charge sensor.""" + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC + @property def native_unit_of_measurement(self): """Return the unit of measurement.""" From 6d5dd3ee77e3fc78bc954b3900a50f1b79dfebb7 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 25 Oct 2021 12:44:03 +0200 Subject: [PATCH 0809/1038] Add entity category to MotionEye (#58370) --- homeassistant/components/motioneye/switch.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motioneye/switch.py b/homeassistant/components/motioneye/switch.py index abe4314447c..9a6fe27441f 100644 --- a/homeassistant/components/motioneye/switch.py +++ b/homeassistant/components/motioneye/switch.py @@ -18,6 +18,7 @@ from motioneye_client.const import ( from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import ENTITY_CATEGORY_CONFIG from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -29,25 +30,37 @@ MOTIONEYE_SWITCHES = [ key=KEY_MOTION_DETECTION, name="Motion Detection", entity_registry_enabled_default=True, + entity_category=ENTITY_CATEGORY_CONFIG, ), SwitchEntityDescription( - key=KEY_TEXT_OVERLAY, name="Text Overlay", entity_registry_enabled_default=False + key=KEY_TEXT_OVERLAY, + name="Text Overlay", + entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_CONFIG, ), SwitchEntityDescription( key=KEY_VIDEO_STREAMING, name="Video Streaming", entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_CONFIG, ), SwitchEntityDescription( - key=KEY_STILL_IMAGES, name="Still Images", entity_registry_enabled_default=True + key=KEY_STILL_IMAGES, + name="Still Images", + entity_registry_enabled_default=True, + entity_category=ENTITY_CATEGORY_CONFIG, ), SwitchEntityDescription( - key=KEY_MOVIES, name="Movies", entity_registry_enabled_default=True + key=KEY_MOVIES, + name="Movies", + entity_registry_enabled_default=True, + entity_category=ENTITY_CATEGORY_CONFIG, ), SwitchEntityDescription( key=KEY_UPLOAD_ENABLED, name="Upload Enabled", entity_registry_enabled_default=False, + entity_category=ENTITY_CATEGORY_CONFIG, ), ] From e529a5643064d2bda4b172144c589a37e7ce3857 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Mon, 25 Oct 2021 20:45:09 +1000 Subject: [PATCH 0810/1038] Add entity category to Advantage Air (#58371) --- .../components/advantage_air/binary_sensor.py | 3 +++ homeassistant/components/advantage_air/sensor.py | 11 ++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/advantage_air/binary_sensor.py b/homeassistant/components/advantage_air/binary_sensor.py index 6a050e4086a..1403458bc12 100644 --- a/homeassistant/components/advantage_air/binary_sensor.py +++ b/homeassistant/components/advantage_air/binary_sensor.py @@ -5,6 +5,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_PROBLEM, BinarySensorEntity, ) +from homeassistant.const import ENTITY_CATEGORY_CONFIG, ENTITY_CATEGORY_DIAGNOSTIC from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN from .entity import AdvantageAirEntity @@ -34,6 +35,7 @@ class AdvantageAirZoneFilter(AdvantageAirEntity, BinarySensorEntity): """Advantage Air Filter.""" _attr_device_class = DEVICE_CLASS_PROBLEM + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__(self, instance, ac_key): """Initialize an Advantage Air Filter.""" @@ -72,6 +74,7 @@ class AdvantageAirZoneMyZone(AdvantageAirEntity, BinarySensorEntity): """Advantage Air Zone MyZone.""" _attr_entity_registry_enabled_default = False + _attr_entity_category = ENTITY_CATEGORY_CONFIG def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Zone MyZone.""" diff --git a/homeassistant/components/advantage_air/sensor.py b/homeassistant/components/advantage_air/sensor.py index 4f3258e824e..ed2e3b78156 100644 --- a/homeassistant/components/advantage_air/sensor.py +++ b/homeassistant/components/advantage_air/sensor.py @@ -6,7 +6,12 @@ from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, SensorEntity, ) -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS +from homeassistant.const import ( + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, + PERCENTAGE, + TEMP_CELSIUS, +) from homeassistant.helpers import config_validation as cv, entity_platform from .const import ADVANTAGE_AIR_STATE_OPEN, DOMAIN as ADVANTAGE_AIR_DOMAIN @@ -50,6 +55,7 @@ class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity): """Representation of Advantage Air timer control.""" _attr_native_unit_of_measurement = ADVANTAGE_AIR_SET_COUNTDOWN_UNIT + _attr_entity_category = ENTITY_CATEGORY_CONFIG def __init__(self, instance, ac_key, action): """Initialize the Advantage Air timer control.""" @@ -84,6 +90,7 @@ class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity): _attr_native_unit_of_measurement = PERCENTAGE _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Zone Vent Sensor.""" @@ -113,6 +120,7 @@ class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity): _attr_native_unit_of_measurement = PERCENTAGE _attr_state_class = STATE_CLASS_MEASUREMENT + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Zone wireless signal sensor.""" @@ -148,6 +156,7 @@ class AdvantageAirZoneTemp(AdvantageAirEntity, SensorEntity): _attr_device_class = DEVICE_CLASS_TEMPERATURE _attr_state_class = STATE_CLASS_MEASUREMENT _attr_entity_registry_enabled_default = False + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Zone Temp Sensor.""" From f3ca61ffe055d7b2da6199de76e036620539815a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 25 Oct 2021 13:09:21 +0200 Subject: [PATCH 0811/1038] Use DeviceInfo on awair (#58395) Co-authored-by: epenet --- homeassistant/components/awair/sensor.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py index 80591e36f2d..1ff1b6e0efb 100644 --- a/homeassistant/components/awair/sensor.py +++ b/homeassistant/components/awair/sensor.py @@ -7,7 +7,12 @@ import voluptuous as vol from homeassistant.components.awair import AwairDataUpdateCoordinator, AwairResult from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import ATTR_ATTRIBUTION, CONF_ACCESS_TOKEN +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_CONNECTIONS, + ATTR_NAME, + CONF_ACCESS_TOKEN, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv @@ -211,17 +216,17 @@ class AwairSensor(CoordinatorEntity, SensorEntity): @property def device_info(self) -> DeviceInfo: """Device information.""" - info = { - "identifiers": {(DOMAIN, self._device.uuid)}, - "manufacturer": "Awair", - "model": self._device.model, - } + info = DeviceInfo( + identifiers={(DOMAIN, self._device.uuid)}, + manufacturer="Awair", + model=self._device.model, + ) if self._device.name: - info["name"] = self._device.name + info[ATTR_NAME] = self._device.name if self._device.mac_address: - info["connections"] = { + info[ATTR_CONNECTIONS] = { (dr.CONNECTION_NETWORK_MAC, self._device.mac_address) } From 4d7c55ff0e3cb9fb0fdbbe7f2e99f9c8d3c646de Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 25 Oct 2021 13:15:00 +0200 Subject: [PATCH 0812/1038] Use DeviceInfo on hassio (#58397) Co-authored-by: epenet --- homeassistant/components/hassio/__init__.py | 40 ++++++++++----------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 01d080ef6a9..d424722b4db 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -17,6 +17,7 @@ from homeassistant.components.homeassistant import ( import homeassistant.config as conf_util from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + ATTR_MANUFACTURER, ATTR_NAME, ATTR_SERVICE, EVENT_CORE_CONFIG_UPDATE, @@ -27,6 +28,7 @@ from homeassistant.core import DOMAIN as HASS_DOMAIN, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, recorder from homeassistant.helpers.device_registry import DeviceRegistry, async_get_registry +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.loader import bind_hass @@ -653,17 +655,16 @@ def async_register_addons_in_dev_reg( ) -> None: """Register addons in the device registry.""" for addon in addons: - params = { - "config_entry_id": entry_id, - "identifiers": {(DOMAIN, addon[ATTR_SLUG])}, - "model": SupervisorEntityModel.ADDON, - "sw_version": addon[ATTR_VERSION], - "name": addon[ATTR_NAME], - "entry_type": ATTR_SERVICE, - } + params = DeviceInfo( + identifiers={(DOMAIN, addon[ATTR_SLUG])}, + model=SupervisorEntityModel.ADDON, + sw_version=addon[ATTR_VERSION], + name=addon[ATTR_NAME], + entry_type=ATTR_SERVICE, + ) if manufacturer := addon.get(ATTR_REPOSITORY) or addon.get(ATTR_URL): - params["manufacturer"] = manufacturer - dev_reg.async_get_or_create(**params) + params[ATTR_MANUFACTURER] = manufacturer + dev_reg.async_get_or_create(config_entry_id=entry_id, **params) @callback @@ -671,16 +672,15 @@ def async_register_os_in_dev_reg( entry_id: str, dev_reg: DeviceRegistry, os_dict: dict[str, Any] ) -> None: """Register OS in the device registry.""" - params = { - "config_entry_id": entry_id, - "identifiers": {(DOMAIN, "OS")}, - "manufacturer": "Home Assistant", - "model": SupervisorEntityModel.OS, - "sw_version": os_dict[ATTR_VERSION], - "name": "Home Assistant Operating System", - "entry_type": ATTR_SERVICE, - } - dev_reg.async_get_or_create(**params) + params = DeviceInfo( + identifiers={(DOMAIN, "OS")}, + manufacturer="Home Assistant", + model=SupervisorEntityModel.OS, + sw_version=os_dict[ATTR_VERSION], + name="Home Assistant Operating System", + entry_type=ATTR_SERVICE, + ) + dev_reg.async_get_or_create(config_entry_id=entry_id, **params) @callback From 48c85fb83960f1c117bf49ca7ee0ba4ac2ddc5af Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 25 Oct 2021 13:16:53 +0200 Subject: [PATCH 0813/1038] Use DeviceInfo on esphome (#58396) Co-authored-by: epenet --- homeassistant/components/esphome/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index f5fd765dd71..6c51de2d8de 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -755,9 +755,9 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): @property def device_info(self) -> DeviceInfo: """Return device registry information for this entity.""" - return { - "connections": {(dr.CONNECTION_NETWORK_MAC, self._device_info.mac_address)} - } + return DeviceInfo( + connections={(dr.CONNECTION_NETWORK_MAC, self._device_info.mac_address)} + ) @property def name(self) -> str: From cfe1bbcda0ca4e71b6d93cee1d7e8cf558e7cbc7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 25 Oct 2021 13:17:55 +0200 Subject: [PATCH 0814/1038] Use DeviceInfo in huawei-lte (#58398) Co-authored-by: epenet --- .../components/huawei_lte/__init__.py | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index b1e95bcd07a..1314344f48f 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -31,6 +31,8 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( + ATTR_MODEL, + ATTR_SW_VERSION, CONF_MAC, CONF_NAME, CONF_PASSWORD, @@ -371,10 +373,10 @@ async def async_setup_entry( # noqa: C901 await hass.async_add_executor_job(router.update) # Check that we found required information - device_info = router.data.get(KEY_DEVICE_INFORMATION) + router_info = router.data.get(KEY_DEVICE_INFORMATION) if not entry.unique_id: # Transitional from < 2021.8: update None config entry and entity unique ids - if device_info and (serial_number := device_info.get("SerialNumber")): + if router_info and (serial_number := router_info.get("SerialNumber")): hass.config_entries.async_update_entry(entry, unique_id=serial_number) ent_reg = entity_registry.async_get(hass) for entity_entry in entity_registry.async_entries_for_config_entry( @@ -419,35 +421,36 @@ async def async_setup_entry( # noqa: C901 except Exception: # pylint: disable=broad-except # Assume not supported, or authentication required but in unauthenticated mode wlan_settings = {} - macs = get_device_macs(device_info or {}, wlan_settings) + macs = get_device_macs(router_info or {}, wlan_settings) # Be careful not to overwrite a previous, more complete set with a partial one - if macs and (not entry.data[CONF_MAC] or (device_info and wlan_settings)): + if macs and (not entry.data[CONF_MAC] or (router_info and wlan_settings)): new_data = dict(entry.data) new_data[CONF_MAC] = macs hass.config_entries.async_update_entry(entry, data=new_data) # Set up device registry if router.device_identifiers or router.device_connections: - device_data = {} + device_info = DeviceInfo( + connections=router.device_connections, + identifiers=router.device_identifiers, + name=router.device_name, + manufacturer="Huawei", + ) sw_version = None - if device_info: - sw_version = device_info.get("SoftwareVersion") - if device_info.get("DeviceName"): - device_data["model"] = device_info["DeviceName"] + if router_info: + sw_version = router_info.get("SoftwareVersion") + if router_info.get("DeviceName"): + device_info[ATTR_MODEL] = router_info["DeviceName"] if not sw_version and router.data.get(KEY_DEVICE_BASIC_INFORMATION): sw_version = router.data[KEY_DEVICE_BASIC_INFORMATION].get( "SoftwareVersion" ) if sw_version: - device_data["sw_version"] = sw_version + device_info[ATTR_SW_VERSION] = sw_version device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, - connections=router.device_connections, - identifiers=router.device_identifiers, - name=router.device_name, - manufacturer="Huawei", - **device_data, + **device_info, ) # Forward config entry setup to platforms From be4b1d15ec75ca6258ea72dac8d66955f6d0ebe8 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Mon, 25 Oct 2021 13:19:04 +0200 Subject: [PATCH 0815/1038] Add configuration_url to upnp device (#58385) --- homeassistant/components/upnp/__init__.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index ef3bad6da47..3982296b419 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -21,6 +21,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -253,12 +254,13 @@ class UpnpEntity(CoordinatorEntity): self.entity_description = entity_description self._attr_name = f"{coordinator.device.name} {entity_description.name}" self._attr_unique_id = f"{coordinator.device.udn}_{entity_description.unique_id or entity_description.key}" - self._attr_device_info = { - "connections": {(dr.CONNECTION_UPNP, coordinator.device.udn)}, - "name": coordinator.device.name, - "manufacturer": coordinator.device.manufacturer, - "model": coordinator.device.model_name, - } + self._attr_device_info = DeviceInfo( + connections={(dr.CONNECTION_UPNP, coordinator.device.udn)}, + name=coordinator.device.name, + manufacturer=coordinator.device.manufacturer, + model=coordinator.device.model_name, + configuration_url=f"http://{coordinator.device.hostname}", + ) @property def available(self) -> bool: From 66ae1160230d95816ad45df2f81486381f7e95c8 Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Mon, 25 Oct 2021 13:43:43 +0200 Subject: [PATCH 0816/1038] Update PyVicare to 2.13.0 (#57700) * Update PyVicare to 2.x With PyViCare 2.8.1 a breaking change was introduced which required changes on sensor and binary_sensor platforms: - Circuit, Burner and Compressor have been separated out from the "main" device - Multiple circuits and burners allow "duplicate sensors". We add the circuit or burner number as suffix now At the same time the sensors are now created only when available: During entity creation we can check if the value is provided for the user's device. Sensors are not created by heating type anymore but instead the new API structure is reflected, providing device, burner or circuit sensors. For details of breaking changes from PyViCare 1.x to 2.x please see https://github.com/somm15/PyViCare#breaking-changes-in-version-2x * Integrate review comments * variables cleanup * Update unique ids The unique ids shall not depend on the name but on the entity description key (which should not change) and the id of the circuit, burner or device. --- homeassistant/components/vicare/__init__.py | 128 ++-- .../components/vicare/binary_sensor.py | 187 +++--- homeassistant/components/vicare/climate.py | 114 ++-- homeassistant/components/vicare/const.py | 39 ++ homeassistant/components/vicare/manifest.json | 2 +- homeassistant/components/vicare/sensor.py | 546 ++++++++++-------- .../components/vicare/water_heater.py | 84 ++- requirements_all.txt | 2 +- 8 files changed, 661 insertions(+), 441 deletions(-) create mode 100644 homeassistant/components/vicare/const.py diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py index 5d5c5548be1..c1571c2f91b 100644 --- a/homeassistant/components/vicare/__init__.py +++ b/homeassistant/components/vicare/__init__.py @@ -1,16 +1,12 @@ """The ViCare integration.""" from __future__ import annotations -from collections.abc import Callable from dataclasses import dataclass -import enum import logging -from typing import Generic, TypeVar +from typing import Callable +from PyViCare.PyViCare import PyViCare from PyViCare.PyViCareDevice import Device -from PyViCare.PyViCareFuelCell import FuelCell -from PyViCare.PyViCareGazBoiler import GazBoiler -from PyViCare.PyViCareHeatPump import HeatPump import voluptuous as vol from homeassistant.const import ( @@ -24,55 +20,51 @@ from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.storage import STORAGE_DIR +from .const import ( + CONF_CIRCUIT, + CONF_HEATING_TYPE, + DEFAULT_HEATING_TYPE, + DOMAIN, + HEATING_TYPE_TO_CREATOR_METHOD, + PLATFORMS, + VICARE_API, + VICARE_CIRCUITS, + VICARE_DEVICE_CONFIG, + VICARE_NAME, + HeatingType, +) + _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["climate", "sensor", "binary_sensor", "water_heater"] - -DOMAIN = "vicare" -VICARE_API = "api" -VICARE_NAME = "name" -VICARE_HEATING_TYPE = "heating_type" - -CONF_CIRCUIT = "circuit" -CONF_HEATING_TYPE = "heating_type" -DEFAULT_HEATING_TYPE = "generic" - - -ApiT = TypeVar("ApiT", bound=Device) - @dataclass() -class ViCareRequiredKeysMixin(Generic[ApiT]): +class ViCareRequiredKeysMixin: """Mixin for required keys.""" - value_getter: Callable[[ApiT], bool] - - -class HeatingType(enum.Enum): - """Possible options for heating type.""" - - generic = "generic" - gas = "gas" - heatpump = "heatpump" - fuelcell = "fuelcell" + value_getter: Callable[[Device], bool] CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=60): vol.All( - cv.time_period, lambda value: value.total_seconds() - ), - vol.Optional(CONF_CIRCUIT): int, - vol.Optional(CONF_NAME, default="ViCare"): cv.string, - vol.Optional(CONF_HEATING_TYPE, default=DEFAULT_HEATING_TYPE): cv.enum( - HeatingType - ), - } + DOMAIN: vol.All( + cv.deprecated(CONF_CIRCUIT), + vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=60): vol.All( + cv.time_period, lambda value: value.total_seconds() + ), + vol.Optional( + CONF_CIRCUIT + ): int, # Ignored: All circuits are now supported. Will be removed when switching to Setup via UI. + vol.Optional(CONF_NAME, default="ViCare"): cv.string, + vol.Optional( + CONF_HEATING_TYPE, default=DEFAULT_HEATING_TYPE + ): cv.enum(HeatingType), + } + ), ) }, extra=vol.ALLOW_EXTRA, @@ -83,34 +75,40 @@ def setup(hass, config): """Create the ViCare component.""" conf = config[DOMAIN] params = {"token_file": hass.config.path(STORAGE_DIR, "vicare_token.save")} - if conf.get(CONF_CIRCUIT) is not None: - params["circuit"] = conf[CONF_CIRCUIT] params["cacheDuration"] = conf.get(CONF_SCAN_INTERVAL) params["client_id"] = conf.get(CONF_CLIENT_ID) - heating_type = conf[CONF_HEATING_TYPE] - - try: - if heating_type == HeatingType.gas: - vicare_api = GazBoiler(conf[CONF_USERNAME], conf[CONF_PASSWORD], **params) - elif heating_type == HeatingType.heatpump: - vicare_api = HeatPump(conf[CONF_USERNAME], conf[CONF_PASSWORD], **params) - elif heating_type == HeatingType.fuelcell: - vicare_api = FuelCell(conf[CONF_USERNAME], conf[CONF_PASSWORD], **params) - else: - vicare_api = Device(conf[CONF_USERNAME], conf[CONF_PASSWORD], **params) - except AttributeError: - _LOGGER.error( - "Failed to create PyViCare API client. Please check your credentials" - ) - return False hass.data[DOMAIN] = {} - hass.data[DOMAIN][VICARE_API] = vicare_api hass.data[DOMAIN][VICARE_NAME] = conf[CONF_NAME] - hass.data[DOMAIN][VICARE_HEATING_TYPE] = heating_type + setup_vicare_api(hass, conf, hass.data[DOMAIN]) + + hass.data[DOMAIN][CONF_HEATING_TYPE] = conf[CONF_HEATING_TYPE] for platform in PLATFORMS: discovery.load_platform(hass, platform, DOMAIN, {}, config) return True + + +def setup_vicare_api(hass, conf, entity_data): + """Set up PyVicare API.""" + vicare_api = PyViCare() + vicare_api.setCacheDuration(conf[CONF_SCAN_INTERVAL]) + vicare_api.initWithCredentials( + conf[CONF_USERNAME], + conf[CONF_PASSWORD], + conf[CONF_CLIENT_ID], + hass.config.path(STORAGE_DIR, "vicare_token.save"), + ) + + device = vicare_api.devices[0] + for device in vicare_api.devices: + _LOGGER.info( + "Found device: %s (online: %s)", device.getModel(), str(device.isOnline()) + ) + entity_data[VICARE_DEVICE_CONFIG] = device + entity_data[VICARE_API] = getattr( + device, HEATING_TYPE_TO_CREATOR_METHOD[conf[CONF_HEATING_TYPE]] + )() + entity_data[VICARE_CIRCUITS] = entity_data[VICARE_API].circuits diff --git a/homeassistant/components/vicare/binary_sensor.py b/homeassistant/components/vicare/binary_sensor.py index 88d6e3ac06a..d025d2b1ba6 100644 --- a/homeassistant/components/vicare/binary_sensor.py +++ b/homeassistant/components/vicare/binary_sensor.py @@ -4,12 +4,12 @@ from __future__ import annotations from contextlib import suppress from dataclasses import dataclass import logging -from typing import Union -from PyViCare.PyViCare import PyViCareNotSupportedFeatureError, PyViCareRateLimitError -from PyViCare.PyViCareDevice import Device -from PyViCare.PyViCareGazBoiler import GazBoiler -from PyViCare.PyViCareHeatPump import HeatPump +from PyViCare.PyViCareUtils import ( + PyViCareInvalidDataError, + PyViCareNotSupportedFeatureError, + PyViCareRateLimitError, +) import requests from homeassistant.components.binary_sensor import ( @@ -18,36 +18,25 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) -from . import ( - DOMAIN as VICARE_DOMAIN, - VICARE_API, - VICARE_HEATING_TYPE, - VICARE_NAME, - ApiT, - HeatingType, - ViCareRequiredKeysMixin, -) +from . import ViCareRequiredKeysMixin +from .const import DOMAIN, VICARE_API, VICARE_DEVICE_CONFIG, VICARE_NAME _LOGGER = logging.getLogger(__name__) SENSOR_CIRCULATION_PUMP_ACTIVE = "circulationpump_active" - -# gas sensors SENSOR_BURNER_ACTIVE = "burner_active" - -# heatpump sensors SENSOR_COMPRESSOR_ACTIVE = "compressor_active" @dataclass class ViCareBinarySensorEntityDescription( - BinarySensorEntityDescription, ViCareRequiredKeysMixin[ApiT] + BinarySensorEntityDescription, ViCareRequiredKeysMixin ): """Describes ViCare binary sensor entity.""" -SENSOR_TYPES_GENERIC: tuple[ViCareBinarySensorEntityDescription[Device]] = ( - ViCareBinarySensorEntityDescription[Device]( +CIRCUIT_SENSORS: tuple[ViCareBinarySensorEntityDescription, ...] = ( + ViCareBinarySensorEntityDescription( key=SENSOR_CIRCULATION_PUMP_ACTIVE, name="Circulation pump active", device_class=DEVICE_CLASS_POWER, @@ -55,80 +44,133 @@ SENSOR_TYPES_GENERIC: tuple[ViCareBinarySensorEntityDescription[Device]] = ( ), ) -SENSOR_TYPES_GAS: tuple[ViCareBinarySensorEntityDescription[GazBoiler]] = ( - ViCareBinarySensorEntityDescription[GazBoiler]( +BURNER_SENSORS: tuple[ViCareBinarySensorEntityDescription, ...] = ( + ViCareBinarySensorEntityDescription( key=SENSOR_BURNER_ACTIVE, name="Burner active", device_class=DEVICE_CLASS_POWER, - value_getter=lambda api: api.getBurnerActive(), + value_getter=lambda api: api.getActive(), ), ) -SENSOR_TYPES_HEATPUMP: tuple[ViCareBinarySensorEntityDescription[HeatPump]] = ( - ViCareBinarySensorEntityDescription[HeatPump]( +COMPRESSOR_SENSORS: tuple[ViCareBinarySensorEntityDescription, ...] = ( + ViCareBinarySensorEntityDescription( key=SENSOR_COMPRESSOR_ACTIVE, name="Compressor active", device_class=DEVICE_CLASS_POWER, - value_getter=lambda api: api.getCompressorActive(), + value_getter=lambda api: api.getActive(), ), ) -SENSORS_GENERIC = [SENSOR_CIRCULATION_PUMP_ACTIVE] -SENSORS_BY_HEATINGTYPE = { - HeatingType.gas: [SENSOR_BURNER_ACTIVE], - HeatingType.heatpump: [SENSOR_COMPRESSOR_ACTIVE], - HeatingType.fuelcell: [SENSOR_BURNER_ACTIVE], -} +def _build_entity(name, vicare_api, device_config, sensor): + """Create a ViCare binary sensor entity.""" + try: + sensor.value_getter(vicare_api) + _LOGGER.debug("Found entity %s", name) + except PyViCareNotSupportedFeatureError: + _LOGGER.info("Feature not supported %s", name) + return None + except AttributeError: + _LOGGER.debug("Attribute Error %s", name) + return None - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Create the ViCare sensor devices.""" - if discovery_info is None: - return - - vicare_api = hass.data[VICARE_DOMAIN][VICARE_API] - heating_type = hass.data[VICARE_DOMAIN][VICARE_HEATING_TYPE] - - sensors = SENSORS_GENERIC.copy() - - if heating_type != HeatingType.generic: - sensors.extend(SENSORS_BY_HEATINGTYPE[heating_type]) - - add_entities( - [ - ViCareBinarySensor( - hass.data[VICARE_DOMAIN][VICARE_NAME], vicare_api, description - ) - for description in ( - *SENSOR_TYPES_GENERIC, - *SENSOR_TYPES_GAS, - *SENSOR_TYPES_HEATPUMP, - ) - if description.key in sensors - ] + return ViCareBinarySensor( + name, + vicare_api, + device_config, + sensor, ) -DescriptionT = Union[ - ViCareBinarySensorEntityDescription[Device], - ViCareBinarySensorEntityDescription[GazBoiler], - ViCareBinarySensorEntityDescription[HeatPump], -] +async def _entities_from_descriptions( + hass, name, all_devices, sensor_descriptions, iterables +): + """Create entities from descriptions and list of burners/circuits.""" + for description in sensor_descriptions: + for current in iterables: + suffix = "" + if len(iterables) > 1: + suffix = f" {current.id}" + entity = await hass.async_add_executor_job( + _build_entity, + f"{name} {description.name}{suffix}", + current, + hass.data[DOMAIN][VICARE_DEVICE_CONFIG], + description, + ) + if entity is not None: + all_devices.append(entity) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Create the ViCare binary sensor devices.""" + if discovery_info is None: + return + + name = hass.data[DOMAIN][VICARE_NAME] + api = hass.data[DOMAIN][VICARE_API] + + all_devices = [] + + for description in CIRCUIT_SENSORS: + for circuit in api.circuits: + suffix = "" + if len(api.circuits) > 1: + suffix = f" {circuit.id}" + entity = await hass.async_add_executor_job( + _build_entity, + f"{name} {description.name}{suffix}", + circuit, + hass.data[DOMAIN][VICARE_DEVICE_CONFIG], + description, + ) + if entity is not None: + all_devices.append(entity) + + try: + _entities_from_descriptions( + hass, name, all_devices, BURNER_SENSORS, api.burners + ) + except PyViCareNotSupportedFeatureError: + _LOGGER.info("No burners found") + + try: + _entities_from_descriptions( + hass, name, all_devices, COMPRESSOR_SENSORS, api.compressors + ) + except PyViCareNotSupportedFeatureError: + _LOGGER.info("No compressors found") + + async_add_entities(all_devices) class ViCareBinarySensor(BinarySensorEntity): """Representation of a ViCare sensor.""" - entity_description: DescriptionT + entity_description: ViCareBinarySensorEntityDescription - def __init__(self, name, api, description: DescriptionT): + def __init__( + self, name, api, device_config, description: ViCareBinarySensorEntityDescription + ): """Initialize the sensor.""" self.entity_description = description - self._attr_name = f"{name} {description.name}" + self._attr_name = name self._api = api + self.entity_description = description + self._device_config = device_config self._state = None + @property + def device_info(self): + """Return device info for this device.""" + return { + "identifiers": {(DOMAIN, self._device_config.getConfig().serial)}, + "name": self._device_config.getModel(), + "manufacturer": "Viessmann", + "model": (DOMAIN, self._device_config.getModel()), + } + @property def available(self): """Return True if entity is available.""" @@ -136,8 +178,13 @@ class ViCareBinarySensor(BinarySensorEntity): @property def unique_id(self): - """Return a unique ID.""" - return f"{self._api.service.id}-{self.entity_description.key}" + """Return unique ID for this device.""" + tmp_id = ( + f"{self._device_config.getConfig().serial}-{self.entity_description.key}" + ) + if hasattr(self._api, "id"): + return f"{tmp_id}-{self._api.id}" + return tmp_id @property def is_on(self): @@ -155,3 +202,5 @@ class ViCareBinarySensor(BinarySensorEntity): _LOGGER.error("Unable to decode data from ViCare server") except PyViCareRateLimitError as limit_exception: _LOGGER.error("Vicare API rate limit exceeded: %s", limit_exception) + except PyViCareInvalidDataError as invalid_data_exception: + _LOGGER.error("Invalid data from Vicare server: %s", invalid_data_exception) diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index a6aa757e124..ba32e65ee52 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -2,7 +2,11 @@ from contextlib import suppress import logging -from PyViCare.PyViCare import PyViCareNotSupportedFeatureError, PyViCareRateLimitError +from PyViCare.PyViCareUtils import ( + PyViCareInvalidDataError, + PyViCareNotSupportedFeatureError, + PyViCareRateLimitError, +) import requests import voluptuous as vol @@ -21,12 +25,13 @@ from homeassistant.components.climate.const import ( from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS from homeassistant.helpers import entity_platform -from . import ( - DOMAIN as VICARE_DOMAIN, +from .const import ( + CONF_HEATING_TYPE, + DOMAIN, VICARE_API, - VICARE_HEATING_TYPE, + VICARE_CIRCUITS, + VICARE_DEVICE_CONFIG, VICARE_NAME, - HeatingType, ) _LOGGER = logging.getLogger(__name__) @@ -87,23 +92,38 @@ HA_TO_VICARE_PRESET_HEATING = { } +def _build_entity(name, vicare_api, circuit, device_config, heating_type): + """Create a ViCare climate entity.""" + _LOGGER.debug("Found device %s", name) + return ViCareClimate(name, vicare_api, device_config, circuit, heating_type) + + async def async_setup_platform( hass, hass_config, async_add_entities, discovery_info=None ): """Create the ViCare climate devices.""" + # Legacy setup. Remove after configuration.yaml deprecation end if discovery_info is None: return - vicare_api = hass.data[VICARE_DOMAIN][VICARE_API] - heating_type = hass.data[VICARE_DOMAIN][VICARE_HEATING_TYPE] - async_add_entities( - [ - ViCareClimate( - f"{hass.data[VICARE_DOMAIN][VICARE_NAME]} Heating", - vicare_api, - heating_type, - ) - ] - ) + + name = hass.data[DOMAIN][VICARE_NAME] + all_devices = [] + + for circuit in hass.data[DOMAIN][VICARE_CIRCUITS]: + suffix = "" + if len(hass.data[DOMAIN][VICARE_CIRCUITS]) > 1: + suffix = f" {circuit.id}" + entity = _build_entity( + f"{name} Heating{suffix}", + hass.data[DOMAIN][VICARE_API], + hass.data[DOMAIN][VICARE_DEVICE_CONFIG], + circuit, + hass.data[DOMAIN][CONF_HEATING_TYPE], + ) + if entity is not None: + all_devices.append(entity) + + async_add_entities(all_devices) platform = entity_platform.async_get_current_platform() @@ -121,11 +141,13 @@ async def async_setup_platform( class ViCareClimate(ClimateEntity): """Representation of the ViCare heating climate device.""" - def __init__(self, name, api, heating_type): + def __init__(self, name, api, circuit, device_config, heating_type): """Initialize the climate device.""" self._name = name self._state = None self._api = api + self._circuit = circuit + self._device_config = device_config self._attributes = {} self._target_temperature = None self._current_mode = None @@ -134,16 +156,31 @@ class ViCareClimate(ClimateEntity): self._heating_type = heating_type self._current_action = None + @property + def unique_id(self): + """Return unique ID for this device.""" + return f"{self._device_config.getConfig().serial}-climate-{self._circuit.id}" + + @property + def device_info(self): + """Return device info for this device.""" + return { + "identifiers": {(DOMAIN, self._device_config.getConfig().serial)}, + "name": self._device_config.getModel(), + "manufacturer": "Viessmann", + "model": (DOMAIN, self._device_config.getModel()), + } + def update(self): """Let HA know there has been an update from the ViCare API.""" try: _room_temperature = None with suppress(PyViCareNotSupportedFeatureError): - _room_temperature = self._api.getRoomTemperature() + _room_temperature = self._circuit.getRoomTemperature() _supply_temperature = None with suppress(PyViCareNotSupportedFeatureError): - _supply_temperature = self._api.getSupplyTemperature() + _supply_temperature = self._circuit.getSupplyTemperature() if _room_temperature is not None: self._current_temperature = _room_temperature @@ -153,13 +190,13 @@ class ViCareClimate(ClimateEntity): self._current_temperature = None with suppress(PyViCareNotSupportedFeatureError): - self._current_program = self._api.getActiveProgram() + self._current_program = self._circuit.getActiveProgram() with suppress(PyViCareNotSupportedFeatureError): - self._target_temperature = self._api.getCurrentDesiredTemperature() + self._target_temperature = self._circuit.getCurrentDesiredTemperature() with suppress(PyViCareNotSupportedFeatureError): - self._current_mode = self._api.getActiveMode() + self._current_mode = self._circuit.getActiveMode() # Update the generic device attributes self._attributes = {} @@ -171,26 +208,33 @@ class ViCareClimate(ClimateEntity): with suppress(PyViCareNotSupportedFeatureError): self._attributes[ "heating_curve_slope" - ] = self._api.getHeatingCurveSlope() + ] = self._circuit.getHeatingCurveSlope() with suppress(PyViCareNotSupportedFeatureError): self._attributes[ "heating_curve_shift" - ] = self._api.getHeatingCurveShift() + ] = self._circuit.getHeatingCurveShift() + self._current_action = False # Update the specific device attributes - if self._heating_type == HeatingType.gas: - with suppress(PyViCareNotSupportedFeatureError): - self._current_action = self._api.getBurnerActive() - elif self._heating_type == HeatingType.heatpump: - with suppress(PyViCareNotSupportedFeatureError): - self._current_action = self._api.getCompressorActive() + with suppress(PyViCareNotSupportedFeatureError): + for burner in self._api.burners: + self._current_action = self._current_action or burner.getActive() + + with suppress(PyViCareNotSupportedFeatureError): + for compressor in self._api.compressors: + self._current_action = ( + self._current_action or compressor.getActive() + ) + except requests.exceptions.ConnectionError: _LOGGER.error("Unable to retrieve data from ViCare server") except PyViCareRateLimitError as limit_exception: _LOGGER.error("Vicare API rate limit exceeded: %s", limit_exception) except ValueError: _LOGGER.error("Unable to decode data from ViCare server") + except PyViCareInvalidDataError as invalid_data_exception: + _LOGGER.error("Invalid data from Vicare server: %s", invalid_data_exception) @property def supported_features(self): @@ -231,7 +275,7 @@ class ViCareClimate(ClimateEntity): ) _LOGGER.debug("Setting hvac mode to %s / %s", hvac_mode, vicare_mode) - self._api.setMode(vicare_mode) + self._circuit.setMode(vicare_mode) @property def hvac_modes(self): @@ -263,7 +307,7 @@ class ViCareClimate(ClimateEntity): def set_temperature(self, **kwargs): """Set new target temperatures.""" if (temp := kwargs.get(ATTR_TEMPERATURE)) is not None: - self._api.setProgramTemperature(self._current_program, temp) + self._circuit.setProgramTemperature(self._current_program, temp) self._target_temperature = temp @property @@ -285,8 +329,8 @@ class ViCareClimate(ClimateEntity): ) _LOGGER.debug("Setting preset to %s / %s", preset_mode, vicare_program) - self._api.deactivateProgram(self._current_program) - self._api.activateProgram(vicare_program) + self._circuit.deactivateProgram(self._current_program) + self._circuit.activateProgram(vicare_program) @property def extra_state_attributes(self): @@ -298,4 +342,4 @@ class ViCareClimate(ClimateEntity): if vicare_mode not in VICARE_TO_HA_HVAC_HEATING: raise ValueError(f"Cannot set invalid vicare mode: {vicare_mode}") - self._api.setMode(vicare_mode) + self._circuit.setMode(vicare_mode) diff --git a/homeassistant/components/vicare/const.py b/homeassistant/components/vicare/const.py new file mode 100644 index 00000000000..2336ef40eaa --- /dev/null +++ b/homeassistant/components/vicare/const.py @@ -0,0 +1,39 @@ +"""Constants for the ViCare integration.""" +import enum + +DOMAIN = "vicare" + +PLATFORMS = ["climate", "sensor", "binary_sensor", "water_heater"] + +VICARE_DEVICE_CONFIG = "device_conf" +VICARE_API = "api" +VICARE_NAME = "name" +VICARE_CIRCUITS = "circuits" + +CONF_CIRCUIT = "circuit" +CONF_HEATING_TYPE = "heating_type" + +DEFAULT_SCAN_INTERVAL = 60 + + +class HeatingType(enum.Enum): + """Possible options for heating type.""" + + auto = "auto" + gas = "gas" + oil = "oil" + pellets = "pellets" + heatpump = "heatpump" + fuelcell = "fuelcell" + + +DEFAULT_HEATING_TYPE = HeatingType.auto + +HEATING_TYPE_TO_CREATOR_METHOD = { + HeatingType.auto: "asAutoDetectDevice", + HeatingType.gas: "asGazBoiler", + HeatingType.fuelcell: "asFuelCell", + HeatingType.heatpump: "asHeatPump", + HeatingType.oil: "asOilBoiler", + HeatingType.pellets: "asPelletsBoiler", +} diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index 88e9a1e4e4b..38344fff70b 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -3,6 +3,6 @@ "name": "Viessmann ViCare", "documentation": "https://www.home-assistant.io/integrations/vicare", "codeowners": ["@oischinger"], - "requirements": ["PyViCare==1.0.0"], + "requirements": ["PyViCare==2.13.0"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index c7e318a6c16..68bb1ff2363 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -4,16 +4,19 @@ from __future__ import annotations from contextlib import suppress from dataclasses import dataclass import logging -from typing import Union -from PyViCare.PyViCare import PyViCareNotSupportedFeatureError, PyViCareRateLimitError -from PyViCare.PyViCareDevice import Device -from PyViCare.PyViCareFuelCell import FuelCell -from PyViCare.PyViCareGazBoiler import GazBoiler -from PyViCare.PyViCareHeatPump import HeatPump +from PyViCare.PyViCareUtils import ( + PyViCareInvalidDataError, + PyViCareNotSupportedFeatureError, + PyViCareRateLimitError, +) import requests -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + STATE_CLASS_TOTAL_INCREASING, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, @@ -24,21 +27,13 @@ from homeassistant.const import ( TEMP_CELSIUS, TIME_HOURS, ) +import homeassistant.util.dt as dt_util -from . import ( - DOMAIN as VICARE_DOMAIN, - VICARE_API, - VICARE_HEATING_TYPE, - VICARE_NAME, - ApiT, - HeatingType, - ViCareRequiredKeysMixin, -) +from . import ViCareRequiredKeysMixin +from .const import DOMAIN, VICARE_API, VICARE_DEVICE_CONFIG, VICARE_NAME _LOGGER = logging.getLogger(__name__) -SENSOR_TYPE_TEMPERATURE = "temperature" - SENSOR_OUTSIDE_TEMPERATURE = "outside_temperature" SENSOR_SUPPLY_TEMPERATURE = "supply_temperature" SENSOR_RETURN_TEMPERATURE = "return_temperature" @@ -76,308 +71,340 @@ SENSOR_POWER_PRODUCTION_THIS_YEAR = "power_production_this_year" @dataclass -class ViCareSensorEntityDescription( - SensorEntityDescription, ViCareRequiredKeysMixin[ApiT] -): +class ViCareSensorEntityDescription(SensorEntityDescription, ViCareRequiredKeysMixin): """Describes ViCare sensor entity.""" -SENSOR_TYPES_GENERIC: tuple[ViCareSensorEntityDescription[Device], ...] = ( - ViCareSensorEntityDescription[Device]( +GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( + ViCareSensorEntityDescription( key=SENSOR_OUTSIDE_TEMPERATURE, name="Outside Temperature", native_unit_of_measurement=TEMP_CELSIUS, value_getter=lambda api: api.getOutsideTemperature(), device_class=DEVICE_CLASS_TEMPERATURE, ), - ViCareSensorEntityDescription[Device]( - key=SENSOR_SUPPLY_TEMPERATURE, - name="Supply Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - value_getter=lambda api: api.getSupplyTemperature(), - device_class=DEVICE_CLASS_TEMPERATURE, - ), -) - -SENSOR_TYPES_GAS: tuple[ViCareSensorEntityDescription[GazBoiler], ...] = ( - ViCareSensorEntityDescription[GazBoiler]( - key=SENSOR_BOILER_TEMPERATURE, - name="Boiler Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - value_getter=lambda api: api.getBoilerTemperature(), - device_class=DEVICE_CLASS_TEMPERATURE, - ), - ViCareSensorEntityDescription[GazBoiler]( - key=SENSOR_BURNER_MODULATION, - name="Burner modulation", - icon="mdi:percent", - native_unit_of_measurement=PERCENTAGE, - value_getter=lambda api: api.getBurnerModulation(), - ), - ViCareSensorEntityDescription[GazBoiler]( - key=SENSOR_DHW_GAS_CONSUMPTION_TODAY, - name="Hot water gas consumption today", - icon="mdi:power", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - value_getter=lambda api: api.getGasConsumptionDomesticHotWaterToday(), - ), - ViCareSensorEntityDescription[GazBoiler]( - key=SENSOR_DHW_GAS_CONSUMPTION_THIS_WEEK, - name="Hot water gas consumption this week", - icon="mdi:power", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - value_getter=lambda api: api.getGasConsumptionDomesticHotWaterThisWeek(), - ), - ViCareSensorEntityDescription[GazBoiler]( - key=SENSOR_DHW_GAS_CONSUMPTION_THIS_MONTH, - name="Hot water gas consumption this month", - icon="mdi:power", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - value_getter=lambda api: api.getGasConsumptionDomesticHotWaterThisMonth(), - ), - ViCareSensorEntityDescription[GazBoiler]( - key=SENSOR_DHW_GAS_CONSUMPTION_THIS_YEAR, - name="Hot water gas consumption this year", - icon="mdi:power", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - value_getter=lambda api: api.getGasConsumptionDomesticHotWaterThisYear(), - ), - ViCareSensorEntityDescription[GazBoiler]( - key=SENSOR_GAS_CONSUMPTION_TODAY, - name="Heating gas consumption today", - icon="mdi:power", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - value_getter=lambda api: api.getGasConsumptionHeatingToday(), - ), - ViCareSensorEntityDescription[GazBoiler]( - key=SENSOR_GAS_CONSUMPTION_THIS_WEEK, - name="Heating gas consumption this week", - icon="mdi:power", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - value_getter=lambda api: api.getGasConsumptionHeatingThisWeek(), - ), - ViCareSensorEntityDescription[GazBoiler]( - key=SENSOR_GAS_CONSUMPTION_THIS_MONTH, - name="Heating gas consumption this month", - icon="mdi:power", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - value_getter=lambda api: api.getGasConsumptionHeatingThisMonth(), - ), - ViCareSensorEntityDescription[GazBoiler]( - key=SENSOR_GAS_CONSUMPTION_THIS_YEAR, - name="Heating gas consumption this year", - icon="mdi:power", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - value_getter=lambda api: api.getGasConsumptionHeatingThisYear(), - ), - ViCareSensorEntityDescription[GazBoiler]( - key=SENSOR_BURNER_STARTS, - name="Burner Starts", - icon="mdi:counter", - value_getter=lambda api: api.getBurnerStarts(), - ), - ViCareSensorEntityDescription[GazBoiler]( - key=SENSOR_BURNER_HOURS, - name="Burner Hours", - icon="mdi:counter", - native_unit_of_measurement=TIME_HOURS, - value_getter=lambda api: api.getBurnerHours(), - ), -) - -SENSOR_TYPES_HEATPUMP: tuple[ViCareSensorEntityDescription[HeatPump], ...] = ( - ViCareSensorEntityDescription[HeatPump]( - key=SENSOR_COMPRESSOR_STARTS, - name="Compressor Starts", - icon="mdi:counter", - value_getter=lambda api: api.getCompressorStarts(), - ), - ViCareSensorEntityDescription[HeatPump]( - key=SENSOR_COMPRESSOR_HOURS, - name="Compressor Hours", - icon="mdi:counter", - native_unit_of_measurement=TIME_HOURS, - value_getter=lambda api: api.getCompressorHours(), - ), - ViCareSensorEntityDescription[HeatPump]( - key=SENSOR_COMPRESSOR_HOURS_LOADCLASS1, - name="Compressor Hours Load Class 1", - icon="mdi:counter", - native_unit_of_measurement=TIME_HOURS, - value_getter=lambda api: api.getCompressorHoursLoadClass1(), - ), - ViCareSensorEntityDescription[HeatPump]( - key=SENSOR_COMPRESSOR_HOURS_LOADCLASS2, - name="Compressor Hours Load Class 2", - icon="mdi:counter", - native_unit_of_measurement=TIME_HOURS, - value_getter=lambda api: api.getCompressorHoursLoadClass2(), - ), - ViCareSensorEntityDescription[HeatPump]( - key=SENSOR_COMPRESSOR_HOURS_LOADCLASS3, - name="Compressor Hours Load Class 3", - icon="mdi:counter", - native_unit_of_measurement=TIME_HOURS, - value_getter=lambda api: api.getCompressorHoursLoadClass3(), - ), - ViCareSensorEntityDescription[HeatPump]( - key=SENSOR_COMPRESSOR_HOURS_LOADCLASS4, - name="Compressor Hours Load Class 4", - icon="mdi:counter", - native_unit_of_measurement=TIME_HOURS, - value_getter=lambda api: api.getCompressorHoursLoadClass4(), - ), - ViCareSensorEntityDescription[HeatPump]( - key=SENSOR_COMPRESSOR_HOURS_LOADCLASS5, - name="Compressor Hours Load Class 5", - icon="mdi:counter", - native_unit_of_measurement=TIME_HOURS, - value_getter=lambda api: api.getCompressorHoursLoadClass5(), - ), - ViCareSensorEntityDescription[HeatPump]( + ViCareSensorEntityDescription( key=SENSOR_RETURN_TEMPERATURE, name="Return Temperature", native_unit_of_measurement=TEMP_CELSIUS, value_getter=lambda api: api.getReturnTemperature(), device_class=DEVICE_CLASS_TEMPERATURE, ), -) - -SENSOR_TYPES_FUELCELL: tuple[ViCareSensorEntityDescription[FuelCell], ...] = ( - ViCareSensorEntityDescription[FuelCell]( + ViCareSensorEntityDescription( + key=SENSOR_BOILER_TEMPERATURE, + name="Boiler Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + value_getter=lambda api: api.getBoilerTemperature(), + device_class=DEVICE_CLASS_TEMPERATURE, + ), + ViCareSensorEntityDescription( + key=SENSOR_DHW_GAS_CONSUMPTION_TODAY, + name="Hot water gas consumption today", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getGasConsumptionDomesticHotWaterToday(), + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key=SENSOR_DHW_GAS_CONSUMPTION_THIS_WEEK, + name="Hot water gas consumption this week", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getGasConsumptionDomesticHotWaterThisWeek(), + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key=SENSOR_DHW_GAS_CONSUMPTION_THIS_MONTH, + name="Hot water gas consumption this month", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getGasConsumptionDomesticHotWaterThisMonth(), + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key=SENSOR_DHW_GAS_CONSUMPTION_THIS_YEAR, + name="Hot water gas consumption this year", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getGasConsumptionDomesticHotWaterThisYear(), + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key=SENSOR_GAS_CONSUMPTION_TODAY, + name="Heating gas consumption today", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getGasConsumptionHeatingToday(), + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key=SENSOR_GAS_CONSUMPTION_THIS_WEEK, + name="Heating gas consumption this week", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getGasConsumptionHeatingThisWeek(), + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key=SENSOR_GAS_CONSUMPTION_THIS_MONTH, + name="Heating gas consumption this month", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getGasConsumptionHeatingThisMonth(), + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key=SENSOR_GAS_CONSUMPTION_THIS_YEAR, + name="Heating gas consumption this year", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getGasConsumptionHeatingThisYear(), + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( key=SENSOR_POWER_PRODUCTION_CURRENT, name="Power production current", native_unit_of_measurement=POWER_WATT, value_getter=lambda api: api.getPowerProductionCurrent(), device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_TOTAL_INCREASING, ), - ViCareSensorEntityDescription[FuelCell]( + ViCareSensorEntityDescription( key=SENSOR_POWER_PRODUCTION_TODAY, name="Power production today", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getPowerProductionToday(), device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), - ViCareSensorEntityDescription[FuelCell]( + ViCareSensorEntityDescription( key=SENSOR_POWER_PRODUCTION_THIS_WEEK, name="Power production this week", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getPowerProductionThisWeek(), device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), - ViCareSensorEntityDescription[FuelCell]( + ViCareSensorEntityDescription( key=SENSOR_POWER_PRODUCTION_THIS_MONTH, name="Power production this month", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getPowerProductionThisMonth(), device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), - ViCareSensorEntityDescription[FuelCell]( + ViCareSensorEntityDescription( key=SENSOR_POWER_PRODUCTION_THIS_YEAR, name="Power production this year", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getPowerProductionThisYear(), device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), ) -SENSORS_GENERIC = [SENSOR_OUTSIDE_TEMPERATURE, SENSOR_SUPPLY_TEMPERATURE] +CIRCUIT_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( + ViCareSensorEntityDescription( + key=SENSOR_SUPPLY_TEMPERATURE, + name="Supply Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + value_getter=lambda api: api.getSupplyTemperature(), + ), +) -SENSORS_BY_HEATINGTYPE = { - HeatingType.gas: [ - SENSOR_BOILER_TEMPERATURE, - SENSOR_BURNER_HOURS, - SENSOR_BURNER_MODULATION, - SENSOR_BURNER_STARTS, - SENSOR_DHW_GAS_CONSUMPTION_TODAY, - SENSOR_DHW_GAS_CONSUMPTION_THIS_WEEK, - SENSOR_DHW_GAS_CONSUMPTION_THIS_MONTH, - SENSOR_DHW_GAS_CONSUMPTION_THIS_YEAR, - SENSOR_GAS_CONSUMPTION_TODAY, - SENSOR_GAS_CONSUMPTION_THIS_WEEK, - SENSOR_GAS_CONSUMPTION_THIS_MONTH, - SENSOR_GAS_CONSUMPTION_THIS_YEAR, - ], - HeatingType.heatpump: [ - SENSOR_COMPRESSOR_STARTS, - SENSOR_COMPRESSOR_HOURS, - SENSOR_COMPRESSOR_HOURS_LOADCLASS1, - SENSOR_COMPRESSOR_HOURS_LOADCLASS2, - SENSOR_COMPRESSOR_HOURS_LOADCLASS3, - SENSOR_COMPRESSOR_HOURS_LOADCLASS4, - SENSOR_COMPRESSOR_HOURS_LOADCLASS5, - SENSOR_RETURN_TEMPERATURE, - ], - HeatingType.fuelcell: [ - # gas - SENSOR_BOILER_TEMPERATURE, - SENSOR_BURNER_HOURS, - SENSOR_BURNER_MODULATION, - SENSOR_BURNER_STARTS, - SENSOR_DHW_GAS_CONSUMPTION_TODAY, - SENSOR_DHW_GAS_CONSUMPTION_THIS_WEEK, - SENSOR_DHW_GAS_CONSUMPTION_THIS_MONTH, - SENSOR_DHW_GAS_CONSUMPTION_THIS_YEAR, - SENSOR_GAS_CONSUMPTION_TODAY, - SENSOR_GAS_CONSUMPTION_THIS_WEEK, - SENSOR_GAS_CONSUMPTION_THIS_MONTH, - SENSOR_GAS_CONSUMPTION_THIS_YEAR, - # fuel cell - SENSOR_POWER_PRODUCTION_CURRENT, - SENSOR_POWER_PRODUCTION_TODAY, - SENSOR_POWER_PRODUCTION_THIS_WEEK, - SENSOR_POWER_PRODUCTION_THIS_MONTH, - SENSOR_POWER_PRODUCTION_THIS_YEAR, - ], -} +BURNER_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( + ViCareSensorEntityDescription( + key=SENSOR_BURNER_STARTS, + name="Burner Starts", + icon="mdi:counter", + value_getter=lambda api: api.getStarts(), + ), + ViCareSensorEntityDescription( + key=SENSOR_BURNER_HOURS, + name="Burner Hours", + icon="mdi:counter", + native_unit_of_measurement=TIME_HOURS, + value_getter=lambda api: api.getHours(), + ), + ViCareSensorEntityDescription( + key=SENSOR_BURNER_MODULATION, + name="Burner Modulation", + icon="mdi:percent", + native_unit_of_measurement=PERCENTAGE, + value_getter=lambda api: api.getModulation(), + ), +) + +COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( + ViCareSensorEntityDescription( + key=SENSOR_COMPRESSOR_STARTS, + name="Compressor Starts", + icon="mdi:counter", + value_getter=lambda api: api.getStarts(), + ), + ViCareSensorEntityDescription( + key=SENSOR_COMPRESSOR_HOURS, + name="Compressor Hours", + icon="mdi:counter", + native_unit_of_measurement=TIME_HOURS, + value_getter=lambda api: api.getHours(), + ), + ViCareSensorEntityDescription( + key=SENSOR_COMPRESSOR_HOURS_LOADCLASS1, + name="Compressor Hours Load Class 1", + icon="mdi:counter", + native_unit_of_measurement=TIME_HOURS, + value_getter=lambda api: api.getHoursLoadClass1(), + ), + ViCareSensorEntityDescription( + key=SENSOR_COMPRESSOR_HOURS_LOADCLASS2, + name="Compressor Hours Load Class 2", + icon="mdi:counter", + native_unit_of_measurement=TIME_HOURS, + value_getter=lambda api: api.getHoursLoadClass2(), + ), + ViCareSensorEntityDescription( + key=SENSOR_COMPRESSOR_HOURS_LOADCLASS3, + name="Compressor Hours Load Class 3", + icon="mdi:counter", + native_unit_of_measurement=TIME_HOURS, + value_getter=lambda api: api.getHoursLoadClass3(), + ), + ViCareSensorEntityDescription( + key=SENSOR_COMPRESSOR_HOURS_LOADCLASS4, + name="Compressor Hours Load Class 4", + icon="mdi:counter", + native_unit_of_measurement=TIME_HOURS, + value_getter=lambda api: api.getHoursLoadClass4(), + ), + ViCareSensorEntityDescription( + key=SENSOR_COMPRESSOR_HOURS_LOADCLASS5, + name="Compressor Hours Load Class 5", + icon="mdi:counter", + native_unit_of_measurement=TIME_HOURS, + value_getter=lambda api: api.getHoursLoadClass5(), + ), +) -def setup_platform(hass, config, add_entities, discovery_info=None): +def _build_entity(name, vicare_api, device_config, sensor): + """Create a ViCare sensor entity.""" + _LOGGER.debug("Found device %s", name) + try: + sensor.value_getter(vicare_api) + _LOGGER.debug("Found entity %s", name) + except PyViCareNotSupportedFeatureError: + _LOGGER.info("Feature not supported %s", name) + return None + except AttributeError: + _LOGGER.debug("Attribute Error %s", name) + return None + + return ViCareSensor( + name, + vicare_api, + device_config, + sensor, + ) + + +async def _entities_from_descriptions( + hass, name, all_devices, sensor_descriptions, iterables +): + """Create entities from descriptions and list of burners/circuits.""" + for description in sensor_descriptions: + for current in iterables: + suffix = "" + if len(iterables) > 1: + suffix = f" {current.id}" + entity = await hass.async_add_executor_job( + _build_entity, + f"{name} {description.name}{suffix}", + current, + hass.data[DOMAIN][VICARE_DEVICE_CONFIG], + description, + ) + if entity is not None: + all_devices.append(entity) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Create the ViCare sensor devices.""" if discovery_info is None: return - vicare_api = hass.data[VICARE_DOMAIN][VICARE_API] - heating_type = hass.data[VICARE_DOMAIN][VICARE_HEATING_TYPE] + name = hass.data[DOMAIN][VICARE_NAME] + api = hass.data[DOMAIN][VICARE_API] - sensors = SENSORS_GENERIC.copy() + all_devices = [] + for description in GLOBAL_SENSORS: + entity = await hass.async_add_executor_job( + _build_entity, + f"{name} {description.name}", + api, + hass.data[DOMAIN][VICARE_DEVICE_CONFIG], + description, + ) + if entity is not None: + all_devices.append(entity) - if heating_type != HeatingType.generic: - sensors.extend(SENSORS_BY_HEATINGTYPE[heating_type]) - - add_entities( - [ - ViCareSensor(hass.data[VICARE_DOMAIN][VICARE_NAME], vicare_api, description) - for description in ( - *SENSOR_TYPES_GENERIC, - *SENSOR_TYPES_GAS, - *SENSOR_TYPES_HEATPUMP, - *SENSOR_TYPES_FUELCELL, + for description in CIRCUIT_SENSORS: + for circuit in api.circuits: + suffix = "" + if len(api.circuits) > 1: + suffix = f" {circuit.id}" + entity = await hass.async_add_executor_job( + _build_entity, + f"{name} {description.name}{suffix}", + circuit, + hass.data[DOMAIN][VICARE_DEVICE_CONFIG], + description, ) - if description.key in sensors - ] - ) + if entity is not None: + all_devices.append(entity) + try: + _entities_from_descriptions( + hass, name, all_devices, BURNER_SENSORS, api.burners + ) + except PyViCareNotSupportedFeatureError: + _LOGGER.info("No burners found") -DescriptionT = Union[ - ViCareSensorEntityDescription[Device], - ViCareSensorEntityDescription[GazBoiler], - ViCareSensorEntityDescription[HeatPump], - ViCareSensorEntityDescription[FuelCell], -] + try: + _entities_from_descriptions( + hass, name, all_devices, COMPRESSOR_SENSORS, api.compressors + ) + except PyViCareNotSupportedFeatureError: + _LOGGER.info("No compressors found") + + async_add_entities(all_devices) class ViCareSensor(SensorEntity): """Representation of a ViCare sensor.""" - entity_description: DescriptionT + entity_description: ViCareSensorEntityDescription - def __init__(self, name, api, description: DescriptionT): + def __init__( + self, name, api, device_config, description: ViCareSensorEntityDescription + ): """Initialize the sensor.""" self.entity_description = description - self._attr_name = f"{name} {description.name}" + self._attr_name = name self._api = api + self._device_config = device_config self._state = None + self._last_reset = dt_util.utcnow() + + @property + def device_info(self): + """Return device info for this device.""" + return { + "identifiers": {(DOMAIN, self._device_config.getConfig().serial)}, + "name": self._device_config.getModel(), + "manufacturer": "Viessmann", + "model": (DOMAIN, self._device_config.getModel()), + } @property def available(self): @@ -386,16 +413,27 @@ class ViCareSensor(SensorEntity): @property def unique_id(self): - """Return a unique ID.""" - return f"{self._api.service.id}-{self.entity_description.key}" + """Return unique ID for this device.""" + tmp_id = ( + f"{self._device_config.getConfig().serial}-{self.entity_description.key}" + ) + if hasattr(self._api, "id"): + return f"{tmp_id}-{self._api.id}" + return tmp_id @property def native_value(self): """Return the state of the sensor.""" return self._state + @property + def last_reset(self): + """Return the time when the sensor was last reset.""" + return self._last_reset + def update(self): """Update state of sensor.""" + self._last_reset = dt_util.start_of_local_day() try: with suppress(PyViCareNotSupportedFeatureError): self._state = self.entity_description.value_getter(self._api) @@ -405,3 +443,5 @@ class ViCareSensor(SensorEntity): _LOGGER.error("Unable to decode data from ViCare server") except PyViCareRateLimitError as limit_exception: _LOGGER.error("Vicare API rate limit exceeded: %s", limit_exception) + except PyViCareInvalidDataError as invalid_data_exception: + _LOGGER.error("Invalid data from Vicare server: %s", invalid_data_exception) diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index 557d5257427..524ea52c756 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -2,7 +2,11 @@ from contextlib import suppress import logging -from PyViCare.PyViCare import PyViCareNotSupportedFeatureError, PyViCareRateLimitError +from PyViCare.PyViCareUtils import ( + PyViCareInvalidDataError, + PyViCareNotSupportedFeatureError, + PyViCareRateLimitError, +) import requests from homeassistant.components.water_heater import ( @@ -11,7 +15,14 @@ from homeassistant.components.water_heater import ( ) from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS -from . import DOMAIN as VICARE_DOMAIN, VICARE_API, VICARE_HEATING_TYPE, VICARE_NAME +from .const import ( + CONF_HEATING_TYPE, + DOMAIN, + VICARE_API, + VICARE_CIRCUITS, + VICARE_DEVICE_CONFIG, + VICARE_NAME, +) _LOGGER = logging.getLogger(__name__) @@ -43,31 +54,53 @@ HA_TO_VICARE_HVAC_DHW = { } -def setup_platform(hass, config, add_entities, discovery_info=None): +def _build_entity(name, vicare_api, circuit, device_config, heating_type): + """Create a ViCare water_heater entity.""" + _LOGGER.debug("Found device %s", name) + return ViCareWater( + name, + vicare_api, + circuit, + device_config, + heating_type, + ) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Create the ViCare water_heater devices.""" if discovery_info is None: return - vicare_api = hass.data[VICARE_DOMAIN][VICARE_API] - heating_type = hass.data[VICARE_DOMAIN][VICARE_HEATING_TYPE] - add_entities( - [ - ViCareWater( - f"{hass.data[VICARE_DOMAIN][VICARE_NAME]} Water", - vicare_api, - heating_type, - ) - ] - ) + + name = hass.data[DOMAIN][VICARE_NAME] + + all_devices = [] + for circuit in hass.data[DOMAIN][VICARE_CIRCUITS]: + suffix = "" + if len(hass.data[DOMAIN][VICARE_CIRCUITS]) > 1: + suffix = f" {circuit.id}" + entity = _build_entity( + f"{name} Water{suffix}", + hass.data[DOMAIN][VICARE_API], + circuit, + hass.data[DOMAIN][VICARE_DEVICE_CONFIG], + hass.data[DOMAIN][CONF_HEATING_TYPE], + ) + if entity is not None: + all_devices.append(entity) + + async_add_entities(all_devices) class ViCareWater(WaterHeaterEntity): """Representation of the ViCare domestic hot water device.""" - def __init__(self, name, api, heating_type): + def __init__(self, name, api, circuit, device_config, heating_type): """Initialize the DHW water_heater device.""" self._name = name self._state = None self._api = api + self._circuit = circuit + self._device_config = device_config self._attributes = {} self._target_temperature = None self._current_temperature = None @@ -84,11 +117,11 @@ class ViCareWater(WaterHeaterEntity): with suppress(PyViCareNotSupportedFeatureError): self._target_temperature = ( - self._api.getDomesticHotWaterConfiguredTemperature() + self._api.getDomesticHotWaterDesiredTemperature() ) with suppress(PyViCareNotSupportedFeatureError): - self._current_mode = self._api.getActiveMode() + self._current_mode = self._circuit.getActiveMode() except requests.exceptions.ConnectionError: _LOGGER.error("Unable to retrieve data from ViCare server") @@ -96,6 +129,23 @@ class ViCareWater(WaterHeaterEntity): _LOGGER.error("Vicare API rate limit exceeded: %s", limit_exception) except ValueError: _LOGGER.error("Unable to decode data from ViCare server") + except PyViCareInvalidDataError as invalid_data_exception: + _LOGGER.error("Invalid data from Vicare server: %s", invalid_data_exception) + + @property + def unique_id(self): + """Return unique ID for this device.""" + return f"{self._device_config.getConfig().serial}-water-{self._circuit.id}" + + @property + def device_info(self): + """Return device info for this device.""" + return { + "identifiers": {(DOMAIN, self._device_config.getConfig().serial)}, + "name": self._device_config.getModel(), + "manufacturer": "Viessmann", + "model": (DOMAIN, self._device_config.getModel()), + } @property def supported_features(self): diff --git a/requirements_all.txt b/requirements_all.txt index a1af8fda1b8..fe91683cc6c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -55,7 +55,7 @@ PyTransportNSW==0.1.1 PyTurboJPEG==1.6.1 # homeassistant.components.vicare -PyViCare==1.0.0 +PyViCare==2.13.0 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.13.4 From a8a8b532d0e38966ba01b327fc7545f05deb9dc5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 25 Oct 2021 13:46:09 +0200 Subject: [PATCH 0817/1038] Use DeviceInfo in mqtt (#58389) * Use DeviceInfo in mqtt * Updates for mypy Co-authored-by: epenet --- homeassistant/components/mqtt/mixins.py | 40 +++++++++++++++---------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 1965cb77e53..7cfc00da578 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -9,6 +9,13 @@ import logging import voluptuous as vol from homeassistant.const import ( + ATTR_CONFIGURATION_URL, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + ATTR_SUGGESTED_AREA, + ATTR_SW_VERSION, + ATTR_VIA_DEVICE, CONF_DEVICE, CONF_ENTITY_CATEGORY, CONF_ICON, @@ -21,7 +28,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA, Entity +from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA, DeviceInfo, Entity from homeassistant.helpers.typing import ConfigType from . import DATA_MQTT, debug_info, publish, subscription @@ -513,36 +520,36 @@ class MqttDiscoveryUpdate(Entity): self._remove_signal = None -def device_info_from_config(config): +def device_info_from_config(config) -> DeviceInfo | None: """Return a device description for device registry.""" if not config: return None - info = { - "identifiers": {(DOMAIN, id_) for id_ in config[CONF_IDENTIFIERS]}, - "connections": {tuple(x) for x in config[CONF_CONNECTIONS]}, - } + info = DeviceInfo( + identifiers={(DOMAIN, id_) for id_ in config[CONF_IDENTIFIERS]}, + connections={(conn_[0], conn_[1]) for conn_ in config[CONF_CONNECTIONS]}, + ) if CONF_MANUFACTURER in config: - info["manufacturer"] = config[CONF_MANUFACTURER] + info[ATTR_MANUFACTURER] = config[CONF_MANUFACTURER] if CONF_MODEL in config: - info["model"] = config[CONF_MODEL] + info[ATTR_MODEL] = config[CONF_MODEL] if CONF_NAME in config: - info["name"] = config[CONF_NAME] + info[ATTR_NAME] = config[CONF_NAME] if CONF_SW_VERSION in config: - info["sw_version"] = config[CONF_SW_VERSION] + info[ATTR_SW_VERSION] = config[CONF_SW_VERSION] if CONF_VIA_DEVICE in config: - info["via_device"] = (DOMAIN, config[CONF_VIA_DEVICE]) + info[ATTR_VIA_DEVICE] = (DOMAIN, config[CONF_VIA_DEVICE]) if CONF_SUGGESTED_AREA in config: - info["suggested_area"] = config[CONF_SUGGESTED_AREA] + info[ATTR_SUGGESTED_AREA] = config[CONF_SUGGESTED_AREA] if CONF_CONFIGURATION_URL in config: - info["configuration_url"] = config[CONF_CONFIGURATION_URL] + info[ATTR_CONFIGURATION_URL] = config[CONF_CONFIGURATION_URL] return info @@ -563,11 +570,12 @@ class MqttEntityDeviceInfo(Entity): device_info = self.device_info if config_entry_id is not None and device_info is not None: - device_info["config_entry_id"] = config_entry_id - device_registry.async_get_or_create(**device_info) + device_registry.async_get_or_create( + config_entry_id=config_entry_id, **device_info + ) @property - def device_info(self): + def device_info(self) -> DeviceInfo | None: """Return a device description for device registry.""" return device_info_from_config(self._device_config) From 640a7fee9ddaba76917af0e2e14c3c6288437e6b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 25 Oct 2021 13:47:06 +0200 Subject: [PATCH 0818/1038] Allow extra keys in MQTT discovery messages (#58390) * Allow extra keys in MQTT discovery messages * Remove extra keys --- .../components/mqtt/alarm_control_panel.py | 6 +- .../components/mqtt/binary_sensor.py | 6 +- homeassistant/components/mqtt/camera.py | 6 +- homeassistant/components/mqtt/climate.py | 6 +- homeassistant/components/mqtt/cover.py | 101 ++++---- .../mqtt/device_tracker/schema_discovery.py | 8 +- .../components/mqtt/device_trigger.py | 3 +- homeassistant/components/mqtt/fan.py | 136 +++++----- homeassistant/components/mqtt/humidifier.py | 88 ++++--- .../components/mqtt/light/__init__.py | 36 ++- .../components/mqtt/light/schema_basic.py | 22 +- .../components/mqtt/light/schema_json.py | 15 +- .../components/mqtt/light/schema_template.py | 4 +- homeassistant/components/mqtt/lock.py | 6 +- homeassistant/components/mqtt/number.py | 39 +-- homeassistant/components/mqtt/scene.py | 6 +- homeassistant/components/mqtt/select.py | 27 +- homeassistant/components/mqtt/sensor.py | 36 +-- homeassistant/components/mqtt/switch.py | 6 +- homeassistant/components/mqtt/tag.py | 3 +- .../components/mqtt/vacuum/__init__.py | 24 +- .../components/mqtt/vacuum/schema_legacy.py | 4 +- .../components/mqtt/vacuum/schema_state.py | 5 +- .../mqtt/test_alarm_control_panel.py | 12 +- tests/components/mqtt/test_binary_sensor.py | 12 +- tests/components/mqtt/test_camera.py | 6 +- tests/components/mqtt/test_climate.py | 6 +- tests/components/mqtt/test_common.py | 12 +- tests/components/mqtt/test_cover.py | 6 +- tests/components/mqtt/test_device_trigger.py | 40 +-- tests/components/mqtt/test_fan.py | 8 +- tests/components/mqtt/test_humidifier.py | 14 +- tests/components/mqtt/test_legacy_vacuum.py | 6 +- tests/components/mqtt/test_light.py | 233 +++++++++--------- tests/components/mqtt/test_light_json.py | 26 +- tests/components/mqtt/test_light_template.py | 34 +-- tests/components/mqtt/test_lock.py | 26 +- tests/components/mqtt/test_number.py | 18 +- tests/components/mqtt/test_scene.py | 7 +- tests/components/mqtt/test_select.py | 16 +- tests/components/mqtt/test_sensor.py | 12 +- tests/components/mqtt/test_state_vacuum.py | 6 +- tests/components/mqtt/test_switch.py | 13 +- tests/components/mqtt/test_tag.py | 2 + 44 files changed, 628 insertions(+), 480 deletions(-) diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 825ad345272..72b01f3087b 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -97,6 +97,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) +DISCOVERY_SCHEMA = PLATFORM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) + async def async_setup_platform( hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None @@ -111,7 +113,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) - await async_setup_entry_helper(hass, alarm.DOMAIN, setup, PLATFORM_SCHEMA) + await async_setup_entry_helper(hass, alarm.DOMAIN, setup, DISCOVERY_SCHEMA) async def _async_setup_entity( @@ -135,7 +137,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): @staticmethod def config_schema(): """Return the config schema.""" - return PLATFORM_SCHEMA + return DISCOVERY_SCHEMA def _setup_from_config(self, config): value_template = self._config.get(CONF_VALUE_TEMPLATE) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 70638f5cc76..213aabdb006 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -58,6 +58,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) +DISCOVERY_SCHEMA = PLATFORM_SCHEMA.extend({}, extra=vol.REMOVE_EXTRA) + async def async_setup_platform( hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None @@ -72,7 +74,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) - await async_setup_entry_helper(hass, binary_sensor.DOMAIN, setup, PLATFORM_SCHEMA) + await async_setup_entry_helper(hass, binary_sensor.DOMAIN, setup, DISCOVERY_SCHEMA) async def _async_setup_entity( @@ -101,7 +103,7 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity): @staticmethod def config_schema(): """Return the config schema.""" - return PLATFORM_SCHEMA + return DISCOVERY_SCHEMA def _setup_from_config(self, config): value_template = self._config.get(CONF_VALUE_TEMPLATE) diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index cc331c0008c..39457dbd629 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -37,6 +37,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) +DISCOVERY_SCHEMA = PLATFORM_SCHEMA.extend({}, extra=vol.REMOVE_EXTRA) + async def async_setup_platform( hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None @@ -51,7 +53,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) - await async_setup_entry_helper(hass, camera.DOMAIN, setup, PLATFORM_SCHEMA) + await async_setup_entry_helper(hass, camera.DOMAIN, setup, DISCOVERY_SCHEMA) async def _async_setup_entity( @@ -76,7 +78,7 @@ class MqttCamera(MqttEntity, Camera): @staticmethod def config_schema(): """Return the config schema.""" - return PLATFORM_SCHEMA + return DISCOVERY_SCHEMA async def _subscribe_topics(self): """(Re)Subscribe to topics.""" diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index fb19c5e7038..16d4ae695c1 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -268,6 +268,8 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) +DISCOVERY_SCHEMA = PLATFORM_SCHEMA.extend({}, extra=vol.REMOVE_EXTRA) + async def async_setup_platform( hass: HomeAssistant, async_add_entities, config: ConfigType, discovery_info=None @@ -282,7 +284,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) - await async_setup_entry_helper(hass, climate.DOMAIN, setup, PLATFORM_SCHEMA) + await async_setup_entry_helper(hass, climate.DOMAIN, setup, DISCOVERY_SCHEMA) async def _async_setup_entity( @@ -319,7 +321,7 @@ class MqttClimate(MqttEntity, ClimateEntity): @staticmethod def config_schema(): """Return the config schema.""" - return PLATFORM_SCHEMA + return DISCOVERY_SCHEMA async def async_added_to_hass(self): """Handle being added to Home Assistant.""" diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index b48f39908fa..0af9d7d3739 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -142,53 +142,58 @@ def validate_options(value): return value +_PLATFORM_SCHEMA_BASE = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_GET_POSITION_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_PAYLOAD_CLOSE, default=DEFAULT_PAYLOAD_CLOSE): vol.Any( + cv.string, None + ), + vol.Optional(CONF_PAYLOAD_OPEN, default=DEFAULT_PAYLOAD_OPEN): vol.Any( + cv.string, None + ), + vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): vol.Any( + cv.string, None + ), + vol.Optional(CONF_POSITION_CLOSED, default=DEFAULT_POSITION_CLOSED): int, + vol.Optional(CONF_POSITION_OPEN, default=DEFAULT_POSITION_OPEN): int, + vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + vol.Optional(CONF_SET_POSITION_TEMPLATE): cv.template, + vol.Optional(CONF_SET_POSITION_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_STATE_CLOSED, default=STATE_CLOSED): cv.string, + vol.Optional(CONF_STATE_CLOSING, default=STATE_CLOSING): cv.string, + vol.Optional(CONF_STATE_OPEN, default=STATE_OPEN): cv.string, + vol.Optional(CONF_STATE_OPENING, default=STATE_OPENING): cv.string, + vol.Optional(CONF_STATE_STOPPED, default=DEFAULT_STATE_STOPPED): cv.string, + vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional( + CONF_TILT_CLOSED_POSITION, default=DEFAULT_TILT_CLOSED_POSITION + ): int, + vol.Optional(CONF_TILT_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_TILT_MAX, default=DEFAULT_TILT_MAX): int, + vol.Optional(CONF_TILT_MIN, default=DEFAULT_TILT_MIN): int, + vol.Optional(CONF_TILT_OPEN_POSITION, default=DEFAULT_TILT_OPEN_POSITION): int, + vol.Optional( + CONF_TILT_STATE_OPTIMISTIC, default=DEFAULT_TILT_OPTIMISTIC + ): cv.boolean, + vol.Optional(CONF_TILT_STATUS_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_TILT_STATUS_TEMPLATE): cv.template, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_GET_POSITION_TEMPLATE): cv.template, + vol.Optional(CONF_TILT_COMMAND_TEMPLATE): cv.template, + } +).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) + PLATFORM_SCHEMA = vol.All( - mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_GET_POSITION_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, - vol.Optional(CONF_PAYLOAD_CLOSE, default=DEFAULT_PAYLOAD_CLOSE): vol.Any( - cv.string, None - ), - vol.Optional(CONF_PAYLOAD_OPEN, default=DEFAULT_PAYLOAD_OPEN): vol.Any( - cv.string, None - ), - vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): vol.Any( - cv.string, None - ), - vol.Optional(CONF_POSITION_CLOSED, default=DEFAULT_POSITION_CLOSED): int, - vol.Optional(CONF_POSITION_OPEN, default=DEFAULT_POSITION_OPEN): int, - vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, - vol.Optional(CONF_SET_POSITION_TEMPLATE): cv.template, - vol.Optional(CONF_SET_POSITION_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_STATE_CLOSED, default=STATE_CLOSED): cv.string, - vol.Optional(CONF_STATE_CLOSING, default=STATE_CLOSING): cv.string, - vol.Optional(CONF_STATE_OPEN, default=STATE_OPEN): cv.string, - vol.Optional(CONF_STATE_OPENING, default=STATE_OPENING): cv.string, - vol.Optional(CONF_STATE_STOPPED, default=DEFAULT_STATE_STOPPED): cv.string, - vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional( - CONF_TILT_CLOSED_POSITION, default=DEFAULT_TILT_CLOSED_POSITION - ): int, - vol.Optional(CONF_TILT_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_TILT_MAX, default=DEFAULT_TILT_MAX): int, - vol.Optional(CONF_TILT_MIN, default=DEFAULT_TILT_MIN): int, - vol.Optional( - CONF_TILT_OPEN_POSITION, default=DEFAULT_TILT_OPEN_POSITION - ): int, - vol.Optional( - CONF_TILT_STATE_OPTIMISTIC, default=DEFAULT_TILT_OPTIMISTIC - ): cv.boolean, - vol.Optional(CONF_TILT_STATUS_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_TILT_STATUS_TEMPLATE): cv.template, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_GET_POSITION_TEMPLATE): cv.template, - vol.Optional(CONF_TILT_COMMAND_TEMPLATE): cv.template, - } - ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema), + _PLATFORM_SCHEMA_BASE, + validate_options, +) + +DISCOVERY_SCHEMA = vol.All( + _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), validate_options, ) @@ -206,7 +211,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) - await async_setup_entry_helper(hass, cover.DOMAIN, setup, PLATFORM_SCHEMA) + await async_setup_entry_helper(hass, cover.DOMAIN, setup, DISCOVERY_SCHEMA) async def _async_setup_entity( @@ -235,7 +240,7 @@ class MqttCover(MqttEntity, CoverEntity): @staticmethod def config_schema(): """Return the config schema.""" - return PLATFORM_SCHEMA + return DISCOVERY_SCHEMA def _setup_from_config(self, config): no_position = ( diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index d6688636bb2..f962d9208a4 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -37,15 +37,15 @@ PLATFORM_SCHEMA_DISCOVERY = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) +DISCOVERY_SCHEMA = PLATFORM_SCHEMA_DISCOVERY.extend({}, extra=vol.REMOVE_EXTRA) + async def async_setup_entry_from_discovery(hass, config_entry, async_add_entities): """Set up MQTT device tracker dynamically through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) - await async_setup_entry_helper( - hass, device_tracker.DOMAIN, setup, PLATFORM_SCHEMA_DISCOVERY - ) + await async_setup_entry_helper(hass, device_tracker.DOMAIN, setup, DISCOVERY_SCHEMA) async def _async_setup_entity( @@ -67,7 +67,7 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity): @staticmethod def config_schema(): """Return the config schema.""" - return PLATFORM_SCHEMA_DISCOVERY + return DISCOVERY_SCHEMA def _setup_from_config(self, config): """(Re)Setup the entity.""" diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 42bd4d19132..78f52e58726 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -47,7 +47,6 @@ from .mixins import ( MQTT_ENTITY_DEVICE_INFO_SCHEMA, cleanup_device_registry, device_info_from_config, - validate_device_has_at_least_one_identifier, ) _LOGGER = logging.getLogger(__name__) @@ -85,7 +84,7 @@ TRIGGER_DISCOVERY_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( vol.Required(CONF_TYPE): cv.string, vol.Optional(CONF_VALUE_TEMPLATE, default=None): vol.Any(None, cv.string), }, - validate_device_has_at_least_one_identifier, + extra=vol.REMOVE_EXTRA, ) DEVICE_TRIGGERS = "mqtt_device_triggers" diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 28a187941bc..f950a4d2c60 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -113,6 +113,64 @@ def valid_preset_mode_configuration(config): return config +_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_OSCILLATION_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_OSCILLATION_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_OSCILLATION_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_OSCILLATION_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_PERCENTAGE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_PERCENTAGE_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_PERCENTAGE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_PERCENTAGE_VALUE_TEMPLATE): cv.template, + # CONF_PRESET_MODE_COMMAND_TOPIC and CONF_PRESET_MODES_LIST must be used together + vol.Inclusive( + CONF_PRESET_MODE_COMMAND_TOPIC, "preset_modes" + ): mqtt.valid_publish_topic, + vol.Inclusive( + CONF_PRESET_MODES_LIST, "preset_modes", default=[] + ): cv.ensure_list, + vol.Optional(CONF_PRESET_MODE_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_PRESET_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_PRESET_MODE_VALUE_TEMPLATE): cv.template, + vol.Optional( + CONF_SPEED_RANGE_MIN, default=DEFAULT_SPEED_RANGE_MIN + ): cv.positive_int, + vol.Optional( + CONF_SPEED_RANGE_MAX, default=DEFAULT_SPEED_RANGE_MAX + ): cv.positive_int, + vol.Optional( + CONF_PAYLOAD_RESET_PERCENTAGE, default=DEFAULT_PAYLOAD_RESET + ): cv.string, + vol.Optional( + CONF_PAYLOAD_RESET_PRESET_MODE, default=DEFAULT_PAYLOAD_RESET + ): cv.string, + vol.Optional(CONF_PAYLOAD_HIGH_SPEED, default=SPEED_HIGH): cv.string, + vol.Optional(CONF_PAYLOAD_LOW_SPEED, default=SPEED_LOW): cv.string, + vol.Optional(CONF_PAYLOAD_MEDIUM_SPEED, default=SPEED_MEDIUM): cv.string, + vol.Optional(CONF_PAYLOAD_OFF_SPEED, default=SPEED_OFF): cv.string, + vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, + vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, + vol.Optional( + CONF_PAYLOAD_OSCILLATION_OFF, default=OSCILLATE_OFF_PAYLOAD + ): cv.string, + vol.Optional( + CONF_PAYLOAD_OSCILLATION_ON, default=OSCILLATE_ON_PAYLOAD + ): cv.string, + vol.Optional(CONF_SPEED_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional( + CONF_SPEED_LIST, + default=[SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH], + ): cv.ensure_list, + vol.Optional(CONF_SPEED_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_SPEED_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, + } +).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) + PLATFORM_SCHEMA = 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, @@ -124,63 +182,23 @@ PLATFORM_SCHEMA = vol.All( cv.deprecated(CONF_SPEED_LIST), cv.deprecated(CONF_SPEED_STATE_TOPIC), cv.deprecated(CONF_SPEED_VALUE_TEMPLATE), - mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, - vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_OSCILLATION_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_OSCILLATION_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_OSCILLATION_STATE_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_OSCILLATION_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_PERCENTAGE_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_PERCENTAGE_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_PERCENTAGE_STATE_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_PERCENTAGE_VALUE_TEMPLATE): cv.template, - # CONF_PRESET_MODE_COMMAND_TOPIC and CONF_PRESET_MODES_LIST must be used together - vol.Inclusive( - CONF_PRESET_MODE_COMMAND_TOPIC, "preset_modes" - ): mqtt.valid_publish_topic, - vol.Inclusive( - CONF_PRESET_MODES_LIST, "preset_modes", default=[] - ): cv.ensure_list, - vol.Optional(CONF_PRESET_MODE_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_PRESET_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_PRESET_MODE_VALUE_TEMPLATE): cv.template, - vol.Optional( - CONF_SPEED_RANGE_MIN, default=DEFAULT_SPEED_RANGE_MIN - ): cv.positive_int, - vol.Optional( - CONF_SPEED_RANGE_MAX, default=DEFAULT_SPEED_RANGE_MAX - ): cv.positive_int, - vol.Optional( - CONF_PAYLOAD_RESET_PERCENTAGE, default=DEFAULT_PAYLOAD_RESET - ): cv.string, - vol.Optional( - CONF_PAYLOAD_RESET_PRESET_MODE, default=DEFAULT_PAYLOAD_RESET - ): cv.string, - vol.Optional(CONF_PAYLOAD_HIGH_SPEED, default=SPEED_HIGH): cv.string, - vol.Optional(CONF_PAYLOAD_LOW_SPEED, default=SPEED_LOW): cv.string, - vol.Optional(CONF_PAYLOAD_MEDIUM_SPEED, default=SPEED_MEDIUM): cv.string, - vol.Optional(CONF_PAYLOAD_OFF_SPEED, default=SPEED_OFF): cv.string, - vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, - vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, - vol.Optional( - CONF_PAYLOAD_OSCILLATION_OFF, default=OSCILLATE_OFF_PAYLOAD - ): cv.string, - vol.Optional( - CONF_PAYLOAD_OSCILLATION_ON, default=OSCILLATE_ON_PAYLOAD - ): cv.string, - vol.Optional(CONF_SPEED_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional( - CONF_SPEED_LIST, - default=[SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH], - ): cv.ensure_list, - vol.Optional(CONF_SPEED_STATE_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_SPEED_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, - } - ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema), + _PLATFORM_SCHEMA_BASE, + valid_speed_range_configuration, + valid_preset_mode_configuration, +) + +DISCOVERY_SCHEMA = 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 deprecated, support will be removed with release 2021.9 + cv.deprecated(CONF_PAYLOAD_HIGH_SPEED), + cv.deprecated(CONF_PAYLOAD_LOW_SPEED), + cv.deprecated(CONF_PAYLOAD_MEDIUM_SPEED), + cv.deprecated(CONF_SPEED_COMMAND_TOPIC), + cv.deprecated(CONF_SPEED_LIST), + cv.deprecated(CONF_SPEED_STATE_TOPIC), + cv.deprecated(CONF_SPEED_VALUE_TEMPLATE), + _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), valid_speed_range_configuration, valid_preset_mode_configuration, ) @@ -199,7 +217,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) - await async_setup_entry_helper(hass, fan.DOMAIN, setup, PLATFORM_SCHEMA) + await async_setup_entry_helper(hass, fan.DOMAIN, setup, DISCOVERY_SCHEMA) async def _async_setup_entity( @@ -236,7 +254,7 @@ class MqttFan(MqttEntity, FanEntity): @staticmethod def config_schema(): """Return the config schema.""" - return PLATFORM_SCHEMA + return DISCOVERY_SCHEMA def _setup_from_config(self, config): """(Re)Setup the entity.""" diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index c1db0a6d4ee..e8bbbc7fd4b 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -86,46 +86,52 @@ def valid_humidity_range_configuration(config): return config +_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( + { + # CONF_AVAIALABLE_MODES_LIST and CONF_MODE_COMMAND_TOPIC must be used together + vol.Inclusive( + CONF_AVAILABLE_MODES_LIST, "available_modes", default=[] + ): cv.ensure_list, + vol.Inclusive( + CONF_MODE_COMMAND_TOPIC, "available_modes" + ): mqtt.valid_publish_topic, + vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_DEVICE_CLASS, default=DEVICE_CLASS_HUMIDIFIER): vol.In( + [DEVICE_CLASS_HUMIDIFIER, DEVICE_CLASS_DEHUMIDIFIER] + ), + vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_MODE_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, + vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, + vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, + vol.Required(CONF_TARGET_HUMIDITY_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_TARGET_HUMIDITY_COMMAND_TEMPLATE): cv.template, + vol.Optional( + CONF_TARGET_HUMIDITY_MAX, default=DEFAULT_MAX_HUMIDITY + ): cv.positive_int, + vol.Optional( + CONF_TARGET_HUMIDITY_MIN, default=DEFAULT_MIN_HUMIDITY + ): cv.positive_int, + vol.Optional(CONF_TARGET_HUMIDITY_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_TARGET_HUMIDITY_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional( + CONF_PAYLOAD_RESET_HUMIDITY, default=DEFAULT_PAYLOAD_RESET + ): cv.string, + vol.Optional(CONF_PAYLOAD_RESET_MODE, default=DEFAULT_PAYLOAD_RESET): cv.string, + } +).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) + PLATFORM_SCHEMA = vol.All( - mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( - { - # CONF_AVAIALABLE_MODES_LIST and CONF_MODE_COMMAND_TOPIC must be used together - vol.Inclusive( - CONF_AVAILABLE_MODES_LIST, "available_modes", default=[] - ): cv.ensure_list, - vol.Inclusive( - CONF_MODE_COMMAND_TOPIC, "available_modes" - ): mqtt.valid_publish_topic, - vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_DEVICE_CLASS, default=DEVICE_CLASS_HUMIDIFIER): vol.In( - [DEVICE_CLASS_HUMIDIFIER, DEVICE_CLASS_DEHUMIDIFIER] - ), - vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_MODE_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, - vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, - vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, - vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, - vol.Required(CONF_TARGET_HUMIDITY_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_TARGET_HUMIDITY_COMMAND_TEMPLATE): cv.template, - vol.Optional( - CONF_TARGET_HUMIDITY_MAX, default=DEFAULT_MAX_HUMIDITY - ): cv.positive_int, - vol.Optional( - CONF_TARGET_HUMIDITY_MIN, default=DEFAULT_MIN_HUMIDITY - ): cv.positive_int, - vol.Optional(CONF_TARGET_HUMIDITY_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_TARGET_HUMIDITY_STATE_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional( - CONF_PAYLOAD_RESET_HUMIDITY, default=DEFAULT_PAYLOAD_RESET - ): cv.string, - vol.Optional( - CONF_PAYLOAD_RESET_MODE, default=DEFAULT_PAYLOAD_RESET - ): cv.string, - } - ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema), + _PLATFORM_SCHEMA_BASE, + valid_humidity_range_configuration, + valid_mode_configuration, +) + +DISCOVERY_SCHEMA = vol.All( + _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), valid_humidity_range_configuration, valid_mode_configuration, ) @@ -144,7 +150,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) - await async_setup_entry_helper(hass, humidifier.DOMAIN, setup, PLATFORM_SCHEMA) + await async_setup_entry_helper(hass, humidifier.DOMAIN, setup, DISCOVERY_SCHEMA) async def _async_setup_entity( @@ -179,7 +185,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): @staticmethod def config_schema(): """Return the config schema.""" - return PLATFORM_SCHEMA + return DISCOVERY_SCHEMA def _setup_from_config(self, config): """(Re)Setup the entity.""" diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index 95a0cde52f4..28cf6ebb480 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -11,9 +11,31 @@ from homeassistant.helpers.typing import ConfigType from .. import DOMAIN, PLATFORMS from ..mixins import async_setup_entry_helper from .schema import CONF_SCHEMA, MQTT_LIGHT_SCHEMA_SCHEMA -from .schema_basic import PLATFORM_SCHEMA_BASIC, async_setup_entity_basic -from .schema_json import PLATFORM_SCHEMA_JSON, async_setup_entity_json -from .schema_template import PLATFORM_SCHEMA_TEMPLATE, async_setup_entity_template +from .schema_basic import ( + DISCOVERY_SCHEMA_BASIC, + PLATFORM_SCHEMA_BASIC, + async_setup_entity_basic, +) +from .schema_json import ( + DISCOVERY_SCHEMA_JSON, + PLATFORM_SCHEMA_JSON, + async_setup_entity_json, +) +from .schema_template import ( + DISCOVERY_SCHEMA_TEMPLATE, + PLATFORM_SCHEMA_TEMPLATE, + async_setup_entity_template, +) + + +def validate_mqtt_light_discovery(value): + """Validate MQTT light schema.""" + schemas = { + "basic": DISCOVERY_SCHEMA_BASIC, + "json": DISCOVERY_SCHEMA_JSON, + "template": DISCOVERY_SCHEMA_TEMPLATE, + } + return schemas[value[CONF_SCHEMA]](value) def validate_mqtt_light(value): @@ -26,6 +48,12 @@ def validate_mqtt_light(value): return schemas[value[CONF_SCHEMA]](value) +DISCOVERY_SCHEMA = vol.All( + MQTT_LIGHT_SCHEMA_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), + validate_mqtt_light_discovery, +) + + PLATFORM_SCHEMA = vol.All( MQTT_LIGHT_SCHEMA_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_mqtt_light ) @@ -44,7 +72,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) - await async_setup_entry_helper(hass, light.DOMAIN, setup, PLATFORM_SCHEMA) + await async_setup_entry_helper(hass, light.DOMAIN, setup, DISCOVERY_SCHEMA) async def _async_setup_entity( diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index cb350ae4c9c..5dd3f13af25 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -152,9 +152,7 @@ VALUE_TEMPLATE_KEYS = [ CONF_XY_VALUE_TEMPLATE, ] -PLATFORM_SCHEMA_BASIC = vol.All( - # CONF_VALUE_TEMPLATE is deprecated, support will be removed in 2021.10 - cv.deprecated(CONF_VALUE_TEMPLATE, CONF_STATE_VALUE_TEMPLATE), +_PLATFORM_SCHEMA_BASE = ( mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_BRIGHTNESS_COMMAND_TOPIC): mqtt.valid_publish_topic, @@ -212,10 +210,22 @@ PLATFORM_SCHEMA_BASIC = vol.All( vol.Optional(CONF_XY_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_XY_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_XY_VALUE_TEMPLATE): cv.template, - } + }, ) .extend(MQTT_ENTITY_COMMON_SCHEMA.schema) - .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema), + .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) +) + +PLATFORM_SCHEMA_BASIC = vol.All( + # CONF_VALUE_TEMPLATE is deprecated, support will be removed in 2021.10 + cv.deprecated(CONF_VALUE_TEMPLATE, CONF_STATE_VALUE_TEMPLATE), + _PLATFORM_SCHEMA_BASE, +) + +DISCOVERY_SCHEMA_BASIC = vol.All( + # CONF_VALUE_TEMPLATE is deprecated, support will be removed in 2021.10 + cv.deprecated(CONF_VALUE_TEMPLATE, CONF_STATE_VALUE_TEMPLATE), + _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), ) @@ -268,7 +278,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): @staticmethod def config_schema(): """Return the config schema.""" - return PLATFORM_SCHEMA_BASIC + return DISCOVERY_SCHEMA_BASIC def _setup_from_config(self, config): """(Re)Setup the entity.""" diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 8538b1169cc..9915c2455df 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -102,7 +102,7 @@ def valid_color_configuration(config): return config -PLATFORM_SCHEMA_JSON = vol.All( +_PLATFORM_SCHEMA_BASE = ( mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_BRIGHTNESS, default=DEFAULT_BRIGHTNESS): cv.boolean, @@ -143,7 +143,16 @@ PLATFORM_SCHEMA_JSON = vol.All( }, ) .extend(MQTT_ENTITY_COMMON_SCHEMA.schema) - .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema), + .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) +) + +PLATFORM_SCHEMA_JSON = vol.All( + _PLATFORM_SCHEMA_BASE, + valid_color_configuration, +) + +DISCOVERY_SCHEMA_JSON = vol.All( + _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), valid_color_configuration, ) @@ -184,7 +193,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): @staticmethod def config_schema(): """Return the config schema.""" - return PLATFORM_SCHEMA_JSON + return DISCOVERY_SCHEMA_JSON def _setup_from_config(self, config): """(Re)Setup the entity.""" diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 0c6522efc4f..6b7396846e1 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -84,6 +84,8 @@ PLATFORM_SCHEMA_TEMPLATE = ( .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) ) +DISCOVERY_SCHEMA_TEMPLATE = PLATFORM_SCHEMA_TEMPLATE.extend({}, extra=vol.REMOVE_EXTRA) + async def async_setup_entity_template( hass, config, async_add_entities, config_entry, discovery_data @@ -117,7 +119,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): @staticmethod def config_schema(): """Return the config schema.""" - return PLATFORM_SCHEMA_TEMPLATE + return DISCOVERY_SCHEMA_TEMPLATE def _setup_from_config(self, config): """(Re)Setup the entity.""" diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 158e3547737..fdcd4294b8c 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -49,6 +49,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) +DISCOVERY_SCHEMA = PLATFORM_SCHEMA.extend({}, extra=vol.REMOVE_EXTRA) + async def async_setup_platform( hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None @@ -63,7 +65,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) - await async_setup_entry_helper(hass, lock.DOMAIN, setup, PLATFORM_SCHEMA) + await async_setup_entry_helper(hass, lock.DOMAIN, setup, DISCOVERY_SCHEMA) async def _async_setup_entity( @@ -88,7 +90,7 @@ class MqttLock(MqttEntity, LockEntity): @staticmethod def config_schema(): """Return the config schema.""" - return PLATFORM_SCHEMA + return DISCOVERY_SCHEMA def _setup_from_config(self, config): """(Re)Setup the entity.""" diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index f2a472709b3..902d828d911 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -59,21 +59,28 @@ def validate_config(config): return config +_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( + { + 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_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, + vol.Optional(CONF_STEP, default=DEFAULT_STEP): vol.All( + vol.Coerce(float), vol.Range(min=1e-3) + ), + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + }, +).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) + PLATFORM_SCHEMA = vol.All( - mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( - { - 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_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, - vol.Optional(CONF_STEP, default=DEFAULT_STEP): vol.All( - vol.Coerce(float), vol.Range(min=1e-3) - ), - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - }, - ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema), + _PLATFORM_SCHEMA_BASE, + validate_config, +) + +DISCOVERY_SCHEMA = vol.All( + _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), validate_config, ) @@ -91,7 +98,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) - await async_setup_entry_helper(hass, number.DOMAIN, setup, PLATFORM_SCHEMA) + await async_setup_entry_helper(hass, number.DOMAIN, setup, DISCOVERY_SCHEMA) async def _async_setup_entity( @@ -120,7 +127,7 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): @staticmethod def config_schema(): """Return the config schema.""" - return PLATFORM_SCHEMA + return DISCOVERY_SCHEMA def _setup_from_config(self, config): """(Re)Setup the entity.""" diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index 5d9f7ba376e..6cf953ccf44 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -35,6 +35,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( } ).extend(MQTT_AVAILABILITY_SCHEMA.schema) +DISCOVERY_SCHEMA = PLATFORM_SCHEMA.extend({}, extra=vol.REMOVE_EXTRA) + async def async_setup_platform( hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None @@ -49,7 +51,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): setup = functools.partial( _async_setup_entity, async_add_entities, config_entry=config_entry ) - await async_setup_entry_helper(hass, scene.DOMAIN, setup, PLATFORM_SCHEMA) + await async_setup_entry_helper(hass, scene.DOMAIN, setup, DISCOVERY_SCHEMA) async def _async_setup_entity( @@ -85,7 +87,7 @@ class MqttScene( async def discovery_update(self, discovery_payload): """Handle updated discovery message.""" - config = PLATFORM_SCHEMA(discovery_payload) + config = DISCOVERY_SCHEMA(discovery_payload) self._setup_from_config(config) await self.availability_discovery_update(config) self.async_write_ha_state() diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 439aaccdc3b..b289bd53f0d 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -41,15 +41,22 @@ def validate_config(config): return config +_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Required(CONF_OPTIONS): cv.ensure_list, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + }, +).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) + PLATFORM_SCHEMA = vol.All( - mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, - vol.Required(CONF_OPTIONS): cv.ensure_list, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - }, - ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema), + _PLATFORM_SCHEMA_BASE, + validate_config, +) + +DISCOVERY_SCHEMA = vol.All( + _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), validate_config, ) @@ -67,7 +74,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) - await async_setup_entry_helper(hass, select.DOMAIN, setup, PLATFORM_SCHEMA) + await async_setup_entry_helper(hass, select.DOMAIN, setup, DISCOVERY_SCHEMA) async def _async_setup_entity( @@ -96,7 +103,7 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): @staticmethod def config_schema(): """Return the config schema.""" - return PLATFORM_SCHEMA + return DISCOVERY_SCHEMA def _setup_from_config(self, config): """(Re)Setup the entity.""" diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 0c0e1e700b7..3881dfee0e8 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -80,20 +80,28 @@ def validate_options(conf): return conf +_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, + vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, + vol.Optional(CONF_LAST_RESET_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_LAST_RESET_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + } +).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) + PLATFORM_SCHEMA = vol.All( cv.deprecated(CONF_LAST_RESET_TOPIC), - mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, - vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, - vol.Optional(CONF_LAST_RESET_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_LAST_RESET_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - } - ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema), + _PLATFORM_SCHEMA_BASE, + validate_options, +) + +DISCOVERY_SCHEMA = vol.All( + cv.deprecated(CONF_LAST_RESET_TOPIC), + _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), validate_options, ) @@ -111,7 +119,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) - await async_setup_entry_helper(hass, sensor.DOMAIN, setup, PLATFORM_SCHEMA) + await async_setup_entry_helper(hass, sensor.DOMAIN, setup, DISCOVERY_SCHEMA) async def _async_setup_entity( @@ -143,7 +151,7 @@ class MqttSensor(MqttEntity, SensorEntity): @staticmethod def config_schema(): """Return the config schema.""" - return PLATFORM_SCHEMA + return DISCOVERY_SCHEMA def _setup_from_config(self, config): """(Re)Setup the entity.""" diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 52addc19d12..3a593adf1e3 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -51,6 +51,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) +DISCOVERY_SCHEMA = PLATFORM_SCHEMA.extend({}, extra=vol.REMOVE_EXTRA) + async def async_setup_platform( hass: HomeAssistant, config: ConfigType, async_add_entities, discovery_info=None @@ -65,7 +67,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) - await async_setup_entry_helper(hass, switch.DOMAIN, setup, PLATFORM_SCHEMA) + await async_setup_entry_helper(hass, switch.DOMAIN, setup, DISCOVERY_SCHEMA) async def _async_setup_entity( @@ -93,7 +95,7 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): @staticmethod def config_schema(): """Return the config schema.""" - return PLATFORM_SCHEMA + return DISCOVERY_SCHEMA def _setup_from_config(self, config): """(Re)Setup the entity.""" diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index 908895f180c..a228d56d5c7 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -29,7 +29,6 @@ from .mixins import ( async_setup_entry_helper, cleanup_device_registry, device_info_from_config, - validate_device_has_at_least_one_identifier, ) from .util import valid_subscribe_topic @@ -45,7 +44,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( vol.Required(CONF_TOPIC): valid_subscribe_topic, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, }, - validate_device_has_at_least_one_identifier, + extra=vol.REMOVE_EXTRA, ) diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index 12d2ff5319c..6ff03a437e5 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -9,8 +9,22 @@ from homeassistant.helpers.reload import async_setup_reload_service from .. import DOMAIN as MQTT_DOMAIN, PLATFORMS from ..mixins import async_setup_entry_helper from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE -from .schema_legacy import PLATFORM_SCHEMA_LEGACY, async_setup_entity_legacy -from .schema_state import PLATFORM_SCHEMA_STATE, async_setup_entity_state +from .schema_legacy import ( + DISCOVERY_SCHEMA_LEGACY, + PLATFORM_SCHEMA_LEGACY, + async_setup_entity_legacy, +) +from .schema_state import ( + DISCOVERY_SCHEMA_STATE, + PLATFORM_SCHEMA_STATE, + async_setup_entity_state, +) + + +def validate_mqtt_vacuum_discovery(value): + """Validate MQTT vacuum schema.""" + schemas = {LEGACY: DISCOVERY_SCHEMA_LEGACY, STATE: DISCOVERY_SCHEMA_STATE} + return schemas[value[CONF_SCHEMA]](value) def validate_mqtt_vacuum(value): @@ -19,6 +33,10 @@ def validate_mqtt_vacuum(value): return schemas[value[CONF_SCHEMA]](value) +DISCOVERY_SCHEMA = vol.All( + MQTT_VACUUM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_mqtt_vacuum_discovery +) + PLATFORM_SCHEMA = vol.All( MQTT_VACUUM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_mqtt_vacuum ) @@ -35,7 +53,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) - await async_setup_entry_helper(hass, DOMAIN, setup, PLATFORM_SCHEMA) + await async_setup_entry_helper(hass, DOMAIN, setup, DISCOVERY_SCHEMA) async def _async_setup_entity( diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index 8543fa177c4..d827853d603 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -156,6 +156,8 @@ PLATFORM_SCHEMA_LEGACY = ( .extend(MQTT_VACUUM_SCHEMA.schema) ) +DISCOVERY_SCHEMA_LEGACY = PLATFORM_SCHEMA_LEGACY.extend({}, extra=vol.REMOVE_EXTRA) + async def async_setup_entity_legacy( hass, config, async_add_entities, config_entry, discovery_data @@ -185,7 +187,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): @staticmethod def config_schema(): """Return the config schema.""" - return PLATFORM_SCHEMA_LEGACY + return DISCOVERY_SCHEMA_LEGACY def _setup_from_config(self, config): supported_feature_strings = config[CONF_SUPPORTED_FEATURES] diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 3bc30d9d5cc..80f566ff5de 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -136,6 +136,9 @@ PLATFORM_SCHEMA_STATE = ( ) +DISCOVERY_SCHEMA_STATE = PLATFORM_SCHEMA_STATE.extend({}, extra=vol.REMOVE_EXTRA) + + async def async_setup_entity_state( hass, config, async_add_entities, config_entry, discovery_data ): @@ -159,7 +162,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): @staticmethod def config_schema(): """Return the config schema.""" - return PLATFORM_SCHEMA_STATE + return DISCOVERY_SCHEMA_STATE def _setup_from_config(self, config): supported_feature_strings = config[CONF_SUPPORTED_FEATURES] diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 7b4b8d22168..1351ae59496 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -625,15 +625,13 @@ async def test_discovery_update_alarm_topic_and_template(hass, mqtt_mock, caplog ([("alarm/state2", '{"state2":{"state":"triggered"}}')], "triggered", None), ] - data1 = json.dumps(config1) - data2 = json.dumps(config2) await help_test_discovery_update( hass, mqtt_mock, caplog, alarm_control_panel.DOMAIN, - data1, - data2, + config1, + config2, state_data1=state_data1, state_data2=state_data2, ) @@ -658,15 +656,13 @@ async def test_discovery_update_alarm_template(hass, mqtt_mock, caplog): ([("alarm/state1", '{"state2":{"state":"triggered"}}')], "triggered", None), ] - data1 = json.dumps(config1) - data2 = json.dumps(config2) await help_test_discovery_update( hass, mqtt_mock, caplog, alarm_control_panel.DOMAIN, - data1, - data2, + config1, + config2, state_data1=state_data1, state_data2=state_data2, ) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index bf88b7901e1..ba601fd094d 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -679,15 +679,13 @@ async def test_discovery_update_binary_sensor_topic_template(hass, mqtt_mock, ca ([("sensor/state2", '{"state2":{"state":"OFF"}}')], "off", None), ] - data1 = json.dumps(config1) - data2 = json.dumps(config2) await help_test_discovery_update( hass, mqtt_mock, caplog, binary_sensor.DOMAIN, - data1, - data2, + config1, + config2, state_data1=state_data1, state_data2=state_data2, ) @@ -714,15 +712,13 @@ async def test_discovery_update_binary_sensor_template(hass, mqtt_mock, caplog): ([("sensor/state1", '{"state2":{"state":"OFF"}}')], "off", None), ] - data1 = json.dumps(config1) - data2 = json.dumps(config2) await help_test_discovery_update( hass, mqtt_mock, caplog, binary_sensor.DOMAIN, - data1, - data2, + config1, + config2, state_data1=state_data1, state_data2=state_data2, ) diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 2036b486f94..5db4362b1fb 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -161,11 +161,11 @@ async def test_discovery_removal_camera(hass, mqtt_mock, caplog): async def test_discovery_update_camera(hass, mqtt_mock, caplog): """Test update of discovered camera.""" - data1 = '{ "name": "Beer", "topic": "test_topic"}' - data2 = '{ "name": "Milk", "topic": "test_topic"}' + config1 = {"name": "Beer", "topic": "test_topic"} + config2 = {"name": "Milk", "topic": "test_topic"} await help_test_discovery_update( - hass, mqtt_mock, caplog, camera.DOMAIN, data1, data2 + hass, mqtt_mock, caplog, camera.DOMAIN, config1, config2 ) diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 24c9e4a5b74..323d75ae091 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -991,10 +991,10 @@ async def test_discovery_removal_climate(hass, mqtt_mock, caplog): async def test_discovery_update_climate(hass, mqtt_mock, caplog): """Test update of discovered climate.""" - data1 = '{ "name": "Beer" }' - data2 = '{ "name": "Milk" }' + config1 = {"name": "Beer"} + config2 = {"name": "Milk"} await help_test_discovery_update( - hass, mqtt_mock, caplog, CLIMATE_DOMAIN, data1, data2 + hass, mqtt_mock, caplog, CLIMATE_DOMAIN, config1, config2 ) diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 72f5f236d28..16af5b8e484 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -662,8 +662,8 @@ async def help_test_discovery_update( mqtt_mock, caplog, domain, - discovery_data1, - discovery_data2, + discovery_config1, + discovery_config2, state_data1=None, state_data2=None, ): @@ -671,6 +671,14 @@ async def help_test_discovery_update( This is a test helper for the MqttDiscoveryUpdate mixin. """ + # Add some future configuration to the configurations + config1 = copy.deepcopy(discovery_config1) + config1["some_future_option_1"] = "future_option_1" + config2 = copy.deepcopy(discovery_config2) + config2["some_future_option_2"] = "future_option_2" + discovery_data1 = json.dumps(config1) + discovery_data2 = json.dumps(config2) + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", discovery_data1) await hass.async_block_till_done() diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index cd2966f6853..794d143ac83 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -2401,10 +2401,10 @@ async def test_discovery_removal_cover(hass, mqtt_mock, caplog): async def test_discovery_update_cover(hass, mqtt_mock, caplog): """Test update of discovered cover.""" - data1 = '{ "name": "Beer", "command_topic": "test_topic" }' - data2 = '{ "name": "Milk", "command_topic": "test_topic" }' + config1 = {"name": "Beer", "command_topic": "test_topic"} + config2 = {"name": "Milk", "command_topic": "test_topic"} await help_test_discovery_update( - hass, mqtt_mock, caplog, cover.DOMAIN, data1, data2 + hass, mqtt_mock, caplog, cover.DOMAIN, config1, config2 ) diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index b0db6169373..c3b16e3de43 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -130,7 +130,7 @@ async def test_discover_bad_triggers(hass, device_reg, entity_reg, mqtt_mock): '{ "automation_type":"trigger",' ' "device":{"identifiers":["0AFFD2"]},' ' "payloads": "short_press",' - ' "topic": "foobar/triggers/button1",' + ' "topics": "foobar/triggers/button1",' ' "type": "button_short_press",' ' "subtype": "button_1" }' ) @@ -167,22 +167,28 @@ async def test_discover_bad_triggers(hass, device_reg, entity_reg, mqtt_mock): async def test_update_remove_triggers(hass, device_reg, entity_reg, mqtt_mock): """Test triggers can be updated and removed.""" - data1 = ( - '{ "automation_type":"trigger",' - ' "device":{"identifiers":["0AFFD2"]},' - ' "payload": "short_press",' - ' "topic": "foobar/triggers/button1",' - ' "type": "button_short_press",' - ' "subtype": "button_1" }' - ) - data2 = ( - '{ "automation_type":"trigger",' - ' "device":{"identifiers":["0AFFD2"]},' - ' "payload": "short_press",' - ' "topic": "foobar/triggers/button1",' - ' "type": "button_short_press",' - ' "subtype": "button_2" }' - ) + config1 = { + "automation_type": "trigger", + "device": {"identifiers": ["0AFFD2"]}, + "payload": "short_press", + "topic": "foobar/triggers/button1", + "type": "button_short_press", + "subtype": "button_1", + } + config1["some_future_option_1"] = "future_option_1" + data1 = json.dumps(config1) + + config2 = { + "automation_type": "trigger", + "device": {"identifiers": ["0AFFD2"]}, + "payload": "short_press", + "topic": "foobar/triggers/button1", + "type": "button_short_press", + "subtype": "button_2", + } + config2["topic"] = "foobar/tag_scanned2" + data2 = json.dumps(config2) + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data1) await hass.async_block_till_done() diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 0501927d003..c310db335ad 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -1590,9 +1590,11 @@ async def test_discovery_removal_fan(hass, mqtt_mock, caplog): async def test_discovery_update_fan(hass, mqtt_mock, caplog): """Test update of discovered fan.""" - data1 = '{ "name": "Beer", "command_topic": "test_topic" }' - data2 = '{ "name": "Milk", "command_topic": "test_topic" }' - await help_test_discovery_update(hass, mqtt_mock, caplog, fan.DOMAIN, data1, data2) + config1 = {"name": "Beer", "command_topic": "test_topic"} + config2 = {"name": "Milk", "command_topic": "test_topic"} + await help_test_discovery_update( + hass, mqtt_mock, caplog, fan.DOMAIN, config1, config2 + ) async def test_discovery_update_unchanged_fan(hass, mqtt_mock, caplog): diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index 4ae834be5da..76c0b6e9f8e 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -975,10 +975,18 @@ async def test_discovery_removal_humidifier(hass, mqtt_mock, caplog): async def test_discovery_update_humidifier(hass, mqtt_mock, caplog): """Test update of discovered humidifier.""" - data1 = '{ "name": "Beer", "command_topic": "test_topic", "target_humidity_command_topic": "test-topic2" }' - data2 = '{ "name": "Milk", "command_topic": "test_topic", "target_humidity_command_topic": "test-topic2" }' + config1 = { + "name": "Beer", + "command_topic": "test_topic", + "target_humidity_command_topic": "test-topic2", + } + config2 = { + "name": "Milk", + "command_topic": "test_topic", + "target_humidity_command_topic": "test-topic2", + } await help_test_discovery_update( - hass, mqtt_mock, caplog, humidifier.DOMAIN, data1, data2 + hass, mqtt_mock, caplog, humidifier.DOMAIN, config1, config2 ) diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index 2dc8993a565..5d9b50252a4 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -651,10 +651,10 @@ async def test_discovery_removal_vacuum(hass, mqtt_mock, caplog): async def test_discovery_update_vacuum(hass, mqtt_mock, caplog): """Test update of discovered vacuum.""" - data1 = '{ "name": "Beer",' ' "command_topic": "test_topic" }' - data2 = '{ "name": "Milk",' ' "command_topic": "test_topic" }' + config1 = {"name": "Beer", " " "command_topic": "test_topic"} + config2 = {"name": "Milk", " " "command_topic": "test_topic"} await help_test_discovery_update( - hass, mqtt_mock, caplog, vacuum.DOMAIN, data1, data2 + hass, mqtt_mock, caplog, vacuum.DOMAIN, config1, config2 ) diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 109d6961a0d..bf327796f57 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -153,7 +153,6 @@ light: payload_off: "off" """ -import json from os import path from unittest.mock import call, patch @@ -2798,65 +2797,61 @@ async def test_discovery_deprecated(hass, mqtt_mock, caplog): async def test_discovery_update_light_topic_and_template(hass, mqtt_mock, caplog): """Test update of discovered light.""" - data1 = json.dumps( - { - "name": "Beer", - "state_topic": "test_light_rgb/state1", - "command_topic": "test_light_rgb/set", - "brightness_command_topic": "test_light_rgb/state1", - "rgb_command_topic": "test_light_rgb/rgb/set", - "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 }}", - "color_temp_value_template": "{{ value_json.state1.ct }}", - "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 }}", - } - ) + config1 = { + "name": "Beer", + "state_topic": "test_light_rgb/state1", + "command_topic": "test_light_rgb/set", + "brightness_command_topic": "test_light_rgb/state1", + "rgb_command_topic": "test_light_rgb/rgb/set", + "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 }}", + "color_temp_value_template": "{{ value_json.state1.ct }}", + "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 }}", + } - data2 = json.dumps( - { - "name": "Milk", - "state_topic": "test_light_rgb/state2", - "command_topic": "test_light_rgb/set", - "brightness_command_topic": "test_light_rgb/state2", - "rgb_command_topic": "test_light_rgb/rgb/set", - "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 }}", - "color_temp_value_template": "{{ value_json.state2.ct }}", - "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 }}", - } - ) + config2 = { + "name": "Milk", + "state_topic": "test_light_rgb/state2", + "command_topic": "test_light_rgb/set", + "brightness_command_topic": "test_light_rgb/state2", + "rgb_command_topic": "test_light_rgb/rgb/set", + "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 }}", + "color_temp_value_template": "{{ value_json.state2.ct }}", + "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 = [ ( [ @@ -3054,8 +3049,8 @@ async def test_discovery_update_light_topic_and_template(hass, mqtt_mock, caplog mqtt_mock, caplog, light.DOMAIN, - data1, - data2, + config1, + config2, state_data1=state_data1, state_data2=state_data2, ) @@ -3063,65 +3058,61 @@ async def test_discovery_update_light_topic_and_template(hass, mqtt_mock, caplog async def test_discovery_update_light_template(hass, mqtt_mock, caplog): """Test update of discovered light.""" - data1 = json.dumps( - { - "name": "Beer", - "state_topic": "test_light_rgb/state1", - "command_topic": "test_light_rgb/set", - "brightness_command_topic": "test_light_rgb/state1", - "rgb_command_topic": "test_light_rgb/rgb/set", - "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 }}", - "color_temp_value_template": "{{ value_json.state1.ct }}", - "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 }}", - } - ) + config1 = { + "name": "Beer", + "state_topic": "test_light_rgb/state1", + "command_topic": "test_light_rgb/set", + "brightness_command_topic": "test_light_rgb/state1", + "rgb_command_topic": "test_light_rgb/rgb/set", + "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 }}", + "color_temp_value_template": "{{ value_json.state1.ct }}", + "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 }}", + } - data2 = json.dumps( - { - "name": "Milk", - "state_topic": "test_light_rgb/state1", - "command_topic": "test_light_rgb/set", - "brightness_command_topic": "test_light_rgb/state1", - "rgb_command_topic": "test_light_rgb/rgb/set", - "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 }}", - "color_temp_value_template": "{{ value_json.state2.ct }}", - "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 }}", - } - ) + config2 = { + "name": "Milk", + "state_topic": "test_light_rgb/state1", + "command_topic": "test_light_rgb/set", + "brightness_command_topic": "test_light_rgb/state1", + "rgb_command_topic": "test_light_rgb/rgb/set", + "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 }}", + "color_temp_value_template": "{{ value_json.state2.ct }}", + "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 = [ ( [ @@ -3277,8 +3268,8 @@ async def test_discovery_update_light_template(hass, mqtt_mock, caplog): mqtt_mock, caplog, light.DOMAIN, - data1, - data2, + config1, + config2, state_data1=state_data1, state_data2=state_data2, ) diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index bf0dd1880a4..677509277c7 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -1786,20 +1786,20 @@ async def test_discovery_removal(hass, mqtt_mock, caplog): async def test_discovery_update_light(hass, mqtt_mock, caplog): """Test update of discovered light.""" - data1 = ( - '{ "name": "Beer",' - ' "schema": "json",' - ' "state_topic": "test_topic",' - ' "command_topic": "test_topic" }' - ) - data2 = ( - '{ "name": "Milk",' - ' "schema": "json",' - ' "state_topic": "test_topic",' - ' "command_topic": "test_topic" }' - ) + config1 = { + "name": "Beer", + "schema": "json", + "state_topic": "test_topic", + "command_topic": "test_topic", + } + config2 = { + "name": "Milk", + "schema": "json", + "state_topic": "test_topic", + "command_topic": "test_topic", + } await help_test_discovery_update( - hass, mqtt_mock, caplog, light.DOMAIN, data1, data2 + hass, mqtt_mock, caplog, light.DOMAIN, config1, config2 ) diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 2dabf0b7e46..fe2d9badf7d 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -950,24 +950,24 @@ async def test_discovery_removal(hass, mqtt_mock, caplog): async def test_discovery_update_light(hass, mqtt_mock, caplog): """Test update of discovered light.""" - data1 = ( - '{ "name": "Beer",' - ' "schema": "template",' - ' "state_topic": "test_topic",' - ' "command_topic": "test_topic",' - ' "command_on_template": "on",' - ' "command_off_template": "off"}' - ) - data2 = ( - '{ "name": "Milk",' - ' "schema": "template",' - ' "state_topic": "test_topic",' - ' "command_topic": "test_topic",' - ' "command_on_template": "on",' - ' "command_off_template": "off"}' - ) + config1 = { + "name": "Beer", + "schema": "template", + "state_topic": "test_topic", + "command_topic": "test_topic", + "command_on_template": "on", + "command_off_template": "off", + } + config2 = { + "name": "Milk", + "schema": "template", + "state_topic": "test_topic", + "command_topic": "test_topic", + "command_on_template": "on", + "command_off_template": "off", + } await help_test_discovery_update( - hass, mqtt_mock, caplog, light.DOMAIN, data1, data2 + hass, mqtt_mock, caplog, light.DOMAIN, config1, config2 ) diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index 39250b0a2fa..97f524d5d82 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -379,19 +379,21 @@ async def test_discovery_removal_lock(hass, mqtt_mock, caplog): async def test_discovery_update_lock(hass, mqtt_mock, caplog): """Test update of discovered lock.""" - data1 = ( - '{ "name": "Beer",' - ' "state_topic": "test_topic",' - ' "command_topic": "command_topic",' - ' "availability_topic": "availability_topic1" }' + config1 = { + "name": "Beer", + "state_topic": "test_topic", + "command_topic": "command_topic", + "availability_topic": "availability_topic1", + } + config2 = { + "name": "Milk", + "state_topic": "test_topic2", + "command_topic": "command_topic", + "availability_topic": "availability_topic2", + } + await help_test_discovery_update( + hass, mqtt_mock, caplog, LOCK_DOMAIN, config1, config2 ) - data2 = ( - '{ "name": "Milk",' - ' "state_topic": "test_topic2",' - ' "command_topic": "command_topic",' - ' "availability_topic": "availability_topic2" }' - ) - await help_test_discovery_update(hass, mqtt_mock, caplog, LOCK_DOMAIN, data1, data2) async def test_discovery_update_unchanged_lock(hass, mqtt_mock, caplog): diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index e1e961aa84a..3b8b370eff8 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -344,15 +344,19 @@ async def test_discovery_removal_number(hass, mqtt_mock, caplog): async def test_discovery_update_number(hass, mqtt_mock, caplog): """Test update of discovered number.""" - data1 = ( - '{ "name": "Beer", "state_topic": "test-topic", "command_topic": "test-topic"}' - ) - data2 = ( - '{ "name": "Milk", "state_topic": "test-topic", "command_topic": "test-topic"}' - ) + config1 = { + "name": "Beer", + "state_topic": "test-topic", + "command_topic": "test-topic", + } + config2 = { + "name": "Milk", + "state_topic": "test-topic", + "command_topic": "test-topic", + } await help_test_discovery_update( - hass, mqtt_mock, caplog, number.DOMAIN, data1, data2 + hass, mqtt_mock, caplog, number.DOMAIN, config1, config2 ) diff --git a/tests/components/mqtt/test_scene.py b/tests/components/mqtt/test_scene.py index 9a233e19fd8..3d4cd0f5c25 100644 --- a/tests/components/mqtt/test_scene.py +++ b/tests/components/mqtt/test_scene.py @@ -1,6 +1,5 @@ """The tests for the MQTT scene platform.""" import copy -import json from unittest.mock import patch import pytest @@ -147,15 +146,13 @@ async def test_discovery_update_payload(hass, mqtt_mock, caplog): config1["payload_on"] = "ON" config2["payload_on"] = "ACTIVATE" - data1 = json.dumps(config1) - data2 = json.dumps(config2) await help_test_discovery_update( hass, mqtt_mock, caplog, scene.DOMAIN, - data1, - data2, + config1, + config2, ) diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index f2e48e10dc5..4843631f98b 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -309,11 +309,21 @@ async def test_discovery_removal_select(hass, mqtt_mock, caplog): async def test_discovery_update_select(hass, mqtt_mock, caplog): """Test update of discovered select.""" - data1 = '{ "name": "Beer", "state_topic": "test-topic", "command_topic": "test-topic", "options": ["milk", "beer"]}' - data2 = '{ "name": "Milk", "state_topic": "test-topic", "command_topic": "test-topic", "options": ["milk", "beer"]}' + config1 = { + "name": "Beer", + "state_topic": "test-topic", + "command_topic": "test-topic", + "options": ["milk", "beer"], + } + config2 = { + "name": "Milk", + "state_topic": "test-topic", + "command_topic": "test-topic", + "options": ["milk", "beer"], + } await help_test_discovery_update( - hass, mqtt_mock, caplog, select.DOMAIN, data1, data2 + hass, mqtt_mock, caplog, select.DOMAIN, config1, config2 ) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index b7120b99f6e..752b1cfa48a 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -661,15 +661,13 @@ async def test_discovery_update_sensor_topic_template(hass, mqtt_mock, caplog): ([("sensor/state2", '{"state":100}')], "200", None), ] - data1 = json.dumps(config1) - data2 = json.dumps(config2) await help_test_discovery_update( hass, mqtt_mock, caplog, sensor.DOMAIN, - data1, - data2, + config1, + config2, state_data1=state_data1, state_data2=state_data2, ) @@ -694,15 +692,13 @@ async def test_discovery_update_sensor_template(hass, mqtt_mock, caplog): ([("sensor/state1", '{"state":100}')], "200", None), ] - data1 = json.dumps(config1) - data2 = json.dumps(config2) await help_test_discovery_update( hass, mqtt_mock, caplog, sensor.DOMAIN, - data1, - data2, + config1, + config2, state_data1=state_data1, state_data2=state_data2, ) diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index 46cc5552b6a..f53f8ebdab3 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -427,10 +427,10 @@ async def test_discovery_removal_vacuum(hass, mqtt_mock, caplog): async def test_discovery_update_vacuum(hass, mqtt_mock, caplog): """Test update of discovered vacuum.""" - data1 = '{ "schema": "state", "name": "Beer", "command_topic": "test_topic"}' - data2 = '{ "schema": "state", "name": "Milk", "command_topic": "test_topic"}' + config1 = {"schema": "state", "name": "Beer", "command_topic": "test_topic"} + config2 = {"schema": "state", "name": "Milk", "command_topic": "test_topic"} await help_test_discovery_update( - hass, mqtt_mock, caplog, vacuum.DOMAIN, data1, data2 + hass, mqtt_mock, caplog, vacuum.DOMAIN, config1, config2 ) diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index d5dcfbd4fa7..263ec0a2825 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -1,6 +1,5 @@ """The tests for the MQTT switch platform.""" import copy -import json from unittest.mock import patch import pytest @@ -339,15 +338,13 @@ async def test_discovery_update_switch_topic_template(hass, mqtt_mock, caplog): ([("switch/state2", '{"state2":{"state":"OFF"}}')], "off", None), ] - data1 = json.dumps(config1) - data2 = json.dumps(config2) await help_test_discovery_update( hass, mqtt_mock, caplog, switch.DOMAIN, - data1, - data2, + config1, + config2, state_data1=state_data1, state_data2=state_data2, ) @@ -374,15 +371,13 @@ async def test_discovery_update_switch_template(hass, mqtt_mock, caplog): ([("switch/state1", '{"state2":{"state":"OFF"}}')], "off", None), ] - data1 = json.dumps(config1) - data2 = json.dumps(config2) await help_test_discovery_update( hass, mqtt_mock, caplog, switch.DOMAIN, - data1, - data2, + config1, + config2, state_data1=state_data1, state_data2=state_data2, ) diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index 5e518c561b3..dfc0bddeec4 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -144,7 +144,9 @@ async def test_if_fires_on_mqtt_message_after_update_with_device( ): """Test tag scanning after update.""" config1 = copy.deepcopy(DEFAULT_CONFIG_DEVICE) + config1["some_future_option_1"] = "future_option_1" config2 = copy.deepcopy(DEFAULT_CONFIG_DEVICE) + config2["some_future_option_2"] = "future_option_2" config2["topic"] = "foobar/tag_scanned2" async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config1)) From 367c4d367613fb9ece1b4ef12ba1939ad4986887 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 25 Oct 2021 13:48:43 +0200 Subject: [PATCH 0819/1038] Add Smart Kettle (bh) device support to Tuya (#58347) --- homeassistant/components/tuya/const.py | 3 ++ homeassistant/components/tuya/number.py | 34 +++++++++++++++++++ homeassistant/components/tuya/sensor.py | 23 +++++++++++++ .../components/tuya/strings.sensor.json | 15 ++++++++ homeassistant/components/tuya/switch.py | 2 +- .../tuya/translations/sensor.en.json | 15 ++++++++ 6 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/tuya/strings.sensor.json create mode 100644 homeassistant/components/tuya/translations/sensor.en.json diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 2b598a83277..ab963ad6b38 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -84,6 +84,7 @@ DEVICE_CLASS_TUYA_LIGHT_MODE = "tuya__light_mode" DEVICE_CLASS_TUYA_MOTION_SENSITIVITY = "tuya__motion_sensitivity" DEVICE_CLASS_TUYA_RECORD_MODE = "tuya__record_mode" DEVICE_CLASS_TUYA_RELAY_STATUS = "tuya__relay_status" +DEVICE_CLASS_TUYA_STATUS = "tuya__status" TUYA_DISCOVERY_NEW = "tuya_discovery_new" TUYA_HA_SIGNAL_UPDATE_ENTITY = "tuya_entry_update" @@ -285,6 +286,8 @@ class DPCode(str, Enum): SWITCH_USB6 = "switch_usb6" # USB 6 SWITCH_VERTICAL = "switch_vertical" # Vertical swing flap switch SWITCH_VOICE = "switch_voice" # Voice switch + TEMP_BOILING_C = "temp_boiling_c" + TEMP_BOILING_F = "temp_boiling_f" TEMP_CONTROLLER = "temp_controller" TEMP_CURRENT = "temp_current" # Current temperature in °C TEMP_CURRENT_F = "temp_current_f" # Current temperature in °F diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index c69756d2998..c724f6e79a3 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -21,6 +21,40 @@ from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode # default instructions set of each category end up being a number. # https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { + # Smart Kettle + # https://developer.tuya.com/en/docs/iot/fbh?id=K9gf484m21yq7 + "bh": ( + NumberEntityDescription( + key=DPCode.TEMP_SET, + name="Temperature", + icon="mdi:thermometer", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + NumberEntityDescription( + key=DPCode.TEMP_SET_F, + name="Temperature", + icon="mdi:thermometer", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + NumberEntityDescription( + key=DPCode.TEMP_BOILING_C, + name="Temperature After Boiling", + icon="mdi:thermometer", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + NumberEntityDescription( + key=DPCode.TEMP_BOILING_F, + name="Temperature After Boiling", + icon="mdi:thermometer", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + NumberEntityDescription( + key=DPCode.WARM_TIME, + name="Heat Preservation Time", + icon="mdi:timer", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), # Human Presence Sensor # https://developer.tuya.com/en/docs/iot/categoryhps?id=Kaiuz42yhn1hs "hps": ( diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index a69ce86d0e3..4abd77fb7bd 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -38,6 +38,7 @@ from homeassistant.helpers.typing import StateType from . import HomeAssistantTuyaData from .base import EnumTypeData, IntegerTypeData, TuyaEntity from .const import ( + DEVICE_CLASS_TUYA_STATUS, DEVICE_CLASS_UNITS, DOMAIN, TUYA_DISCOVERY_NEW, @@ -82,6 +83,27 @@ BATTERY_SENSORS: tuple[SensorEntityDescription, ...] = ( # end up being a sensor. # https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq SENSORS: dict[str, tuple[SensorEntityDescription, ...]] = { + # Smart Kettle + # https://developer.tuya.com/en/docs/iot/fbh?id=K9gf484m21yq7 + "bh": ( + SensorEntityDescription( + key=DPCode.TEMP_CURRENT, + name="Current Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.TEMP_CURRENT_F, + name="Current Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key=DPCode.STATUS, + name="Status", + device_class=DEVICE_CLASS_TUYA_STATUS, + ), + ), # CO2 Detector # https://developer.tuya.com/en/docs/iot/categoryco2bj?id=Kaiuz3wes7yuy "co2bj": ( @@ -515,6 +537,7 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity): # match Home Assistants requirements. if ( self.device_class is not None + and not self.device_class.startswith(DOMAIN) and description.native_unit_of_measurement is None ): # We cannot have a device class, if the UOM isn't set or the diff --git a/homeassistant/components/tuya/strings.sensor.json b/homeassistant/components/tuya/strings.sensor.json new file mode 100644 index 00000000000..ff246817f61 --- /dev/null +++ b/homeassistant/components/tuya/strings.sensor.json @@ -0,0 +1,15 @@ +{ + "state": { + "tuya__status": { + "boiling_temp": "Boiling temperature", + "cooling": "Cooling", + "heating_temp": "Heating temperature", + "heating": "Heating", + "reserve_1": "Reserve 1", + "reserve_2": "Reserve 2", + "reserve_3": "Reserve 3", + "standby": "Standby", + "warm": "Heat preservation" + } + } +} diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 1a37af30e2b..bc83c085783 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -34,7 +34,7 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { ), SwitchEntityDescription( key=DPCode.WARM, - name="Heat preservation", + name="Heat Preservation", entity_category=ENTITY_CATEGORY_CONFIG, ), ), diff --git a/homeassistant/components/tuya/translations/sensor.en.json b/homeassistant/components/tuya/translations/sensor.en.json new file mode 100644 index 00000000000..4057f75c1ea --- /dev/null +++ b/homeassistant/components/tuya/translations/sensor.en.json @@ -0,0 +1,15 @@ +{ + "state": { + "tuya__status": { + "boiling_temp": "Boiling temperature", + "cooling": "Cooling", + "heating": "Heating", + "heating_temp": "Heating temperature", + "reserve_1": "Reserve 1", + "reserve_2": "Reserve 2", + "reserve_3": "Reserve 3", + "standby": "Standby", + "warm": "Heat preservation" + } + } +} \ No newline at end of file From 3c83f31dea914db05de6e6aa9ce73086341436d0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 25 Oct 2021 14:10:56 +0200 Subject: [PATCH 0820/1038] Drop unused ATTR_ENTRY_TYPE constant (#58400) Co-authored-by: epenet --- homeassistant/components/forecast_solar/const.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/forecast_solar/const.py b/homeassistant/components/forecast_solar/const.py index ea76ed7da2a..1ec8c3e4df1 100644 --- a/homeassistant/components/forecast_solar/const.py +++ b/homeassistant/components/forecast_solar/const.py @@ -21,7 +21,6 @@ CONF_DECLINATION = "declination" CONF_AZIMUTH = "azimuth" CONF_MODULES_POWER = "modules power" CONF_DAMPING = "damping" -ATTR_ENTRY_TYPE: Final = "entry_type" ENTRY_TYPE_SERVICE: Final = "service" SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( From d7c41c0b0571dbfaff039695e446082a6a7f8f40 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 25 Oct 2021 14:12:49 +0200 Subject: [PATCH 0821/1038] Use DeviceInfo in asuswrt (#58399) Co-authored-by: epenet --- homeassistant/components/asuswrt/device_tracker.py | 12 +++++++----- homeassistant/const.py | 1 + 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/asuswrt/device_tracker.py b/homeassistant/components/asuswrt/device_tracker.py index d5d3d9026b5..380f7a60c32 100644 --- a/homeassistant/components/asuswrt/device_tracker.py +++ b/homeassistant/components/asuswrt/device_tracker.py @@ -4,9 +4,11 @@ from __future__ import annotations from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_DEFAULT_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from .const import DATA_ASUSWRT, DOMAIN from .router import AsusWrtRouter @@ -60,12 +62,12 @@ class AsusWrtDevice(ScannerEntity): self._device = device self._attr_unique_id = device.mac self._attr_name = device.name or DEFAULT_DEVICE_NAME - self._attr_device_info = { - "connections": {(CONNECTION_NETWORK_MAC, device.mac)}, - "default_model": "ASUSWRT Tracked device", - } + self._attr_device_info = DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, device.mac)}, + default_model="ASUSWRT Tracked device", + ) if device.name: - self._attr_device_info["default_name"] = device.name + self._attr_device_info[ATTR_DEFAULT_NAME] = device.name @property def is_connected(self): diff --git a/homeassistant/const.py b/homeassistant/const.py index 50a26bd208e..85f7ad1bd6e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -354,6 +354,7 @@ ATTR_MODE: Final = "mode" ATTR_CONFIGURATION_URL: Final = "configuration_url" ATTR_CONNECTIONS: Final = "connections" +ATTR_DEFAULT_NAME: Final = "default_name" ATTR_MANUFACTURER: Final = "manufacturer" ATTR_MODEL: Final = "model" ATTR_SUGGESTED_AREA: Final = "suggested_area" From 71230f1f1ce0fa5e06ba259a2e657eb1db01ef44 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Mon, 25 Oct 2021 22:14:41 +1000 Subject: [PATCH 0822/1038] Advantage Air fix logic for motion sensors (#58376) * Check correct value for motion * Update fixture for motion * Small cleanup in fixture --- homeassistant/components/advantage_air/binary_sensor.py | 2 +- tests/fixtures/advantage_air/getSystemData.json | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/advantage_air/binary_sensor.py b/homeassistant/components/advantage_air/binary_sensor.py index 1403458bc12..a7d7308d78c 100644 --- a/homeassistant/components/advantage_air/binary_sensor.py +++ b/homeassistant/components/advantage_air/binary_sensor.py @@ -67,7 +67,7 @@ class AdvantageAirZoneMotion(AdvantageAirEntity, BinarySensorEntity): @property def is_on(self): """Return if motion is detect.""" - return self._zone["motion"] + return self._zone["motion"] == 20 class AdvantageAirZoneMyZone(AdvantageAirEntity, BinarySensorEntity): diff --git a/tests/fixtures/advantage_air/getSystemData.json b/tests/fixtures/advantage_air/getSystemData.json index 4ed610f9649..76894cbb022 100644 --- a/tests/fixtures/advantage_air/getSystemData.json +++ b/tests/fixtures/advantage_air/getSystemData.json @@ -20,7 +20,7 @@ "maxDamper": 100, "measuredTemp": 25, "minDamper": 0, - "motion": 1, + "motion": 20, "motionConfig": 2, "name": "Zone open with Sensor", "number": 1, @@ -35,7 +35,7 @@ "maxDamper": 100, "measuredTemp": 25, "minDamper": 0, - "motion": 0, + "motion": 21, "motionConfig": 2, "name": "Zone closed with Sensor", "number": 2, @@ -50,8 +50,8 @@ "maxDamper": 100, "measuredTemp": 25, "minDamper": 0, - "motion": 1, - "motionConfig": 1, + "motion": 22, + "motionConfig": 2, "name": "Zone 3", "number": 3, "rssi": 25, From 29c95a0b3472a21ee5feccd6a15b843633fad0eb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 25 Oct 2021 15:42:01 +0200 Subject: [PATCH 0823/1038] Use constants in renault tests (#58406) Co-authored-by: epenet --- tests/components/renault/__init__.py | 25 +- tests/components/renault/const.py | 368 ++++++++++++------------ tests/components/renault/test_sensor.py | 8 +- 3 files changed, 209 insertions(+), 192 deletions(-) diff --git a/tests/components/renault/__init__.py b/tests/components/renault/__init__.py index 7b3bb9e3d0a..f13adae1982 100644 --- a/tests/components/renault/__init__.py +++ b/tests/components/renault/__init__.py @@ -4,11 +4,13 @@ from __future__ import annotations from types import MappingProxyType from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_ICON, ATTR_IDENTIFIERS, ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME, + ATTR_STATE, ATTR_SW_VERSION, STATE_UNAVAILABLE, ) @@ -16,12 +18,17 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceRegistry from homeassistant.helpers.entity_registry import EntityRegistry -from .const import DYNAMIC_ATTRIBUTES, FIXED_ATTRIBUTES, ICON_FOR_EMPTY_VALUES +from .const import ( + ATTR_UNIQUE_ID, + DYNAMIC_ATTRIBUTES, + FIXED_ATTRIBUTES, + ICON_FOR_EMPTY_VALUES, +) def get_no_data_icon(expected_entity: MappingProxyType): """Check icon attribute for inactive sensors.""" - entity_id = expected_entity["entity_id"] + entity_id = expected_entity[ATTR_ENTITY_ID] return ICON_FOR_EMPTY_VALUES.get(entity_id, expected_entity.get(ATTR_ICON)) @@ -46,12 +53,12 @@ def check_entities( ) -> None: """Ensure that the expected_entities are correct.""" for expected_entity in expected_entities: - entity_id = expected_entity["entity_id"] + entity_id = expected_entity[ATTR_ENTITY_ID] registry_entry = entity_registry.entities.get(entity_id) assert registry_entry is not None - assert registry_entry.unique_id == expected_entity["unique_id"] + assert registry_entry.unique_id == expected_entity[ATTR_UNIQUE_ID] state = hass.states.get(entity_id) - assert state.state == expected_entity["result"] + assert state.state == expected_entity[ATTR_STATE] for attr in FIXED_ATTRIBUTES + DYNAMIC_ATTRIBUTES: assert state.attributes.get(attr) == expected_entity.get(attr) @@ -64,10 +71,10 @@ def check_entities_no_data( ) -> None: """Ensure that the expected_entities are correct.""" for expected_entity in expected_entities: - entity_id = expected_entity["entity_id"] + entity_id = expected_entity[ATTR_ENTITY_ID] registry_entry = entity_registry.entities.get(entity_id) assert registry_entry is not None - assert registry_entry.unique_id == expected_entity["unique_id"] + assert registry_entry.unique_id == expected_entity[ATTR_UNIQUE_ID] state = hass.states.get(entity_id) assert state.state == expected_state for attr in FIXED_ATTRIBUTES: @@ -83,10 +90,10 @@ def check_entities_unavailable( ) -> None: """Ensure that the expected_entities are correct.""" for expected_entity in expected_entities: - entity_id = expected_entity["entity_id"] + entity_id = expected_entity[ATTR_ENTITY_ID] registry_entry = entity_registry.entities.get(entity_id) assert registry_entry is not None - assert registry_entry.unique_id == expected_entity["unique_id"] + assert registry_entry.unique_id == expected_entity[ATTR_UNIQUE_ID] state = hass.states.get(entity_id) assert state.state == STATE_UNAVAILABLE for attr in FIXED_ATTRIBUTES: diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py index f1296ba2ce3..e3703173ad0 100644 --- a/tests/components/renault/const.py +++ b/tests/components/renault/const.py @@ -23,7 +23,14 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import ( ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, ATTR_ICON, + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + ATTR_STATE, + ATTR_SW_VERSION, ATTR_UNIT_OF_MEASUREMENT, CONF_PASSWORD, CONF_USERNAME, @@ -47,6 +54,9 @@ from homeassistant.const import ( VOLUME_LITERS, ) +ATTR_DEFAULT_DISABLED = "default_disabled" +ATTR_UNIQUE_ID = "unique_id" + FIXED_ATTRIBUTES = ( ATTR_DEVICE_CLASS, ATTR_OPTIONS, @@ -74,11 +84,11 @@ MOCK_CONFIG = { MOCK_VEHICLES = { "zoe_40": { "expected_device": { - "identifiers": {(DOMAIN, "VF1AAAAA555777999")}, - "manufacturer": "Renault", - "model": "Zoe", - "name": "REG-NUMBER", - "sw_version": "X101VE", + ATTR_IDENTIFIERS: {(DOMAIN, "VF1AAAAA555777999")}, + ATTR_MANUFACTURER: "Renault", + ATTR_MODEL: "Zoe", + ATTR_NAME: "REG-NUMBER", + ATTR_SW_VERSION: "X101VE", }, "endpoints_available": [ True, # cockpit @@ -95,124 +105,124 @@ MOCK_VEHICLES = { }, BINARY_SENSOR_DOMAIN: [ { - "entity_id": "binary_sensor.reg_number_plugged_in", - "unique_id": "vf1aaaaa555777999_plugged_in", - "result": STATE_ON, ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG, + ATTR_ENTITY_ID: "binary_sensor.reg_number_plugged_in", + ATTR_STATE: STATE_ON, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_plugged_in", }, { - "entity_id": "binary_sensor.reg_number_charging", - "unique_id": "vf1aaaaa555777999_charging", - "result": STATE_ON, ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY_CHARGING, + ATTR_ENTITY_ID: "binary_sensor.reg_number_charging", + ATTR_STATE: STATE_ON, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_charging", }, ], DEVICE_TRACKER_DOMAIN: [], SELECT_DOMAIN: [ { - "entity_id": "select.reg_number_charge_mode", - "unique_id": "vf1aaaaa555777999_charge_mode", - "result": "always", ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_MODE, + ATTR_ENTITY_ID: "select.reg_number_charge_mode", ATTR_ICON: "mdi:calendar-remove", ATTR_OPTIONS: ["always", "always_charging", "schedule_mode"], + ATTR_STATE: "always", + ATTR_UNIQUE_ID: "vf1aaaaa555777999_charge_mode", }, ], SENSOR_DOMAIN: [ { - "entity_id": "sensor.reg_number_battery_autonomy", - "unique_id": "vf1aaaaa555777999_battery_autonomy", - "result": "141", + ATTR_ENTITY_ID: "sensor.reg_number_battery_autonomy", ATTR_ICON: "mdi:ev-station", + ATTR_STATE: "141", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_autonomy", ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { - "entity_id": "sensor.reg_number_battery_available_energy", - "unique_id": "vf1aaaaa555777999_battery_available_energy", - "result": "31", ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_ENTITY_ID: "sensor.reg_number_battery_available_energy", + ATTR_STATE: "31", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_available_energy", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, }, { - "entity_id": "sensor.reg_number_battery_level", - "unique_id": "vf1aaaaa555777999_battery_level", - "result": "60", ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY, + ATTR_ENTITY_ID: "sensor.reg_number_battery_level", + ATTR_STATE: "60", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_level", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, { - "entity_id": "sensor.reg_number_battery_last_activity", - "unique_id": "vf1aaaaa555777999_battery_last_activity", - "result": "2020-01-12T21:40:16+00:00", - "default_disabled": True, + ATTR_DEFAULT_DISABLED: True, ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + ATTR_ENTITY_ID: "sensor.reg_number_battery_last_activity", + ATTR_STATE: "2020-01-12T21:40:16+00:00", + ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_last_activity", }, { - "entity_id": "sensor.reg_number_battery_temperature", - "unique_id": "vf1aaaaa555777999_battery_temperature", - "result": "20", ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ENTITY_ID: "sensor.reg_number_battery_temperature", + ATTR_STATE: "20", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_temperature", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, { - "entity_id": "sensor.reg_number_charge_state", - "unique_id": "vf1aaaaa555777999_charge_state", - "result": "charge_in_progress", ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_STATE, + ATTR_ENTITY_ID: "sensor.reg_number_charge_state", ATTR_ICON: "mdi:flash", + ATTR_STATE: "charge_in_progress", + ATTR_UNIQUE_ID: "vf1aaaaa555777999_charge_state", }, { - "entity_id": "sensor.reg_number_charging_power", - "unique_id": "vf1aaaaa555777999_charging_power", - "result": "0.027", ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_ENTITY_ID: "sensor.reg_number_charging_power", + ATTR_STATE: "0.027", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_charging_power", ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT, }, { - "entity_id": "sensor.reg_number_charging_remaining_time", - "unique_id": "vf1aaaaa555777999_charging_remaining_time", - "result": "145", + ATTR_ENTITY_ID: "sensor.reg_number_charging_remaining_time", ATTR_ICON: "mdi:timer", + ATTR_STATE: "145", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_charging_remaining_time", ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, }, { - "entity_id": "sensor.reg_number_mileage", - "unique_id": "vf1aaaaa555777999_mileage", - "result": "49114", + ATTR_ENTITY_ID: "sensor.reg_number_mileage", ATTR_ICON: "mdi:sign-direction", + ATTR_STATE: "49114", ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_mileage", ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { - "entity_id": "sensor.reg_number_outside_temperature", - "unique_id": "vf1aaaaa555777999_outside_temperature", - "result": "8.0", ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ENTITY_ID: "sensor.reg_number_outside_temperature", + ATTR_STATE: "8.0", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_outside_temperature", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, { - "entity_id": "sensor.reg_number_plug_state", - "unique_id": "vf1aaaaa555777999_plug_state", - "result": "plugged", ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG_STATE, + ATTR_ENTITY_ID: "sensor.reg_number_plug_state", ATTR_ICON: "mdi:power-plug", + ATTR_STATE: "plugged", + ATTR_UNIQUE_ID: "vf1aaaaa555777999_plug_state", }, ], }, "zoe_50": { "expected_device": { - "identifiers": {(DOMAIN, "VF1AAAAA555777999")}, - "manufacturer": "Renault", - "model": "Zoe", - "name": "REG-NUMBER", - "sw_version": "X102VE", + ATTR_IDENTIFIERS: {(DOMAIN, "VF1AAAAA555777999")}, + ATTR_MANUFACTURER: "Renault", + ATTR_MODEL: "Zoe", + ATTR_NAME: "REG-NUMBER", + ATTR_SW_VERSION: "X102VE", }, "endpoints_available": [ True, # cockpit @@ -229,130 +239,130 @@ MOCK_VEHICLES = { }, BINARY_SENSOR_DOMAIN: [ { - "entity_id": "binary_sensor.reg_number_plugged_in", - "unique_id": "vf1aaaaa555777999_plugged_in", - "result": STATE_OFF, ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG, + ATTR_ENTITY_ID: "binary_sensor.reg_number_plugged_in", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_plugged_in", }, { - "entity_id": "binary_sensor.reg_number_charging", - "unique_id": "vf1aaaaa555777999_charging", - "result": STATE_OFF, ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY_CHARGING, + ATTR_ENTITY_ID: "binary_sensor.reg_number_charging", + ATTR_STATE: STATE_OFF, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_charging", }, ], DEVICE_TRACKER_DOMAIN: [ { - "entity_id": "device_tracker.reg_number_location", - "unique_id": "vf1aaaaa555777999_location", - "result": STATE_NOT_HOME, + ATTR_ENTITY_ID: "device_tracker.reg_number_location", ATTR_ICON: "mdi:car", + ATTR_STATE: STATE_NOT_HOME, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_location", } ], SELECT_DOMAIN: [ { - "entity_id": "select.reg_number_charge_mode", - "unique_id": "vf1aaaaa555777999_charge_mode", - "result": "schedule_mode", ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_MODE, + ATTR_ENTITY_ID: "select.reg_number_charge_mode", ATTR_ICON: "mdi:calendar-clock", ATTR_OPTIONS: ["always", "always_charging", "schedule_mode"], + ATTR_STATE: "schedule_mode", + ATTR_UNIQUE_ID: "vf1aaaaa555777999_charge_mode", }, ], SENSOR_DOMAIN: [ { - "entity_id": "sensor.reg_number_battery_autonomy", - "unique_id": "vf1aaaaa555777999_battery_autonomy", - "result": "128", + ATTR_ENTITY_ID: "sensor.reg_number_battery_autonomy", ATTR_ICON: "mdi:ev-station", + ATTR_STATE: "128", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_autonomy", ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { - "entity_id": "sensor.reg_number_battery_available_energy", - "unique_id": "vf1aaaaa555777999_battery_available_energy", - "result": "0", ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_ENTITY_ID: "sensor.reg_number_battery_available_energy", + ATTR_STATE: "0", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_available_energy", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, }, { - "entity_id": "sensor.reg_number_battery_level", - "unique_id": "vf1aaaaa555777999_battery_level", - "result": "50", ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY, + ATTR_ENTITY_ID: "sensor.reg_number_battery_level", + ATTR_STATE: "50", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_level", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, { - "entity_id": "sensor.reg_number_battery_last_activity", - "unique_id": "vf1aaaaa555777999_battery_last_activity", - "result": "2020-11-17T08:06:48+00:00", - "default_disabled": True, + ATTR_DEFAULT_DISABLED: True, ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + ATTR_ENTITY_ID: "sensor.reg_number_battery_last_activity", + ATTR_STATE: "2020-11-17T08:06:48+00:00", + ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_last_activity", }, { - "entity_id": "sensor.reg_number_battery_temperature", - "unique_id": "vf1aaaaa555777999_battery_temperature", - "result": STATE_UNKNOWN, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ENTITY_ID: "sensor.reg_number_battery_temperature", + ATTR_STATE: STATE_UNKNOWN, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_temperature", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, { - "entity_id": "sensor.reg_number_charge_state", - "unique_id": "vf1aaaaa555777999_charge_state", - "result": "charge_error", ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_STATE, + ATTR_ENTITY_ID: "sensor.reg_number_charge_state", ATTR_ICON: "mdi:flash-off", + ATTR_STATE: "charge_error", + ATTR_UNIQUE_ID: "vf1aaaaa555777999_charge_state", }, { - "entity_id": "sensor.reg_number_charging_power", - "unique_id": "vf1aaaaa555777999_charging_power", - "result": STATE_UNKNOWN, ATTR_DEVICE_CLASS: DEVICE_CLASS_CURRENT, + ATTR_ENTITY_ID: "sensor.reg_number_charging_power", + ATTR_STATE: STATE_UNKNOWN, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_charging_power", ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, }, { - "entity_id": "sensor.reg_number_charging_remaining_time", - "unique_id": "vf1aaaaa555777999_charging_remaining_time", - "result": STATE_UNKNOWN, + ATTR_ENTITY_ID: "sensor.reg_number_charging_remaining_time", ATTR_ICON: "mdi:timer", + ATTR_STATE: STATE_UNKNOWN, ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_charging_remaining_time", ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, }, { - "entity_id": "sensor.reg_number_mileage", - "unique_id": "vf1aaaaa555777999_mileage", - "result": "49114", + ATTR_ENTITY_ID: "sensor.reg_number_mileage", ATTR_ICON: "mdi:sign-direction", + ATTR_STATE: "49114", ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_UNIQUE_ID: "vf1aaaaa555777999_mileage", ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { - "entity_id": "sensor.reg_number_plug_state", - "unique_id": "vf1aaaaa555777999_plug_state", - "result": "unplugged", ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG_STATE, + ATTR_ENTITY_ID: "sensor.reg_number_plug_state", ATTR_ICON: "mdi:power-plug-off", + ATTR_STATE: "unplugged", + ATTR_UNIQUE_ID: "vf1aaaaa555777999_plug_state", }, { - "entity_id": "sensor.reg_number_location_last_activity", - "unique_id": "vf1aaaaa555777999_location_last_activity", - "result": "2020-02-18T16:58:38+00:00", - "default_disabled": True, + ATTR_DEFAULT_DISABLED: True, ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + ATTR_ENTITY_ID: "sensor.reg_number_location_last_activity", + ATTR_STATE: "2020-02-18T16:58:38+00:00", + ATTR_UNIQUE_ID: "vf1aaaaa555777999_location_last_activity", }, ], }, "captur_phev": { "expected_device": { - "identifiers": {(DOMAIN, "VF1AAAAA555777123")}, - "manufacturer": "Renault", - "model": "Captur ii", - "name": "REG-NUMBER", - "sw_version": "XJB1SU", + ATTR_IDENTIFIERS: {(DOMAIN, "VF1AAAAA555777123")}, + ATTR_MANUFACTURER: "Renault", + ATTR_MODEL: "Captur ii", + ATTR_NAME: "REG-NUMBER", + ATTR_SW_VERSION: "XJB1SU", }, "endpoints_available": [ True, # cockpit @@ -369,146 +379,146 @@ MOCK_VEHICLES = { }, BINARY_SENSOR_DOMAIN: [ { - "entity_id": "binary_sensor.reg_number_plugged_in", - "unique_id": "vf1aaaaa555777123_plugged_in", - "result": STATE_ON, ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG, + ATTR_ENTITY_ID: "binary_sensor.reg_number_plugged_in", + ATTR_STATE: STATE_ON, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_plugged_in", }, { - "entity_id": "binary_sensor.reg_number_charging", - "unique_id": "vf1aaaaa555777123_charging", - "result": STATE_ON, ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY_CHARGING, + ATTR_ENTITY_ID: "binary_sensor.reg_number_charging", + ATTR_STATE: STATE_ON, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_charging", }, ], DEVICE_TRACKER_DOMAIN: [ { - "entity_id": "device_tracker.reg_number_location", - "unique_id": "vf1aaaaa555777123_location", - "result": STATE_NOT_HOME, + ATTR_ENTITY_ID: "device_tracker.reg_number_location", ATTR_ICON: "mdi:car", + ATTR_STATE: STATE_NOT_HOME, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_location", } ], SELECT_DOMAIN: [ { - "entity_id": "select.reg_number_charge_mode", - "unique_id": "vf1aaaaa555777123_charge_mode", - "result": "always", ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_MODE, + ATTR_ENTITY_ID: "select.reg_number_charge_mode", ATTR_ICON: "mdi:calendar-remove", ATTR_OPTIONS: ["always", "always_charging", "schedule_mode"], + ATTR_STATE: "always", + ATTR_UNIQUE_ID: "vf1aaaaa555777123_charge_mode", }, ], SENSOR_DOMAIN: [ { - "entity_id": "sensor.reg_number_battery_autonomy", - "unique_id": "vf1aaaaa555777123_battery_autonomy", - "result": "141", + ATTR_ENTITY_ID: "sensor.reg_number_battery_autonomy", ATTR_ICON: "mdi:ev-station", + ATTR_STATE: "141", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_battery_autonomy", ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { - "entity_id": "sensor.reg_number_battery_available_energy", - "unique_id": "vf1aaaaa555777123_battery_available_energy", - "result": "31", ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_ENTITY_ID: "sensor.reg_number_battery_available_energy", + ATTR_STATE: "31", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_battery_available_energy", ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, }, { - "entity_id": "sensor.reg_number_battery_level", - "unique_id": "vf1aaaaa555777123_battery_level", - "result": "60", ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY, + ATTR_ENTITY_ID: "sensor.reg_number_battery_level", + ATTR_STATE: "60", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_battery_level", ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, { - "entity_id": "sensor.reg_number_battery_last_activity", - "unique_id": "vf1aaaaa555777123_battery_last_activity", - "result": "2020-01-12T21:40:16+00:00", - "default_disabled": True, + ATTR_DEFAULT_DISABLED: True, ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + ATTR_ENTITY_ID: "sensor.reg_number_battery_last_activity", + ATTR_STATE: "2020-01-12T21:40:16+00:00", + ATTR_UNIQUE_ID: "vf1aaaaa555777123_battery_last_activity", }, { - "entity_id": "sensor.reg_number_battery_temperature", - "unique_id": "vf1aaaaa555777123_battery_temperature", - "result": "20", ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ENTITY_ID: "sensor.reg_number_battery_temperature", + ATTR_STATE: "20", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_battery_temperature", ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, { - "entity_id": "sensor.reg_number_charge_state", - "unique_id": "vf1aaaaa555777123_charge_state", - "result": "charge_in_progress", ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_STATE, + ATTR_ENTITY_ID: "sensor.reg_number_charge_state", ATTR_ICON: "mdi:flash", + ATTR_STATE: "charge_in_progress", + ATTR_UNIQUE_ID: "vf1aaaaa555777123_charge_state", }, { - "entity_id": "sensor.reg_number_charging_power", - "unique_id": "vf1aaaaa555777123_charging_power", - "result": "27.0", ATTR_DEVICE_CLASS: DEVICE_CLASS_CURRENT, + ATTR_ENTITY_ID: "sensor.reg_number_charging_power", + ATTR_STATE: "27.0", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_charging_power", ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, }, { - "entity_id": "sensor.reg_number_charging_remaining_time", - "unique_id": "vf1aaaaa555777123_charging_remaining_time", - "result": "145", + ATTR_ENTITY_ID: "sensor.reg_number_charging_remaining_time", ATTR_ICON: "mdi:timer", + ATTR_STATE: "145", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_charging_remaining_time", ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, }, { - "entity_id": "sensor.reg_number_fuel_autonomy", - "unique_id": "vf1aaaaa555777123_fuel_autonomy", - "result": "35", + ATTR_ENTITY_ID: "sensor.reg_number_fuel_autonomy", ATTR_ICON: "mdi:gas-station", + ATTR_STATE: "35", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_fuel_autonomy", ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { - "entity_id": "sensor.reg_number_fuel_quantity", - "unique_id": "vf1aaaaa555777123_fuel_quantity", - "result": "3", + ATTR_ENTITY_ID: "sensor.reg_number_fuel_quantity", ATTR_ICON: "mdi:fuel", + ATTR_STATE: "3", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_fuel_quantity", ATTR_UNIT_OF_MEASUREMENT: VOLUME_LITERS, }, { - "entity_id": "sensor.reg_number_mileage", - "unique_id": "vf1aaaaa555777123_mileage", - "result": "5567", + ATTR_ENTITY_ID: "sensor.reg_number_mileage", ATTR_ICON: "mdi:sign-direction", + ATTR_STATE: "5567", ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_mileage", ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { - "entity_id": "sensor.reg_number_plug_state", - "unique_id": "vf1aaaaa555777123_plug_state", - "result": "plugged", ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG_STATE, + ATTR_ENTITY_ID: "sensor.reg_number_plug_state", ATTR_ICON: "mdi:power-plug", + ATTR_STATE: "plugged", + ATTR_UNIQUE_ID: "vf1aaaaa555777123_plug_state", }, { - "entity_id": "sensor.reg_number_location_last_activity", - "unique_id": "vf1aaaaa555777123_location_last_activity", - "result": "2020-02-18T16:58:38+00:00", - "default_disabled": True, + ATTR_DEFAULT_DISABLED: True, ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + ATTR_ENTITY_ID: "sensor.reg_number_location_last_activity", + ATTR_STATE: "2020-02-18T16:58:38+00:00", + ATTR_UNIQUE_ID: "vf1aaaaa555777123_location_last_activity", }, ], }, "captur_fuel": { "expected_device": { - "identifiers": {(DOMAIN, "VF1AAAAA555777123")}, - "manufacturer": "Renault", - "model": "Captur ii", - "name": "REG-NUMBER", - "sw_version": "XJB1SU", + ATTR_IDENTIFIERS: {(DOMAIN, "VF1AAAAA555777123")}, + ATTR_MANUFACTURER: "Renault", + ATTR_MODEL: "Captur ii", + ATTR_NAME: "REG-NUMBER", + ATTR_SW_VERSION: "XJB1SU", }, "endpoints_available": [ True, # cockpit @@ -524,44 +534,44 @@ MOCK_VEHICLES = { BINARY_SENSOR_DOMAIN: [], DEVICE_TRACKER_DOMAIN: [ { - "entity_id": "device_tracker.reg_number_location", - "unique_id": "vf1aaaaa555777123_location", - "result": STATE_NOT_HOME, + ATTR_ENTITY_ID: "device_tracker.reg_number_location", ATTR_ICON: "mdi:car", + ATTR_STATE: STATE_NOT_HOME, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_location", } ], SELECT_DOMAIN: [], SENSOR_DOMAIN: [ { - "entity_id": "sensor.reg_number_fuel_autonomy", - "unique_id": "vf1aaaaa555777123_fuel_autonomy", - "result": "35", + ATTR_ENTITY_ID: "sensor.reg_number_fuel_autonomy", ATTR_ICON: "mdi:gas-station", + ATTR_STATE: "35", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_fuel_autonomy", ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { - "entity_id": "sensor.reg_number_fuel_quantity", - "unique_id": "vf1aaaaa555777123_fuel_quantity", - "result": "3", + ATTR_ENTITY_ID: "sensor.reg_number_fuel_quantity", ATTR_ICON: "mdi:fuel", + ATTR_STATE: "3", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_fuel_quantity", ATTR_UNIT_OF_MEASUREMENT: VOLUME_LITERS, }, { - "entity_id": "sensor.reg_number_mileage", - "unique_id": "vf1aaaaa555777123_mileage", - "result": "5567", + ATTR_ENTITY_ID: "sensor.reg_number_mileage", ATTR_ICON: "mdi:sign-direction", + ATTR_STATE: "5567", ATTR_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING, + ATTR_UNIQUE_ID: "vf1aaaaa555777123_mileage", ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, { - "entity_id": "sensor.reg_number_location_last_activity", - "unique_id": "vf1aaaaa555777123_location_last_activity", - "result": "2020-02-18T16:58:38+00:00", - "default_disabled": True, + ATTR_DEFAULT_DISABLED: True, ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + ATTR_ENTITY_ID: "sensor.reg_number_location_last_activity", + ATTR_STATE: "2020-02-18T16:58:38+00:00", + ATTR_UNIQUE_ID: "vf1aaaaa555777123_location_last_activity", }, ], }, diff --git a/tests/components/renault/test_sensor.py b/tests/components/renault/test_sensor.py index 9e83a131aa8..c9a70c8e026 100644 --- a/tests/components/renault/test_sensor.py +++ b/tests/components/renault/test_sensor.py @@ -6,7 +6,7 @@ import pytest from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_UNKNOWN +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import EntityRegistry @@ -16,7 +16,7 @@ from . import ( check_entities_no_data, check_entities_unavailable, ) -from .const import MOCK_VEHICLES +from .const import ATTR_DEFAULT_DISABLED, MOCK_VEHICLES from tests.common import mock_device_registry, mock_registry @@ -35,8 +35,8 @@ def _check_and_enable_disabled_entities( ) -> None: """Ensure that the expected_entities are correctly disabled.""" for expected_entity in expected_entities: - if expected_entity.get("default_disabled"): - entity_id = expected_entity["entity_id"] + if expected_entity.get(ATTR_DEFAULT_DISABLED): + entity_id = expected_entity[ATTR_ENTITY_ID] registry_entry = entity_registry.entities.get(entity_id) assert registry_entry.disabled assert registry_entry.disabled_by == "integration" From 42793927f7931155b23cf9810c7c1677bf3d98f5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 25 Oct 2021 15:42:40 +0200 Subject: [PATCH 0824/1038] Use ATTR_VIA_DEVICE constant in onewire tests (#58405) Co-authored-by: epenet --- tests/components/onewire/__init__.py | 3 ++- tests/components/onewire/const.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/components/onewire/__init__.py b/tests/components/onewire/__init__.py index 25ff4a15cfd..36035c1c85b 100644 --- a/tests/components/onewire/__init__.py +++ b/tests/components/onewire/__init__.py @@ -15,6 +15,7 @@ from homeassistant.const import ( ATTR_MODEL, ATTR_NAME, ATTR_STATE, + ATTR_VIA_DEVICE, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceRegistry @@ -57,7 +58,7 @@ def check_device_registry( assert registry_entry.manufacturer == expected_device[ATTR_MANUFACTURER] assert registry_entry.name == expected_device[ATTR_NAME] assert registry_entry.model == expected_device[ATTR_MODEL] - if expected_via_device := expected_device.get("via_device"): + if expected_via_device := expected_device.get(ATTR_VIA_DEVICE): assert registry_entry.via_device_id is not None parent_entry = device_registry.async_get_device({expected_via_device}) assert parent_entry is not None diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index 91b7b618c37..2642e4e7454 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -20,6 +20,7 @@ from homeassistant.const import ( ATTR_NAME, ATTR_STATE, ATTR_UNIT_OF_MEASUREMENT, + ATTR_VIA_DEVICE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, @@ -224,7 +225,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_MANUFACTURER: MANUFACTURER, ATTR_MODEL: "DS2423", ATTR_NAME: "1D.111111111111", - "via_device": (DOMAIN, "1F.111111111111"), + ATTR_VIA_DEVICE: (DOMAIN, "1F.111111111111"), }, ], "branches": { From ffd7c998d0edaca521c5a92d3505fbdd8b0a94b2 Mon Sep 17 00:00:00 2001 From: David Le Brun Date: Mon, 25 Oct 2021 15:55:49 +0200 Subject: [PATCH 0825/1038] Meteofrance - Add state_class to appropriate sensors (#58401) Co-authored-by: Erik Montnemery --- homeassistant/components/meteo_france/const.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py index 09b48bc1b3e..b84f3ae14fa 100644 --- a/homeassistant/components/meteo_france/const.py +++ b/homeassistant/components/meteo_france/const.py @@ -3,7 +3,10 @@ from __future__ import annotations from dataclasses import dataclass -from homeassistant.components.sensor import SensorEntityDescription +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntityDescription, +) from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, @@ -72,6 +75,7 @@ SENSOR_TYPES: tuple[MeteoFranceSensorEntityDescription, ...] = ( name="Pressure", native_unit_of_measurement=PRESSURE_HPA, device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, entity_registry_enabled_default=False, data_path="current_forecast:sea_level", ), @@ -79,6 +83,7 @@ SENSOR_TYPES: tuple[MeteoFranceSensorEntityDescription, ...] = ( key="wind_gust", name="Wind gust", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + state_class=STATE_CLASS_MEASUREMENT, icon="mdi:weather-windy-variant", entity_registry_enabled_default=False, data_path="current_forecast:wind:gust", @@ -87,6 +92,7 @@ SENSOR_TYPES: tuple[MeteoFranceSensorEntityDescription, ...] = ( key="wind_speed", name="Wind speed", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + state_class=STATE_CLASS_MEASUREMENT, icon="mdi:weather-windy", entity_registry_enabled_default=False, data_path="current_forecast:wind:speed", @@ -96,6 +102,7 @@ SENSOR_TYPES: tuple[MeteoFranceSensorEntityDescription, ...] = ( name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, entity_registry_enabled_default=False, data_path="current_forecast:T:value", ), From 0c684cee51ceb55827be554011f22da9083b1471 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 25 Oct 2021 10:29:47 -0400 Subject: [PATCH 0826/1038] Bump up ZHA dependencies (#58409) --- 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 4e169c20a48..acb5651de1f 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,7 +9,7 @@ "pyserial-asyncio==0.5", "zha-quirks==0.0.62", "zigpy-deconz==0.13.0", - "zigpy==0.38.0", + "zigpy==0.39.0", "zigpy-xbee==0.14.0", "zigpy-zigate==0.7.3", "zigpy-znp==0.5.4" diff --git a/requirements_all.txt b/requirements_all.txt index fe91683cc6c..650e75bdb5a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2489,7 +2489,7 @@ zigpy-zigate==0.7.3 zigpy-znp==0.5.4 # homeassistant.components.zha -zigpy==0.38.0 +zigpy==0.39.0 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 66334d02fad..a0b1e78711f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1445,7 +1445,7 @@ zigpy-zigate==0.7.3 zigpy-znp==0.5.4 # homeassistant.components.zha -zigpy==0.38.0 +zigpy==0.39.0 # homeassistant.components.zwave_js zwave-js-server-python==0.31.3 From fe15736418d738f13b7b6d768cab5ca0205bf97b Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 25 Oct 2021 10:30:00 -0400 Subject: [PATCH 0827/1038] Log correct ZHA channel initialization step (#58410) --- homeassistant/components/zha/core/channels/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zha/core/channels/base.py b/homeassistant/components/zha/core/channels/base.py index d297b5187c0..b6981b4cd74 100644 --- a/homeassistant/components/zha/core/channels/base.py +++ b/homeassistant/components/zha/core/channels/base.py @@ -332,7 +332,7 @@ class ZigbeeChannel(LogMixin): if ch_specific_init: await ch_specific_init(from_cache=from_cache) - self.debug("finished channel configuration") + self.debug("finished channel initialization") self._status = ChannelStatus.INITIALIZED @callback From 585dcf84f1babc7e46bdf9e1284dbd2eb41c72ae Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 25 Oct 2021 16:32:29 +0200 Subject: [PATCH 0828/1038] Add state_class/entity_category to Verisure (#58403) --- homeassistant/components/verisure/binary_sensor.py | 2 ++ homeassistant/components/verisure/sensor.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/homeassistant/components/verisure/binary_sensor.py b/homeassistant/components/verisure/binary_sensor.py index d2bdd05a9ac..a14efc7d4b1 100644 --- a/homeassistant/components/verisure/binary_sensor.py +++ b/homeassistant/components/verisure/binary_sensor.py @@ -7,6 +7,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -87,6 +88,7 @@ class VerisureEthernetStatus(CoordinatorEntity, BinarySensorEntity): _attr_name = "Verisure Ethernet status" _attr_device_class = DEVICE_CLASS_CONNECTIVITY + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC @property def unique_id(self) -> str: diff --git a/homeassistant/components/verisure/sensor.py b/homeassistant/components/verisure/sensor.py index 2075dbe4a97..2016c4dbb83 100644 --- a/homeassistant/components/verisure/sensor.py +++ b/homeassistant/components/verisure/sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations from homeassistant.components.sensor import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, SensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -52,6 +53,7 @@ class VerisureThermometer(CoordinatorEntity, SensorEntity): _attr_device_class = DEVICE_CLASS_TEMPERATURE _attr_native_unit_of_measurement = TEMP_CELSIUS + _attr_state_class = STATE_CLASS_MEASUREMENT def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str @@ -106,6 +108,7 @@ class VerisureHygrometer(CoordinatorEntity, SensorEntity): _attr_device_class = DEVICE_CLASS_HUMIDITY _attr_native_unit_of_measurement = PERCENTAGE + _attr_state_class = STATE_CLASS_MEASUREMENT def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str From 5642350070a680bff22a189c50bb3281c2184fb9 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 25 Oct 2021 18:14:44 +0200 Subject: [PATCH 0829/1038] Add zwave_js sensor entity categories (#58416) --- homeassistant/components/zwave_js/sensor.py | 4 ++++ tests/components/zwave_js/common.py | 1 + tests/components/zwave_js/test_sensor.py | 15 +++++++++++++++ 3 files changed, 20 insertions(+) diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 7da7ba9ab9b..815158ee967 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -40,6 +40,7 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_VOLTAGE, + ENTITY_CATEGORY_DIAGNOSTIC, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) @@ -100,6 +101,7 @@ ENTITY_DESCRIPTION_KEY_MAP: dict[str, ZwaveSensorEntityDescription] = { ENTITY_DESC_KEY_BATTERY: ZwaveSensorEntityDescription( ENTITY_DESC_KEY_BATTERY, device_class=DEVICE_CLASS_BATTERY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, state_class=STATE_CLASS_MEASUREMENT, ), ENTITY_DESC_KEY_CURRENT: ZwaveSensorEntityDescription( @@ -160,6 +162,8 @@ ENTITY_DESCRIPTION_KEY_MAP: dict[str, ZwaveSensorEntityDescription] = { ENTITY_DESC_KEY_SIGNAL_STRENGTH: ZwaveSensorEntityDescription( ENTITY_DESC_KEY_SIGNAL_STRENGTH, device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + entity_registry_enabled_default=False, state_class=STATE_CLASS_MEASUREMENT, ), ENTITY_DESC_KEY_TEMPERATURE: ZwaveSensorEntityDescription( diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index e8e3151134c..c16ab00b2eb 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -1,5 +1,6 @@ """Provide common test tools for Z-Wave JS.""" AIR_TEMPERATURE_SENSOR = "sensor.multisensor_6_air_temperature" +BATTERY_SENSOR = "sensor.multisensor_6_battery_level" HUMIDITY_SENSOR = "sensor.multisensor_6_humidity" POWER_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed" ENERGY_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_2" diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index a18bead36c2..9e77d877b5c 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -21,6 +21,7 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_ICON, + DEVICE_CLASS_BATTERY, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, @@ -30,6 +31,7 @@ from homeassistant.const import ( ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, + ENTITY_CATEGORY_DIAGNOSTIC, POWER_WATT, STATE_UNAVAILABLE, TEMP_CELSIUS, @@ -38,6 +40,7 @@ from homeassistant.helpers import entity_registry as er from .common import ( AIR_TEMPERATURE_SENSOR, + BATTERY_SENSOR, CURRENT_SENSOR, ENERGY_SENSOR, HUMIDITY_SENSOR, @@ -59,6 +62,18 @@ async def test_numeric_sensor(hass, multisensor_6, integration): assert state.attributes["unit_of_measurement"] == TEMP_CELSIUS assert state.attributes["device_class"] == DEVICE_CLASS_TEMPERATURE + state = hass.states.get(BATTERY_SENSOR) + + assert state + assert state.state == "100.0" + assert state.attributes["unit_of_measurement"] == "%" + assert state.attributes["device_class"] == DEVICE_CLASS_BATTERY + + ent_reg = er.async_get(hass) + entity_entry = ent_reg.async_get(BATTERY_SENSOR) + assert entity_entry + assert entity_entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC + state = hass.states.get(HUMIDITY_SENSOR) assert state From 7ccfaed7361604aa83cc55f059015327b544b5a7 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 25 Oct 2021 12:26:03 -0400 Subject: [PATCH 0830/1038] Use DeviceInfo Class P-R (#58324) --- homeassistant/components/p1_monitor/const.py | 1 - homeassistant/components/p1_monitor/sensor.py | 17 ++++----- .../panasonic_viera/media_player.py | 19 +++++----- .../components/panasonic_viera/remote.py | 19 +++++----- homeassistant/components/philips_js/light.py | 15 ++++---- .../components/philips_js/media_player.py | 17 +++++---- homeassistant/components/philips_js/remote.py | 17 +++++---- homeassistant/components/picnic/sensor.py | 19 +++++----- homeassistant/components/plaato/entity.py | 21 +++++------ .../components/plum_lightpad/light.py | 29 ++++++++------- homeassistant/components/point/__init__.py | 2 +- .../components/point/alarm_control_panel.py | 13 ++++--- homeassistant/components/powerwall/entity.py | 20 +++++----- homeassistant/components/ps4/media_player.py | 37 ++++++++----------- .../components/rainforest_eagle/sensor.py | 14 +++---- .../components/renault/renault_vehicle.py | 21 ++++------- homeassistant/components/rfxtrx/__init__.py | 11 +++--- homeassistant/components/ring/entity.py | 15 ++++---- .../components/risco/binary_sensor.py | 13 ++++--- .../rituals_perfume_genie/entity.py | 15 ++++---- .../components/roomba/irobot_base.py | 21 ++++++----- homeassistant/components/roon/media_player.py | 1 - 22 files changed, 174 insertions(+), 183 deletions(-) diff --git a/homeassistant/components/p1_monitor/const.py b/homeassistant/components/p1_monitor/const.py index 1af76d49176..79b53eeed9e 100644 --- a/homeassistant/components/p1_monitor/const.py +++ b/homeassistant/components/p1_monitor/const.py @@ -9,7 +9,6 @@ DOMAIN: Final = "p1_monitor" LOGGER = logging.getLogger(__package__) SCAN_INTERVAL = timedelta(seconds=5) -ATTR_ENTRY_TYPE: Final = "entry_type" ENTRY_TYPE_SERVICE: Final = "service" SERVICE_SMARTMETER: Final = "smartmeter" diff --git a/homeassistant/components/p1_monitor/sensor.py b/homeassistant/components/p1_monitor/sensor.py index 8c86d3d4529..e3cebf94a68 100644 --- a/homeassistant/components/p1_monitor/sensor.py +++ b/homeassistant/components/p1_monitor/sensor.py @@ -12,9 +12,6 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_NAME, CURRENCY_EURO, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, @@ -28,13 +25,13 @@ from homeassistant.const import ( VOLUME_CUBIC_METERS, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import P1MonitorDataUpdateCoordinator from .const import ( - ATTR_ENTRY_TYPE, DOMAIN, ENTRY_TYPE_SERVICE, SERVICE_PHASES, @@ -266,14 +263,14 @@ class P1MonitorSensorEntity(CoordinatorEntity, SensorEntity): f"{coordinator.config_entry.entry_id}_{service_key}_{description.key}" ) - self._attr_device_info = { - ATTR_IDENTIFIERS: { + self._attr_device_info = DeviceInfo( + entry_type=ENTRY_TYPE_SERVICE, + identifiers={ (DOMAIN, f"{coordinator.config_entry.entry_id}_{service_key}") }, - ATTR_NAME: service, - ATTR_MANUFACTURER: "P1 Monitor", - ATTR_ENTRY_TYPE: ENTRY_TYPE_SERVICE, - } + manufacturer="P1 Monitor", + name=service, + ) @property def native_value(self) -> StateType: diff --git a/homeassistant/components/panasonic_viera/media_player.py b/homeassistant/components/panasonic_viera/media_player.py index c59c77b569c..9058574354b 100644 --- a/homeassistant/components/panasonic_viera/media_player.py +++ b/homeassistant/components/panasonic_viera/media_player.py @@ -1,4 +1,6 @@ """Media player support for Panasonic Viera TV.""" +from __future__ import annotations + import logging from panasonic_viera import Keys @@ -19,6 +21,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_STEP, ) from homeassistant.const import CONF_NAME +from homeassistant.helpers.entity import DeviceInfo from .const import ( ATTR_DEVICE_INFO, @@ -78,18 +81,16 @@ class PanasonicVieraTVEntity(MediaPlayerEntity): return self._device_info[ATTR_UDN] @property - def device_info(self): + def device_info(self) -> DeviceInfo | None: """Return device specific attributes.""" if self._device_info is None: return None - return { - "name": self._name, - "identifiers": {(DOMAIN, self._device_info[ATTR_UDN])}, - "manufacturer": self._device_info.get( - ATTR_MANUFACTURER, DEFAULT_MANUFACTURER - ), - "model": self._device_info.get(ATTR_MODEL_NUMBER, DEFAULT_MODEL_NUMBER), - } + return DeviceInfo( + identifiers={(DOMAIN, self._device_info[ATTR_UDN])}, + manufacturer=self._device_info.get(ATTR_MANUFACTURER, DEFAULT_MANUFACTURER), + model=self._device_info.get(ATTR_MODEL_NUMBER, DEFAULT_MODEL_NUMBER), + name=self._name, + ) @property def device_class(self): diff --git a/homeassistant/components/panasonic_viera/remote.py b/homeassistant/components/panasonic_viera/remote.py index acd1d49f1be..30653847b43 100644 --- a/homeassistant/components/panasonic_viera/remote.py +++ b/homeassistant/components/panasonic_viera/remote.py @@ -1,6 +1,9 @@ """Remote control support for Panasonic Viera TV.""" +from __future__ import annotations + from homeassistant.components.remote import RemoteEntity from homeassistant.const import CONF_NAME, STATE_ON +from homeassistant.helpers.entity import DeviceInfo from .const import ( ATTR_DEVICE_INFO, @@ -44,18 +47,16 @@ class PanasonicVieraRemoteEntity(RemoteEntity): return self._device_info[ATTR_UDN] @property - def device_info(self): + def device_info(self) -> DeviceInfo | None: """Return device specific attributes.""" if self._device_info is None: return None - return { - "name": self._name, - "identifiers": {(DOMAIN, self._device_info[ATTR_UDN])}, - "manufacturer": self._device_info.get( - ATTR_MANUFACTURER, DEFAULT_MANUFACTURER - ), - "model": self._device_info.get(ATTR_MODEL_NUMBER, DEFAULT_MODEL_NUMBER), - } + return DeviceInfo( + identifiers={(DOMAIN, self._device_info[ATTR_UDN])}, + manufacturer=self._device_info.get(ATTR_MANUFACTURER, DEFAULT_MANUFACTURER), + model=self._device_info.get(ATTR_MODEL_NUMBER, DEFAULT_MODEL_NUMBER), + name=self._name, + ) @property def name(self): diff --git a/homeassistant/components/philips_js/light.py b/homeassistant/components/philips_js/light.py index 3dbca7611ab..799b3b41631 100644 --- a/homeassistant/components/philips_js/light.py +++ b/homeassistant/components/philips_js/light.py @@ -19,6 +19,7 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.color import color_hsv_to_RGB, color_RGB_to_hsv @@ -154,15 +155,15 @@ class PhilipsTVLightEntity(CoordinatorEntity, LightEntity): self._attr_name = self._system["name"] self._attr_unique_id = unique_id self._attr_icon = "mdi:television-ambient-light" - self._attr_device_info = { - "name": self._system["name"], - "identifiers": { + self._attr_device_info = DeviceInfo( + identifiers={ (DOMAIN, self._attr_unique_id), }, - "model": self._system.get("model"), - "manufacturer": "Philips", - "sw_version": self._system.get("softwareversion"), - } + manufacturer="Philips", + model=self._system.get("model"), + name=self._system["name"], + sw_version=self._system.get("softwareversion"), + ) self._update_from_coordinator() diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 1b2d5c25fd4..9b6ddfeb76e 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -45,6 +45,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import LOGGER as _LOGGER, PhilipsTVDataUpdateCoordinator @@ -324,17 +325,17 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): return self._unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" - return { - "name": self._system["name"], - "identifiers": { + return DeviceInfo( + identifiers={ (DOMAIN, self._unique_id), }, - "model": self._system.get("model"), - "manufacturer": "Philips", - "sw_version": self._system.get("softwareversion"), - } + manufacturer="Philips", + model=self._system.get("model"), + sw_version=self._system.get("softwareversion"), + name=self._system["name"], + ) async def async_play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" diff --git a/homeassistant/components/philips_js/remote.py b/homeassistant/components/philips_js/remote.py index 98ecab96fd4..639f5ab7560 100644 --- a/homeassistant/components/philips_js/remote.py +++ b/homeassistant/components/philips_js/remote.py @@ -10,6 +10,7 @@ from homeassistant.components.remote import ( DEFAULT_DELAY_SECS, RemoteEntity, ) +from homeassistant.helpers.entity import DeviceInfo from . import LOGGER, PhilipsTVDataUpdateCoordinator from .const import CONF_SYSTEM, DOMAIN @@ -67,17 +68,17 @@ class PhilipsTVRemote(RemoteEntity): return self._unique_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" - return { - "name": self._system["name"], - "identifiers": { + return DeviceInfo( + identifiers={ (DOMAIN, self._unique_id), }, - "model": self._system.get("model"), - "manufacturer": "Philips", - "sw_version": self._system.get("softwareversion"), - } + manufacturer="Philips", + model=self._system.get("model"), + name=self._system["name"], + sw_version=self._system.get("softwareversion"), + ) async def async_turn_on(self, **kwargs): """Turn the device on.""" diff --git a/homeassistant/components/picnic/sensor.py b/homeassistant/components/picnic/sensor.py index 7eafef17982..d3de3d1dfb3 100644 --- a/homeassistant/components/picnic/sensor.py +++ b/homeassistant/components/picnic/sensor.py @@ -1,11 +1,12 @@ """Definition of Picnic sensors.""" from __future__ import annotations -from typing import Any +from typing import Any, cast from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -75,15 +76,15 @@ class PicnicSensor(SensorEntity, CoordinatorEntity): return self.coordinator.last_update_success and self.state is not None @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info.""" - return { - "identifiers": {(DOMAIN, self._service_unique_id)}, - "manufacturer": "Picnic", - "model": self._service_unique_id, - "name": f"Picnic: {self.coordinator.data[ADDRESS]}", - "entry_type": "service", - } + return DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, cast(str, self._service_unique_id))}, + manufacturer="Picnic", + model=self._service_unique_id, + name=f"Picnic: {self.coordinator.data[ADDRESS]}", + ) @staticmethod def _to_capitalized_name(name: str) -> str: diff --git a/homeassistant/components/plaato/entity.py b/homeassistant/components/plaato/entity.py index 3c04c5d597d..b0297ec3024 100644 --- a/homeassistant/components/plaato/entity.py +++ b/homeassistant/components/plaato/entity.py @@ -53,19 +53,16 @@ class PlaatoEntity(entity.Entity): return f"{self._device_id}_{self._sensor_type}" @property - def device_info(self): + def device_info(self) -> entity.DeviceInfo: """Get device info.""" - device_info = { - "identifiers": {(DOMAIN, self._device_id)}, - "name": self._device_name, - "manufacturer": "Plaato", - "model": self._device_type, - } - - if self._sensor_data.firmware_version != "": - device_info["sw_version"] = self._sensor_data.firmware_version - - return device_info + sw_version = self._sensor_data.firmware_version + return entity.DeviceInfo( + identifiers={(DOMAIN, self._device_id)}, + manufacturer="Plaato", + model=self._device_type, + name=self._device_name, + sw_version=sw_version if sw_version != "" else None, + ) @property def extra_state_attributes(self): diff --git a/homeassistant/components/plum_lightpad/light.py b/homeassistant/components/plum_lightpad/light.py index f358d81dfef..5ded654156d 100644 --- a/homeassistant/components/plum_lightpad/light.py +++ b/homeassistant/components/plum_lightpad/light.py @@ -15,6 +15,7 @@ from homeassistant.components.light import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.color as color_util @@ -94,14 +95,14 @@ class PlumLight(LightEntity): return self._load.name @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" - return { - "name": self.name, - "identifiers": {(DOMAIN, self.unique_id)}, - "model": "Dimmer", - "manufacturer": "Plum", - } + return DeviceInfo( + identifiers={(DOMAIN, self.unique_id)}, + manufacturer="Plum", + model="Dimmer", + name=self.name, + ) @property def brightness(self) -> int: @@ -185,14 +186,14 @@ class GlowRing(LightEntity): return self._name @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" - return { - "name": self.name, - "identifiers": {(DOMAIN, self.unique_id)}, - "model": "Glow Ring", - "manufacturer": "Plum", - } + return DeviceInfo( + identifiers={(DOMAIN, self.unique_id)}, + manufacturer="Plum", + model="Glow Ring", + name=self.name, + ) @property def brightness(self) -> int: diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index 45b7dbb9e3e..6865664af7c 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -315,7 +315,7 @@ class MinutPointEntity(Entity): connections={ (device_registry.CONNECTION_NETWORK_MAC, device["device_mac"]) }, - identifieres=device["device_id"], + identifiers=device["device_id"], manufacturer="Minut", model=f"Point v{device['hardware_version']}", name=device["description"], diff --git a/homeassistant/components/point/alarm_control_panel.py b/homeassistant/components/point/alarm_control_panel.py index 8325c89f129..12532154f8d 100644 --- a/homeassistant/components/point/alarm_control_panel.py +++ b/homeassistant/components/point/alarm_control_panel.py @@ -10,6 +10,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from .const import DOMAIN as POINT_DOMAIN, POINT_DISCOVERY_NEW, SIGNAL_WEBHOOK @@ -117,10 +118,10 @@ class MinutPointAlarmControl(AlarmControlPanelEntity): return f"point.{self._home_id}" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" - return { - "identifiers": {(POINT_DOMAIN, self._home_id)}, - "name": self.name, - "manufacturer": "Minut", - } + return DeviceInfo( + identifiers={(POINT_DOMAIN, self._home_id)}, + manufacturer="Minut", + name=self.name, + ) diff --git a/homeassistant/components/powerwall/entity.py b/homeassistant/components/powerwall/entity.py index bcc21615066..ae647a080c0 100644 --- a/homeassistant/components/powerwall/entity.py +++ b/homeassistant/components/powerwall/entity.py @@ -1,5 +1,6 @@ """The Tesla Powerwall integration base entity.""" +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, MANUFACTURER, MODEL @@ -20,15 +21,12 @@ class PowerWallEntity(CoordinatorEntity): self.base_unique_id = "_".join(powerwalls_serial_numbers) @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Powerwall device info.""" - device_info = { - "identifiers": {(DOMAIN, self.base_unique_id)}, - "name": self._site_info.site_name, - "manufacturer": MANUFACTURER, - } - model = MODEL - model += f" ({self._device_type.name})" - device_info["model"] = model - device_info["sw_version"] = self._version - return device_info + return DeviceInfo( + identifiers={(DOMAIN, self.base_unique_id)}, + manufacturer=MANUFACTURER, + model=f"{MODEL} ({self._device_type.name})", + name=self._site_info.site_name, + sw_version=self._version, + ) diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index bdc8ba9714d..cc096e96bd5 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -32,6 +32,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers import device_registry, entity_registry +from homeassistant.helpers.entity import DeviceInfo from .const import ( ATTR_MEDIA_IMAGE_URL, @@ -92,7 +93,6 @@ class PS4Device(MediaPlayerEntity): self._source_list = [] self._retry = 0 self._disconnected = False - self._info = None self._unique_id = None @callback @@ -150,7 +150,7 @@ class PS4Device(MediaPlayerEntity): if self._ps4.ddp_protocol is None: # Use socket.socket. await self.hass.async_add_executor_job(self._ps4.get_status) - if self._info is None: + if self._attr_device_info is None: # Add entity to registry. await self.async_get_device_info(self._ps4.status) self._ps4.ddp_protocol = self.hass.data[PS4_DATA].protocol @@ -337,26 +337,26 @@ class PS4Device(MediaPlayerEntity): break for device in d_registry.devices.values(): if self._entry_id in device.config_entries: - self._info = { - "name": device.name, - "model": device.model, - "identifiers": device.identifiers, - "manufacturer": device.manufacturer, - "sw_version": device.sw_version, - } + self._attr_device_info = DeviceInfo( + identifiers=device.identifiers, + manufacturer=device.manufacturer, + model=device.model, + name=device.name, + sw_version=device.sw_version, + ) break else: _sw_version = status["system-version"] _sw_version = _sw_version[1:4] sw_version = f"{_sw_version[0]}.{_sw_version[1:]}" - self._info = { - "name": status["host-name"], - "model": "PlayStation 4", - "identifiers": {(PS4_DOMAIN, status["host-id"])}, - "manufacturer": "Sony Interactive Entertainment Inc.", - "sw_version": sw_version, - } + self._attr_device_info = DeviceInfo( + identifiers={(PS4_DOMAIN, status["host-id"])}, + manufacturer="Sony Interactive Entertainment Inc.", + model="PlayStation 4", + name=status["host-name"], + sw_version=sw_version, + ) self._unique_id = format_unique_id(self._creds, status["host-id"]) @@ -368,11 +368,6 @@ class PS4Device(MediaPlayerEntity): self.unsubscribe_to_protocol() self.hass.data[PS4_DATA].devices.remove(self) - @property - def device_info(self): - """Return information about the device.""" - return self._info - @property def unique_id(self): """Return Unique ID for entity.""" diff --git a/homeassistant/components/rainforest_eagle/sensor.py b/homeassistant/components/rainforest_eagle/sensor.py index 35864e55fa4..bdd6ac541a9 100644 --- a/homeassistant/components/rainforest_eagle/sensor.py +++ b/homeassistant/components/rainforest_eagle/sensor.py @@ -98,11 +98,11 @@ class EagleSensor(CoordinatorEntity, SensorEntity): return self.coordinator.data.get(self.entity_description.key) @property - def device_info(self) -> DeviceInfo | None: + def device_info(self) -> DeviceInfo: """Return device info.""" - return { - "name": self.coordinator.model, - "identifiers": {(DOMAIN, self.coordinator.cloud_id)}, - "manufacturer": "Rainforest Automation", - "model": self.coordinator.model, - } + return DeviceInfo( + identifiers={(DOMAIN, self.coordinator.cloud_id)}, + manufacturer="Rainforest Automation", + model=self.coordinator.model, + name=self.coordinator.model, + ) diff --git a/homeassistant/components/renault/renault_vehicle.py b/homeassistant/components/renault/renault_vehicle.py index 12f5f4e8671..462c5bbc239 100644 --- a/homeassistant/components/renault/renault_vehicle.py +++ b/homeassistant/components/renault/renault_vehicle.py @@ -11,13 +11,6 @@ from typing import cast from renault_api.kamereon import models from renault_api.renault_vehicle import RenaultVehicle -from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, - ATTR_SW_VERSION, -) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo @@ -55,13 +48,13 @@ class RenaultVehicleProxy: self.hass = hass self._vehicle = vehicle self._details = details - self._device_info: DeviceInfo = { - ATTR_IDENTIFIERS: {(DOMAIN, cast(str, details.vin))}, - ATTR_MANUFACTURER: (details.get_brand_label() or "").capitalize(), - ATTR_MODEL: (details.get_model_label() or "").capitalize(), - ATTR_NAME: details.registrationNumber or "", - ATTR_SW_VERSION: details.get_model_code() or "", - } + self._device_info = DeviceInfo( + identifiers={(DOMAIN, cast(str, details.vin))}, + manufacturer=(details.get_brand_label() or "").capitalize(), + model=(details.get_model_label() or "").capitalize(), + name=details.registrationNumber or "", + sw_version=details.get_model_code() or "", + ) self.coordinators: dict[str, RenaultDataUpdateCoordinator] = {} self.hvac_target_temperature = 21 self._scan_interval = scan_interval diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 970aed38335..9a5b7ecf57a 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -22,6 +22,7 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceRegistry +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.restore_state import RestoreEntity from .const import ( @@ -393,11 +394,11 @@ class RfxtrxEntity(RestoreEntity): @property def device_info(self): """Return the device info.""" - return { - "identifiers": {(DOMAIN, *self._device_id)}, - "name": f"{self._device.type_string} {self._device.id_string}", - "model": self._device.type_string, - } + return DeviceInfo( + identifiers={(DOMAIN, *self._device_id)}, + model=self._device.type_string, + name=f"{self._device.type_string} {self._device.id_string}", + ) def _event_applies(self, event, device_id): """Check if event applies to me.""" diff --git a/homeassistant/components/ring/entity.py b/homeassistant/components/ring/entity.py index 7a1c8ae7bdf..84f4816115f 100644 --- a/homeassistant/components/ring/entity.py +++ b/homeassistant/components/ring/entity.py @@ -1,6 +1,7 @@ """Base class for Ring entity.""" from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import callback +from homeassistant.helpers.entity import DeviceInfo from . import ATTRIBUTION, DOMAIN @@ -43,11 +44,11 @@ class RingEntityMixin: return {ATTR_ATTRIBUTION: ATTRIBUTION} @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info.""" - return { - "identifiers": {(DOMAIN, self._device.device_id)}, - "name": self._device.name, - "model": self._device.model, - "manufacturer": "Ring", - } + return DeviceInfo( + identifiers={(DOMAIN, self._device.device_id)}, + manufacturer="Ring", + model=self._device.model, + name=self._device.name, + ) diff --git a/homeassistant/components/risco/binary_sensor.py b/homeassistant/components/risco/binary_sensor.py index 0e1d4d235c2..cc93d7c11d4 100644 --- a/homeassistant/components/risco/binary_sensor.py +++ b/homeassistant/components/risco/binary_sensor.py @@ -4,6 +4,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.helpers import entity_platform +from homeassistant.helpers.entity import DeviceInfo from .const import DATA_COORDINATOR, DOMAIN from .entity import RiscoEntity, binary_sensor_unique_id @@ -41,13 +42,13 @@ class RiscoBinarySensor(BinarySensorEntity, RiscoEntity): self._zone = self.coordinator.data.zones[self._zone_id] @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info for this device.""" - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "manufacturer": "Risco", - } + return DeviceInfo( + identifiers={(DOMAIN, self.unique_id)}, + manufacturer="Risco", + name=self.name, + ) @property def name(self): diff --git a/homeassistant/components/rituals_perfume_genie/entity.py b/homeassistant/components/rituals_perfume_genie/entity.py index 16c4e76686c..3ad71cdad67 100644 --- a/homeassistant/components/rituals_perfume_genie/entity.py +++ b/homeassistant/components/rituals_perfume_genie/entity.py @@ -3,6 +3,7 @@ from __future__ import annotations from pyrituals import Diffuser +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import RitualsDataUpdateCoordinator @@ -33,13 +34,13 @@ class DiffuserEntity(CoordinatorEntity): self._attr_name = f"{hubname}{entity_suffix}" self._attr_unique_id = f"{hublot}{entity_suffix}" - self._attr_device_info = { - "name": hubname, - "identifiers": {(DOMAIN, hublot)}, - "manufacturer": MANUFACTURER, - "model": MODEL if diffuser.has_battery else MODEL2, - "sw_version": diffuser.version, - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, hublot)}, + manufacturer=MANUFACTURER, + model=MODEL if diffuser.has_battery else MODEL2, + name=hubname, + sw_version=diffuser.version, + ) @property def available(self) -> bool: diff --git a/homeassistant/components/roomba/irobot_base.py b/homeassistant/components/roomba/irobot_base.py index 8cee2a1ce61..fd33f850bd7 100644 --- a/homeassistant/components/roomba/irobot_base.py +++ b/homeassistant/components/roomba/irobot_base.py @@ -24,7 +24,7 @@ from homeassistant.components.vacuum import ( StateVacuumEntity, ) import homeassistant.helpers.device_registry as dr -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity import homeassistant.util.dt as dt_util from . import roomba_reported_state @@ -96,18 +96,19 @@ class IRobotEntity(Entity): @property def device_info(self): """Return the device info of the vacuum cleaner.""" - info = { - "identifiers": {(DOMAIN, self.robot_unique_id)}, - "manufacturer": "iRobot", - "name": str(self._name), - "sw_version": self._version, - "model": self._sku, - } + connections = None if mac_address := self.vacuum_state.get("hwPartsRev", {}).get( "wlan0HwAddr", self.vacuum_state.get("mac") ): - info["connections"] = {(dr.CONNECTION_NETWORK_MAC, mac_address)} - return info + connections = {(dr.CONNECTION_NETWORK_MAC, mac_address)} + return DeviceInfo( + connections=connections, + identifiers={(DOMAIN, self.robot_unique_id)}, + manufacturer="iRobot", + model=self._sku, + name=str(self._name), + sw_version=self._version, + ) @property def _battery_level(self): diff --git a/homeassistant/components/roon/media_player.py b/homeassistant/components/roon/media_player.py index 08dbe12849b..84ebb41254a 100644 --- a/homeassistant/components/roon/media_player.py +++ b/homeassistant/components/roon/media_player.py @@ -163,7 +163,6 @@ class RoonDevice(MediaPlayerEntity): @property def device_info(self) -> DeviceInfo: """Return the device info.""" - dev_model = "player" if self.player_data.get("source_controls"): dev_model = self.player_data["source_controls"][0].get("display_name") return DeviceInfo( From d5142bcf51a8351ecf4a63cf24e76da4cf528f45 Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Mon, 25 Oct 2021 21:54:21 +0200 Subject: [PATCH 0831/1038] Update PyVicare to 2.13.1 (#58422) See changelog: https://github.com/somm15/PyViCare/releases/tag/2.13.1 --- homeassistant/components/vicare/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index 38344fff70b..0fe1c1f95e2 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -3,6 +3,6 @@ "name": "Viessmann ViCare", "documentation": "https://www.home-assistant.io/integrations/vicare", "codeowners": ["@oischinger"], - "requirements": ["PyViCare==2.13.0"], + "requirements": ["PyViCare==2.13.1"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 650e75bdb5a..7a76d36637d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -55,7 +55,7 @@ PyTransportNSW==0.1.1 PyTurboJPEG==1.6.1 # homeassistant.components.vicare -PyViCare==2.13.0 +PyViCare==2.13.1 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.13.4 From 787de8ba66cebdb79f1448b45e128ad54bde6298 Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Mon, 25 Oct 2021 22:58:17 +0300 Subject: [PATCH 0832/1038] bump pylgnetcast to 0.3.5 (#58419) --- homeassistant/components/lg_netcast/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lg_netcast/manifest.json b/homeassistant/components/lg_netcast/manifest.json index 556dbec6cd6..4424eaf477f 100644 --- a/homeassistant/components/lg_netcast/manifest.json +++ b/homeassistant/components/lg_netcast/manifest.json @@ -2,7 +2,7 @@ "domain": "lg_netcast", "name": "LG Netcast", "documentation": "https://www.home-assistant.io/integrations/lg_netcast", - "requirements": ["pylgnetcast==0.3.4"], + "requirements": ["pylgnetcast==0.3.5"], "codeowners": ["@Drafteed"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 7a76d36637d..273e48a3f1b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1589,7 +1589,7 @@ pylast==4.2.1 pylaunches==1.0.0 # homeassistant.components.lg_netcast -pylgnetcast==0.3.4 +pylgnetcast==0.3.5 # homeassistant.components.forked_daapd pylibrespot-java==0.1.0 From 6bc5961f8aefad421856325fbb2363dcb08a8c85 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 25 Oct 2021 16:18:33 -0400 Subject: [PATCH 0833/1038] Switch to UpdateCoordinator for eight sleep (#52614) * Switch to UpdateCoordinator for eight sleep * use super call * add self as codeowner * Call API update method directly when creating coordinator * Update homeassistant/components/eight_sleep/__init__.py Co-authored-by: J. Nick Koston * Update homeassistant/components/eight_sleep/__init__.py Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- CODEOWNERS | 2 +- .../components/eight_sleep/__init__.py | 120 +++++++----------- .../components/eight_sleep/binary_sensor.py | 27 ++-- .../components/eight_sleep/manifest.json | 2 +- .../components/eight_sleep/sensor.py | 58 ++++++--- 5 files changed, 104 insertions(+), 105 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index eeac3b6af78..e2494b8299a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -138,7 +138,7 @@ homeassistant/components/ecovacs/* @OverloadUT homeassistant/components/edl21/* @mtdcr homeassistant/components/efergy/* @tkdrob homeassistant/components/egardia/* @jeroenterheerdt -homeassistant/components/eight_sleep/* @mezz64 +homeassistant/components/eight_sleep/* @mezz64 @raman325 homeassistant/components/elgato/* @frenck homeassistant/components/elkm1/* @gwww @bdraco homeassistant/components/elv/* @majuss diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index f839b3fcc74..07474c44c62 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -12,23 +12,22 @@ from homeassistant.const import ( CONF_SENSORS, CONF_USERNAME, ) -from homeassistant.core import callback 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.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, ) -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.util.dt import utcnow _LOGGER = logging.getLogger(__name__) CONF_PARTNER = "partner" DATA_EIGHT = "eight_sleep" +DATA_HEAT = "heat" +DATA_USER = "user" +DATA_API = "api" DOMAIN = "eight_sleep" HEAT_ENTITY = "heat" @@ -115,7 +114,7 @@ async def async_setup(hass, config): eight = EightSleep(user, password, timezone, async_get_clientsession(hass)) - hass.data[DATA_EIGHT] = eight + hass.data.setdefault(DATA_EIGHT, {})[DATA_API] = eight # Authenticate, build sensors success = await eight.start() @@ -123,26 +122,14 @@ async def async_setup(hass, config): # Authentication failed, cannot continue return False - async def async_update_heat_data(now): - """Update heat data from eight in HEAT_SCAN_INTERVAL.""" - await eight.update_device_data() - async_dispatcher_send(hass, SIGNAL_UPDATE_HEAT) - - async_track_point_in_utc_time( - hass, async_update_heat_data, utcnow() + HEAT_SCAN_INTERVAL - ) - - async def async_update_user_data(now): - """Update user data from eight in USER_SCAN_INTERVAL.""" - await eight.update_user_data() - async_dispatcher_send(hass, SIGNAL_UPDATE_USER) - - async_track_point_in_utc_time( - hass, async_update_user_data, utcnow() + USER_SCAN_INTERVAL - ) - - await async_update_heat_data(None) - await async_update_user_data(None) + heat_coordinator = hass.data[DOMAIN][DATA_HEAT] = EightSleepHeatDataCoordinator( + hass, eight + ) + user_coordinator = hass.data[DOMAIN][DATA_USER] = EightSleepUserDataCoordinator( + hass, eight + ) + await heat_coordinator.async_config_entry_first_refresh() + await user_coordinator.async_config_entry_first_refresh() # Load sub components sensors = [] @@ -183,7 +170,7 @@ async def async_setup(hass, config): usrobj = eight.users[userid] await usrobj.set_heating_level(target, duration) - async_dispatcher_send(hass, SIGNAL_UPDATE_HEAT) + await heat_coordinator.async_request_refresh() # Register services hass.services.async_register( @@ -193,55 +180,40 @@ async def async_setup(hass, config): return True -class EightSleepUserEntity(Entity): - """The Eight Sleep device entity.""" +class EightSleepHeatDataCoordinator(DataUpdateCoordinator): + """Class to retrieve heat data from Eight Sleep.""" - def __init__(self, eight): - """Initialize the data object.""" - self._eight = eight - - async def async_added_to_hass(self): - """Register update dispatcher.""" - - @callback - def async_eight_user_update(): - """Update callback.""" - self.async_schedule_update_ha_state(True) - - self.async_on_remove( - async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_USER, async_eight_user_update - ) + def __init__(self, hass, api): + """Initialize coordinator.""" + self.api = api + super().__init__( + hass, + _LOGGER, + name=f"{DOMAIN}_heat", + update_interval=HEAT_SCAN_INTERVAL, + update_method=self.api.update_device_data, ) - @property - def should_poll(self): - """Return True if entity has to be polled for state.""" - return False +class EightSleepUserDataCoordinator(DataUpdateCoordinator): + """Class to retrieve user data from Eight Sleep.""" -class EightSleepHeatEntity(Entity): - """The Eight Sleep device entity.""" - - def __init__(self, eight): - """Initialize the data object.""" - self._eight = eight - - async def async_added_to_hass(self): - """Register update dispatcher.""" - - @callback - def async_eight_heat_update(): - """Update callback.""" - self.async_schedule_update_ha_state(True) - - self.async_on_remove( - async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_HEAT, async_eight_heat_update - ) + def __init__(self, hass, api): + """Initialize coordinator.""" + self.api = api + super().__init__( + hass, + _LOGGER, + name=f"{DOMAIN}_user", + update_interval=USER_SCAN_INTERVAL, + update_method=self.api.update_user_data, ) - @property - def should_poll(self): - """Return True if entity has to be polled for state.""" - return False + +class EightSleepEntity(CoordinatorEntity): + """The Eight Sleep device entity.""" + + def __init__(self, coordinator, eight): + """Initialize the data object.""" + super().__init__(coordinator) + self._eight = eight diff --git a/homeassistant/components/eight_sleep/binary_sensor.py b/homeassistant/components/eight_sleep/binary_sensor.py index d8a763c2e54..ca8a10b0f93 100644 --- a/homeassistant/components/eight_sleep/binary_sensor.py +++ b/homeassistant/components/eight_sleep/binary_sensor.py @@ -5,8 +5,16 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_OCCUPANCY, BinarySensorEntity, ) +from homeassistant.core import callback -from . import CONF_BINARY_SENSORS, DATA_EIGHT, NAME_MAP, EightSleepHeatEntity +from . import ( + CONF_BINARY_SENSORS, + DATA_API, + DATA_EIGHT, + DATA_HEAT, + NAME_MAP, + EightSleepEntity, +) _LOGGER = logging.getLogger(__name__) @@ -18,22 +26,23 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= name = "Eight" sensors = discovery_info[CONF_BINARY_SENSORS] - eight = hass.data[DATA_EIGHT] + eight = hass.data[DATA_EIGHT][DATA_API] + heat_coordinator = hass.data[DATA_EIGHT][DATA_HEAT] all_sensors = [] for sensor in sensors: - all_sensors.append(EightHeatSensor(name, eight, sensor)) + all_sensors.append(EightHeatSensor(name, heat_coordinator, eight, sensor)) async_add_entities(all_sensors, True) -class EightHeatSensor(EightSleepHeatEntity, BinarySensorEntity): +class EightHeatSensor(EightSleepEntity, BinarySensorEntity): """Representation of a Eight Sleep heat-based sensor.""" - def __init__(self, name, eight, sensor): + def __init__(self, name, coordinator, eight, sensor): """Initialize the sensor.""" - super().__init__(eight) + super().__init__(coordinator, eight) self._sensor = sensor self._mapped_name = NAME_MAP.get(self._sensor, self._sensor) @@ -58,6 +67,8 @@ class EightHeatSensor(EightSleepHeatEntity, BinarySensorEntity): """Return true if the binary sensor is on.""" return self._state - async def async_update(self): - """Retrieve latest state.""" + @callback + def _handle_coordinator_update(self): + """Handle updated data from the coordinator.""" self._state = self._usrobj.bed_presence + super()._handle_coordinator_update() diff --git a/homeassistant/components/eight_sleep/manifest.json b/homeassistant/components/eight_sleep/manifest.json index 1c3944a985e..e722f73c4e7 100644 --- a/homeassistant/components/eight_sleep/manifest.json +++ b/homeassistant/components/eight_sleep/manifest.json @@ -3,6 +3,6 @@ "name": "Eight Sleep", "documentation": "https://www.home-assistant.io/integrations/eight_sleep", "requirements": ["pyeight==0.1.9"], - "codeowners": ["@mezz64"], + "codeowners": ["@mezz64", "@raman325"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/eight_sleep/sensor.py b/homeassistant/components/eight_sleep/sensor.py index df0d7882491..0e84eea64f6 100644 --- a/homeassistant/components/eight_sleep/sensor.py +++ b/homeassistant/components/eight_sleep/sensor.py @@ -8,13 +8,16 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +from homeassistant.core import callback from . import ( CONF_SENSORS, + DATA_API, DATA_EIGHT, + DATA_HEAT, + DATA_USER, NAME_MAP, - EightSleepHeatEntity, - EightSleepUserEntity, + EightSleepEntity, ) ATTR_ROOM_TEMP = "Room Temperature" @@ -52,7 +55,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= name = "Eight" sensors = discovery_info[CONF_SENSORS] - eight = hass.data[DATA_EIGHT] + eight = hass.data[DATA_EIGHT][DATA_API] + heat_coordinator = hass.data[DATA_EIGHT][DATA_HEAT] + user_coordinator = hass.data[DATA_EIGHT][DATA_USER] if hass.config.units.is_metric: units = "si" @@ -63,21 +68,25 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= for sensor in sensors: if "bed_state" in sensor: - all_sensors.append(EightHeatSensor(name, eight, sensor)) + all_sensors.append(EightHeatSensor(name, heat_coordinator, eight, sensor)) elif "room_temp" in sensor: - all_sensors.append(EightRoomSensor(name, eight, sensor, units)) + all_sensors.append( + EightRoomSensor(name, user_coordinator, eight, sensor, units) + ) else: - all_sensors.append(EightUserSensor(name, eight, sensor, units)) + all_sensors.append( + EightUserSensor(name, user_coordinator, eight, sensor, units) + ) async_add_entities(all_sensors, True) -class EightHeatSensor(EightSleepHeatEntity, SensorEntity): +class EightHeatSensor(EightSleepEntity, SensorEntity): """Representation of an eight sleep heat-based sensor.""" - def __init__(self, name, eight, sensor): + def __init__(self, name, coordinator, eight, sensor): """Initialize the sensor.""" - super().__init__(eight) + super().__init__(coordinator, eight) self._sensor = sensor self._mapped_name = NAME_MAP.get(self._sensor, self._sensor) @@ -110,10 +119,12 @@ class EightHeatSensor(EightSleepHeatEntity, SensorEntity): """Return the unit the value is expressed in.""" return PERCENTAGE - async def async_update(self): - """Retrieve latest state.""" + @callback + def _handle_coordinator_update(self): + """Handle updated data from the coordinator.""" _LOGGER.debug("Updating Heat sensor: %s", self._sensor) self._state = self._usrobj.heating_level + super()._handle_coordinator_update() @property def extra_state_attributes(self): @@ -125,12 +136,12 @@ class EightHeatSensor(EightSleepHeatEntity, SensorEntity): } -class EightUserSensor(EightSleepUserEntity, SensorEntity): +class EightUserSensor(EightSleepEntity, SensorEntity): """Representation of an eight sleep user-based sensor.""" - def __init__(self, name, eight, sensor, units): + def __init__(self, name, coordinator, eight, sensor, units): """Initialize the sensor.""" - super().__init__(eight) + super().__init__(coordinator, eight) self._sensor = sensor self._sensor_root = self._sensor.split("_", 1)[1] @@ -183,8 +194,9 @@ class EightUserSensor(EightSleepUserEntity, SensorEntity): return DEVICE_CLASS_TEMPERATURE return None - async def async_update(self): - """Retrieve latest state.""" + @callback + def _handle_coordinator_update(self): + """Handle updated data from the coordinator.""" _LOGGER.debug("Updating User sensor: %s", self._sensor) if "current" in self._sensor: if "fitness" in self._sensor: @@ -208,6 +220,8 @@ class EightUserSensor(EightSleepUserEntity, SensorEntity): elif "sleep_stage" in self._sensor: self._state = self._usrobj.current_values["stage"] + super()._handle_coordinator_update() + @property def extra_state_attributes(self): """Return device state attributes.""" @@ -296,12 +310,12 @@ class EightUserSensor(EightSleepUserEntity, SensorEntity): return state_attr -class EightRoomSensor(EightSleepUserEntity, SensorEntity): +class EightRoomSensor(EightSleepEntity, SensorEntity): """Representation of an eight sleep room sensor.""" - def __init__(self, name, eight, sensor, units): + def __init__(self, name, coordinator, eight, sensor, units): """Initialize the sensor.""" - super().__init__(eight) + super().__init__(coordinator, eight) self._sensor = sensor self._mapped_name = NAME_MAP.get(self._sensor, self._sensor) @@ -320,8 +334,9 @@ class EightRoomSensor(EightSleepUserEntity, SensorEntity): """Return the state of the sensor.""" return self._state - async def async_update(self): - """Retrieve latest state.""" + @callback + def _handle_coordinator_update(self): + """Handle updated data from the coordinator.""" _LOGGER.debug("Updating Room sensor: %s", self._sensor) temp = self._eight.room_temperature() try: @@ -331,6 +346,7 @@ class EightRoomSensor(EightSleepUserEntity, SensorEntity): self._state = round((temp * 1.8) + 32, 2) except TypeError: self._state = None + super()._handle_coordinator_update() @property def native_unit_of_measurement(self): From 9d231ac2f85369a903ae1e4d46a96d95e7c05aa5 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 25 Oct 2021 22:44:23 +0200 Subject: [PATCH 0834/1038] Abort Fritz config flow for configured hostnames (#58140) * Abort Fritz config flow for configured hostnames * Fix tests + consider all combinations * Fix async context --- homeassistant/components/fritz/config_flow.py | 11 +++++- tests/components/fritz/test_config_flow.py | 35 ++++++++++++++----- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index 5ca351cdec1..55c60cc41a8 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +import socket from typing import Any from urllib.parse import ParseResult, urlparse @@ -85,8 +86,16 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): async def async_check_configured_entry(self) -> ConfigEntry | None: """Check if entry is configured.""" + + current_host = await self.hass.async_add_executor_job( + socket.gethostbyname, self._host + ) + for entry in self._async_current_entries(include_ignore=False): - if entry.data[CONF_HOST] == self._host: + entry_host = await self.hass.async_add_executor_job( + socket.gethostbyname, entry.data[CONF_HOST] + ) + if entry_host == current_host: return entry return None diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index 0aecefedf0d..2d276293baa 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -41,6 +41,7 @@ ATTR_HOST = "host" ATTR_NEW_SERIAL_NUMBER = "NewSerialNumber" MOCK_HOST = "fake_host" +MOCK_IP = "192.168.178.1" MOCK_SERIAL_NUMBER = "fake_serial_number" MOCK_FIRMWARE_INFO = [True, "1.1.1"] @@ -51,7 +52,7 @@ MOCK_DEVICE_INFO = { } MOCK_IMPORT_CONFIG = {CONF_HOST: MOCK_HOST, CONF_USERNAME: "username"} MOCK_SSDP_DATA = { - ATTR_SSDP_LOCATION: "https://fake_host:12345/test", + ATTR_SSDP_LOCATION: f"https://{MOCK_IP}:12345/test", ATTR_UPNP_FRIENDLY_NAME: "fake_name", ATTR_UPNP_UDN: "uuid:only-a-test", } @@ -81,7 +82,10 @@ async def test_user(hass: HomeAssistant, fc_class_mock, mock_get_source_ip): "requests.get" ) as mock_request_get, patch( "requests.post" - ) as mock_request_post: + ) as mock_request_post, patch( + "homeassistant.components.fritz.config_flow.socket.gethostbyname", + return_value=MOCK_IP, + ): mock_request_get.return_value.status_code = 200 mock_request_get.return_value.content = MOCK_REQUEST @@ -129,7 +133,10 @@ async def test_user_already_configured( "requests.get" ) as mock_request_get, patch( "requests.post" - ) as mock_request_post: + ) as mock_request_post, patch( + "homeassistant.components.fritz.config_flow.socket.gethostbyname", + return_value=MOCK_IP, + ): mock_request_get.return_value.status_code = 200 mock_request_get.return_value.content = MOCK_REQUEST @@ -319,7 +326,10 @@ async def test_ssdp_already_configured( with patch( "homeassistant.components.fritz.common.FritzConnection", side_effect=fc_class_mock, - ), patch("homeassistant.components.fritz.common.FritzStatus"): + ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( + "homeassistant.components.fritz.config_flow.socket.gethostbyname", + return_value=MOCK_IP, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA @@ -343,7 +353,10 @@ async def test_ssdp_already_configured_host( with patch( "homeassistant.components.fritz.common.FritzConnection", side_effect=fc_class_mock, - ), patch("homeassistant.components.fritz.common.FritzStatus"): + ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( + "homeassistant.components.fritz.config_flow.socket.gethostbyname", + return_value=MOCK_IP, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA @@ -367,7 +380,10 @@ async def test_ssdp_already_configured_host_uuid( with patch( "homeassistant.components.fritz.common.FritzConnection", side_effect=fc_class_mock, - ), patch("homeassistant.components.fritz.common.FritzStatus"): + ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( + "homeassistant.components.fritz.config_flow.socket.gethostbyname", + return_value=MOCK_IP, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA @@ -436,7 +452,7 @@ async def test_ssdp(hass: HomeAssistant, fc_class_mock, mock_get_source_ip): ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_HOST] == MOCK_IP assert result["data"][CONF_PASSWORD] == "fake_pass" assert result["data"][CONF_USERNAME] == "fake_user" @@ -482,7 +498,10 @@ async def test_import(hass: HomeAssistant, fc_class_mock, mock_get_source_ip): "requests.get" ) as mock_request_get, patch( "requests.post" - ) as mock_request_post: + ) as mock_request_post, patch( + "homeassistant.components.fritz.config_flow.socket.gethostbyname", + return_value=MOCK_IP, + ): mock_request_get.return_value.status_code = 200 mock_request_get.return_value.content = MOCK_REQUEST From ba901bbbbfcec6ad0818a197ff6c518bb98bd64d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 25 Oct 2021 22:55:21 +0200 Subject: [PATCH 0835/1038] Complete Air Conditioner (kt) device support for Tuya (#58417) --- homeassistant/components/tuya/light.py | 8 ++++++++ homeassistant/components/tuya/switch.py | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 10aa8806a81..dbae8e2e57f 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -127,6 +127,14 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { name="Backlight", ), ), + # Air conditioner + # https://developer.tuya.com/en/docs/iot/categorykt?id=Kaiuz0z71ov2n + "kt": ( + TuyaLightEntityDescription( + key=DPCode.LIGHT, + name="Backlight", + ), + ), # Smart Camera # https://developer.tuya.com/en/docs/iot/categorysp?id=Kaiuz35leyo12 "sp": ( diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index bc83c085783..6f0f7b85b17 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -189,6 +189,22 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { entity_category=ENTITY_CATEGORY_CONFIG, ), ), + # Air conditioner + # https://developer.tuya.com/en/docs/iot/categorykt?id=Kaiuz0z71ov2n + "kt": ( + SwitchEntityDescription( + key=DPCode.ANION, + name="Ionizer", + icon="mdi:minus-circle-outline", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + SwitchEntityDescription( + key=DPCode.LOCK, + name="Child Lock", + icon="mdi:account-lock", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + ), # Power Socket # https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s "pc": ( From 6341dd4883b70faf4c77bc019655442950715567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 25 Oct 2021 23:05:27 +0200 Subject: [PATCH 0836/1038] Add running device class to binary sensor (#58423) --- homeassistant/components/binary_sensor/__init__.py | 4 ++++ .../components/binary_sensor/device_condition.py | 9 +++++++++ .../components/binary_sensor/device_trigger.py | 6 ++++++ homeassistant/components/binary_sensor/strings.json | 10 +++++++++- 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index d478780f9ac..aff7f9a3135 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -83,6 +83,9 @@ DEVICE_CLASS_PRESENCE = "presence" # On means problem detected, Off means no problem (OK) DEVICE_CLASS_PROBLEM = "problem" +# On means running, Off means not running +DEVICE_CLASS_RUNNING = "running" + # On means unsafe, Off means safe DEVICE_CLASS_SAFETY = "safety" @@ -124,6 +127,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_POWER, DEVICE_CLASS_PRESENCE, DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_RUNNING, DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index ad305beb11b..8351234182d 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -34,6 +34,7 @@ from . import ( DEVICE_CLASS_POWER, DEVICE_CLASS_PRESENCE, DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_RUNNING, DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, @@ -80,6 +81,8 @@ CONF_IS_PRESENT = "is_present" CONF_IS_NOT_PRESENT = "is_not_present" CONF_IS_PROBLEM = "is_problem" CONF_IS_NO_PROBLEM = "is_no_problem" +CONF_IS_RUNNING = "is_running" +CONF_IS_NOT_RUNNING = "is_not_running" CONF_IS_UNSAFE = "is_unsafe" CONF_IS_NOT_UNSAFE = "is_not_unsafe" CONF_IS_SMOKE = "is_smoke" @@ -113,6 +116,7 @@ IS_ON = [ CONF_IS_POWERED, CONF_IS_PRESENT, CONF_IS_PROBLEM, + CONF_IS_RUNNING, CONF_IS_SMOKE, CONF_IS_SOUND, CONF_IS_TAMPERED, @@ -142,6 +146,7 @@ IS_OFF = [ CONF_IS_NO_LIGHT, CONF_IS_NO_MOTION, CONF_IS_NO_PROBLEM, + CONF_IS_NOT_RUNNING, CONF_IS_NO_SMOKE, CONF_IS_NO_SOUND, CONF_IS_NO_UPDATE, @@ -196,6 +201,10 @@ ENTITY_CONDITIONS = { {CONF_TYPE: CONF_IS_PROBLEM}, {CONF_TYPE: CONF_IS_NO_PROBLEM}, ], + DEVICE_CLASS_RUNNING: [ + {CONF_TYPE: CONF_IS_RUNNING}, + {CONF_TYPE: CONF_IS_NOT_RUNNING}, + ], DEVICE_CLASS_SAFETY: [{CONF_TYPE: CONF_IS_UNSAFE}, {CONF_TYPE: CONF_IS_NOT_UNSAFE}], DEVICE_CLASS_SMOKE: [{CONF_TYPE: CONF_IS_SMOKE}, {CONF_TYPE: CONF_IS_NO_SMOKE}], DEVICE_CLASS_SOUND: [{CONF_TYPE: CONF_IS_SOUND}, {CONF_TYPE: CONF_IS_NO_SOUND}], diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index f3eb1851247..72cd885d467 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -32,6 +32,7 @@ from . import ( DEVICE_CLASS_POWER, DEVICE_CLASS_PRESENCE, DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_RUNNING, DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, @@ -78,6 +79,8 @@ CONF_PRESENT = "present" CONF_NOT_PRESENT = "not_present" CONF_PROBLEM = "problem" CONF_NO_PROBLEM = "no_problem" +CONF_RUNNING = "running" +CONF_NOT_RUNNING = "not_running" CONF_UNSAFE = "unsafe" CONF_NOT_UNSAFE = "not_unsafe" CONF_SMOKE = "smoke" @@ -111,6 +114,7 @@ TURNED_ON = [ CONF_POWERED, CONF_PRESENT, CONF_PROBLEM, + CONF_RUNNING, CONF_SMOKE, CONF_SOUND, CONF_UNSAFE, @@ -139,6 +143,7 @@ TURNED_OFF = [ CONF_NO_LIGHT, CONF_NO_MOTION, CONF_NO_PROBLEM, + CONF_NOT_RUNNING, CONF_NO_SMOKE, CONF_NO_SOUND, CONF_NO_VIBRATION, @@ -175,6 +180,7 @@ ENTITY_TRIGGERS = { DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_POWERED}, {CONF_TYPE: CONF_NOT_POWERED}], DEVICE_CLASS_PRESENCE: [{CONF_TYPE: CONF_PRESENT}, {CONF_TYPE: CONF_NOT_PRESENT}], DEVICE_CLASS_PROBLEM: [{CONF_TYPE: CONF_PROBLEM}, {CONF_TYPE: CONF_NO_PROBLEM}], + DEVICE_CLASS_RUNNING: [{CONF_TYPE: CONF_RUNNING}, {CONF_TYPE: CONF_NOT_RUNNING}], DEVICE_CLASS_SAFETY: [{CONF_TYPE: CONF_UNSAFE}, {CONF_TYPE: CONF_NOT_UNSAFE}], DEVICE_CLASS_SMOKE: [{CONF_TYPE: CONF_SMOKE}, {CONF_TYPE: CONF_NO_SMOKE}], DEVICE_CLASS_SOUND: [{CONF_TYPE: CONF_SOUND}, {CONF_TYPE: CONF_NO_SOUND}], diff --git a/homeassistant/components/binary_sensor/strings.json b/homeassistant/components/binary_sensor/strings.json index 70991f58759..a96808d68a3 100644 --- a/homeassistant/components/binary_sensor/strings.json +++ b/homeassistant/components/binary_sensor/strings.json @@ -32,6 +32,8 @@ "is_not_present": "{entity_name} is not present", "is_problem": "{entity_name} is detecting problem", "is_no_problem": "{entity_name} is not detecting problem", + "is_running": "{entity_name} is running", + "is_not_running": "{entity_name} is not running", "is_unsafe": "{entity_name} is unsafe", "is_not_unsafe": "{entity_name} is safe", "is_smoke": "{entity_name} is detecting smoke", @@ -80,6 +82,8 @@ "not_present": "{entity_name} not present", "problem": "{entity_name} started detecting problem", "no_problem": "{entity_name} stopped detecting problem", + "running": "{entity_name} started running", + "not_running": "{entity_name} is no longer running", "unsafe": "{entity_name} became unsafe", "not_unsafe": "{entity_name} became safe", "smoke": "{entity_name} started detecting smoke", @@ -171,6 +175,10 @@ "off": "OK", "on": "Problem" }, + "running": { + "off": "Not running", + "on": "Running" + }, "safety": { "off": "Safe", "on": "Unsafe" @@ -200,4 +208,4 @@ "on": "[%key:common::state::on%]" } } -} +} \ No newline at end of file From f2a5c4602e1d15076a8ebff90c8d74084c1fe7c8 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 25 Oct 2021 17:26:40 -0400 Subject: [PATCH 0837/1038] Use DeviceInfo Class L-M (#58312) --- homeassistant/components/lifx/light.py | 36 ++++++++++--------- .../components/litterrobot/entity.py | 12 +++---- .../components/logi_circle/camera.py | 17 ++++----- .../components/logi_circle/sensor.py | 17 ++++----- .../components/lutron_caseta/__init__.py | 4 +-- homeassistant/components/lyric/__init__.py | 12 +++---- homeassistant/components/mazda/__init__.py | 15 ++++---- homeassistant/components/melcloud/__init__.py | 25 ++++++------- homeassistant/components/met/weather.py | 15 ++++---- .../components/met_eireann/weather.py | 15 ++++---- .../components/meteo_france/sensor.py | 17 ++++----- .../components/meteo_france/weather.py | 17 ++++----- .../components/meteoclimatic/sensor.py | 15 ++++---- .../components/meteoclimatic/weather.py | 15 ++++---- .../components/mikrotik/device_tracker.py | 18 +++++----- homeassistant/components/mill/climate.py | 2 +- homeassistant/components/mill/sensor.py | 2 +- .../components/minecraft_server/__init__.py | 19 ++++------ .../components/mobile_app/helpers.py | 14 ++++---- .../components/modern_forms/__init__.py | 23 +++++------- .../components/monoprice/media_player.py | 15 ++++---- .../components/motion_blinds/cover.py | 2 +- .../components/motion_blinds/sensor.py | 5 +-- .../components/motioneye/__init__.py | 2 +- .../components/mutesync/binary_sensor.py | 17 ++++----- homeassistant/components/myq/__init__.py | 31 ++++++++-------- homeassistant/components/mysensors/device.py | 12 +++---- 27 files changed, 197 insertions(+), 197 deletions(-) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index b30315ac77d..bdb0337c1a1 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -34,11 +34,18 @@ from homeassistant.components.light import ( LightEntity, preprocess_turn_on_alternatives, ) -from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_MODE, + ATTR_MODEL, + ATTR_SW_VERSION, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.core import callback 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.event import async_track_point_in_utc_time import homeassistant.util.color as color_util @@ -449,24 +456,19 @@ class LIFXLight(LightEntity): self.lock = asyncio.Lock() @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return information about the device.""" - info = { - "identifiers": {(LIFX_DOMAIN, self.unique_id)}, - "name": self.name, - "connections": {(dr.CONNECTION_NETWORK_MAC, self.bulb.mac_addr)}, - "manufacturer": "LIFX", - } - + _map = aiolifx().products.product_map + info = DeviceInfo( + identifiers={(LIFX_DOMAIN, self.unique_id)}, + connections={(dr.CONNECTION_NETWORK_MAC, self.bulb.mac_addr)}, + manufacturer="LIFX", + name=self.name, + ) + if model := (_map.get(self.bulb.product) or self.bulb.product) is not None: + info[ATTR_MODEL] = str(model) if (version := self.bulb.host_firmware_version) is not None: - info["sw_version"] = version - - product_map = aiolifx().products.product_map - - model = product_map.get(self.bulb.product) or self.bulb.product - if model is not None: - info["model"] = str(model) - + info[ATTR_SW_VERSION] = version return info @property diff --git a/homeassistant/components/litterrobot/entity.py b/homeassistant/components/litterrobot/entity.py index d75207fb80d..fbcd129411a 100644 --- a/homeassistant/components/litterrobot/entity.py +++ b/homeassistant/components/litterrobot/entity.py @@ -46,12 +46,12 @@ class LitterRobotEntity(CoordinatorEntity): @property def device_info(self) -> DeviceInfo: """Return the device information for a Litter-Robot.""" - return { - "identifiers": {(DOMAIN, self.robot.serial)}, - "name": self.robot.name, - "manufacturer": "Litter-Robot", - "model": self.robot.model, - } + return DeviceInfo( + identifiers={(DOMAIN, self.robot.serial)}, + manufacturer="Litter-Robot", + model=self.robot.model, + name=self.robot.name, + ) class LitterRobotControlEntity(LitterRobotEntity): diff --git a/homeassistant/components/logi_circle/camera.py b/homeassistant/components/logi_circle/camera.py index 30407f03ecf..5146ffca69f 100644 --- a/homeassistant/components/logi_circle/camera.py +++ b/homeassistant/components/logi_circle/camera.py @@ -14,6 +14,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from .const import ( ATTRIBUTION, @@ -116,15 +117,15 @@ class LogiCam(Camera): return SUPPORT_ON_OFF @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return information about the device.""" - return { - "name": self._camera.name, - "identifiers": {(LOGI_CIRCLE_DOMAIN, self._camera.id)}, - "model": self._camera.model_name, - "sw_version": self._camera.firmware, - "manufacturer": DEVICE_BRAND, - } + return DeviceInfo( + identifiers={(LOGI_CIRCLE_DOMAIN, self._camera.id)}, + manufacturer=DEVICE_BRAND, + model=self._camera.model_name, + name=self._camera.name, + sw_version=self._camera.firmware, + ) @property def extra_state_attributes(self): diff --git a/homeassistant/components/logi_circle/sensor.py b/homeassistant/components/logi_circle/sensor.py index 50671152587..3d48c322756 100644 --- a/homeassistant/components/logi_circle/sensor.py +++ b/homeassistant/components/logi_circle/sensor.py @@ -13,6 +13,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.util.dt import as_local @@ -56,15 +57,15 @@ class LogiSensor(SensorEntity): self._tz = time_zone @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return information about the device.""" - return { - "name": self._camera.name, - "identifiers": {(LOGI_CIRCLE_DOMAIN, self._camera.id)}, - "model": self._camera.model_name, - "sw_version": self._camera.firmware, - "manufacturer": DEVICE_BRAND, - } + return DeviceInfo( + identifiers={(LOGI_CIRCLE_DOMAIN, self._camera.id)}, + manufacturer=DEVICE_BRAND, + model=self._camera.model_name, + name=self._camera.name, + sw_version=self._camera.firmware, + ) @property def extra_state_attributes(self): diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 7e3cde2ccac..786e21f2d0b 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -333,10 +333,10 @@ class LutronCasetaDevice(Entity): """Return the device info.""" return DeviceInfo( identifiers={(DOMAIN, self.serial)}, - name=self.name, - suggested_area=self._device["name"].split("_")[0], manufacturer=MANUFACTURER, model=f"{self._device['model']} ({self._device['type']})", + name=self.name, + suggested_area=self._device["name"].split("_")[0], via_device=(DOMAIN, self._bridge_device["serial"]), ) diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index 7189c5ce74e..8b80fa61d2b 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -170,9 +170,9 @@ class LyricDeviceEntity(LyricEntity): @property def device_info(self) -> DeviceInfo: """Return device information about this Honeywell Lyric instance.""" - return { - "connections": {(dr.CONNECTION_NETWORK_MAC, self._mac_id)}, - "manufacturer": "Honeywell", - "model": self.device.deviceModel, - "name": self.device.name, - } + return DeviceInfo( + connections={(dr.CONNECTION_NETWORK_MAC, self._mac_id)}, + manufacturer="Honeywell", + model=self.device.deviceModel, + name=self.device.name, + ) diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index a9775b6d6df..d165220aa8e 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -23,6 +23,7 @@ from homeassistant.exceptions import ( ) from homeassistant.helpers import aiohttp_client, device_registry import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -229,14 +230,14 @@ class MazdaEntity(CoordinatorEntity): return self.coordinator.data[self.index] @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info for the Mazda entity.""" - return { - "identifiers": {(DOMAIN, self.vin)}, - "name": self.get_vehicle_name(), - "manufacturer": "Mazda", - "model": f"{self.data['modelYear']} {self.data['carlineName']}", - } + return DeviceInfo( + identifiers={(DOMAIN, self.vin)}, + manufacturer="Mazda", + model=f"{self.data['modelYear']} {self.data['carlineName']}", + name=self.get_vehicle_name(), + ) def get_vehicle_name(self): """Return the vehicle name, to be used as a prefix for names of other entities.""" diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index 4547e690eb8..3ab3f603dbd 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -17,6 +17,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import ConfigType from homeassistant.util import Throttle @@ -126,19 +127,19 @@ class MelCloudDevice: return self.device.building_id @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" - _device_info = { - "connections": {(CONNECTION_NETWORK_MAC, self.device.mac)}, - "identifiers": {(DOMAIN, f"{self.device.mac}-{self.device.serial}")}, - "manufacturer": "Mitsubishi Electric", - "name": self.name, - } - if (unit_infos := self.device.units) is not None: - _device_info["model"] = ", ".join( - [x["model"] for x in unit_infos if x["model"]] - ) - return _device_info + model = None + unit_infos = self.device.units + if unit_infos is not None: + model = ", ".join([x["model"] for x in unit_infos if x["model"]]) + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, self.device.mac)}, + identifiers={(DOMAIN, f"{self.device.mac}-{self.device.serial}")}, + manufacturer="Mitsubishi Electric", + model=model, + name=self.name, + ) async def mel_devices_setup(hass, token) -> list[MelCloudDevice]: diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 4657da9e5d4..9cec6f93279 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -30,6 +30,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.distance import convert as convert_distance from homeassistant.util.pressure import convert as convert_pressure @@ -247,10 +248,10 @@ class MetWeather(CoordinatorEntity, WeatherEntity): @property def device_info(self): """Device info.""" - return { - "identifiers": {(DOMAIN,)}, - "manufacturer": "Met.no", - "model": "Forecast", - "default_name": "Forecast", - "entry_type": "service", - } + return DeviceInfo( + default_name="Forecast", + entry_type="service", + identifiers={(DOMAIN,)}, + manufacturer="Met.no", + model="Forecast", + ) diff --git a/homeassistant/components/met_eireann/weather.py b/homeassistant/components/met_eireann/weather.py index 190da06f3d9..3a8eed0d7be 100644 --- a/homeassistant/components/met_eireann/weather.py +++ b/homeassistant/components/met_eireann/weather.py @@ -20,6 +20,7 @@ from homeassistant.const import ( PRESSURE_INHG, TEMP_CELSIUS, ) +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util from homeassistant.util.distance import convert as convert_distance @@ -182,10 +183,10 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity): @property def device_info(self): """Device info.""" - return { - "identifiers": {(DOMAIN,)}, - "manufacturer": "Met Éireann", - "model": "Forecast", - "default_name": "Forecast", - "entry_type": "service", - } + return DeviceInfo( + default_name="Forecast", + entry_type="service", + identifiers={(DOMAIN,)}, + manufacturer="Met Éireann", + model="Forecast", + ) diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 1a5b3c4a33a..3cbd56bf94e 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -8,6 +8,7 @@ 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.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -92,15 +93,15 @@ class MeteoFranceSensor(CoordinatorEntity, SensorEntity): self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" - return { - "identifiers": {(DOMAIN, self.platform.config_entry.unique_id)}, - "name": self.coordinator.name, - "manufacturer": MANUFACTURER, - "model": MODEL, - "entry_type": "service", - } + return DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, self.platform.config_entry.unique_id)}, + manufacturer=MANUFACTURER, + model=MODEL, + name=self.coordinator.name, + ) @property def native_value(self): diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index d084198bd5b..5892944cf6a 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -15,6 +15,7 @@ from homeassistant.components.weather import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MODE, TEMP_CELSIUS from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -86,15 +87,15 @@ class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): return self._city_name @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" - return { - "identifiers": {(DOMAIN, self.platform.config_entry.unique_id)}, - "name": self.coordinator.name, - "manufacturer": MANUFACTURER, - "model": MODEL, - "entry_type": "service", - } + return DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, self.platform.config_entry.unique_id)}, + manufacturer=MANUFACTURER, + model=MODEL, + name=self.coordinator.name, + ) @property def condition(self): diff --git a/homeassistant/components/meteoclimatic/sensor.py b/homeassistant/components/meteoclimatic/sensor.py index e4f7a1525d5..8e4e9f8fe13 100644 --- a/homeassistant/components/meteoclimatic/sensor.py +++ b/homeassistant/components/meteoclimatic/sensor.py @@ -3,6 +3,7 @@ from homeassistant.components.sensor import SensorEntity, SensorEntityDescriptio 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.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -41,13 +42,13 @@ class MeteoclimaticSensor(CoordinatorEntity, SensorEntity): @property def device_info(self): """Return the device info.""" - return { - "identifiers": {(DOMAIN, self.platform.config_entry.unique_id)}, - "name": self.coordinator.name, - "manufacturer": MANUFACTURER, - "model": MODEL, - "entry_type": "service", - } + return DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, self.platform.config_entry.unique_id)}, + manufacturer=MANUFACTURER, + model=MODEL, + name=self.coordinator.name, + ) @property def native_value(self): diff --git a/homeassistant/components/meteoclimatic/weather.py b/homeassistant/components/meteoclimatic/weather.py index 1326d700826..36c3229777f 100644 --- a/homeassistant/components/meteoclimatic/weather.py +++ b/homeassistant/components/meteoclimatic/weather.py @@ -5,6 +5,7 @@ from homeassistant.components.weather import WeatherEntity from homeassistant.config_entries import ConfigEntry 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.update_coordinator import ( CoordinatorEntity, @@ -55,13 +56,13 @@ class MeteoclimaticWeather(CoordinatorEntity, WeatherEntity): @property def device_info(self): """Return the device info.""" - return { - "identifiers": {(DOMAIN, self.platform.config_entry.unique_id)}, - "name": self.coordinator.name, - "manufacturer": MANUFACTURER, - "model": MODEL, - "entry_type": "service", - } + return DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, self.platform.config_entry.unique_id)}, + manufacturer=MANUFACTURER, + model=MODEL, + name=self.coordinator.name, + ) @property def condition(self): diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index 025eff8d07a..31a279aeee7 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -10,6 +10,7 @@ from homeassistant.core import callback from homeassistant.helpers import entity_registry from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo import homeassistant.util.dt as dt_util from .const import DOMAIN @@ -130,16 +131,15 @@ class MikrotikHubTracker(ScannerEntity): return None @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return a client description for device registry.""" - info = { - "connections": {(CONNECTION_NETWORK_MAC, self.device.mac)}, - "identifiers": {(DOMAIN, self.device.mac)}, - # We only get generic info from device discovery and so don't want - # to override API specific info that integrations can provide - "default_name": self.name, - } - return info + # We only get generic info from device discovery and so don't want + # to override API specific info that integrations can provide + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, self.device.mac)}, + default_name=self.name, + identifiers={(DOMAIN, self.device.mac)}, + ) async def async_added_to_hass(self): """Client entity created.""" diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index 3890a08de2b..e464065a8d9 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -86,9 +86,9 @@ class MillHeater(CoordinatorEntity, ClimateEntity): self._attr_name = heater.name self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, heater.device_id)}, - name=self.name, manufacturer=MANUFACTURER, model=f"generation {1 if heater.is_gen1 else 2}", + name=self.name, ) if heater.is_gen1: self._attr_hvac_modes = [HVAC_MODE_HEAT] diff --git a/homeassistant/components/mill/sensor.py b/homeassistant/components/mill/sensor.py index caec13d7dee..7d3ac40f608 100644 --- a/homeassistant/components/mill/sensor.py +++ b/homeassistant/components/mill/sensor.py @@ -44,9 +44,9 @@ class MillHeaterEnergySensor(CoordinatorEntity, SensorEntity): self._attr_unique_id = f"{heater.device_id}_{sensor_type}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, heater.device_id)}, - name=self.name, manufacturer=MANUFACTURER, model=f"generation {1 if heater.is_gen1 else 2}", + name=self.name, ) self._update_attr(heater) diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index a41f0018a4f..4876f6ea1fb 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -224,13 +224,13 @@ class MinecraftServerEntity(Entity): self._name = f"{server.name} {type_name}" self._icon = icon self._unique_id = f"{self._server.unique_id}-{type_name}" - self._device_info = { - "identifiers": {(DOMAIN, self._server.unique_id)}, - "name": self._server.name, - "manufacturer": MANUFACTURER, - "model": f"Minecraft Server ({self._server.version})", - "sw_version": self._server.protocol_version, - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._server.unique_id)}, + manufacturer=MANUFACTURER, + model=f"Minecraft Server ({self._server.version})", + name=self._server.name, + sw_version=self._server.protocol_version, + ) self._device_class = device_class self._extra_state_attributes = None self._disconnect_dispatcher = None @@ -245,11 +245,6 @@ class MinecraftServerEntity(Entity): """Return unique ID.""" return self._unique_id - @property - def device_info(self) -> DeviceInfo: - """Return device information.""" - return self._device_info - @property def device_class(self) -> str: """Return device class.""" diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index 6b6b9b51d13..c4e0a81560b 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -174,10 +174,10 @@ def webhook_response( def device_info(registration: dict) -> DeviceInfo: """Return the device info for this registration.""" - return { - "identifiers": {(DOMAIN, registration[ATTR_DEVICE_ID])}, - "manufacturer": registration[ATTR_MANUFACTURER], - "model": registration[ATTR_MODEL], - "name": registration[ATTR_DEVICE_NAME], - "sw_version": registration[ATTR_OS_VERSION], - } + return DeviceInfo( + identifiers={(DOMAIN, registration[ATTR_DEVICE_ID])}, + manufacturer=registration[ATTR_MANUFACTURER], + model=registration[ATTR_MODEL], + name=registration[ATTR_DEVICE_NAME], + sw_version=registration[ATTR_OS_VERSION], + ) diff --git a/homeassistant/components/modern_forms/__init__.py b/homeassistant/components/modern_forms/__init__.py index 09ca43797af..46ab0877bbb 100644 --- a/homeassistant/components/modern_forms/__init__.py +++ b/homeassistant/components/modern_forms/__init__.py @@ -17,14 +17,7 @@ from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, - ATTR_SW_VERSION, - CONF_HOST, -) +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import DeviceInfo @@ -159,10 +152,10 @@ class ModernFormsDeviceEntity(CoordinatorEntity[ModernFormsDataUpdateCoordinator @property def device_info(self) -> DeviceInfo: """Return device information about this Modern Forms device.""" - return { - ATTR_IDENTIFIERS: {(DOMAIN, self.coordinator.data.info.mac_address)}, - ATTR_NAME: self.coordinator.data.info.device_name, - ATTR_MANUFACTURER: "Modern Forms", - ATTR_MODEL: self.coordinator.data.info.fan_type, - ATTR_SW_VERSION: f"{self.coordinator.data.info.firmware_version} / {self.coordinator.data.info.main_mcu_firmware_version}", - } + return DeviceInfo( + identifiers={(DOMAIN, self.coordinator.data.info.mac_address)}, + name=self.coordinator.data.info.device_name, + manufacturer="Modern Forms", + model=self.coordinator.data.info.fan_type, + sw_version=f"{self.coordinator.data.info.firmware_version} / {self.coordinator.data.info.main_mcu_firmware_version}", + ) diff --git a/homeassistant/components/monoprice/media_player.py b/homeassistant/components/monoprice/media_player.py index 8b3de8903a3..68bad42f78e 100644 --- a/homeassistant/components/monoprice/media_player.py +++ b/homeassistant/components/monoprice/media_player.py @@ -15,6 +15,7 @@ from homeassistant.components.media_player.const import ( ) from homeassistant.const import CONF_PORT, STATE_OFF, STATE_ON from homeassistant.helpers import config_validation as cv, entity_platform, service +from homeassistant.helpers.entity import DeviceInfo from .const import ( CONF_SOURCES, @@ -167,14 +168,14 @@ class MonopriceZone(MediaPlayerEntity): return self._zone_id < 20 or self._update_success @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info for this device.""" - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "manufacturer": "Monoprice", - "model": "6-Zone Amplifier", - } + return DeviceInfo( + identifiers={(DOMAIN, self.unique_id)}, + manufacturer="Monoprice", + model="6-Zone Amplifier", + name=self.name, + ) @property def unique_id(self): diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index 1e4251c22f2..ad846e2f690 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -142,8 +142,8 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, blind.mac)}, manufacturer=MANUFACTURER, - name=f"{blind.blind_type}-{blind.mac[12:]}", model=blind.blind_type, + name=f"{blind.blind_type}-{blind.mac[12:]}", via_device=(DOMAIN, config_entry.unique_id), ) diff --git a/homeassistant/components/motion_blinds/sensor.py b/homeassistant/components/motion_blinds/sensor.py index 194f0ae315c..c46798d81bf 100644 --- a/homeassistant/components/motion_blinds/sensor.py +++ b/homeassistant/components/motion_blinds/sensor.py @@ -8,6 +8,7 @@ from homeassistant.const import ( PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ) +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ATTR_AVAILABLE, DOMAIN, KEY_COORDINATOR, KEY_GATEWAY @@ -50,7 +51,7 @@ class MotionBatterySensor(CoordinatorEntity, SensorEntity): super().__init__(coordinator) self._blind = blind - self._attr_device_info = {"identifiers": {(DOMAIN, blind.mac)}} + self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, blind.mac)}) self._attr_name = f"{blind.blind_type}-battery-{blind.mac[12:]}" self._attr_unique_id = f"{blind.mac}-battery" @@ -128,7 +129,7 @@ class MotionSignalStrengthSensor(CoordinatorEntity, SensorEntity): self._device = device self._device_type = device_type - self._attr_device_info = {"identifiers": {(DOMAIN, device.mac)}} + self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device.mac)}) self._attr_unique_id = f"{device.mac}-RSSI" @property diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index b0ee5241ec7..ec501f9f112 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -477,4 +477,4 @@ class MotionEyeEntity(CoordinatorEntity): @property def device_info(self) -> DeviceInfo: """Return the device information.""" - return {"identifiers": {self._device_identifier}} + return DeviceInfo(identifiers={self._device_identifier}) diff --git a/homeassistant/components/mutesync/binary_sensor.py b/homeassistant/components/mutesync/binary_sensor.py index a2f87bf9017..3186b6fc8f0 100644 --- a/homeassistant/components/mutesync/binary_sensor.py +++ b/homeassistant/components/mutesync/binary_sensor.py @@ -1,6 +1,7 @@ """mütesync binary sensor entities.""" from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.helpers import update_coordinator +from homeassistant.helpers.entity import DeviceInfo from .const import DOMAIN @@ -42,12 +43,12 @@ class MuteStatus(update_coordinator.CoordinatorEntity, BinarySensorEntity): return self.coordinator.data[self._sensor_type] @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info of the sensor.""" - return { - "identifiers": {(DOMAIN, self.coordinator.data["user-id"])}, - "name": "mutesync", - "manufacturer": "mütesync", - "model": "mutesync app", - "entry_type": "service", - } + return DeviceInfo( + entry_type="service", + identifiers={(DOMAIN, self.coordinator.data["user-id"])}, + manufacturer="mütesync", + model="mutesync app", + name="mutesync", + ) diff --git a/homeassistant/components/myq/__init__.py b/homeassistant/components/myq/__init__.py index 8e7300bf2bd..8bdf07dad75 100644 --- a/homeassistant/components/myq/__init__.py +++ b/homeassistant/components/myq/__init__.py @@ -1,4 +1,6 @@ """The MyQ integration.""" +from __future__ import annotations + from datetime import timedelta import logging @@ -13,12 +15,7 @@ from pymyq.device import MyQDevice from pymyq.errors import InvalidCredentialsError, MyQError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_MODEL, - ATTR_VIA_DEVICE, - CONF_PASSWORD, - CONF_USERNAME, -) +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client @@ -97,24 +94,24 @@ class MyQEntity(CoordinatorEntity): return self._device.name @property - def device_info(self) -> DeviceInfo: + def device_info(self): """Return the device_info of the device.""" - device_info = DeviceInfo( - identifiers={(DOMAIN, self._device.device_id)}, - name=self._device.name, - manufacturer=MANUFACTURER, - sw_version=self._device.firmware_version, - ) model = ( KNOWN_MODELS.get(self._device.device_id[2:4]) if self._device.device_id is not None else None ) - if model: - device_info[ATTR_MODEL] = model + via_device: tuple[str, str] | None = None if self._device.parent_device_id: - device_info[ATTR_VIA_DEVICE] = (DOMAIN, self._device.parent_device_id) - return device_info + via_device = (DOMAIN, self._device.parent_device_id) + return DeviceInfo( + identifiers={(DOMAIN, self._device.device_id)}, + manufacturer=MANUFACTURER, + model=model, + name=self._device.name, + sw_version=self._device.firmware_version, + via_device=via_device, + ) @property def available(self): diff --git a/homeassistant/components/mysensors/device.py b/homeassistant/components/mysensors/device.py index 32305061ca7..0e6c5231bc8 100644 --- a/homeassistant/components/mysensors/device.py +++ b/homeassistant/components/mysensors/device.py @@ -121,12 +121,12 @@ class MySensorsDevice: @property def device_info(self) -> DeviceInfo: """Return the device info.""" - return { - "identifiers": {(DOMAIN, f"{self.gateway_id}-{self.node_id}")}, - "name": self.node_name, - "manufacturer": DOMAIN, - "sw_version": self.sketch_version, - } + return DeviceInfo( + identifiers={(DOMAIN, f"{self.gateway_id}-{self.node_id}")}, + manufacturer=DOMAIN, + name=self.node_name, + sw_version=self.sketch_version, + ) @property def name(self) -> str: From 2d6fa5c4531521b1292a6eb45d31a04096783bca Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 25 Oct 2021 16:36:26 -0500 Subject: [PATCH 0838/1038] Fix updating sensor on unlinked Plex server (#58418) --- homeassistant/components/plex/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index af8d96cce55..4a4fe84096c 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -454,7 +454,7 @@ class PlexServer: _LOGGER.debug("Photo session detected, skipping: %s", session) continue - session_username = session.usernames[0] + 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: From dad5d19a350c5d9c782f3821caf8d2d0a1336e7e Mon Sep 17 00:00:00 2001 From: Tim Rightnour <6556271+garbled1@users.noreply.github.com> Date: Mon, 25 Oct 2021 14:40:36 -0700 Subject: [PATCH 0839/1038] Add config flow to venstar (#58152) --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/venstar/__init__.py | 108 +++++++++++++++ homeassistant/components/venstar/climate.py | 85 +++++------- .../components/venstar/config_flow.py | 96 +++++++++++++ homeassistant/components/venstar/const.py | 29 ++++ .../components/venstar/manifest.json | 7 +- homeassistant/components/venstar/strings.json | 23 ++++ .../components/venstar/translations/en.json | 20 +++ homeassistant/generated/config_flows.py | 1 + tests/components/venstar/__init__.py | 61 +++++++++ tests/components/venstar/test_config_flow.py | 128 ++++++++++++++++++ tests/components/venstar/test_init.py | 71 ++++++++++ 13 files changed, 578 insertions(+), 53 deletions(-) create mode 100644 homeassistant/components/venstar/config_flow.py create mode 100644 homeassistant/components/venstar/const.py create mode 100644 homeassistant/components/venstar/strings.json create mode 100644 homeassistant/components/venstar/translations/en.json create mode 100644 tests/components/venstar/test_config_flow.py create mode 100644 tests/components/venstar/test_init.py diff --git a/.coveragerc b/.coveragerc index a4a221db69a..bb84820783b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1168,6 +1168,7 @@ omit = homeassistant/components/velbus/sensor.py homeassistant/components/velbus/switch.py homeassistant/components/velux/* + homeassistant/components/venstar/__init__.py homeassistant/components/venstar/climate.py homeassistant/components/verisure/__init__.py homeassistant/components/verisure/alarm_control_panel.py diff --git a/CODEOWNERS b/CODEOWNERS index e2494b8299a..12ab4d6b472 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -563,6 +563,7 @@ homeassistant/components/utility_meter/* @dgomes homeassistant/components/vallox/* @andre-richter homeassistant/components/velbus/* @Cereal2nd @brefra homeassistant/components/velux/* @Julius2342 +homeassistant/components/venstar/* @garbled1 homeassistant/components/vera/* @pavoni homeassistant/components/verisure/* @frenck homeassistant/components/versasense/* @flamm3blemuff1n diff --git a/homeassistant/components/venstar/__init__.py b/homeassistant/components/venstar/__init__.py index abc35a0d6bd..27d3e77754a 100644 --- a/homeassistant/components/venstar/__init__.py +++ b/homeassistant/components/venstar/__init__.py @@ -1 +1,109 @@ """The venstar component.""" +import asyncio + +from requests import RequestException +from venstarcolortouch import VenstarColorTouch + +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PIN, + CONF_SSL, + CONF_USERNAME, +) +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.entity import Entity + +from .const import _LOGGER, DOMAIN, VENSTAR_TIMEOUT + +PLATFORMS = ["climate"] + + +async def async_setup_entry(hass, config): + """Set up the Venstar thermostat.""" + username = config.data.get(CONF_USERNAME) + password = config.data.get(CONF_PASSWORD) + pin = config.data.get(CONF_PIN) + host = config.data[CONF_HOST] + timeout = VENSTAR_TIMEOUT + protocol = "https" if config.data[CONF_SSL] else "http" + + client = VenstarColorTouch( + addr=host, + timeout=timeout, + user=username, + password=password, + pin=pin, + proto=protocol, + ) + + try: + await hass.async_add_executor_job(client.update_info) + except (OSError, RequestException) as ex: + raise ConfigEntryNotReady(f"Unable to connect to the thermostat: {ex}") from ex + hass.data.setdefault(DOMAIN, {})[config.entry_id] = client + hass.config_entries.async_setup_platforms(config, PLATFORMS) + + return True + + +async def async_unload_entry(hass, config): + """Unload the config config and platforms.""" + unload_ok = await hass.config_entries.async_unload_platforms(config, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(config.entry_id) + return unload_ok + + +class VenstarEntity(Entity): + """Get the latest data and update.""" + + def __init__(self, config, client): + """Initialize the data object.""" + self._config = config + self._client = client + + async def async_update(self): + """Update the state.""" + try: + info_success = await self.hass.async_add_executor_job( + self._client.update_info + ) + except (OSError, RequestException) as ex: + _LOGGER.error("Exception during info update: %s", ex) + + # older venstars sometimes cannot handle rapid sequential connections + await asyncio.sleep(3) + + try: + sensor_success = await self.hass.async_add_executor_job( + self._client.update_sensors + ) + except (OSError, RequestException) as ex: + _LOGGER.error("Exception during sensor update: %s", ex) + + if not info_success or not sensor_success: + _LOGGER.error("Failed to update data") + + @property + def name(self): + """Return the name of the thermostat.""" + return self._client.name + + @property + def unique_id(self): + """Set unique_id for this entity.""" + return f"{self._config.entry_id}" + + @property + def device_info(self): + """Return the device information for this entity.""" + return { + "identifiers": {(DOMAIN, self._config.entry_id)}, + "name": self._client.name, + "manufacturer": "Venstar", + # pylint: disable=protected-access + "model": f"{self._client.model}-{self._client._type}", + # pylint: disable=protected-access + "sw_version": self._client._api_ver, + } diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index 72e9ecc3de4..d86a5953169 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -1,7 +1,4 @@ """Support for Venstar WiFi Thermostats.""" -import logging - -from venstarcolortouch import VenstarColorTouch import voluptuous as vol from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity @@ -27,6 +24,7 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_TEMPERATURE, CONF_HOST, @@ -42,20 +40,18 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv -_LOGGER = logging.getLogger(__name__) - -ATTR_FAN_STATE = "fan_state" -ATTR_HVAC_STATE = "hvac_mode" - -CONF_HUMIDIFIER = "humidifier" - -DEFAULT_SSL = False - -VALID_FAN_STATES = [STATE_ON, HVAC_MODE_AUTO] -VALID_THERMOSTAT_MODES = [HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_OFF, HVAC_MODE_AUTO] - -HOLD_MODE_OFF = "off" -HOLD_MODE_TEMPERATURE = "temperature" +from . import VenstarEntity +from .const import ( + _LOGGER, + ATTR_FAN_STATE, + ATTR_HVAC_STATE, + CONF_HUMIDIFIER, + DEFAULT_SSL, + DOMAIN, + HOLD_MODE_TEMPERATURE, + VALID_FAN_STATES, + VALID_THERMOSTAT_MODES, +) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -72,50 +68,42 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Venstar thermostat.""" + client = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities([VenstarThermostat(config_entry, client)], True) - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - pin = config.get(CONF_PIN) - host = config.get(CONF_HOST) - timeout = config.get(CONF_TIMEOUT) - humidifier = config.get(CONF_HUMIDIFIER) - protocol = "https" if config[CONF_SSL] else "http" +async def async_setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Venstar thermostat platform. - client = VenstarColorTouch( - addr=host, - timeout=timeout, - user=username, - password=password, - pin=pin, - proto=protocol, + Venstar uses config flow for configuration now. If an entry exists in + configuration.yaml, the import flow will attempt to import it and create + a config entry. + """ + _LOGGER.warning( + "Loading venstar via platform config is deprecated; The configuration" + " has been migrated to a config entry and can be safely removed" ) - - add_entities([VenstarThermostat(client, humidifier)], True) + # No config entry exists and configuration.yaml config exists, trigger the import flow. + if not hass.config_entries.async_entries(DOMAIN): + await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) -class VenstarThermostat(ClimateEntity): +class VenstarThermostat(VenstarEntity, ClimateEntity): """Representation of a Venstar thermostat.""" - def __init__(self, client, humidifier): + def __init__(self, config, client): """Initialize the thermostat.""" - self._client = client - self._humidifier = humidifier + super().__init__(config, client) self._mode_map = { HVAC_MODE_HEAT: self._client.MODE_HEAT, HVAC_MODE_COOL: self._client.MODE_COOL, HVAC_MODE_AUTO: self._client.MODE_AUTO, } - def update(self): - """Update the data from the thermostat.""" - info_success = self._client.update_info() - sensor_success = self._client.update_sensors() - if not info_success or not sensor_success: - _LOGGER.error("Failed to update data") - @property def supported_features(self): """Return the list of supported features.""" @@ -124,16 +112,11 @@ class VenstarThermostat(ClimateEntity): if self._client.mode == self._client.MODE_AUTO: features |= SUPPORT_TARGET_TEMPERATURE_RANGE - if self._humidifier and self._client.hum_setpoint is not None: + if self._client.hum_setpoint is not None: features |= SUPPORT_TARGET_HUMIDITY return features - @property - def name(self): - """Return the name of the thermostat.""" - return self._client.name - @property def precision(self): """Return the precision of the system. diff --git a/homeassistant/components/venstar/config_flow.py b/homeassistant/components/venstar/config_flow.py new file mode 100644 index 00000000000..d97c5ada9e6 --- /dev/null +++ b/homeassistant/components/venstar/config_flow.py @@ -0,0 +1,96 @@ +"""Config flow to configure the Venstar integration.""" +from venstarcolortouch import VenstarColorTouch +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PIN, + CONF_SSL, + CONF_USERNAME, +) + +from .const import _LOGGER, DOMAIN, VENSTAR_TIMEOUT + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Optional(CONF_USERNAME): str, + vol.Optional(CONF_PASSWORD): str, + vol.Optional(CONF_PIN): str, + vol.Optional(CONF_SSL, default=False): bool, + } +) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect.""" + username = data.get(CONF_USERNAME) + password = data.get(CONF_PASSWORD) + pin = data.get(CONF_PIN) + host = data[CONF_HOST] + timeout = VENSTAR_TIMEOUT + protocol = "https" if data[CONF_SSL] else "http" + + client = VenstarColorTouch( + addr=host, + timeout=timeout, + user=username, + password=password, + pin=pin, + proto=protocol, + ) + + # perform a full info pull, because this calls login also. + + info_success = await hass.async_add_executor_job(client.update_info) + if not info_success: + raise CannotConnect + + return {"title": client.name} + + +class VenstarConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a venstar config flow.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Create config entry. Show the setup form to the user.""" + errors = {} + info = {} + + if user_input is not None: + self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) + + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + 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=DATA_SCHEMA, errors=errors + ) + + async def async_step_import(self, import_data): + """Import entry from configuration.yaml.""" + self._async_abort_entries_match({CONF_HOST: import_data[CONF_HOST]}) + return await self.async_step_user( + { + CONF_HOST: import_data[CONF_HOST], + CONF_USERNAME: import_data.get(CONF_USERNAME), + CONF_PASSWORD: import_data.get(CONF_PASSWORD), + CONF_PIN: import_data.get(CONF_PIN), + CONF_SSL: import_data[CONF_SSL], + } + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/venstar/const.py b/homeassistant/components/venstar/const.py new file mode 100644 index 00000000000..999e08384dd --- /dev/null +++ b/homeassistant/components/venstar/const.py @@ -0,0 +1,29 @@ +"""The venstar component.""" +import logging + +from homeassistant.components.climate.const import ( + HVAC_MODE_AUTO, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, +) +from homeassistant.const import STATE_ON + +DOMAIN = "venstar" + +ATTR_FAN_STATE = "fan_state" +ATTR_HVAC_STATE = "hvac_mode" + +CONF_HUMIDIFIER = "humidifier" + +DEFAULT_SSL = False + +VALID_FAN_STATES = [STATE_ON, HVAC_MODE_AUTO] +VALID_THERMOSTAT_MODES = [HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_OFF, HVAC_MODE_AUTO] + +HOLD_MODE_OFF = "off" +HOLD_MODE_TEMPERATURE = "temperature" + +VENSTAR_TIMEOUT = 5 + +_LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/venstar/manifest.json b/homeassistant/components/venstar/manifest.json index 444a3fabf9a..943790b532e 100644 --- a/homeassistant/components/venstar/manifest.json +++ b/homeassistant/components/venstar/manifest.json @@ -1,8 +1,11 @@ { "domain": "venstar", "name": "Venstar", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/venstar", - "requirements": ["venstarcolortouch==0.14"], - "codeowners": [], + "requirements": [ + "venstarcolortouch==0.14" + ], + "codeowners": ["@garbled1"], "iot_class": "local_polling" } diff --git a/homeassistant/components/venstar/strings.json b/homeassistant/components/venstar/strings.json new file mode 100644 index 00000000000..9b031d94188 --- /dev/null +++ b/homeassistant/components/venstar/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "title": "Connect to the Venstar Thermostat", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "pin": "[%key:common::config_flow::data::pin%]", + "ssl": "[%key:common::config_flow::data::ssl%]" + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + } +} diff --git a/homeassistant/components/venstar/translations/en.json b/homeassistant/components/venstar/translations/en.json new file mode 100644 index 00000000000..e56d0fe1a83 --- /dev/null +++ b/homeassistant/components/venstar/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "title": "Connect to the Venstar Thermostat", + "data": { + "host": "Hostname or IP", + "username": "Username for thermostat (optional)", + "password": "Password for thermostat (optional)", + "pin": "Pin for Lockscreen (required if lock screen enabled)", + "ssl": "Whether to use SSL or not when communicating" + } + } + }, + "error": { + "cannot_connect": "Unable to connect to thermostat, please validate username/password if supplied, hostname/ip, and that LOCAL API is enabled on the thermostat.", + "unknown": "An unknown error has occurred." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 39beb7bcb10..dba466e181c 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -306,6 +306,7 @@ FLOWS = [ "upnp", "uptimerobot", "velbus", + "venstar", "vera", "verisure", "vesync", diff --git a/tests/components/venstar/__init__.py b/tests/components/venstar/__init__.py index 908755a585f..326aeeeb0e2 100644 --- a/tests/components/venstar/__init__.py +++ b/tests/components/venstar/__init__.py @@ -1 +1,62 @@ """Tests for the venstar integration.""" +from requests import RequestException + + +class VenstarColorTouchMock: + """Mock Venstar Library.""" + + def __init__( + self, + addr, + timeout, + user=None, + password=None, + pin=None, + proto="http", + SSLCert=False, + ): + """Initialize the Venstar library.""" + self.status = {} + self.model = "COLORTOUCH" + self._api_ver = 5 + self.name = "TestVenstar" + self._info = {} + self._sensors = {} + self.alerts = {} + self.MODE_OFF = 0 + self.MODE_HEAT = 1 + self.MODE_COOL = 2 + self.MODE_AUTO = 3 + self._type = "residential" + + def login(self): + """Mock login.""" + return True + + def _request(self, path, data=None): + """Mock request.""" + self.status = {} + + def update(self): + """Mock update.""" + return True + + def update_info(self): + """Mock update_info.""" + return True + + def broken_update_info(self): + """Mock a update_info that raises Exception.""" + raise RequestException + + def update_sensors(self): + """Mock update_sensors.""" + return True + + def update_runtimes(self): + """Mock update_runtimes.""" + return True + + def update_alerts(self): + """Mock update_alerts.""" + return True diff --git a/tests/components/venstar/test_config_flow.py b/tests/components/venstar/test_config_flow.py new file mode 100644 index 00000000000..f568655ec8d --- /dev/null +++ b/tests/components/venstar/test_config_flow.py @@ -0,0 +1,128 @@ +"""Test the Venstar config flow.""" +import logging +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.venstar.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PIN, + CONF_SSL, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from . import VenstarColorTouchMock + +from tests.common import MockConfigEntry + +_LOGGER = logging.getLogger(__name__) + +TEST_DATA = { + CONF_HOST: "1.1.1.1", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_PIN: "test-pin", + CONF_SSL: False, +} +TEST_ID = "VenstarUniqueID" + + +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"] == {} + + with patch( + "homeassistant.components.venstar.config_flow.VenstarColorTouch.update_info", + new=VenstarColorTouchMock.update_info, + ), patch( + "homeassistant.components.venstar.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + TEST_DATA, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["data"] == TEST_DATA + assert len(mock_setup_entry.mock_calls) == 1 + + +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.venstar.config_flow.VenstarColorTouch.update_info", + return_value=False, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + TEST_DATA, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +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.venstar.config_flow.VenstarColorTouch.update_info", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + TEST_DATA, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_already_configured(hass: HomeAssistant) -> None: + """Test when provided credentials are already configured.""" + MockConfigEntry(domain=DOMAIN, data=TEST_DATA, unique_id=TEST_ID).add_to_hass(hass) + + 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"] == SOURCE_USER + + with patch( + "homeassistant.components.venstar.VenstarColorTouch.update_info", + new=VenstarColorTouchMock.update_info, + ), patch( + "homeassistant.components.venstar.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + TEST_DATA, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "already_configured" diff --git a/tests/components/venstar/test_init.py b/tests/components/venstar/test_init.py new file mode 100644 index 00000000000..03739f19616 --- /dev/null +++ b/tests/components/venstar/test_init.py @@ -0,0 +1,71 @@ +"""Tests of the initialization of the venstar integration.""" +from unittest.mock import patch + +from homeassistant.components.venstar.const import DOMAIN as VENSTAR_DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_HOST, CONF_SSL +from homeassistant.core import HomeAssistant + +from . import VenstarColorTouchMock + +from tests.common import MockConfigEntry + +TEST_HOST = "venstartest.localdomain" + + +async def test_setup_entry(hass: HomeAssistant): + """Validate that setup entry also configure the client.""" + config_entry = MockConfigEntry( + domain=VENSTAR_DOMAIN, + data={ + CONF_HOST: TEST_HOST, + CONF_SSL: False, + }, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.venstar.VenstarColorTouch._request", + new=VenstarColorTouchMock._request, + ), patch( + "homeassistant.components.venstar.VenstarColorTouch.update_sensors", + new=VenstarColorTouchMock.update_sensors, + ), patch( + "homeassistant.components.venstar.VenstarColorTouch.update_info", + new=VenstarColorTouchMock.update_info, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.LOADED + + await hass.config_entries.async_unload(config_entry.entry_id) + + assert config_entry.state == ConfigEntryState.NOT_LOADED + + +async def test_setup_entry_exception(hass: HomeAssistant): + """Validate that setup entry also configure the client.""" + config_entry = MockConfigEntry( + domain=VENSTAR_DOMAIN, + data={ + CONF_HOST: TEST_HOST, + CONF_SSL: False, + }, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.venstar.VenstarColorTouch._request", + new=VenstarColorTouchMock._request, + ), patch( + "homeassistant.components.venstar.VenstarColorTouch.update_sensors", + new=VenstarColorTouchMock.update_sensors, + ), patch( + "homeassistant.components.venstar.VenstarColorTouch.update_info", + new=VenstarColorTouchMock.broken_update_info, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.SETUP_RETRY From fbdd4459990acce97225a3672044b66e01eaf4c7 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 25 Oct 2021 15:52:14 -0600 Subject: [PATCH 0840/1038] Add WattTime config option for showing the monitored location on the map (#57129) * Add WattTime configuration option for showing the monitored location on the map * Update tests * Explicitly pass entry * Tests --- homeassistant/components/watttime/__init__.py | 7 +++ .../components/watttime/config_flow.py | 35 ++++++++++++++ homeassistant/components/watttime/const.py | 1 + homeassistant/components/watttime/sensor.py | 46 +++++++++++++------ .../components/watttime/strings.json | 10 ++++ .../components/watttime/translations/en.json | 10 ++++ tests/components/watttime/test_config_flow.py | 42 +++++++++++++---- 7 files changed, 127 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/watttime/__init__.py b/homeassistant/components/watttime/__init__.py index a4e1acb4b7e..779fc0791b6 100644 --- a/homeassistant/components/watttime/__init__.py +++ b/homeassistant/components/watttime/__init__.py @@ -69,6 +69,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(async_reload_entry)) + return True @@ -79,3 +81,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Handle an options update.""" + await hass.config_entries.async_reload(config_entry.entry_id) diff --git a/homeassistant/components/watttime/config_flow.py b/homeassistant/components/watttime/config_flow.py index a00ba4c8c86..415697670b0 100644 --- a/homeassistant/components/watttime/config_flow.py +++ b/homeassistant/components/watttime/config_flow.py @@ -8,6 +8,7 @@ from aiowatttime.errors import CoordinatesNotFoundError, InvalidCredentialsError import voluptuous as vol from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, OptionsFlow from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, @@ -22,6 +23,7 @@ from homeassistant.helpers.typing import ConfigType from .const import ( CONF_BALANCING_AUTHORITY, CONF_BALANCING_AUTHORITY_ABBREV, + CONF_SHOW_ON_MAP, DOMAIN, LOGGER, ) @@ -118,6 +120,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._data[CONF_PASSWORD] = password return await self.async_step_location() + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: + """Define the config flow to handle options.""" + return WattTimeOptionsFlowHandler(config_entry) + async def async_step_coordinates( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -222,3 +230,30 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): "user", STEP_USER_DATA_SCHEMA, ) + + +class WattTimeOptionsFlowHandler(config_entries.OptionsFlow): + """Handle a WattTime options flow.""" + + def __init__(self, entry: ConfigEntry) -> None: + """Initialize.""" + self.entry = entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Required( + CONF_SHOW_ON_MAP, + default=self.entry.options.get(CONF_SHOW_ON_MAP, True), + ): bool + } + ), + ) diff --git a/homeassistant/components/watttime/const.py b/homeassistant/components/watttime/const.py index 680505c8d43..07ea0e47167 100644 --- a/homeassistant/components/watttime/const.py +++ b/homeassistant/components/watttime/const.py @@ -7,5 +7,6 @@ LOGGER = logging.getLogger(__package__) CONF_BALANCING_AUTHORITY = "balancing_authority" CONF_BALANCING_AUTHORITY_ABBREV = "balancing_authority_abbreviation" +CONF_SHOW_ON_MAP = "show_on_map" DATA_COORDINATOR = "coordinator" diff --git a/homeassistant/components/watttime/sensor.py b/homeassistant/components/watttime/sensor.py index 50389de35c6..b1ebc262134 100644 --- a/homeassistant/components/watttime/sensor.py +++ b/homeassistant/components/watttime/sensor.py @@ -1,7 +1,8 @@ """Support for WattTime sensors.""" from __future__ import annotations -from typing import TYPE_CHECKING, cast +from collections.abc import Mapping +from typing import Any, cast from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, @@ -27,6 +28,7 @@ from homeassistant.helpers.update_coordinator import ( from .const import ( CONF_BALANCING_AUTHORITY, CONF_BALANCING_AUTHORITY_ABBREV, + CONF_SHOW_ON_MAP, DATA_COORDINATOR, DOMAIN, ) @@ -64,7 +66,7 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] async_add_entities( [ - RealtimeEmissionsSensor(coordinator, description) + RealtimeEmissionsSensor(coordinator, entry, description) for description in REALTIME_EMISSIONS_SENSOR_DESCRIPTIONS if description.key in coordinator.data ] @@ -77,26 +79,40 @@ class RealtimeEmissionsSensor(CoordinatorEntity, SensorEntity): def __init__( self, coordinator: DataUpdateCoordinator, + entry: ConfigEntry, description: SensorEntityDescription, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) - if TYPE_CHECKING: - assert coordinator.config_entry - - self._attr_extra_state_attributes = { - ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION, - ATTR_BALANCING_AUTHORITY: coordinator.config_entry.data[ - CONF_BALANCING_AUTHORITY - ], - ATTR_LATITUDE: coordinator.config_entry.data[ATTR_LATITUDE], - ATTR_LONGITUDE: coordinator.config_entry.data[ATTR_LONGITUDE], - } - self._attr_name = f"{description.name} ({coordinator.config_entry.data[CONF_BALANCING_AUTHORITY_ABBREV]})" - self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}" + self._attr_name = ( + f"{description.name} ({entry.data[CONF_BALANCING_AUTHORITY_ABBREV]})" + ) + self._attr_unique_id = f"{entry.entry_id}_{description.key}" + self._entry = entry self.entity_description = description + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return entity specific state attributes.""" + attrs = { + ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION, + ATTR_BALANCING_AUTHORITY: self._entry.data[CONF_BALANCING_AUTHORITY], + } + + # Displaying the geography on the map relies upon putting the latitude/longitude + # in the entity attributes with "latitude" and "longitude" as the keys. + # Conversely, we can hide the location on the map by using other keys, like + # "lati" and "long". + if self._entry.options.get(CONF_SHOW_ON_MAP) is not False: + attrs[ATTR_LATITUDE] = self._entry.data[ATTR_LATITUDE] + attrs[ATTR_LONGITUDE] = self._entry.data[ATTR_LONGITUDE] + else: + attrs["lati"] = self._entry.data[ATTR_LATITUDE] + attrs["long"] = self._entry.data[ATTR_LONGITUDE] + + return attrs + @property def native_value(self) -> StateType: """Return the value reported by the sensor.""" diff --git a/homeassistant/components/watttime/strings.json b/homeassistant/components/watttime/strings.json index 594848afce1..1650856a669 100644 --- a/homeassistant/components/watttime/strings.json +++ b/homeassistant/components/watttime/strings.json @@ -38,5 +38,15 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "options": { + "step": { + "init": { + "title": "Configure WattTime", + "data": { + "show_on_map": "Show monitored location on the map" + } + } + } } } diff --git a/homeassistant/components/watttime/translations/en.json b/homeassistant/components/watttime/translations/en.json index 49b264851f2..a261615ea81 100644 --- a/homeassistant/components/watttime/translations/en.json +++ b/homeassistant/components/watttime/translations/en.json @@ -38,5 +38,15 @@ "description": "Input your username and password:" } } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Show monitored location on the map" + }, + "title": "Configure WattTime" + } + } } } \ No newline at end of file diff --git a/tests/components/watttime/test_config_flow.py b/tests/components/watttime/test_config_flow.py index 672f294c099..de6e16a400a 100644 --- a/tests/components/watttime/test_config_flow.py +++ b/tests/components/watttime/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, patch from aiowatttime.errors import CoordinatesNotFoundError, InvalidCredentialsError import pytest -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components.watttime.config_flow import ( CONF_LOCATION_TYPE, LOCATION_TYPE_COORDINATES, @@ -13,6 +13,7 @@ from homeassistant.components.watttime.config_flow import ( from homeassistant.components.watttime.const import ( CONF_BALANCING_AUTHORITY, CONF_BALANCING_AUTHORITY_ABBREV, + CONF_SHOW_ON_MAP, DOMAIN, ) from homeassistant.const import ( @@ -77,12 +78,42 @@ async def test_duplicate_error(hass: HomeAssistant, client_login): result["flow_id"], user_input={CONF_LOCATION_TYPE: LOCATION_TYPE_HOME}, ) - await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" +async def test_options_flow(hass): + """Test config flow options.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="32.87336, -117.22743", + data={ + CONF_USERNAME: "user", + CONF_PASSWORD: "password", + CONF_LATITUDE: 32.87336, + CONF_LONGITUDE: -117.22743, + }, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.watttime.async_setup_entry", return_value=True + ): + await hass.config_entries.async_setup(entry.entry_id) + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_SHOW_ON_MAP: False} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert entry.options == {CONF_SHOW_ON_MAP: False} + + async def test_show_form_coordinates(hass: HomeAssistant, client_login) -> None: """Test showing the form to input custom latitude/longitude.""" result = await hass.config_entries.flow.async_init( @@ -97,7 +128,6 @@ async def test_show_form_coordinates(hass: HomeAssistant, client_login) -> None: user_input={CONF_LOCATION_TYPE: LOCATION_TYPE_COORDINATES}, ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "coordinates" @@ -109,7 +139,6 @@ async def test_show_form_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -137,7 +166,6 @@ async def test_step_coordinates_unknown_coordinates( result["flow_id"], user_input={CONF_LATITUDE: "0", CONF_LONGITUDE: "0"}, ) - await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {"latitude": "unknown_coordinates"} @@ -158,7 +186,6 @@ async def test_step_coordinates_unknown_error( result["flow_id"], user_input={CONF_LOCATION_TYPE: LOCATION_TYPE_HOME}, ) - await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {"base": "unknown"} @@ -241,7 +268,6 @@ async def test_step_reauth_invalid_credentials(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_PASSWORD: "password"}, ) - await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {"base": "invalid_auth"} @@ -323,7 +349,6 @@ async def test_step_user_invalid_credentials(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data={CONF_USERNAME: "user", CONF_PASSWORD: "password"}, ) - await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {"base": "invalid_auth"} @@ -342,7 +367,6 @@ async def test_step_user_unknown_error(hass: HomeAssistant, client_login) -> Non context={"source": config_entries.SOURCE_USER}, data={CONF_USERNAME: "user", CONF_PASSWORD: "password"}, ) - await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {"base": "unknown"} From b71773fd1d93188ca25505ae71a37c57a65fa239 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 25 Oct 2021 17:54:13 -0400 Subject: [PATCH 0841/1038] Set entity_category for node status sensor (#58434) --- homeassistant/components/zwave_js/sensor.py | 2 +- tests/components/zwave_js/test_sensor.py | 21 +++------------------ 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 815158ee967..715affe351e 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -456,7 +456,7 @@ class ZWaveNodeStatusSensor(SensorEntity): """Representation of a node status sensor.""" _attr_should_poll = False - _attr_entity_registry_enabled_default = False + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__( self, config_entry: ConfigEntry, client: ZwaveClient, node: ZwaveNode diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index 9e77d877b5c..de290e14760 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -166,16 +166,9 @@ async def test_node_status_sensor(hass, client, lock_id_lock_as_id150, integrati node = lock_id_lock_as_id150 ent_reg = er.async_get(hass) entity_entry = ent_reg.async_get(NODE_STATUS_ENTITY) - assert entity_entry.disabled - assert entity_entry.disabled_by == er.DISABLED_INTEGRATION - updated_entry = ent_reg.async_update_entity( - entity_entry.entity_id, **{"disabled_by": None} - ) - await hass.config_entries.async_reload(integration.entry_id) - await hass.async_block_till_done() - - assert not updated_entry.disabled + assert not entity_entry.disabled + assert entity_entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC assert hass.states.get(NODE_STATUS_ENTITY).state == "alive" # Test transitions work @@ -227,16 +220,8 @@ async def test_node_status_sensor_not_ready( assert not node.ready ent_reg = er.async_get(hass) entity_entry = ent_reg.async_get(NODE_STATUS_ENTITY) - assert entity_entry.disabled - assert entity_entry.disabled_by == er.DISABLED_INTEGRATION - updated_entry = ent_reg.async_update_entity( - entity_entry.entity_id, **{"disabled_by": None} - ) - await hass.config_entries.async_reload(integration.entry_id) - await hass.async_block_till_done() - - assert not updated_entry.disabled + assert not entity_entry.disabled assert hass.states.get(NODE_STATUS_ENTITY) assert hass.states.get(NODE_STATUS_ENTITY).state == "alive" From 8b223be0733dbe31aa66514cd4f5d82152cbf3aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 26 Oct 2021 00:21:44 +0200 Subject: [PATCH 0842/1038] Add binary sensor to add-ons to show if they are running (#58120) --- .../components/hassio/binary_sensor.py | 31 ++++++++++++++++--- homeassistant/components/hassio/const.py | 1 + 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/hassio/binary_sensor.py b/homeassistant/components/hassio/binary_sensor.py index dfd13adbde6..8953bd47942 100644 --- a/homeassistant/components/hassio/binary_sensor.py +++ b/homeassistant/components/hassio/binary_sensor.py @@ -1,7 +1,10 @@ """Binary sensor platform for Hass.io addons.""" from __future__ import annotations +from dataclasses import dataclass + from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_RUNNING, DEVICE_CLASS_UPDATE, BinarySensorEntity, BinarySensorEntityDescription, @@ -11,16 +14,31 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ADDONS_COORDINATOR -from .const import ATTR_UPDATE_AVAILABLE, DATA_KEY_ADDONS, DATA_KEY_OS +from .const import ATTR_STATE, ATTR_UPDATE_AVAILABLE, DATA_KEY_ADDONS, DATA_KEY_OS from .entity import HassioAddonEntity, HassioOSEntity + +@dataclass +class HassioBinarySensorEntityDescription(BinarySensorEntityDescription): + """Hassio binary sensor entity description.""" + + target: str | None = None + + ENTITY_DESCRIPTIONS = ( - BinarySensorEntityDescription( + HassioBinarySensorEntityDescription( device_class=DEVICE_CLASS_UPDATE, entity_registry_enabled_default=False, key=ATTR_UPDATE_AVAILABLE, name="Update Available", ), + HassioBinarySensorEntityDescription( + device_class=DEVICE_CLASS_RUNNING, + entity_registry_enabled_default=False, + key=ATTR_STATE, + name="Running", + target="started", + ), ) @@ -56,14 +74,19 @@ async def async_setup_entry( class HassioAddonBinarySensor(HassioAddonEntity, BinarySensorEntity): - """Binary sensor to track whether an update is available for a Hass.io add-on.""" + """Binary sensor for Hass.io add-ons.""" + + entity_description: HassioBinarySensorEntityDescription @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" - return self.coordinator.data[DATA_KEY_ADDONS][self._addon_slug][ + value = self.coordinator.data[DATA_KEY_ADDONS][self._addon_slug][ self.entity_description.key ] + if self.entity_description.target is None: + return value + return value == self.entity_description.target class HassioOSBinarySensor(HassioOSEntity, BinarySensorEntity): diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index 1b24013163f..540d00b4906 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -45,6 +45,7 @@ ATTR_UPDATE_AVAILABLE = "update_available" ATTR_CPU_PERCENT = "cpu_percent" ATTR_MEMORY_PERCENT = "memory_percent" ATTR_SLUG = "slug" +ATTR_STATE = "state" ATTR_URL = "url" ATTR_REPOSITORY = "repository" From a81360818599de2dafeb446257de3a45b062973e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 25 Oct 2021 16:45:13 -0600 Subject: [PATCH 0843/1038] Re-add support for realtime SimpliSafe websocket (#58061) * Migrate SimpliSafe to new web-based authentication * Ensure we're storing data correctly * Re-add support for realtime websocket in SimpliSafe * Updates * Better lock state from websocket * Unknown states * Streamline * Unnecessary assertion * Remove conditions we can't reach * Require multiple error states from REST API before reacting * Only disconnect when needed * Typing * Code review --- .../components/simplisafe/__init__.py | 244 ++++++++++++++++-- .../simplisafe/alarm_control_panel.py | 97 ++++++- homeassistant/components/simplisafe/lock.py | 30 ++- .../components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 342 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index fb6e09a202b..ae5c3cd9527 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -2,12 +2,12 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Iterable from datetime import timedelta -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, Any, cast from simplipy import API -from simplipy.device import Device +from simplipy.device import Device, DeviceTypes from simplipy.errors import ( EndpointUnavailableError, InvalidCredentialsError, @@ -22,17 +22,42 @@ from simplipy.system.v3 import ( VOLUME_OFF, SystemV3, ) +from simplipy.websocket import ( + EVENT_AUTOMATIC_TEST, + EVENT_CAMERA_MOTION_DETECTED, + EVENT_CONNECTION_LOST, + EVENT_CONNECTION_RESTORED, + EVENT_DEVICE_TEST, + EVENT_DOORBELL_DETECTED, + EVENT_LOCK_LOCKED, + EVENT_LOCK_UNLOCKED, + EVENT_POWER_OUTAGE, + EVENT_POWER_RESTORED, + EVENT_SECRET_ALERT_TRIGGERED, + EVENT_SENSOR_PAIRED_AND_NAMED, + EVENT_USER_INITIATED_TEST, + WebsocketEvent, +) import voluptuous as vol from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_CODE, CONF_CODE, CONF_TOKEN -from homeassistant.core import CoreState, HomeAssistant, ServiceCall, callback +from homeassistant.const import ( + ATTR_CODE, + CONF_CODE, + CONF_TOKEN, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.core import CoreState, Event, HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import ( aiohttp_client, config_validation as cv, device_registry as dr, ) +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.service import ( async_register_admin_service, @@ -60,13 +85,32 @@ from .const import ( LOGGER, ) -EVENT_SIMPLISAFE_NOTIFICATION = "SIMPLISAFE_NOTIFICATION" +ATTR_CATEGORY = "category" +ATTR_LAST_EVENT_CHANGED_BY = "last_event_changed_by" +ATTR_LAST_EVENT_INFO = "last_event_info" +ATTR_LAST_EVENT_SENSOR_NAME = "last_event_sensor_name" +ATTR_LAST_EVENT_SENSOR_SERIAL = "last_event_sensor_serial" +ATTR_LAST_EVENT_SENSOR_TYPE = "last_event_sensor_type" +ATTR_LAST_EVENT_TIMESTAMP = "last_event_timestamp" +ATTR_LAST_EVENT_TYPE = "last_event_type" +ATTR_LAST_EVENT_TYPE = "last_event_type" +ATTR_MESSAGE = "message" +ATTR_PIN_LABEL = "label" +ATTR_PIN_LABEL_OR_VALUE = "label_or_pin" +ATTR_PIN_VALUE = "pin" +ATTR_SYSTEM_ID = "system_id" +ATTR_TIMESTAMP = "timestamp" DEFAULT_ENTITY_MODEL = "alarm_control_panel" DEFAULT_ENTITY_NAME = "Alarm Control Panel" DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) DEFAULT_SOCKET_MIN_RETRY = 15 +DISPATCHER_TOPIC_WEBSOCKET_EVENT = "simplisafe_websocket_event_{0}" + +EVENT_SIMPLISAFE_EVENT = "SIMPLISAFE_EVENT" +EVENT_SIMPLISAFE_NOTIFICATION = "SIMPLISAFE_NOTIFICATION" + PLATFORMS = ( "alarm_control_panel", "binary_sensor", @@ -74,14 +118,6 @@ PLATFORMS = ( "sensor", ) -ATTR_CATEGORY = "category" -ATTR_MESSAGE = "message" -ATTR_PIN_LABEL = "label" -ATTR_PIN_LABEL_OR_VALUE = "label_or_pin" -ATTR_PIN_VALUE = "pin" -ATTR_SYSTEM_ID = "system_id" -ATTR_TIMESTAMP = "timestamp" - VOLUMES = [VOLUME_OFF, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_HIGH] SERVICE_BASE_SCHEMA = vol.Schema({vol.Required(ATTR_SYSTEM_ID): cv.positive_int}) @@ -126,6 +162,17 @@ SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = SERVICE_BASE_SCHEMA.extend( } ) +WEBSOCKET_EVENTS_REQUIRING_SERIAL = [EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED] +WEBSOCKET_EVENTS_TO_FIRE_HASS_EVENT = [ + EVENT_AUTOMATIC_TEST, + EVENT_CAMERA_MOTION_DETECTED, + EVENT_DOORBELL_DETECTED, + EVENT_DEVICE_TEST, + EVENT_SECRET_ALERT_TRIGGERED, + EVENT_SENSOR_PAIRED_AND_NAMED, + EVENT_USER_INITIATED_TEST, +] + CONFIG_SCHEMA = cv.deprecated(DOMAIN) @@ -317,6 +364,7 @@ class SimpliSafe: self._hass = hass self._system_notifications: dict[int, set[SystemNotification]] = {} self.entry = entry + self.initial_event_to_use: dict[int, dict[str, Any]] = {} self.systems: dict[int, SystemV2 | SystemV3] = {} # This will get filled in by async_init: @@ -359,8 +407,68 @@ class SimpliSafe: self._system_notifications[system.system_id] = latest_notifications + async def _async_websocket_on_connect(self) -> None: + """Define a callback for connecting to the websocket.""" + if TYPE_CHECKING: + assert self._api.websocket + await self._api.websocket.async_listen() + + @callback + def _async_websocket_on_event(self, event: WebsocketEvent) -> None: + """Define a callback for receiving a websocket event.""" + LOGGER.debug("New websocket event: %s", event) + + async_dispatcher_send( + self._hass, DISPATCHER_TOPIC_WEBSOCKET_EVENT.format(event.system_id), event + ) + + if event.event_type not in WEBSOCKET_EVENTS_TO_FIRE_HASS_EVENT: + return + + sensor_type: str | None + if event.sensor_type: + sensor_type = event.sensor_type.name + else: + sensor_type = None + + self._hass.bus.async_fire( + EVENT_SIMPLISAFE_EVENT, + event_data={ + ATTR_LAST_EVENT_CHANGED_BY: event.changed_by, + ATTR_LAST_EVENT_TYPE: event.event_type, + ATTR_LAST_EVENT_INFO: event.info, + ATTR_LAST_EVENT_SENSOR_NAME: event.sensor_name, + ATTR_LAST_EVENT_SENSOR_SERIAL: event.sensor_serial, + ATTR_LAST_EVENT_SENSOR_TYPE: sensor_type, + ATTR_SYSTEM_ID: event.system_id, + ATTR_LAST_EVENT_TIMESTAMP: event.timestamp, + }, + ) + async def async_init(self) -> None: - """Initialize the data class.""" + """Initialize the SimpliSafe "manager" class.""" + if TYPE_CHECKING: + assert self._api.refresh_token + assert self._api.websocket + + self._api.websocket.add_connect_listener(self._async_websocket_on_connect) + self._api.websocket.add_event_listener(self._async_websocket_on_event) + asyncio.create_task(self._api.websocket.async_connect()) + + async def async_websocket_disconnect_listener(_: Event) -> None: + """Define an event handler to disconnect from the websocket.""" + if TYPE_CHECKING: + assert self._api.websocket + + if self._api.websocket.connected: + await self._api.websocket.async_disconnect() + + self.entry.async_on_unload( + self._hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, async_websocket_disconnect_listener + ) + ) + self.systems = await self._api.async_get_systems() for system in self.systems.values(): self._system_notifications[system.system_id] = set() @@ -369,6 +477,17 @@ class SimpliSafe: async_register_base_station(self._hass, self.entry, system) ) + # Future events will come from the websocket, but since subscription to the + # websocket doesn't provide the most recent event, we grab it from the REST + # API to ensure event-related attributes aren't empty on startup: + try: + self.initial_event_to_use[ + system.system_id + ] = await system.async_get_latest_event() + except SimplipyError as err: + LOGGER.error("Error while fetching initial event: %s", err) + self.initial_event_to_use[system.system_id] = {} + self.coordinator = DataUpdateCoordinator( self._hass, LOGGER, @@ -390,8 +509,6 @@ class SimpliSafe: self._api.add_refresh_token_listener(async_save_refresh_token) ) - if TYPE_CHECKING: - assert self._api.refresh_token async_save_refresh_token(self._api.refresh_token) async def async_update(self) -> None: @@ -428,6 +545,7 @@ class SimpliSafeEntity(CoordinatorEntity): system: SystemV2 | SystemV3, *, device: Device | None = None, + additional_websocket_events: Iterable[str] | None = None, ) -> None: """Initialize.""" assert simplisafe.coordinator @@ -442,7 +560,23 @@ class SimpliSafeEntity(CoordinatorEntity): device_name = DEFAULT_ENTITY_NAME serial = system.serial - self._attr_extra_state_attributes = {ATTR_SYSTEM_ID: system.system_id} + try: + device_type = DeviceTypes( + simplisafe.initial_event_to_use[system.system_id].get("sensorType") + ) + except ValueError: + device_type = DeviceTypes.unknown + + event = simplisafe.initial_event_to_use[system.system_id] + + self._attr_extra_state_attributes = { + ATTR_LAST_EVENT_INFO: event.get("info"), + ATTR_LAST_EVENT_SENSOR_NAME: event.get("sensorName"), + ATTR_LAST_EVENT_SENSOR_TYPE: device_type.name, + ATTR_LAST_EVENT_TIMESTAMP: event.get("eventTimestamp"), + ATTR_SYSTEM_ID: system.system_id, + } + self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, serial)}, manufacturer="SimpliSafe", @@ -450,12 +584,21 @@ class SimpliSafeEntity(CoordinatorEntity): name=device_name, via_device=(DOMAIN, system.system_id), ) + self._attr_name = f"{system.address} {device_name} {' '.join([w.title() for w in model.split('_')])}" self._attr_unique_id = serial self._device = device self._online = True self._simplisafe = simplisafe self._system = system + self._websocket_events_to_listen_for = [ + EVENT_CONNECTION_LOST, + EVENT_CONNECTION_RESTORED, + EVENT_POWER_OUTAGE, + EVENT_POWER_RESTORED, + ] + if additional_websocket_events: + self._websocket_events_to_listen_for += additional_websocket_events @property def available(self) -> bool: @@ -478,12 +621,75 @@ class SimpliSafeEntity(CoordinatorEntity): self.async_update_from_rest_api() self.async_write_ha_state() + @callback + def _handle_websocket_update(self, event: WebsocketEvent) -> None: + """Update the entity with new websocket data.""" + # Ignore this event if it belongs to a system other than this one: + if event.system_id != self._system.system_id: + return + + # Ignore this event if this entity hasn't expressed interest in its type: + if event.event_type not in self._websocket_events_to_listen_for: + return + + # Ignore this event if it belongs to a entity with a different serial + # number from this one's: + if ( + self._device + and event.event_type in WEBSOCKET_EVENTS_REQUIRING_SERIAL + and event.sensor_serial != self._device.serial + ): + return + + if event.event_type in (EVENT_CONNECTION_LOST, EVENT_POWER_OUTAGE): + self._online = False + elif event.event_type in (EVENT_CONNECTION_RESTORED, EVENT_POWER_RESTORED): + self._online = True + + # It's uncertain whether SimpliSafe events will still propagate down the + # websocket when the base station is offline. Just in case, we guard against + # further action until connection is restored: + if not self._online: + return + + sensor_type: str | None + if event.sensor_type: + sensor_type = event.sensor_type.name + else: + sensor_type = None + + self._attr_extra_state_attributes.update( + { + ATTR_LAST_EVENT_INFO: event.info, + ATTR_LAST_EVENT_SENSOR_NAME: event.sensor_name, + ATTR_LAST_EVENT_SENSOR_TYPE: sensor_type, + ATTR_LAST_EVENT_TIMESTAMP: event.timestamp, + } + ) + + self.async_update_from_websocket_event(event) + self.async_write_ha_state() + async def async_added_to_hass(self) -> None: """Register callbacks.""" await super().async_added_to_hass() + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + DISPATCHER_TOPIC_WEBSOCKET_EVENT.format(self._system.system_id), + self._handle_websocket_update, + ) + ) + self.async_update_from_rest_api() @callback def async_update_from_rest_api(self) -> None: - """Update the entity with the provided REST API data.""" + """Update the entity when new data comes from the REST API.""" + raise NotImplementedError() + + @callback + def async_update_from_websocket_event(self, event: WebsocketEvent) -> None: + """Update the entity when new data comes from the websocket.""" raise NotImplementedError() diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 1355669129d..6643bd3a1a1 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -1,6 +1,8 @@ """Support for SimpliSafe alarm control panels.""" from __future__ import annotations +from typing import TYPE_CHECKING + from simplipy.errors import SimplipyError from simplipy.system import SystemStates from simplipy.system.v2 import SystemV2 @@ -11,6 +13,20 @@ from simplipy.system.v3 import ( VOLUME_OFF, SystemV3, ) +from simplipy.websocket import ( + EVENT_ALARM_CANCELED, + EVENT_ALARM_TRIGGERED, + EVENT_ARMED_AWAY, + EVENT_ARMED_AWAY_BY_KEYPAD, + EVENT_ARMED_AWAY_BY_REMOTE, + EVENT_ARMED_HOME, + EVENT_AWAY_EXIT_DELAY_BY_KEYPAD, + EVENT_AWAY_EXIT_DELAY_BY_REMOTE, + EVENT_DISARMED_BY_MASTER_PIN, + EVENT_DISARMED_BY_REMOTE, + EVENT_HOME_EXIT_DELAY, + WebsocketEvent, +) from homeassistant.components.alarm_control_panel import ( FORMAT_NUMBER, @@ -56,6 +72,8 @@ ATTR_RF_JAMMING = "rf_jamming" ATTR_WALL_POWER_LEVEL = "wall_power_level" ATTR_WIFI_STRENGTH = "wifi_strength" +DEFAULT_ERRORS_TO_ACCOMMODATE = 2 + VOLUME_STRING_MAP = { VOLUME_HIGH: "high", VOLUME_LOW: "low", @@ -63,6 +81,43 @@ VOLUME_STRING_MAP = { VOLUME_OFF: "off", } +STATE_MAP_FROM_REST_API = { + SystemStates.alarm: STATE_ALARM_TRIGGERED, + SystemStates.away: STATE_ALARM_ARMED_AWAY, + SystemStates.away_count: STATE_ALARM_ARMING, + SystemStates.exit_delay: STATE_ALARM_ARMING, + SystemStates.home: STATE_ALARM_ARMED_HOME, + SystemStates.off: STATE_ALARM_DISARMED, +} + +STATE_MAP_FROM_WEBSOCKET_EVENT = { + EVENT_ALARM_CANCELED: STATE_ALARM_DISARMED, + EVENT_ALARM_TRIGGERED: STATE_ALARM_TRIGGERED, + EVENT_ARMED_AWAY: STATE_ALARM_ARMED_AWAY, + EVENT_ARMED_AWAY_BY_KEYPAD: STATE_ALARM_ARMED_AWAY, + EVENT_ARMED_AWAY_BY_REMOTE: STATE_ALARM_ARMED_AWAY, + EVENT_ARMED_HOME: STATE_ALARM_ARMED_HOME, + EVENT_AWAY_EXIT_DELAY_BY_KEYPAD: STATE_ALARM_ARMING, + EVENT_AWAY_EXIT_DELAY_BY_REMOTE: STATE_ALARM_ARMING, + EVENT_DISARMED_BY_MASTER_PIN: STATE_ALARM_DISARMED, + EVENT_DISARMED_BY_REMOTE: STATE_ALARM_DISARMED, + EVENT_HOME_EXIT_DELAY: STATE_ALARM_ARMING, +} + +WEBSOCKET_EVENTS_TO_LISTEN_FOR = ( + EVENT_ALARM_CANCELED, + EVENT_ALARM_TRIGGERED, + EVENT_ARMED_AWAY, + EVENT_ARMED_AWAY_BY_KEYPAD, + EVENT_ARMED_AWAY_BY_REMOTE, + EVENT_ARMED_HOME, + EVENT_AWAY_EXIT_DELAY_BY_KEYPAD, + EVENT_AWAY_EXIT_DELAY_BY_REMOTE, + EVENT_DISARMED_BY_MASTER_PIN, + EVENT_DISARMED_BY_REMOTE, + EVENT_HOME_EXIT_DELAY, +) + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -80,7 +135,13 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): def __init__(self, simplisafe: SimpliSafe, system: SystemV2 | SystemV3) -> None: """Initialize the SimpliSafe alarm.""" - super().__init__(simplisafe, system) + super().__init__( + simplisafe, + system, + additional_websocket_events=WEBSOCKET_EVENTS_TO_LISTEN_FOR, + ) + + self._errors = 0 if code := self._simplisafe.entry.options.get(CONF_CODE): if code.isdigit(): @@ -192,15 +253,29 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanelEntity): } ) - if self._system.state == SystemStates.alarm: - self._attr_state = STATE_ALARM_TRIGGERED - elif self._system.state == SystemStates.away: - self._attr_state = STATE_ALARM_ARMED_AWAY - elif self._system.state in (SystemStates.away_count, SystemStates.exit_delay): - self._attr_state = STATE_ALARM_ARMING - elif self._system.state == SystemStates.home: - self._attr_state = STATE_ALARM_ARMED_HOME - elif self._system.state == SystemStates.off: - self._attr_state = STATE_ALARM_DISARMED + # SimpliSafe can incorrectly return an error state when there isn't any + # error. This can lead to the system having an unknown state frequently. + # To protect against that, we measure how many "error states" we receive + # and only alter the state if we detect a few in a row: + if self._system.state == SystemStates.error: + if self._errors > DEFAULT_ERRORS_TO_ACCOMMODATE: + self._attr_state = None + else: + self._errors += 1 + return + + self._errors = 0 + + if state := STATE_MAP_FROM_REST_API.get(self._system.state): + self._attr_state = state else: + LOGGER.error("Unknown system state (REST API): %s", self._system.state) self._attr_state = None + + @callback + def async_update_from_websocket_event(self, event: WebsocketEvent) -> None: + """Update the entity when new data comes from the websocket.""" + self._attr_changed_by = event.changed_by + if TYPE_CHECKING: + assert event.event_type + self._attr_state = STATE_MAP_FROM_WEBSOCKET_EVENT.get(event.event_type) diff --git a/homeassistant/components/simplisafe/lock.py b/homeassistant/components/simplisafe/lock.py index 3375a413edb..dc09eb0b62e 100644 --- a/homeassistant/components/simplisafe/lock.py +++ b/homeassistant/components/simplisafe/lock.py @@ -1,11 +1,17 @@ """Support for SimpliSafe locks.""" from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any from simplipy.device.lock import Lock, LockStates from simplipy.errors import SimplipyError from simplipy.system.v3 import SystemV3 +from simplipy.websocket import ( + EVENT_LOCK_ERROR, + EVENT_LOCK_LOCKED, + EVENT_LOCK_UNLOCKED, + WebsocketEvent, +) from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry @@ -18,6 +24,14 @@ from .const import DATA_CLIENT, DOMAIN, LOGGER ATTR_LOCK_LOW_BATTERY = "lock_low_battery" ATTR_PIN_PAD_LOW_BATTERY = "pin_pad_low_battery" +STATE_MAP_FROM_WEBSOCKET_EVENT = { + EVENT_LOCK_ERROR: None, + EVENT_LOCK_LOCKED: True, + EVENT_LOCK_UNLOCKED: False, +} + +WEBSOCKET_EVENTS_TO_LISTEN_FOR = (EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED) + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -42,7 +56,12 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity): def __init__(self, simplisafe: SimpliSafe, system: SystemV3, lock: Lock) -> None: """Initialize.""" - super().__init__(simplisafe, system, device=lock) + super().__init__( + simplisafe, + system, + device=lock, + additional_websocket_events=WEBSOCKET_EVENTS_TO_LISTEN_FOR, + ) self._device: Lock @@ -80,3 +99,10 @@ class SimpliSafeLock(SimpliSafeEntity, LockEntity): self._attr_is_jammed = self._device.state == LockStates.jammed self._attr_is_locked = self._device.state == LockStates.locked + + @callback + def async_update_from_websocket_event(self, event: WebsocketEvent) -> None: + """Update the entity when new data comes from the websocket.""" + if TYPE_CHECKING: + assert event.event_type + self._attr_is_locked = STATE_MAP_FROM_WEBSOCKET_EVENT[event.event_type] diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 6f6025308eb..956157a237d 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==12.0.0"], + "requirements": ["simplisafe-python==12.0.2"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 273e48a3f1b..f04ff45af8e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2140,7 +2140,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==12.0.0 +simplisafe-python==12.0.2 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a0b1e78711f..2eb5501a527 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1239,7 +1239,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==12.0.0 +simplisafe-python==12.0.2 # homeassistant.components.slack slackclient==2.5.0 From cd3e51b3e71ddedcef533f76e80fbe9d011b15a7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 25 Oct 2021 15:56:07 -0700 Subject: [PATCH 0844/1038] Ensure domain is correct format (#58429) --- homeassistant/loader.py | 3 +++ tests/test_loader.py | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 1d8d7e8e98f..84d9cd2a72f 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -576,6 +576,9 @@ async def async_get_integration(hass: HomeAssistant, domain: str) -> Integration async def _async_get_integration(hass: HomeAssistant, domain: str) -> Integration: + if "." in domain: + raise ValueError(f"Invalid domain {domain}") + # Instead of using resolve_from_root we use the cache of custom # components to find the integration. if integration := (await async_get_custom_components(hass)).get(domain): diff --git a/tests/test_loader.py b/tests/test_loader.py index c51e805f400..2bffee75d1e 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -540,3 +540,9 @@ async def test_custom_integration_missing(hass, caplog): with pytest.raises(loader.IntegrationNotFound): await loader.async_get_integration(hass, "test1") + + +async def test_validation(hass): + """Test we raise if invalid domain passed in.""" + with pytest.raises(ValueError): + await loader.async_get_integration(hass, "some.thing") From a9a74e0415a2defd3b7d35ebcb6a2f91057d2b78 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 26 Oct 2021 00:12:23 +0000 Subject: [PATCH 0845/1038] [ci skip] Translation update --- .../components/airthings/translations/pl.json | 2 +- .../binary_sensor/translations/de.json | 2 +- .../binary_sensor/translations/en.json | 8 +++ .../binary_sensor/translations/is.json | 4 ++ .../components/bosch_shc/translations/pl.json | 2 +- .../crownstone/translations/pl.json | 12 ++--- .../components/dlna_dmr/translations/nl.json | 11 +++++ .../components/dlna_dmr/translations/no.json | 15 +++++- .../components/dlna_dmr/translations/pl.json | 12 ++++- .../components/lookin/translations/no.json | 31 ++++++++++++ .../components/lookin/translations/pl.json | 26 ++++++++++ .../modem_callerid/translations/pl.json | 5 +- .../moon/translations/sensor.is.json | 14 ++++++ .../components/motioneye/translations/pl.json | 4 +- .../components/netgear/translations/pl.json | 6 +-- .../components/octoprint/translations/nl.json | 29 +++++++++++ .../components/octoprint/translations/no.json | 29 +++++++++++ .../components/octoprint/translations/pl.json | 22 +++++++++ .../components/ps4/translations/de.json | 2 +- .../components/shelly/translations/no.json | 2 +- .../components/soma/translations/pl.json | 2 +- .../surepetcare/translations/pl.json | 2 +- .../components/switchbot/translations/pl.json | 2 +- .../tuya/translations/select.nl.json | 28 +++++++++++ .../tuya/translations/select.no.json | 49 +++++++++++++++++++ .../tuya/translations/select.zh-Hant.json | 27 ++++++++++ .../components/venstar/translations/en.json | 39 ++++++++------- .../vlc_telnet/translations/de.json | 2 +- .../components/whirlpool/translations/pl.json | 2 +- .../components/yeelight/translations/pl.json | 2 +- 30 files changed, 348 insertions(+), 45 deletions(-) create mode 100644 homeassistant/components/lookin/translations/no.json create mode 100644 homeassistant/components/lookin/translations/pl.json create mode 100644 homeassistant/components/moon/translations/sensor.is.json create mode 100644 homeassistant/components/octoprint/translations/nl.json create mode 100644 homeassistant/components/octoprint/translations/no.json create mode 100644 homeassistant/components/octoprint/translations/pl.json create mode 100644 homeassistant/components/tuya/translations/select.no.json diff --git a/homeassistant/components/airthings/translations/pl.json b/homeassistant/components/airthings/translations/pl.json index 671360f0ecd..08b4f80938a 100644 --- a/homeassistant/components/airthings/translations/pl.json +++ b/homeassistant/components/airthings/translations/pl.json @@ -4,7 +4,7 @@ "already_configured": "Konto jest ju\u017c skonfigurowane" }, "error": { - "cannot_connect": "Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, diff --git a/homeassistant/components/binary_sensor/translations/de.json b/homeassistant/components/binary_sensor/translations/de.json index e2b4c1962eb..b4168528475 100644 --- a/homeassistant/components/binary_sensor/translations/de.json +++ b/homeassistant/components/binary_sensor/translations/de.json @@ -43,7 +43,7 @@ "is_problem": "{entity_name} hat ein Problem festgestellt", "is_smoke": "{entity_name} hat Rauch detektiert", "is_sound": "{entity_name} hat Ger\u00e4usche detektiert", - "is_tampered": "{Einheit_Name} erkennt Manipulationen", + "is_tampered": "{entity_name} erkennt Manipulationen", "is_unsafe": "{entity_name} ist unsicher", "is_update": "{entity_name} hat ein Update verf\u00fcgbar", "is_vibration": "{entity_name} erkennt Vibrationen." diff --git a/homeassistant/components/binary_sensor/translations/en.json b/homeassistant/components/binary_sensor/translations/en.json index be831864f38..2ecb7e4e868 100644 --- a/homeassistant/components/binary_sensor/translations/en.json +++ b/homeassistant/components/binary_sensor/translations/en.json @@ -31,6 +31,7 @@ "is_not_plugged_in": "{entity_name} is unplugged", "is_not_powered": "{entity_name} is not powered", "is_not_present": "{entity_name} is not present", + "is_not_running": "{entity_name} is not running", "is_not_tampered": "{entity_name} is not detecting tampering", "is_not_unsafe": "{entity_name} is safe", "is_occupied": "{entity_name} is occupied", @@ -41,6 +42,7 @@ "is_powered": "{entity_name} is powered", "is_present": "{entity_name} is present", "is_problem": "{entity_name} is detecting problem", + "is_running": "{entity_name} is running", "is_smoke": "{entity_name} is detecting smoke", "is_sound": "{entity_name} is detecting sound", "is_tampered": "{entity_name} is detecting tampering", @@ -81,6 +83,7 @@ "not_plugged_in": "{entity_name} unplugged", "not_powered": "{entity_name} not powered", "not_present": "{entity_name} not present", + "not_running": "{entity_name} is no longer running", "not_unsafe": "{entity_name} became safe", "occupied": "{entity_name} became occupied", "opened": "{entity_name} opened", @@ -88,6 +91,7 @@ "powered": "{entity_name} powered", "present": "{entity_name} present", "problem": "{entity_name} started detecting problem", + "running": "{entity_name} started running", "smoke": "{entity_name} started detecting smoke", "sound": "{entity_name} started detecting sound", "turned_off": "{entity_name} turned off", @@ -174,6 +178,10 @@ "off": "OK", "on": "Problem" }, + "running": { + "off": "Not running", + "on": "Running" + }, "safety": { "off": "Safe", "on": "Unsafe" diff --git a/homeassistant/components/binary_sensor/translations/is.json b/homeassistant/components/binary_sensor/translations/is.json index bd1ed9c389a..46b44913169 100644 --- a/homeassistant/components/binary_sensor/translations/is.json +++ b/homeassistant/components/binary_sensor/translations/is.json @@ -48,6 +48,10 @@ "off": "Engin vi\u00f0vera", "on": "Uppg\u00f6tva\u00f0" }, + "opening": { + "off": "Loka\u00f0", + "on": "Opi\u00f0" + }, "presence": { "off": "Fjarverandi", "on": "Heima" diff --git a/homeassistant/components/bosch_shc/translations/pl.json b/homeassistant/components/bosch_shc/translations/pl.json index c140bf6b6f8..6b21fd87b7c 100644 --- a/homeassistant/components/bosch_shc/translations/pl.json +++ b/homeassistant/components/bosch_shc/translations/pl.json @@ -18,7 +18,7 @@ }, "credentials": { "data": { - "password": "Has\u0142o kontrolera" + "password": "Has\u0142o" } }, "reauth_confirm": { diff --git a/homeassistant/components/crownstone/translations/pl.json b/homeassistant/components/crownstone/translations/pl.json index e201e7da0c7..c71c27c4601 100644 --- a/homeassistant/components/crownstone/translations/pl.json +++ b/homeassistant/components/crownstone/translations/pl.json @@ -10,12 +10,12 @@ "step": { "usb_config": { "data": { - "usb_path": "\u015acie\u017cka do urz\u0105dzenia USB" + "usb_path": "\u015acie\u017cka urz\u0105dzenia USB" } }, "usb_manual_config": { "data": { - "usb_manual_path": "\u015acie\u017cka do urz\u0105dzenia USB" + "usb_manual_path": "\u015acie\u017cka urz\u0105dzenia USB" } }, "user": { @@ -31,22 +31,22 @@ "step": { "usb_config": { "data": { - "usb_path": "\u015acie\u017cka do urz\u0105dzenia USB" + "usb_path": "\u015acie\u017cka urz\u0105dzenia USB" } }, "usb_config_option": { "data": { - "usb_path": "\u015acie\u017cka do urz\u0105dzenia USB" + "usb_path": "\u015acie\u017cka urz\u0105dzenia USB" } }, "usb_manual_config": { "data": { - "usb_manual_path": "\u015acie\u017cka do urz\u0105dzenia USB" + "usb_manual_path": "\u015acie\u017cka urz\u0105dzenia USB" } }, "usb_manual_config_option": { "data": { - "usb_manual_path": "\u015acie\u017cka do urz\u0105dzenia USB" + "usb_manual_path": "\u015acie\u017cka urz\u0105dzenia USB" } } } diff --git a/homeassistant/components/dlna_dmr/translations/nl.json b/homeassistant/components/dlna_dmr/translations/nl.json index 9a39aebb4ce..ff14a957526 100644 --- a/homeassistant/components/dlna_dmr/translations/nl.json +++ b/homeassistant/components/dlna_dmr/translations/nl.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", + "alternative_integration": "Apparaat wordt beter ondersteund door een andere integratie", + "cannot_connect": "Kan geen verbinding maken", "could_not_connect": "Mislukt om te verbinden met DNLA apparaat", "discovery_error": "Kan geen overeenkomend DLNA-apparaat vinden", "incomplete_config": "Configuratie mist een vereiste variabele", @@ -9,6 +11,7 @@ "not_dmr": "Apparaat is geen Digital Media Renderer" }, "error": { + "cannot_connect": "Kan geen verbinding maken", "could_not_connect": "Mislukt om te verbinden met DNLA apparaat", "not_dmr": "Apparaat is geen Digital Media Renderer" }, @@ -20,8 +23,16 @@ "import_turn_on": { "description": "Zet het apparaat aan en klik op verzenden om door te gaan met de migratie" }, + "manual": { + "data": { + "url": "URL" + }, + "description": "URL naar een XML-bestand met apparaatbeschrijvingen", + "title": "Handmatige DLNA DMR-apparaatverbinding" + }, "user": { "data": { + "host": "Host", "url": "URL" }, "description": "URL naar een XML-bestand met apparaatbeschrijvingen", diff --git a/homeassistant/components/dlna_dmr/translations/no.json b/homeassistant/components/dlna_dmr/translations/no.json index 83e3ba573b3..3b0f5854aca 100644 --- a/homeassistant/components/dlna_dmr/translations/no.json +++ b/homeassistant/components/dlna_dmr/translations/no.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", + "alternative_integration": "Enheten st\u00f8ttes bedre av en annen integrasjon", + "cannot_connect": "Tilkobling mislyktes", "could_not_connect": "Kunne ikke koble til DLNA -enhet", "discovery_error": "Kunne ikke finne en matchende DLNA -enhet", "incomplete_config": "Konfigurasjonen mangler en n\u00f8dvendig variabel", @@ -9,6 +11,7 @@ "not_dmr": "Enheten er ikke en Digital Media Renderer" }, "error": { + "cannot_connect": "Tilkobling mislyktes", "could_not_connect": "Kunne ikke koble til DLNA -enhet", "not_dmr": "Enheten er ikke en Digital Media Renderer" }, @@ -20,12 +23,20 @@ "import_turn_on": { "description": "Sl\u00e5 p\u00e5 enheten og klikk p\u00e5 send for \u00e5 fortsette overf\u00f8ringen" }, - "user": { + "manual": { "data": { "url": "URL" }, "description": "URL til en enhetsbeskrivelse XML -fil", - "title": "DLNA Digital Media Renderer" + "title": "Manuell DLNA DMR -enhetstilkobling" + }, + "user": { + "data": { + "host": "Vert", + "url": "URL" + }, + "description": "Velg en enhet du vil konfigurere, eller la den st\u00e5 tom for \u00e5 angi en URL", + "title": "Oppdaget DLNA DMR -enheter" } } }, diff --git a/homeassistant/components/dlna_dmr/translations/pl.json b/homeassistant/components/dlna_dmr/translations/pl.json index b7687d3831a..09fb3dce509 100644 --- a/homeassistant/components/dlna_dmr/translations/pl.json +++ b/homeassistant/components/dlna_dmr/translations/pl.json @@ -1,15 +1,25 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "flow_title": "{name}", "step": { "confirm": { "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" }, + "manual": { + "data": { + "url": "URL" + } + }, "user": { "data": { + "host": "Nazwa hosta lub adres IP", "url": "URL" } } diff --git a/homeassistant/components/lookin/translations/no.json b/homeassistant/components/lookin/translations/no.json new file mode 100644 index 00000000000..0f3aa19bd41 --- /dev/null +++ b/homeassistant/components/lookin/translations/no.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "cannot_connect": "Tilkobling mislyktes", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "unknown": "Uventet feil" + }, + "flow_title": "{name} ({host})", + "step": { + "device_name": { + "data": { + "name": "Navn" + } + }, + "discovery_confirm": { + "description": "Vil du konfigurere {name} ({host})?" + }, + "user": { + "data": { + "ip_address": "IP adresse" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lookin/translations/pl.json b/homeassistant/components/lookin/translations/pl.json new file mode 100644 index 00000000000..d6edb1d50da --- /dev/null +++ b/homeassistant/components/lookin/translations/pl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "device_name": { + "data": { + "name": "Nazwa" + } + }, + "user": { + "data": { + "ip_address": "Adres IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/pl.json b/homeassistant/components/modem_callerid/translations/pl.json index 12638ae272b..b608a467395 100644 --- a/homeassistant/components/modem_callerid/translations/pl.json +++ b/homeassistant/components/modem_callerid/translations/pl.json @@ -2,10 +2,11 @@ "config": { "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "already_in_progress": "Konfiguracja jest ju\u017c w toku" + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" }, "error": { - "cannot_connect": "Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "step": { "usb_confirm": { diff --git a/homeassistant/components/moon/translations/sensor.is.json b/homeassistant/components/moon/translations/sensor.is.json new file mode 100644 index 00000000000..85bb2470e4c --- /dev/null +++ b/homeassistant/components/moon/translations/sensor.is.json @@ -0,0 +1,14 @@ +{ + "state": { + "moon__phase": { + "first_quarter": "Fyrsta kvartil", + "full_moon": "Fullt tungl", + "last_quarter": "S\u00ed\u00f0asta kvartil", + "new_moon": "N\u00fdtt tungl", + "waning_crescent": "Minnkandi sig\u00f0", + "waning_gibbous": "Gleitt minnkandi", + "waxing_crescent": "Vaxandi sig\u00f0", + "waxing_gibbous": "Gleitt vaxandi" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/pl.json b/homeassistant/components/motioneye/translations/pl.json index 7267a70b677..a34af5491ec 100644 --- a/homeassistant/components/motioneye/translations/pl.json +++ b/homeassistant/components/motioneye/translations/pl.json @@ -17,8 +17,8 @@ }, "user": { "data": { - "admin_password": "Has\u0142o admina", - "admin_username": "Nazwa u\u017cytkownika admina", + "admin_password": "Has\u0142o administratora", + "admin_username": "Nazwa u\u017cytkownika administratora", "surveillance_password": "Has\u0142o podgl\u0105du", "surveillance_username": "[%key::common::config_flow::data::username%] podgl\u0105du", "url": "URL" diff --git a/homeassistant/components/netgear/translations/pl.json b/homeassistant/components/netgear/translations/pl.json index a9c0c4af199..3cd56289fbd 100644 --- a/homeassistant/components/netgear/translations/pl.json +++ b/homeassistant/components/netgear/translations/pl.json @@ -6,11 +6,11 @@ "step": { "user": { "data": { - "host": "Nazwa hosta lub adres IP (Opcjonalne)", + "host": "Nazwa hosta lub adres IP (opcjonalnie)", "password": "Has\u0142o", - "port": "Port (Opcjonalnie)", + "port": "Port (opcjonalnie)", "ssl": "Certyfikat SSL", - "username": "Nazwa u\u017cytkownika (Opcjonalnie)" + "username": "Nazwa u\u017cytkownika (opcjonalnie)" }, "title": "Netgear" } diff --git a/homeassistant/components/octoprint/translations/nl.json b/homeassistant/components/octoprint/translations/nl.json new file mode 100644 index 00000000000..5b36b59f5cc --- /dev/null +++ b/homeassistant/components/octoprint/translations/nl.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "auth_failed": "Kan applicatie API-sleutel niet ophalen", + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "flow_title": "OctoPrint Printer: {host}", + "progress": { + "get_api_key": "Open de OctoPrint UI en klik op 'Toestaan' op het toegangsverzoek voor 'Home Assistant'." + }, + "step": { + "user": { + "data": { + "host": "Host", + "path": "Applicatiepad", + "port": "Poortnummer", + "ssl": "SSL gebruiken", + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/octoprint/translations/no.json b/homeassistant/components/octoprint/translations/no.json new file mode 100644 index 00000000000..b7761c5370f --- /dev/null +++ b/homeassistant/components/octoprint/translations/no.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "auth_failed": "Kan ikke hente APAn\u00f8kkel for program", + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, + "flow_title": "OctoPrint-skriver: {host}", + "progress": { + "get_api_key": "\u00c5pne OctoPrint UI og klikk \"Tillat\" p\u00e5 tilgangsforesp\u00f8rselen for \"Hjemmeassistent\"." + }, + "step": { + "user": { + "data": { + "host": "Vert", + "path": "Bane til program", + "port": "Portnummer", + "ssl": "Bruk SSL", + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/octoprint/translations/pl.json b/homeassistant/components/octoprint/translations/pl.json new file mode 100644 index 00000000000..4040dea0278 --- /dev/null +++ b/homeassistant/components/octoprint/translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "port": "Port", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/translations/de.json b/homeassistant/components/ps4/translations/de.json index ca1a0ab24b1..c177c5d81cc 100644 --- a/homeassistant/components/ps4/translations/de.json +++ b/homeassistant/components/ps4/translations/de.json @@ -15,7 +15,7 @@ }, "step": { "creds": { - "description": "Anmeldeinformationen ben\u00f6tigt. Klicke auf \"Senden\" und dann in der PS4 Second Screen app, aktualisiere die Ger\u00e4te und w\u00e4hle das \"Home-Assistant\"-Ger\u00e4t aus, um fortzufahren.", + "description": "Anmeldeinformationen ben\u00f6tigt. Klicke auf \"Senden\" und dann in der PS4 Second Screen App, aktualisiere die Ger\u00e4te und w\u00e4hle das \"Home-Assistant\"-Ger\u00e4t aus, um fortzufahren.", "title": "PlayStation 4" }, "link": { diff --git a/homeassistant/components/shelly/translations/no.json b/homeassistant/components/shelly/translations/no.json index dd587e56a6b..fe7d30c098f 100644 --- a/homeassistant/components/shelly/translations/no.json +++ b/homeassistant/components/shelly/translations/no.json @@ -41,7 +41,7 @@ "btn_up": "{subtype} -knappen opp", "double": "{subtype} dobbeltklikket", "double_push": "{subtype} dobbelt trykk", - "long": "{subtype} lenge klikket", + "long": "{subtype} klikket lenge", "long_push": "{subtype} langt trykk", "long_single": "{subtype} lengre klikk og deretter et enkeltklikk", "single": "{subtype} enkeltklikket", diff --git a/homeassistant/components/soma/translations/pl.json b/homeassistant/components/soma/translations/pl.json index 955e1f1daf4..902a4d2beb5 100644 --- a/homeassistant/components/soma/translations/pl.json +++ b/homeassistant/components/soma/translations/pl.json @@ -3,7 +3,7 @@ "abort": { "already_setup": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.", "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", - "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z SOMA Connect", + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", "result_error": "SOMA Connect odpowiedzia\u0142 statusem b\u0142\u0119du" }, diff --git a/homeassistant/components/surepetcare/translations/pl.json b/homeassistant/components/surepetcare/translations/pl.json index b44e3977ba5..41654933a6f 100644 --- a/homeassistant/components/surepetcare/translations/pl.json +++ b/homeassistant/components/surepetcare/translations/pl.json @@ -4,7 +4,7 @@ "already_configured": "Konto jest ju\u017c skonfigurowane" }, "error": { - "cannot_connect": "Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, diff --git a/homeassistant/components/switchbot/translations/pl.json b/homeassistant/components/switchbot/translations/pl.json index 473535519af..2fdc90d59cf 100644 --- a/homeassistant/components/switchbot/translations/pl.json +++ b/homeassistant/components/switchbot/translations/pl.json @@ -6,7 +6,7 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { - "cannot_connect": "Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/tuya/translations/select.nl.json b/homeassistant/components/tuya/translations/select.nl.json index 479a6d5e7d3..5efdae61193 100644 --- a/homeassistant/components/tuya/translations/select.nl.json +++ b/homeassistant/components/tuya/translations/select.nl.json @@ -1,13 +1,41 @@ { "state": { + "tuya__basic_anti_flickr": { + "0": "Uitgeschakeld", + "1": "50 Hz", + "2": "60 Hz" + }, + "tuya__basic_nightvision": { + "0": "Automatisch", + "1": "Uit", + "2": "Aan" + }, + "tuya__decibel_sensitivity": { + "0": "Lage gevoeligheid", + "1": "Hoge gevoeligheid" + }, + "tuya__ipc_work_mode": { + "0": "Energiezuinige modus", + "1": "Continue werkmodus:" + }, "tuya__led_type": { "halogen": "Halogeen", "led": "LED" }, "tuya__light_mode": { "none": "Uit", + "pos": "Plaats van schakelaar aangeven", "relay": "Aan/uit-toestand aangeven" }, + "tuya__motion_sensitivity": { + "0": "Lage gevoeligheid", + "1": "Gemiddelde gevoeligheid", + "2": "Hoge gevoeligheid" + }, + "tuya__record_mode": { + "1": "Alleen gebeurtenissen opnemen", + "2": "Continu opnemen" + }, "tuya__relay_status": { "last": "Onthoud laatste staat", "memory": "Onthoud laatste staat", diff --git a/homeassistant/components/tuya/translations/select.no.json b/homeassistant/components/tuya/translations/select.no.json new file mode 100644 index 00000000000..e5bc8dbba53 --- /dev/null +++ b/homeassistant/components/tuya/translations/select.no.json @@ -0,0 +1,49 @@ +{ + "state": { + "tuya__basic_anti_flickr": { + "0": "Deaktivert", + "1": "50 Hz", + "2": "60 Hz" + }, + "tuya__basic_nightvision": { + "0": "Automatisk", + "1": "Av", + "2": "P\u00e5" + }, + "tuya__decibel_sensitivity": { + "0": "Lav f\u00f8lsomhet", + "1": "H\u00f8y f\u00f8lsomhet" + }, + "tuya__ipc_work_mode": { + "0": "Lav effekt modus", + "1": "Kontinuerlig arbeidsmodus" + }, + "tuya__led_type": { + "halogen": "Halogen", + "incandescent": "Gl\u00f8dende", + "led": "LED" + }, + "tuya__light_mode": { + "none": "Av", + "pos": "Angi bytteplassering", + "relay": "Angi sl\u00e5 av/p\u00e5 -tilstand" + }, + "tuya__motion_sensitivity": { + "0": "Lav f\u00f8lsomhet", + "1": "Middels f\u00f8lsomhet", + "2": "H\u00f8y f\u00f8lsomhet" + }, + "tuya__record_mode": { + "1": "Registrer bare hendelser", + "2": "Kontinuerlig opptak" + }, + "tuya__relay_status": { + "last": "Husk siste tilstand", + "memory": "Husk siste tilstand", + "off": "Av", + "on": "P\u00e5", + "power_off": "Av", + "power_on": "P\u00e5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.zh-Hant.json b/homeassistant/components/tuya/translations/select.zh-Hant.json index 142619d255f..86cd7342a7f 100644 --- a/homeassistant/components/tuya/translations/select.zh-Hant.json +++ b/homeassistant/components/tuya/translations/select.zh-Hant.json @@ -1,5 +1,23 @@ { "state": { + "tuya__basic_anti_flickr": { + "0": "\u95dc\u9589", + "1": "50 Hz", + "2": "60 Hz" + }, + "tuya__basic_nightvision": { + "0": "\u81ea\u52d5", + "1": "\u95dc\u9589", + "2": "\u958b\u555f" + }, + "tuya__decibel_sensitivity": { + "0": "\u4f4e\u654f\u611f\u5ea6", + "1": "\u9ad8\u654f\u611f\u5ea6" + }, + "tuya__ipc_work_mode": { + "0": "\u4f4e\u529f\u8017\u6a21\u5f0f", + "1": "\u6301\u7e8c\u5de5\u4f5c\u6a21\u5f0f" + }, "tuya__led_type": { "halogen": "\u9e75\u7d20\u71c8", "incandescent": "\u767d\u71be\u71c8", @@ -10,6 +28,15 @@ "pos": "\u6307\u793a\u958b\u95dc\u4f4d\u7f6e", "relay": "\u6307\u793a\u958b\u95dc\u958b\u555f/\u95dc\u9589\u72c0\u614b" }, + "tuya__motion_sensitivity": { + "0": "\u4f4e\u654f\u611f\u5ea6", + "1": "\u4e2d\u654f\u611f\u5ea6", + "2": "\u9ad8\u654f\u611f\u5ea6" + }, + "tuya__record_mode": { + "1": "\u50c5\u8a18\u9304\u4e8b\u4ef6", + "2": "\u6301\u7e8c\u7d00\u9304" + }, "tuya__relay_status": { "last": "\u8a18\u4f4f\u6700\u5f8c\u72c0\u614b", "memory": "\u8a18\u4f4f\u6700\u5f8c\u72c0\u614b", diff --git a/homeassistant/components/venstar/translations/en.json b/homeassistant/components/venstar/translations/en.json index e56d0fe1a83..8b423713f2c 100644 --- a/homeassistant/components/venstar/translations/en.json +++ b/homeassistant/components/venstar/translations/en.json @@ -1,20 +1,23 @@ { - "config": { - "step": { - "user": { - "title": "Connect to the Venstar Thermostat", - "data": { - "host": "Hostname or IP", - "username": "Username for thermostat (optional)", - "password": "Password for thermostat (optional)", - "pin": "Pin for Lockscreen (required if lock screen enabled)", - "ssl": "Whether to use SSL or not when communicating" - } - } - }, - "error": { - "cannot_connect": "Unable to connect to thermostat, please validate username/password if supplied, hostname/ip, and that LOCAL API is enabled on the thermostat.", - "unknown": "An unknown error has occurred." + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "pin": "PIN Code", + "ssl": "Uses an SSL certificate", + "username": "Username" + }, + "title": "Connect to the Venstar Thermostat" + } + } } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/vlc_telnet/translations/de.json b/homeassistant/components/vlc_telnet/translations/de.json index 8174c4355a6..39496627959 100644 --- a/homeassistant/components/vlc_telnet/translations/de.json +++ b/homeassistant/components/vlc_telnet/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service ist bereits konfiguriert", + "already_configured": "Der Dienst ist bereits konfiguriert", "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "reauth_successful": "Die erneute Authentifizierung war erfolgreich", diff --git a/homeassistant/components/whirlpool/translations/pl.json b/homeassistant/components/whirlpool/translations/pl.json index f66fe9bdc33..91d1ceab868 100644 --- a/homeassistant/components/whirlpool/translations/pl.json +++ b/homeassistant/components/whirlpool/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, diff --git a/homeassistant/components/yeelight/translations/pl.json b/homeassistant/components/yeelight/translations/pl.json index a11706dfd64..2827e926a0d 100644 --- a/homeassistant/components/yeelight/translations/pl.json +++ b/homeassistant/components/yeelight/translations/pl.json @@ -29,7 +29,7 @@ "step": { "init": { "data": { - "model": "Model (opcjonalne)", + "model": "Model (opcjonalnie)", "nightlight_switch": "U\u017cyj prze\u0142\u0105cznika Nocnego \u015bwiat\u0142a", "save_on_change": "Zachowaj status po zmianie", "transition": "Czas przej\u015bcia (ms)", From 44aa1fdc66c2912c5f85c4d3895d42b520ff105e Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Tue, 26 Oct 2021 11:54:58 +1100 Subject: [PATCH 0846/1038] dlna_dmr won't support devices that don't provide all DMR services (#58374) --- .../components/dlna_dmr/config_flow.py | 21 +++++-- .../components/dlna_dmr/strings.json | 4 +- .../components/dlna_dmr/translations/en.json | 4 +- homeassistant/components/ssdp/__init__.py | 1 + tests/components/dlna_dmr/conftest.py | 22 ++++++- tests/components/dlna_dmr/test_config_flow.py | 63 ++++++++++++++++++- 6 files changed, 103 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/dlna_dmr/config_flow.py b/homeassistant/components/dlna_dmr/config_flow.py index ee81d1be88f..454a28c9f7d 100644 --- a/homeassistant/components/dlna_dmr/config_flow.py +++ b/homeassistant/components/dlna_dmr/config_flow.py @@ -216,6 +216,19 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if _is_ignored_device(discovery_info): return self.async_abort(reason="alternative_integration") + # Abort if the device doesn't support all services required for a DmrDevice. + # Use the discovery_info instead of DmrDevice.is_profile_device to avoid + # contacting the device again. + discovery_service_list = discovery_info.get(ssdp.ATTR_UPNP_SERVICE_LIST) + if not discovery_service_list: + return self.async_abort(reason="not_dmr") + discovery_service_ids = { + service.get("serviceId") + for service in discovery_service_list.get("service") or [] + } + if not DmrDevice.SERVICE_IDS.issubset(discovery_service_ids): + return self.async_abort(reason="not_dmr") + # Abort if a migration flow for the device's location is in progress for progress in self._async_in_progress(include_uninitialized=True): if progress["context"].get("unique_id") == self._location: @@ -277,10 +290,10 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except UpnpError as err: raise ConnectError("cannot_connect") from err - try: - device = find_device_of_type(device, DmrDevice.DEVICE_TYPES) - except UpnpError as err: - raise ConnectError("not_dmr") from err + if not DmrDevice.is_profile_device(device): + raise ConnectError("not_dmr") + + device = find_device_of_type(device, DmrDevice.DEVICE_TYPES) if not self._udn: self._udn = device.udn diff --git a/homeassistant/components/dlna_dmr/strings.json b/homeassistant/components/dlna_dmr/strings.json index ac6a35194fe..ac77009e0cb 100644 --- a/homeassistant/components/dlna_dmr/strings.json +++ b/homeassistant/components/dlna_dmr/strings.json @@ -30,11 +30,11 @@ "discovery_error": "Failed to discover a matching DLNA device", "incomplete_config": "Configuration is missing a required variable", "non_unique_id": "Multiple devices found with the same unique ID", - "not_dmr": "Device is not a Digital Media Renderer" + "not_dmr": "Device is not a supported Digital Media Renderer" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "not_dmr": "Device is not a Digital Media Renderer" + "not_dmr": "Device is not a supported Digital Media Renderer" } }, "options": { diff --git a/homeassistant/components/dlna_dmr/translations/en.json b/homeassistant/components/dlna_dmr/translations/en.json index 6711a861344..512dfe7f11c 100644 --- a/homeassistant/components/dlna_dmr/translations/en.json +++ b/homeassistant/components/dlna_dmr/translations/en.json @@ -8,12 +8,12 @@ "discovery_error": "Failed to discover a matching DLNA device", "incomplete_config": "Configuration is missing a required variable", "non_unique_id": "Multiple devices found with the same unique ID", - "not_dmr": "Device is not a Digital Media Renderer" + "not_dmr": "Device is not a supported Digital Media Renderer" }, "error": { "cannot_connect": "Failed to connect", "could_not_connect": "Failed to connect to DLNA device", - "not_dmr": "Device is not a Digital Media Renderer" + "not_dmr": "Device is not a supported Digital Media Renderer" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index f5f4feaa70a..c937f210368 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -51,6 +51,7 @@ ATTR_UPNP_MODEL_NAME = "modelName" ATTR_UPNP_MODEL_NUMBER = "modelNumber" ATTR_UPNP_MODEL_URL = "modelURL" ATTR_UPNP_SERIAL = "serialNumber" +ATTR_UPNP_SERVICE_LIST = "serviceList" ATTR_UPNP_UDN = "UDN" ATTR_UPNP_UPC = "UPC" ATTR_UPNP_PRESENTATION_URL = "presentationURL" diff --git a/tests/components/dlna_dmr/conftest.py b/tests/components/dlna_dmr/conftest.py index 71910ec1cd8..3a9025a9a29 100644 --- a/tests/components/dlna_dmr/conftest.py +++ b/tests/components/dlna_dmr/conftest.py @@ -5,7 +5,7 @@ from collections.abc import Iterable from socket import AddressFamily # pylint: disable=no-name-in-module from unittest.mock import Mock, create_autospec, patch, seal -from async_upnp_client import UpnpDevice, UpnpFactory +from async_upnp_client import UpnpDevice, UpnpFactory, UpnpService import pytest from homeassistant.components.dlna_dmr.const import DOMAIN as DLNA_DOMAIN @@ -49,6 +49,26 @@ def domain_data_mock(hass: HomeAssistant) -> Iterable[Mock]: upnp_device.parent_device = None upnp_device.root_device = upnp_device upnp_device.all_devices = [upnp_device] + upnp_device.services = { + "urn:schemas-upnp-org:service:AVTransport:1": create_autospec( + UpnpService, + instance=True, + service_type="urn:schemas-upnp-org:service:AVTransport:1", + service_id="urn:upnp-org:serviceId:AVTransport", + ), + "urn:schemas-upnp-org:service:ConnectionManager:1": create_autospec( + UpnpService, + instance=True, + service_type="urn:schemas-upnp-org:service:ConnectionManager:1", + service_id="urn:upnp-org:serviceId:ConnectionManager", + ), + "urn:schemas-upnp-org:service:RenderingControl:1": create_autospec( + UpnpService, + instance=True, + service_type="urn:schemas-upnp-org:service:RenderingControl:1", + service_id="urn:upnp-org:serviceId:RenderingControl", + ), + } seal(upnp_device) domain_data.upnp_factory.async_create_device.return_value = upnp_device diff --git a/tests/components/dlna_dmr/test_config_flow.py b/tests/components/dlna_dmr/test_config_flow.py index 245d97be4aa..5a2327ecce9 100644 --- a/tests/components/dlna_dmr/test_config_flow.py +++ b/tests/components/dlna_dmr/test_config_flow.py @@ -23,6 +23,7 @@ from homeassistant.const import ( CONF_URL, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import DiscoveryInfoType from .conftest import ( MOCK_DEVICE_LOCATION, @@ -51,13 +52,38 @@ MOCK_CONFIG_IMPORT_DATA = { MOCK_ROOT_DEVICE_UDN = "ROOT_DEVICE" -MOCK_DISCOVERY = { +MOCK_DISCOVERY: DiscoveryInfoType = { ssdp.ATTR_SSDP_LOCATION: MOCK_DEVICE_LOCATION, ssdp.ATTR_SSDP_UDN: MOCK_DEVICE_UDN, ssdp.ATTR_SSDP_ST: MOCK_DEVICE_TYPE, ssdp.ATTR_UPNP_UDN: MOCK_ROOT_DEVICE_UDN, ssdp.ATTR_UPNP_DEVICE_TYPE: MOCK_DEVICE_TYPE, ssdp.ATTR_UPNP_FRIENDLY_NAME: MOCK_DEVICE_NAME, + ssdp.ATTR_UPNP_SERVICE_LIST: { + "service": [ + { + "SCPDURL": "/AVTransport/scpd.xml", + "controlURL": "/AVTransport/control.xml", + "eventSubURL": "/AVTransport/event.xml", + "serviceId": "urn:upnp-org:serviceId:AVTransport", + "serviceType": "urn:schemas-upnp-org:service:AVTransport:1", + }, + { + "SCPDURL": "/ConnectionManager/scpd.xml", + "controlURL": "/ConnectionManager/control.xml", + "eventSubURL": "/ConnectionManager/event.xml", + "serviceId": "urn:upnp-org:serviceId:ConnectionManager", + "serviceType": "urn:schemas-upnp-org:service:ConnectionManager:1", + }, + { + "SCPDURL": "/RenderingControl/scpd.xml", + "controlURL": "/RenderingControl/control.xml", + "eventSubURL": "/RenderingControl/event.xml", + "serviceId": "urn:upnp-org:serviceId:RenderingControl", + "serviceType": "urn:schemas-upnp-org:service:RenderingControl:1", + }, + ] + }, ssdp.ATTR_HA_MATCHING_DOMAINS: {DLNA_DOMAIN}, } @@ -197,6 +223,8 @@ async def test_user_flow_embedded_st( embedded_device.udn = MOCK_DEVICE_UDN embedded_device.device_type = MOCK_DEVICE_TYPE embedded_device.name = MOCK_DEVICE_NAME + embedded_device.services = upnp_device.services + upnp_device.services = {} upnp_device.all_devices.append(embedded_device) result = await hass.config_entries.flow.async_init( @@ -552,9 +580,38 @@ async def test_ssdp_flow_upnp_udn( assert config_entry_mock.data[CONF_URL] == NEW_DEVICE_LOCATION +async def test_ssdp_missing_services(hass: HomeAssistant) -> None: + """Test SSDP ignores devices that are missing required services.""" + # No services defined at all + discovery = dict(MOCK_DISCOVERY) + del discovery[ssdp.ATTR_UPNP_SERVICE_LIST] + result = await hass.config_entries.flow.async_init( + DLNA_DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=discovery, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "not_dmr" + + # AVTransport service is missing + discovery = dict(MOCK_DISCOVERY) + discovery[ssdp.ATTR_UPNP_SERVICE_LIST] = { + "service": [ + service + for service in discovery[ssdp.ATTR_UPNP_SERVICE_LIST]["service"] + if service.get("serviceId") != "urn:upnp-org:serviceId:AVTransport" + ] + } + result = await hass.config_entries.flow.async_init( + DLNA_DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=discovery + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "not_dmr" + + async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: """Test SSDP discovery ignores certain devices.""" - discovery = MOCK_DISCOVERY.copy() + discovery = dict(MOCK_DISCOVERY) discovery[ssdp.ATTR_HA_MATCHING_DOMAINS] = {DLNA_DOMAIN, "other_domain"} result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, @@ -564,7 +621,7 @@ async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "alternative_integration" - discovery = MOCK_DISCOVERY.copy() + discovery = dict(MOCK_DISCOVERY) discovery[ssdp.ATTR_UPNP_DEVICE_TYPE] = "urn:schemas-upnp-org:device:ZonePlayer:1" result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, From e22aaea5b2bcd8766535cdda6c61ea98298ee722 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Tue, 26 Oct 2021 02:04:42 +0100 Subject: [PATCH 0847/1038] Aurora abb (solar) configflow (#36300) Co-authored-by: J. Nick Koston --- .coveragerc | 1 - .../aurora_abb_powerone/__init__.py | 48 +++++ .../aurora_abb_powerone/aurora_device.py | 51 +++++ .../aurora_abb_powerone/config_flow.py | 147 ++++++++++++++ .../components/aurora_abb_powerone/const.py | 22 +++ .../aurora_abb_powerone/manifest.json | 9 +- .../components/aurora_abb_powerone/sensor.py | 100 +++++++--- .../aurora_abb_powerone/strings.json | 23 +++ .../aurora_abb_powerone/translations/en.json | 23 +++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + .../aurora_abb_powerone/__init__.py | 1 + .../aurora_abb_powerone/test_config_flow.py | 165 ++++++++++++++++ .../aurora_abb_powerone/test_init.py | 37 ++++ .../aurora_abb_powerone/test_sensor.py | 185 ++++++++++++++++++ 15 files changed, 786 insertions(+), 30 deletions(-) create mode 100644 homeassistant/components/aurora_abb_powerone/aurora_device.py create mode 100644 homeassistant/components/aurora_abb_powerone/config_flow.py create mode 100644 homeassistant/components/aurora_abb_powerone/const.py create mode 100644 homeassistant/components/aurora_abb_powerone/strings.json create mode 100644 homeassistant/components/aurora_abb_powerone/translations/en.json create mode 100644 tests/components/aurora_abb_powerone/__init__.py create mode 100644 tests/components/aurora_abb_powerone/test_config_flow.py create mode 100644 tests/components/aurora_abb_powerone/test_init.py create mode 100644 tests/components/aurora_abb_powerone/test_sensor.py diff --git a/.coveragerc b/.coveragerc index bb84820783b..74da8acf8b8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -83,7 +83,6 @@ omit = homeassistant/components/aurora/binary_sensor.py homeassistant/components/aurora/const.py homeassistant/components/aurora/sensor.py - homeassistant/components/aurora_abb_powerone/sensor.py homeassistant/components/avea/light.py homeassistant/components/avion/light.py homeassistant/components/azure_devops/__init__.py diff --git a/homeassistant/components/aurora_abb_powerone/__init__.py b/homeassistant/components/aurora_abb_powerone/__init__.py index 087172d1bb5..ff26c3770f0 100644 --- a/homeassistant/components/aurora_abb_powerone/__init__.py +++ b/homeassistant/components/aurora_abb_powerone/__init__.py @@ -1 +1,49 @@ """The Aurora ABB Powerone PV inverter sensor integration.""" + +# Reference info: +# https://s1.solacity.com/docs/PVI-3.0-3.6-4.2-OUTD-US%20Manual.pdf +# http://www.drhack.it/images/PDF/AuroraCommunicationProtocol_4_2.pdf +# +# Developer note: +# vscode devcontainer: use the following to access USB device: +# "runArgs": ["-e", "GIT_EDITOR=code --wait", "--device=/dev/ttyUSB0"], + +import logging + +from aurorapy.client import AuroraSerialClient + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ADDRESS, CONF_PORT +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS = ["sensor"] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Aurora ABB PowerOne from a config entry.""" + + comport = entry.data[CONF_PORT] + address = entry.data[CONF_ADDRESS] + serclient = AuroraSerialClient(address, comport, parity="N", timeout=1) + + hass.data.setdefault(DOMAIN, {})[entry.unique_id] = serclient + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + # It should not be necessary to close the serial port because we close + # it after every use in sensor.py, i.e. no need to do entry["client"].close() + if unload_ok: + hass.data[DOMAIN].pop(entry.unique_id) + + return unload_ok diff --git a/homeassistant/components/aurora_abb_powerone/aurora_device.py b/homeassistant/components/aurora_abb_powerone/aurora_device.py new file mode 100644 index 00000000000..0a7aab4a921 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/aurora_device.py @@ -0,0 +1,51 @@ +"""Top level class for AuroraABBPowerOneSolarPV inverters and sensors.""" +import logging + +from aurorapy.client import AuroraSerialClient + +from homeassistant.helpers.entity import Entity + +from .const import ( + ATTR_DEVICE_NAME, + ATTR_FIRMWARE, + ATTR_MODEL, + ATTR_SERIAL_NUMBER, + DEFAULT_DEVICE_NAME, + DOMAIN, + MANUFACTURER, +) + +_LOGGER = logging.getLogger(__name__) + + +class AuroraDevice(Entity): + """Representation of an Aurora ABB PowerOne device.""" + + def __init__(self, client: AuroraSerialClient, data) -> None: + """Initialise the basic device.""" + self._data = data + self.type = "device" + self.client = client + self._available = True + + @property + def unique_id(self) -> str: + """Return the unique id for this device.""" + serial = self._data[ATTR_SERIAL_NUMBER] + return f"{serial}_{self.type}" + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + @property + def device_info(self): + """Return device specific attributes.""" + return { + "identifiers": {(DOMAIN, self._data[ATTR_SERIAL_NUMBER])}, + "manufacturer": MANUFACTURER, + "model": self._data[ATTR_MODEL], + "name": self._data.get(ATTR_DEVICE_NAME, DEFAULT_DEVICE_NAME), + "sw_version": self._data[ATTR_FIRMWARE], + } diff --git a/homeassistant/components/aurora_abb_powerone/config_flow.py b/homeassistant/components/aurora_abb_powerone/config_flow.py new file mode 100644 index 00000000000..c0c87e9e103 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/config_flow.py @@ -0,0 +1,147 @@ +"""Config flow for Aurora ABB PowerOne integration.""" +import logging + +from aurorapy.client import AuroraError, AuroraSerialClient +import serial.tools.list_ports +import voluptuous as vol + +from homeassistant import config_entries, core +from homeassistant.const import CONF_ADDRESS, CONF_PORT + +from .const import ( + ATTR_FIRMWARE, + ATTR_MODEL, + ATTR_SERIAL_NUMBER, + DEFAULT_ADDRESS, + DEFAULT_INTEGRATION_TITLE, + DOMAIN, + MAX_ADDRESS, + MIN_ADDRESS, +) + +_LOGGER = logging.getLogger(__name__) + + +def validate_and_connect(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + comport = data[CONF_PORT] + address = data[CONF_ADDRESS] + _LOGGER.debug("Intitialising com port=%s", comport) + ret = {} + ret["title"] = DEFAULT_INTEGRATION_TITLE + try: + client = AuroraSerialClient(address, comport, parity="N", timeout=1) + client.connect() + ret[ATTR_SERIAL_NUMBER] = client.serial_number() + ret[ATTR_MODEL] = f"{client.version()} ({client.pn()})" + ret[ATTR_FIRMWARE] = client.firmware(1) + _LOGGER.info("Returning device info=%s", ret) + except AuroraError as err: + _LOGGER.warning("Could not connect to device=%s", comport) + raise err + finally: + if client.serline.isOpen(): + client.close() + + # Return info we want to store in the config entry. + return ret + + +def scan_comports(): + """Find and store available com ports for the GUI dropdown.""" + comports = serial.tools.list_ports.comports(include_links=True) + comportslist = [] + for port in comports: + comportslist.append(port.device) + _LOGGER.debug("COM port option: %s", port.device) + if len(comportslist) > 0: + return comportslist, comportslist[0] + _LOGGER.warning("No com ports found. Need a valid RS485 device to communicate") + return None, None + + +class AuroraABBConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Aurora ABB PowerOne.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialise the config flow.""" + self.config = None + self._comportslist = None + self._defaultcomport = None + + async def async_step_import(self, config: dict): + """Import a configuration from config.yaml.""" + if self.hass.config_entries.async_entries(DOMAIN): + return self.async_abort(reason="already_setup") + + conf = {} + conf[ATTR_SERIAL_NUMBER] = "sn_unknown_yaml" + conf[ATTR_MODEL] = "model_unknown_yaml" + conf[ATTR_FIRMWARE] = "fw_unknown_yaml" + conf[CONF_PORT] = config["device"] + conf[CONF_ADDRESS] = config["address"] + # config["name"] from yaml is ignored. + + await self.async_set_unique_id(self.flow_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry(title=DEFAULT_INTEGRATION_TITLE, data=conf) + + async def async_step_user(self, user_input=None): + """Handle a flow initialised by the user.""" + + errors = {} + if self._comportslist is None: + result = await self.hass.async_add_executor_job(scan_comports) + self._comportslist, self._defaultcomport = result + if self._defaultcomport is None: + return self.async_abort(reason="no_serial_ports") + + # Handle the initial step. + if user_input is not None: + try: + info = await self.hass.async_add_executor_job( + validate_and_connect, self.hass, user_input + ) + info.update(user_input) + # Bomb out early if someone has already set up this device. + device_unique_id = info["serial_number"] + await self.async_set_unique_id(device_unique_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry(title=info["title"], data=info) + + except OSError as error: + if error.errno == 19: # No such device. + errors["base"] = "invalid_serial_port" + except AuroraError as error: + if "could not open port" in str(error): + errors["base"] = "cannot_open_serial_port" + elif "No response after" in str(error): + errors["base"] = "cannot_connect" # could be dark + else: + _LOGGER.error( + "Unable to communicate with Aurora ABB Inverter at %s: %s %s", + user_input[CONF_PORT], + type(error), + error, + ) + errors["base"] = "cannot_connect" + # If no user input, must be first pass through the config. Show initial form. + config_options = { + vol.Required(CONF_PORT, default=self._defaultcomport): vol.In( + self._comportslist + ), + vol.Required(CONF_ADDRESS, default=DEFAULT_ADDRESS): vol.In( + range(MIN_ADDRESS, MAX_ADDRESS + 1) + ), + } + schema = vol.Schema(config_options) + + return self.async_show_form(step_id="user", data_schema=schema, errors=errors) diff --git a/homeassistant/components/aurora_abb_powerone/const.py b/homeassistant/components/aurora_abb_powerone/const.py new file mode 100644 index 00000000000..3711dd6d800 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/const.py @@ -0,0 +1,22 @@ +"""Constants for the Aurora ABB PowerOne integration.""" + +DOMAIN = "aurora_abb_powerone" + +# Min max addresses and default according to here: +# https://library.e.abb.com/public/e57212c407344a16b4644cee73492b39/PVI-3.0_3.6_4.2-TL-OUTD-Product%20manual%20EN-RevB(M000016BG).pdf + +MIN_ADDRESS = 2 +MAX_ADDRESS = 63 +DEFAULT_ADDRESS = 2 + +DEFAULT_INTEGRATION_TITLE = "PhotoVoltaic Inverters" +DEFAULT_DEVICE_NAME = "Solar Inverter" + +DEVICES = "devices" +MANUFACTURER = "ABB" + +ATTR_DEVICE_NAME = "device_name" +ATTR_DEVICE_ID = "device_id" +ATTR_SERIAL_NUMBER = "serial_number" +ATTR_MODEL = "model" +ATTR_FIRMWARE = "firmware" diff --git a/homeassistant/components/aurora_abb_powerone/manifest.json b/homeassistant/components/aurora_abb_powerone/manifest.json index 69798ce4906..9849c0d84ee 100644 --- a/homeassistant/components/aurora_abb_powerone/manifest.json +++ b/homeassistant/components/aurora_abb_powerone/manifest.json @@ -1,8 +1,11 @@ { "domain": "aurora_abb_powerone", - "name": "Aurora ABB Solar PV", - "documentation": "https://www.home-assistant.io/integrations/aurora_abb_powerone/", - "codeowners": ["@davet2001"], + "name": "Aurora ABB PowerOne Solar PV", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/aurora_abb_powerone", "requirements": ["aurorapy==0.2.6"], + "codeowners": [ + "@davet2001" + ], "iot_class": "local_polling" } diff --git a/homeassistant/components/aurora_abb_powerone/sensor.py b/homeassistant/components/aurora_abb_powerone/sensor.py index b1bcec18796..bbbd026bb2e 100644 --- a/homeassistant/components/aurora_abb_powerone/sensor.py +++ b/homeassistant/components/aurora_abb_powerone/sensor.py @@ -10,54 +10,85 @@ from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, SensorEntity, ) +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_ADDRESS, CONF_DEVICE, CONF_NAME, DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, POWER_WATT, + TEMP_CELSIUS, ) +from homeassistant.exceptions import InvalidStateError import homeassistant.helpers.config_validation as cv +from .aurora_device import AuroraDevice +from .const import DEFAULT_ADDRESS, DOMAIN + _LOGGER = logging.getLogger(__name__) -DEFAULT_ADDRESS = 2 -DEFAULT_NAME = "Solar PV" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_DEVICE): cv.string, vol.Optional(CONF_ADDRESS, default=DEFAULT_ADDRESS): cv.positive_int, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME, default="Solar PV"): cv.string, } ) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Aurora ABB PowerOne device.""" - devices = [] - comport = config[CONF_DEVICE] - address = config[CONF_ADDRESS] - name = config[CONF_NAME] - - _LOGGER.debug("Intitialising com port=%s address=%s", comport, address) - client = AuroraSerialClient(address, comport, parity="N", timeout=1) - - devices.append(AuroraABBSolarPVMonitorSensor(client, name, "Power")) - add_entities(devices, True) +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up based on configuration.yaml (DEPRECATED).""" + _LOGGER.warning( + "Loading aurora_abb_powerone via platform config is deprecated; The configuration" + " has been migrated to a config entry and can be safely removed from configuration.yaml" + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) + ) -class AuroraABBSolarPVMonitorSensor(SensorEntity): - """Representation of a Sensor.""" +async def async_setup_entry(hass, config_entry, async_add_entities) -> None: + """Set up aurora_abb_powerone sensor based on a config entry.""" + entities = [] + + sensortypes = [ + {"parameter": "instantaneouspower", "name": "Power Output"}, + {"parameter": "temperature", "name": "Temperature"}, + ] + client = hass.data[DOMAIN][config_entry.unique_id] + data = config_entry.data + + for sens in sensortypes: + entities.append(AuroraSensor(client, data, sens["name"], sens["parameter"])) + + _LOGGER.debug("async_setup_entry adding %d entities", len(entities)) + async_add_entities(entities, True) + + +class AuroraSensor(AuroraDevice, SensorEntity): + """Representation of a Sensor on a Aurora ABB PowerOne Solar inverter.""" _attr_state_class = STATE_CLASS_MEASUREMENT - _attr_native_unit_of_measurement = POWER_WATT - _attr_device_class = DEVICE_CLASS_POWER - def __init__(self, client, name, typename): + def __init__(self, client: AuroraSerialClient, data, name, typename): """Initialize the sensor.""" - self._attr_name = f"{name} {typename}" - self.client = client + super().__init__(client, data) + if typename == "instantaneouspower": + self.type = typename + self._attr_unit_of_measurement = POWER_WATT + self._attr_device_class = DEVICE_CLASS_POWER + elif typename == "temperature": + self.type = typename + self._attr_native_unit_of_measurement = TEMP_CELSIUS + self._attr_device_class = DEVICE_CLASS_TEMPERATURE + else: + raise InvalidStateError(f"Unrecognised typename '{typename}'") + self._attr_name = f"{name}" + self.availableprev = True def update(self): """Fetch new state data for the sensor. @@ -65,11 +96,21 @@ class AuroraABBSolarPVMonitorSensor(SensorEntity): This is the only method that should fetch new data for Home Assistant. """ try: + self.availableprev = self._attr_available self.client.connect() - # read ADC channel 3 (grid power output) - power_watts = self.client.measure(3, True) - self._attr_native_value = round(power_watts, 1) + if self.type == "instantaneouspower": + # read ADC channel 3 (grid power output) + power_watts = self.client.measure(3, True) + self._attr_state = round(power_watts, 1) + elif self.type == "temperature": + temperature_c = self.client.measure(21) + self._attr_native_value = round(temperature_c, 1) + self._attr_available = True + except AuroraError as error: + self._attr_state = None + self._attr_native_value = None + self._attr_available = False # aurorapy does not have different exceptions (yet) for dealing # with timeout vs other comms errors. # This means the (normal) situation of no response during darkness @@ -82,7 +123,14 @@ class AuroraABBSolarPVMonitorSensor(SensorEntity): _LOGGER.debug("No response from inverter (could be dark)") else: raise error - self._attr_native_value = None finally: + if self._attr_available != self.availableprev: + if self._attr_available: + _LOGGER.info("Communication with %s back online", self.name) + else: + _LOGGER.warning( + "Communication with %s lost", + self.name, + ) if self.client.serline.isOpen(): self.client.close() diff --git a/homeassistant/components/aurora_abb_powerone/strings.json b/homeassistant/components/aurora_abb_powerone/strings.json new file mode 100644 index 00000000000..b705c5f69a5 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "description": "The inverter must be connected via an RS485 adaptor, please select serial port and the inverter's address as configured on the LCD panel", + "data": { + "port": "RS485 or USB-RS485 Adaptor Port", + "address": "Inverter Address" + } + } + }, + "error": { + "cannot_connect": "Unable to connect, please check serial port, address, electrical connection and that inverter is on (in daylight)", + "invalid_serial_port": "Serial port is not a valid device or could not be openned", + "cannot_open_serial_port": "Cannot open serial port, please check and try again", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "Device is already configured", + "no_serial_ports": "No com ports found. Need a valid RS485 device to communicate." + } + } +} diff --git a/homeassistant/components/aurora_abb_powerone/translations/en.json b/homeassistant/components/aurora_abb_powerone/translations/en.json new file mode 100644 index 00000000000..97e0fc50908 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "no_serial_ports": "No com ports found. Need a valid RS485 device to communicate." + }, + "error": { + "cannot_connect": "Unable to connect, please check serial port, address, electrical connection and that inverter is on (in daylight)", + "cannot_open_serial_port": "Cannot open serial port, please check and try again", + "invalid_serial_port": "Serial port is not a valid device or could not be openned", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "address": "Inverter Address", + "port": "RS485 or USB-RS485 Adaptor Port" + }, + "description": "The inverter must be connected via an RS485 adaptor, please select serial port and the inverter's address as configured on the LCD panel" + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index dba466e181c..aef37105170 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -31,6 +31,7 @@ FLOWS = [ "atag", "august", "aurora", + "aurora_abb_powerone", "awair", "axis", "azure_devops", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2eb5501a527..eefe731f51a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -235,6 +235,9 @@ async-upnp-client==0.22.10 # homeassistant.components.aurora auroranoaa==0.0.2 +# homeassistant.components.aurora_abb_powerone +aurorapy==0.2.6 + # homeassistant.components.stream av==8.0.3 diff --git a/tests/components/aurora_abb_powerone/__init__.py b/tests/components/aurora_abb_powerone/__init__.py new file mode 100644 index 00000000000..960412f6d97 --- /dev/null +++ b/tests/components/aurora_abb_powerone/__init__.py @@ -0,0 +1 @@ +"""Tests for the Aurora ABB PowerOne Solar PV integration.""" diff --git a/tests/components/aurora_abb_powerone/test_config_flow.py b/tests/components/aurora_abb_powerone/test_config_flow.py new file mode 100644 index 00000000000..d385d33ddd9 --- /dev/null +++ b/tests/components/aurora_abb_powerone/test_config_flow.py @@ -0,0 +1,165 @@ +"""Test the Aurora ABB PowerOne Solar PV config flow.""" +from logging import INFO +from unittest.mock import patch + +from aurorapy.client import AuroraError +from serial.tools import list_ports_common + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.aurora_abb_powerone.const import ( + ATTR_FIRMWARE, + ATTR_MODEL, + ATTR_SERIAL_NUMBER, + DOMAIN, +) +from homeassistant.const import CONF_ADDRESS, CONF_PORT + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + fakecomports = [] + fakecomports.append(list_ports_common.ListPortInfo("/dev/ttyUSB7")) + with patch( + "serial.tools.list_ports.comports", + return_value=fakecomports, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None,), patch( + "aurorapy.client.AuroraSerialClient.serial_number", + return_value="9876543", + ), patch( + "aurorapy.client.AuroraSerialClient.version", + return_value="9.8.7.6", + ), patch( + "aurorapy.client.AuroraSerialClient.pn", + return_value="A.B.C", + ), patch( + "aurorapy.client.AuroraSerialClient.firmware", + return_value="1.234", + ), patch( + "homeassistant.components.aurora_abb_powerone.config_flow._LOGGER.getEffectiveLevel", + return_value=INFO, + ) as mock_setup, patch( + "homeassistant.components.aurora_abb_powerone.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PORT: "/dev/ttyUSB7", CONF_ADDRESS: 7}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + assert result2["data"] == { + CONF_PORT: "/dev/ttyUSB7", + CONF_ADDRESS: 7, + ATTR_FIRMWARE: "1.234", + ATTR_MODEL: "9.8.7.6 (A.B.C)", + ATTR_SERIAL_NUMBER: "9876543", + "title": "PhotoVoltaic Inverters", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_no_comports(hass): + """Test we display correct info when there are no com ports..""" + + fakecomports = [] + with patch( + "serial.tools.list_ports.comports", + return_value=fakecomports, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "abort" + assert result["reason"] == "no_serial_ports" + + +async def test_form_invalid_com_ports(hass): + """Test we display correct info when the comport is invalid..""" + + fakecomports = [] + fakecomports.append(list_ports_common.ListPortInfo("/dev/ttyUSB7")) + with patch( + "serial.tools.list_ports.comports", + return_value=fakecomports, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "aurorapy.client.AuroraSerialClient.connect", + side_effect=OSError(19, "...no such device..."), + return_value=None, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PORT: "/dev/ttyUSB7", CONF_ADDRESS: 7}, + ) + assert result2["errors"] == {"base": "invalid_serial_port"} + + with patch( + "aurorapy.client.AuroraSerialClient.connect", + side_effect=AuroraError("..could not open port..."), + return_value=None, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PORT: "/dev/ttyUSB7", CONF_ADDRESS: 7}, + ) + assert result2["errors"] == {"base": "cannot_open_serial_port"} + + with patch( + "aurorapy.client.AuroraSerialClient.connect", + side_effect=AuroraError("...No response after..."), + return_value=None, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PORT: "/dev/ttyUSB7", CONF_ADDRESS: 7}, + ) + assert result2["errors"] == {"base": "cannot_connect"} + + with patch( + "aurorapy.client.AuroraSerialClient.connect", + side_effect=AuroraError("...Some other message!!!123..."), + return_value=None, + ), patch("serial.Serial.isOpen", return_value=True,), patch( + "aurorapy.client.AuroraSerialClient.close", + ) as mock_clientclose: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PORT: "/dev/ttyUSB7", CONF_ADDRESS: 7}, + ) + assert result2["errors"] == {"base": "cannot_connect"} + assert len(mock_clientclose.mock_calls) == 1 + + +# Tests below can be deleted after deprecation period is finished. +async def test_import(hass): + """Test configuration.yaml import used during migration.""" + TESTDATA = {"device": "/dev/ttyUSB7", "address": 3, "name": "MyAuroraPV"} + with patch( + "homeassistant.components.generic.camera.GenericCamera.async_camera_image", + return_value=None, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_PORT] == "/dev/ttyUSB7" + assert result["data"][CONF_ADDRESS] == 3 diff --git a/tests/components/aurora_abb_powerone/test_init.py b/tests/components/aurora_abb_powerone/test_init.py new file mode 100644 index 00000000000..bd0f1c727cd --- /dev/null +++ b/tests/components/aurora_abb_powerone/test_init.py @@ -0,0 +1,37 @@ +"""Pytest modules for Aurora ABB Powerone PV inverter sensor integration.""" +from unittest.mock import patch + +from homeassistant.components.aurora_abb_powerone.const import ( + ATTR_FIRMWARE, + ATTR_MODEL, + ATTR_SERIAL_NUMBER, + DOMAIN, +) +from homeassistant.const import CONF_ADDRESS, CONF_PORT +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def test_unload_entry(hass): + """Test unloading the aurora_abb_powerone entry.""" + + with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( + "homeassistant.components.aurora_abb_powerone.sensor.AuroraSensor.update", + return_value=None, + ): + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_PORT: "/dev/ttyUSB7", + CONF_ADDRESS: 7, + ATTR_MODEL: "model123", + ATTR_SERIAL_NUMBER: "876", + ATTR_FIRMWARE: "1.2.3.4", + }, + ) + mock_entry.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert await hass.config_entries.async_unload(mock_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/aurora_abb_powerone/test_sensor.py b/tests/components/aurora_abb_powerone/test_sensor.py new file mode 100644 index 00000000000..26486c6a116 --- /dev/null +++ b/tests/components/aurora_abb_powerone/test_sensor.py @@ -0,0 +1,185 @@ +"""Test the Aurora ABB PowerOne Solar PV sensors.""" +from datetime import timedelta +from unittest.mock import patch + +from aurorapy.client import AuroraError +import pytest + +from homeassistant.components.aurora_abb_powerone.const import ( + ATTR_DEVICE_NAME, + ATTR_FIRMWARE, + ATTR_MODEL, + ATTR_SERIAL_NUMBER, + DEFAULT_INTEGRATION_TITLE, + DOMAIN, +) +from homeassistant.components.aurora_abb_powerone.sensor import AuroraSensor +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import CONF_ADDRESS, CONF_PORT +from homeassistant.exceptions import InvalidStateError +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from tests.common import ( + MockConfigEntry, + assert_setup_component, + async_fire_time_changed, +) + +TEST_CONFIG = { + "sensor": { + "platform": "aurora_abb_powerone", + "device": "/dev/fakedevice0", + "address": 2, + } +} + + +def _simulated_returns(index, global_measure=None): + returns = { + 3: 45.678, # power + 21: 9.876, # temperature + } + return returns[index] + + +def _mock_config_entry(): + return MockConfigEntry( + version=1, + domain=DOMAIN, + title=DEFAULT_INTEGRATION_TITLE, + data={ + CONF_PORT: "/dev/usb999", + CONF_ADDRESS: 3, + ATTR_DEVICE_NAME: "mydevicename", + ATTR_MODEL: "mymodel", + ATTR_SERIAL_NUMBER: "123456", + ATTR_FIRMWARE: "1.2.3.4", + }, + source="dummysource", + entry_id="13579", + ) + + +async def test_setup_platform_valid_config(hass): + """Test that (deprecated) yaml import still works.""" + with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( + "aurorapy.client.AuroraSerialClient.measure", + side_effect=_simulated_returns, + ), assert_setup_component(1, "sensor"): + assert await async_setup_component(hass, "sensor", TEST_CONFIG) + await hass.async_block_till_done() + power = hass.states.get("sensor.power_output") + assert power + assert power.state == "45.7" + + # try to set up a second time - should abort. + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=TEST_CONFIG, + context={"source": SOURCE_IMPORT}, + ) + assert result["type"] == "abort" + assert result["reason"] == "already_setup" + + +async def test_sensors(hass): + """Test data coming back from inverter.""" + mock_entry = _mock_config_entry() + + with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( + "aurorapy.client.AuroraSerialClient.measure", + side_effect=_simulated_returns, + ): + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + power = hass.states.get("sensor.power_output") + assert power + assert power.state == "45.7" + + temperature = hass.states.get("sensor.temperature") + assert temperature + assert temperature.state == "9.9" + + +async def test_sensor_invalid_type(hass): + """Test invalid sensor type during setup.""" + entities = [] + mock_entry = _mock_config_entry() + + with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( + "aurorapy.client.AuroraSerialClient.measure", + side_effect=_simulated_returns, + ): + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + client = hass.data[DOMAIN][mock_entry.unique_id] + data = mock_entry.data + with pytest.raises(InvalidStateError): + entities.append(AuroraSensor(client, data, "WrongSensor", "wrongparameter")) + + +async def test_sensor_dark(hass): + """Test that darkness (no comms) is handled correctly.""" + mock_entry = _mock_config_entry() + + utcnow = dt_util.utcnow() + # sun is up + with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( + "aurorapy.client.AuroraSerialClient.measure", side_effect=_simulated_returns + ): + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + power = hass.states.get("sensor.power_output") + assert power is not None + assert power.state == "45.7" + + # sunset + with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( + "aurorapy.client.AuroraSerialClient.measure", + side_effect=AuroraError("No response after 10 seconds"), + ): + async_fire_time_changed(hass, utcnow + timedelta(seconds=60)) + await hass.async_block_till_done() + power = hass.states.get("sensor.power_output") + assert power.state == "unknown" + # sun rose again + with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( + "aurorapy.client.AuroraSerialClient.measure", side_effect=_simulated_returns + ): + async_fire_time_changed(hass, utcnow + timedelta(seconds=60)) + await hass.async_block_till_done() + power = hass.states.get("sensor.power_output") + assert power is not None + assert power.state == "45.7" + # sunset + with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( + "aurorapy.client.AuroraSerialClient.measure", + side_effect=AuroraError("No response after 10 seconds"), + ): + async_fire_time_changed(hass, utcnow + timedelta(seconds=60)) + await hass.async_block_till_done() + power = hass.states.get("sensor.power_output") + assert power.state == "unknown" # should this be 'available'? + + +async def test_sensor_unknown_error(hass): + """Test other comms error is handled correctly.""" + mock_entry = _mock_config_entry() + + with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( + "aurorapy.client.AuroraSerialClient.measure", + side_effect=AuroraError("another error"), + ): + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + power = hass.states.get("sensor.power_output") + assert power is None From 207a5029e8b0d7fd8d779a7f970088a49671f72d Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 25 Oct 2021 21:51:11 -0400 Subject: [PATCH 0848/1038] Use class attribute instead of property decorator (#58448) --- homeassistant/components/zwave_js/entity.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index f9bba52c95b..aa915cd5822 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -27,6 +27,8 @@ EVENT_ALIVE = "alive" class ZWaveBaseEntity(Entity): """Generic Entity Class for a Z-Wave Device.""" + _attr_should_poll = False + def __init__( self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo ) -> None: @@ -243,8 +245,3 @@ class ZWaveBaseEntity(Entity): ): self.watched_value_ids.add(return_value.value_id) return return_value - - @property - def should_poll(self) -> bool: - """No polling needed.""" - return False From 3732ae738e78b258d9fba755d44b4cf2cbae51ff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 25 Oct 2021 21:53:13 -0500 Subject: [PATCH 0849/1038] Fix flux_led with RGB/W bulbs (model 0x44) (#58438) --- homeassistant/components/flux_led/light.py | 23 +++- .../components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/flux_led/test_light.py | 112 ++++++++++++++++++ 5 files changed, 134 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 655b868a4f5..6f1db96a7aa 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -30,12 +30,14 @@ from homeassistant.components.light import ( ATTR_RGB_COLOR, ATTR_RGBW_COLOR, ATTR_RGBWW_COLOR, + ATTR_WHITE, COLOR_MODE_BRIGHTNESS, COLOR_MODE_COLOR_TEMP, COLOR_MODE_ONOFF, COLOR_MODE_RGB, COLOR_MODE_RGBW, COLOR_MODE_RGBWW, + COLOR_MODE_WHITE, EFFECT_COLORLOOP, EFFECT_RANDOM, PLATFORM_SCHEMA, @@ -100,7 +102,6 @@ FLUX_COLOR_MODE_TO_HASS: Final = { FLUX_COLOR_MODE_RGBW: COLOR_MODE_RGBW, FLUX_COLOR_MODE_RGBWW: COLOR_MODE_RGBWW, FLUX_COLOR_MODE_CCT: COLOR_MODE_COLOR_TEMP, - FLUX_COLOR_MODE_DIM: COLOR_MODE_BRIGHTNESS, } EFFECT_SUPPORT_MODES = {COLOR_MODE_RGB, COLOR_MODE_RGBW, COLOR_MODE_RGBWW} @@ -195,6 +196,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) +def _flux_color_mode_to_hass(flux_color_mode: str, flux_color_modes: set[str]) -> str: + """Map the flux color mode to Home Assistant color mode.""" + if flux_color_mode == FLUX_COLOR_MODE_DIM: + if len(flux_color_modes) > 1: + return COLOR_MODE_WHITE + return COLOR_MODE_BRIGHTNESS + return FLUX_COLOR_MODE_TO_HASS.get(flux_color_mode, COLOR_MODE_ONOFF) + + async def async_setup_platform( hass: HomeAssistant, config: ConfigType, @@ -300,7 +310,7 @@ class FluxLight(FluxEntity, CoordinatorEntity, LightEntity): ) # for rounding self._attr_max_mireds = color_temperature_kelvin_to_mired(self._device.min_temp) self._attr_supported_color_modes = { - FLUX_COLOR_MODE_TO_HASS.get(mode, COLOR_MODE_ONOFF) + _flux_color_mode_to_hass(mode, self._device.color_modes) for mode in self._device.color_modes } if self._attr_supported_color_modes.intersection(EFFECT_SUPPORT_MODES): @@ -352,7 +362,9 @@ class FluxLight(FluxEntity, CoordinatorEntity, LightEntity): @property def color_mode(self) -> str: """Return the color mode of the light.""" - return FLUX_COLOR_MODE_TO_HASS.get(self._device.color_mode, COLOR_MODE_ONOFF) + return _flux_color_mode_to_hass( + self._device.color_mode, self._device.color_modes + ) @property def effect(self) -> str | None: @@ -410,6 +422,9 @@ class FluxLight(FluxEntity, CoordinatorEntity, LightEntity): rgbcw = kwargs[ATTR_RGBWW_COLOR] await self._device.async_set_levels(*rgbcw_to_rgbwc(rgbcw)) return + if ATTR_WHITE in kwargs: + await self._device.async_set_levels(w=kwargs[ATTR_WHITE]) + return if ATTR_EFFECT in kwargs: effect = kwargs[ATTR_EFFECT] # Random color effect @@ -456,7 +471,7 @@ class FluxLight(FluxEntity, CoordinatorEntity, LightEntity): await self._device.async_set_levels(*rgbww_brightness(rgbwc, brightness)) return # Handle Brightness Only Color Mode - if self.color_mode == COLOR_MODE_BRIGHTNESS: + if self.color_mode in {COLOR_MODE_WHITE, COLOR_MODE_BRIGHTNESS}: await self._device.async_set_levels(w=brightness) return raise ValueError(f"Unsupported color mode {self.color_mode}") diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index dfe166e65cf..4b5a63542c9 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -3,7 +3,7 @@ "name": "Flux LED/MagicHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.24.12"], + "requirements": ["flux_led==0.24.13"], "quality_scale": "platinum", "codeowners": ["@icemanch"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index f04ff45af8e..6ee4e154dde 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -655,7 +655,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.12 +flux_led==0.24.13 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eefe731f51a..d153bef0caa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -390,7 +390,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.1 # homeassistant.components.flux_led -flux_led==0.24.12 +flux_led==0.24.13 # homeassistant.components.homekit fnvhash==0.1.0 diff --git a/tests/components/flux_led/test_light.py b/tests/components/flux_led/test_light.py index a3f381b0c1d..6f0ad5aa253 100644 --- a/tests/components/flux_led/test_light.py +++ b/tests/components/flux_led/test_light.py @@ -9,6 +9,7 @@ from flux_led.const import ( COLOR_MODE_RGB as FLUX_COLOR_MODE_RGB, COLOR_MODE_RGBW as FLUX_COLOR_MODE_RGBW, COLOR_MODE_RGBWW as FLUX_COLOR_MODE_RGBWW, + COLOR_MODES_RGB_W as FLUX_COLOR_MODES_RGB_W, ) import pytest @@ -38,6 +39,7 @@ from homeassistant.components.light import ( ATTR_RGBW_COLOR, ATTR_RGBWW_COLOR, ATTR_SUPPORTED_COLOR_MODES, + ATTR_WHITE, DOMAIN as LIGHT_DOMAIN, ) from homeassistant.const import ( @@ -485,6 +487,116 @@ async def test_rgbw_light(hass: HomeAssistant) -> None: bulb.async_set_preset_pattern.reset_mock() +async def test_rgb_or_w_light(hass: HomeAssistant) -> None: + """Test an rgb or w light.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.color_modes = FLUX_COLOR_MODES_RGB_W + bulb.color_mode = FLUX_COLOR_MODE_RGB + with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): + await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.az120444_aabbccddeeff" + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 128 + assert attributes[ATTR_COLOR_MODE] == "rgb" + assert attributes[ATTR_EFFECT_LIST] == FLUX_EFFECT_LIST + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgb", "white"] + assert attributes[ATTR_RGB_COLOR] == (255, 0, 0) + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.async_turn_off.assert_called_once() + await async_mock_device_turn_off(hass, bulb) + + assert hass.states.get(entity_id).state == STATE_OFF + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.async_turn_on.assert_called_once() + bulb.async_turn_on.reset_mock() + bulb.is_on = True + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + bulb.async_set_levels.assert_called_with(255, 0, 0, brightness=100) + bulb.async_set_levels.reset_mock() + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + ATTR_ENTITY_ID: entity_id, + ATTR_RGB_COLOR: (255, 255, 255), + ATTR_BRIGHTNESS: 128, + }, + blocking=True, + ) + bulb.async_set_levels.assert_called_with(255, 255, 255, brightness=128) + bulb.async_set_levels.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"}, + blocking=True, + ) + bulb.async_set_levels.assert_called_once() + bulb.async_set_levels.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"}, + blocking=True, + ) + bulb.async_set_preset_pattern.assert_called_with(43, 50) + bulb.async_set_preset_pattern.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + ATTR_ENTITY_ID: entity_id, + ATTR_WHITE: 128, + }, + blocking=True, + ) + bulb.async_set_levels.assert_called_with(w=128) + bulb.async_set_levels.reset_mock() + + bulb.color_mode = FLUX_COLOR_MODE_DIM + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + ATTR_ENTITY_ID: entity_id, + ATTR_BRIGHTNESS: 100, + }, + blocking=True, + ) + bulb.async_set_levels.assert_called_with(w=100) + bulb.async_set_levels.reset_mock() + + async def test_rgbcw_light(hass: HomeAssistant) -> None: """Test an rgbcw light.""" config_entry = MockConfigEntry( From 65b19da3ff1a8ea5f2b18f119db506eb09dc4f3c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 26 Oct 2021 05:38:06 +0200 Subject: [PATCH 0850/1038] Refactor input_select (#53334) --- .strict-typing | 1 + .../components/input_select/__init__.py | 149 +++++++-------- mypy.ini | 11 ++ tests/components/input_select/test_init.py | 174 ++++++++---------- tests/components/template/test_select.py | 2 +- 5 files changed, 162 insertions(+), 175 deletions(-) diff --git a/.strict-typing b/.strict-typing index 2dadf1a1d5b..97fc7587a9d 100644 --- a/.strict-typing +++ b/.strict-typing @@ -58,6 +58,7 @@ homeassistant.components.http.* homeassistant.components.huawei_lte.* homeassistant.components.hyperion.* homeassistant.components.image_processing.* +homeassistant.components.input_select.* homeassistant.components.integration.* homeassistant.components.iqvia.* homeassistant.components.jewish_calendar.* diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index 859ae6f91e1..1443ac6d1e1 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -2,9 +2,11 @@ from __future__ import annotations import logging +from typing import Any, Dict, cast import voluptuous as vol +from homeassistant.components.select import SelectEntity from homeassistant.const import ( ATTR_EDITABLE, ATTR_OPTION, @@ -55,7 +57,7 @@ UPDATE_FIELDS = { } -def _cv_input_select(cfg): +def _cv_input_select(cfg: dict[str, Any]) -> dict[str, Any]: """Configure validation helper for input select (voluptuous).""" options = cfg[CONF_OPTIONS] initial = cfg.get(CONF_INITIAL) @@ -183,138 +185,137 @@ class InputSelectStorageCollection(collection.StorageCollection): CREATE_SCHEMA = vol.Schema(vol.All(CREATE_FIELDS, _cv_input_select)) UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) - async def _process_create_data(self, data: dict) -> dict: + async def _process_create_data(self, data: dict[str, Any]) -> dict[str, Any]: """Validate the config is valid.""" - return self.CREATE_SCHEMA(data) + return cast(Dict[str, Any], self.CREATE_SCHEMA(data)) @callback - def _get_suggested_id(self, info: dict) -> str: + def _get_suggested_id(self, info: dict[str, Any]) -> str: """Suggest an ID based on the config.""" - return info[CONF_NAME] + return cast(str, info[CONF_NAME]) - async def _update_data(self, data: dict, update_data: dict) -> dict: + async def _update_data( + self, data: dict[str, Any], update_data: dict[str, Any] + ) -> dict[str, Any]: """Return a new updated data object.""" update_data = self.UPDATE_SCHEMA(update_data) return _cv_input_select({**data, **update_data}) -class InputSelect(RestoreEntity): +class InputSelect(SelectEntity, RestoreEntity): """Representation of a select input.""" - def __init__(self, config: dict) -> None: + _attr_should_poll = False + editable = True + + def __init__(self, config: ConfigType) -> None: """Initialize a select input.""" - self._config = config - self.editable = True - self._current_option = config.get(CONF_INITIAL) + self._attr_current_option = config.get(CONF_INITIAL) + self._attr_icon = config.get(CONF_ICON) + self._attr_name = config.get(CONF_NAME) + self._attr_options = config[CONF_OPTIONS] + self._attr_unique_id = config[CONF_ID] @classmethod - def from_yaml(cls, config: dict) -> InputSelect: + def from_yaml(cls, config: ConfigType) -> InputSelect: """Return entity instance initialized from yaml storage.""" input_select = cls(config) input_select.entity_id = f"{DOMAIN}.{config[CONF_ID]}" input_select.editable = False return input_select - 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() - if self._current_option is not None: + if self.current_option is not None: return state = await self.async_get_last_state() - if not state or state.state not in self._options: - self._current_option = self._options[0] + if not state or state.state not in self.options: + self._attr_current_option = self.options[0] else: - self._current_option = state.state + self._attr_current_option = state.state @property - def should_poll(self): - """If entity should be polled.""" - return False - - @property - def name(self): - """Return the name of the select input.""" - return self._config.get(CONF_NAME) - - @property - def icon(self): - """Return the icon to be used for this entity.""" - return self._config.get(CONF_ICON) - - @property - def _options(self) -> list[str]: - """Return a list of selection options.""" - return self._config[CONF_OPTIONS] - - @property - def state(self): - """Return the state of the component.""" - return self._current_option - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, bool]: """Return the state attributes.""" - return {ATTR_OPTIONS: self._config[ATTR_OPTIONS], ATTR_EDITABLE: self.editable} + return {ATTR_EDITABLE: self.editable} - @property - def unique_id(self) -> str | None: - """Return unique id for the entity.""" - return self._config[CONF_ID] - - @callback - def async_select_option(self, option): + async def async_select_option(self, option: str) -> None: """Select new option.""" - if option not in self._options: + if option not in self.options: _LOGGER.warning( "Invalid option: %s (possible options: %s)", option, - ", ".join(self._options), + ", ".join(self.options), ) return - self._current_option = option + self._attr_current_option = option self.async_write_ha_state() @callback - def async_select_index(self, idx): + def async_select_index(self, idx: int) -> None: """Select new option by index.""" - new_index = idx % len(self._options) - self._current_option = self._options[new_index] + new_index = idx % len(self.options) + self._attr_current_option = self.options[new_index] self.async_write_ha_state() @callback - def async_offset_index(self, offset, cycle): + def async_offset_index(self, offset: int, cycle: bool) -> None: """Offset current index.""" - current_index = self._options.index(self._current_option) + + current_index = ( + self.options.index(self.current_option) + if self.current_option is not None + else 0 + ) + new_index = current_index + offset if cycle: - new_index = new_index % len(self._options) - else: - if new_index < 0: - new_index = 0 - elif new_index >= len(self._options): - new_index = len(self._options) - 1 - self._current_option = self._options[new_index] + new_index = new_index % len(self.options) + elif new_index < 0: + new_index = 0 + elif new_index >= len(self.options): + new_index = len(self.options) - 1 + + self._attr_current_option = self.options[new_index] self.async_write_ha_state() @callback - def async_next(self, cycle): + def async_next(self, cycle: bool) -> None: """Select next option.""" + # If there is no current option, first item is the next + if self.current_option is None: + self.async_select_index(0) + return self.async_offset_index(1, cycle) @callback - def async_previous(self, cycle): + def async_previous(self, cycle: bool) -> None: """Select previous option.""" + # If there is no current option, last item is the previous + if self.current_option is None: + self.async_select_index(-1) + return self.async_offset_index(-1, cycle) - @callback - def async_set_options(self, options): + async def async_set_options(self, options: list[str]) -> None: """Set options.""" - self._current_option = options[0] - self._config[CONF_OPTIONS] = options + self._attr_options = options + + if self.current_option not in self.options: + _LOGGER.warning( + "Current option: %s no longer valid (possible options: %s)", + self.current_option, + ", ".join(self.options), + ) + self._attr_current_option = options[0] + self.async_write_ha_state() - async def async_update_config(self, config: dict) -> None: + async def async_update_config(self, config: ConfigType) -> None: """Handle when the config is updated.""" - self._config = config + self._attr_icon = config.get(CONF_ICON) + self._attr_name = config.get(CONF_NAME) + self._attr_options = config[CONF_OPTIONS] self.async_write_ha_state() diff --git a/mypy.ini b/mypy.ini index 9ea98222163..058d587ec22 100644 --- a/mypy.ini +++ b/mypy.ini @@ -649,6 +649,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.input_select.*] +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.integration.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/input_select/test_init.py b/tests/components/input_select/test_init.py index f5f7956e9c5..783c4c2b9e4 100644 --- a/tests/components/input_select/test_init.py +++ b/tests/components/input_select/test_init.py @@ -27,7 +27,6 @@ from homeassistant.const import ( from homeassistant.core import Context, State from homeassistant.exceptions import Unauthorized from homeassistant.helpers import entity_registry as er -from homeassistant.loader import bind_hass from homeassistant.setup import async_setup_component from tests.common import mock_restore_cache @@ -65,80 +64,12 @@ def storage_setup(hass, hass_storage): return _storage -@bind_hass -def select_option(hass, entity_id, option): - """Set value of input_select. - - This is a legacy helper method. Do not use it for new tests. - """ - hass.async_create_task( - hass.services.async_call( - DOMAIN, - SERVICE_SELECT_OPTION, - {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: option}, - ) - ) - - -@bind_hass -def select_next(hass, entity_id): - """Set next value of input_select. - - This is a legacy helper method. Do not use it for new tests. - """ - hass.async_create_task( - hass.services.async_call( - DOMAIN, SERVICE_SELECT_NEXT, {ATTR_ENTITY_ID: entity_id} - ) - ) - - -@bind_hass -def select_previous(hass, entity_id): - """Set previous value of input_select. - - This is a legacy helper method. Do not use it for new tests. - """ - hass.async_create_task( - hass.services.async_call( - DOMAIN, SERVICE_SELECT_PREVIOUS, {ATTR_ENTITY_ID: entity_id} - ) - ) - - -@bind_hass -def select_first(hass, entity_id): - """Set first value of input_select. - - This is a legacy helper method. Do not use it for new tests. - """ - hass.async_create_task( - hass.services.async_call( - DOMAIN, SERVICE_SELECT_FIRST, {ATTR_ENTITY_ID: entity_id} - ) - ) - - -@bind_hass -def select_last(hass, entity_id): - """Set last value of input_select. - - This is a legacy helper method. Do not use it for new tests. - """ - hass.async_create_task( - hass.services.async_call( - DOMAIN, SERVICE_SELECT_LAST, {ATTR_ENTITY_ID: entity_id} - ) - ) - - async def test_config(hass): """Test config.""" invalid_configs = [ None, {}, {"name with space": None}, - # {'bad_options': {'options': None}}, {"bad_initial": {"options": [1, 2], "initial": 3}}, ] @@ -158,15 +89,21 @@ async def test_select_option(hass): state = hass.states.get(entity_id) assert state.state == "some option" - select_option(hass, entity_id, "another option") - await hass.async_block_till_done() - + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "another option"}, + blocking=True, + ) state = hass.states.get(entity_id) assert state.state == "another option" - select_option(hass, entity_id, "non existing option") - await hass.async_block_till_done() - + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "non existing option"}, + blocking=True, + ) state = hass.states.get(entity_id) assert state.state == "another option" @@ -190,15 +127,21 @@ async def test_select_next(hass): state = hass.states.get(entity_id) assert state.state == "middle option" - select_next(hass, entity_id) - await hass.async_block_till_done() - + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_NEXT, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) state = hass.states.get(entity_id) assert state.state == "last option" - select_next(hass, entity_id) - await hass.async_block_till_done() - + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_NEXT, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) state = hass.states.get(entity_id) assert state.state == "first option" @@ -222,15 +165,21 @@ async def test_select_previous(hass): state = hass.states.get(entity_id) assert state.state == "middle option" - select_previous(hass, entity_id) - await hass.async_block_till_done() - + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_PREVIOUS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) state = hass.states.get(entity_id) assert state.state == "first option" - select_previous(hass, entity_id) - await hass.async_block_till_done() - + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_PREVIOUS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) state = hass.states.get(entity_id) assert state.state == "last option" @@ -254,14 +203,22 @@ async def test_select_first_last(hass): state = hass.states.get(entity_id) assert state.state == "middle option" - select_first(hass, entity_id) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_FIRST, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) state = hass.states.get(entity_id) assert state.state == "first option" - select_last(hass, entity_id) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_LAST, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) state = hass.states.get(entity_id) assert state.state == "last option" @@ -326,20 +283,39 @@ async def test_set_options_service(hass): state = hass.states.get(entity_id) assert state.state == "middle option" - data = {ATTR_OPTIONS: ["test1", "test2"], "entity_id": entity_id} - await hass.services.async_call(DOMAIN, SERVICE_SET_OPTIONS, data) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_OPTIONS, + {ATTR_OPTIONS: ["first option", "middle option"], ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + state = hass.states.get(entity_id) + assert state.state == "middle option" + await hass.services.async_call( + DOMAIN, + SERVICE_SET_OPTIONS, + {ATTR_OPTIONS: ["test1", "test2"], ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) state = hass.states.get(entity_id) assert state.state == "test1" - select_option(hass, entity_id, "first option") - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "first option"}, + blocking=True, + ) state = hass.states.get(entity_id) assert state.state == "test1" - select_option(hass, entity_id, "test2") - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "test2"}, + blocking=True, + ) state = hass.states.get(entity_id) assert state.state == "test2" @@ -488,7 +464,6 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user): blocking=True, context=Context(user_id=hass_admin_user.id), ) - await hass.async_block_till_done() assert count_start + 2 == len(hass.states.async_entity_ids()) @@ -671,6 +646,5 @@ async def test_setup_no_config(hass, hass_admin_user): 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()) diff --git a/tests/components/template/test_select.py b/tests/components/template/test_select.py index 40ead0d637c..66f67d93754 100644 --- a/tests/components/template/test_select.py +++ b/tests/components/template/test_select.py @@ -214,7 +214,7 @@ async def test_templates_with_entities(hass, calls): blocking=True, ) await hass.async_block_till_done() - _verify(hass, "a", ["a", "b", "c"]) + _verify(hass, "b", ["a", "b", "c"]) await hass.services.async_call( SELECT_DOMAIN, From 2517ba59b57ac6ba4d3af2d6a689881ab759f518 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 26 Oct 2021 00:12:21 -0400 Subject: [PATCH 0851/1038] Fix Aurora abb incorrect attr (#58450) --- homeassistant/components/aurora_abb_powerone/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/aurora_abb_powerone/sensor.py b/homeassistant/components/aurora_abb_powerone/sensor.py index bbbd026bb2e..946f5645bdc 100644 --- a/homeassistant/components/aurora_abb_powerone/sensor.py +++ b/homeassistant/components/aurora_abb_powerone/sensor.py @@ -79,7 +79,7 @@ class AuroraSensor(AuroraDevice, SensorEntity): super().__init__(client, data) if typename == "instantaneouspower": self.type = typename - self._attr_unit_of_measurement = POWER_WATT + self._attr_native_unit_of_measurement = POWER_WATT self._attr_device_class = DEVICE_CLASS_POWER elif typename == "temperature": self.type = typename @@ -101,7 +101,7 @@ class AuroraSensor(AuroraDevice, SensorEntity): if self.type == "instantaneouspower": # read ADC channel 3 (grid power output) power_watts = self.client.measure(3, True) - self._attr_state = round(power_watts, 1) + self._attr_native_value = round(power_watts, 1) elif self.type == "temperature": temperature_c = self.client.measure(21) self._attr_native_value = round(temperature_c, 1) From cee51ead91090976dde11d7372850f823b05ab36 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 26 Oct 2021 00:33:44 -0400 Subject: [PATCH 0852/1038] Add typehints to eight_sleep (#58442) --- .../components/eight_sleep/__init__.py | 26 ++++-- .../components/eight_sleep/binary_sensor.py | 31 +++++-- .../components/eight_sleep/sensor.py | 88 +++++++++++++------ 3 files changed, 103 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index 07474c44c62..7413e5009de 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -1,8 +1,11 @@ """Support for Eight smart mattress covers and mattresses.""" +from __future__ import annotations + from datetime import timedelta import logging from pyeight.eight import EightSleep +from pyeight.user import EightUser import voluptuous as vol from homeassistant.const import ( @@ -12,9 +15,11 @@ from homeassistant.const import ( CONF_SENSORS, CONF_USERNAME, ) +from homeassistant.core import HomeAssistant, ServiceCall 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 homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -99,12 +104,15 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Eight Sleep component.""" - conf = config.get(DOMAIN) - user = conf.get(CONF_USERNAME) - password = conf.get(CONF_PASSWORD) + if DOMAIN not in config: + return True + + conf = config[DOMAIN] + user = conf[CONF_USERNAME] + password = conf[CONF_PASSWORD] if hass.config.time_zone is None: _LOGGER.error("Timezone is not set in Home Assistant") @@ -156,7 +164,7 @@ async def async_setup(hass, config): ) ) - async def async_service_handler(service): + async def async_service_handler(service: ServiceCall) -> None: """Handle eight sleep service calls.""" params = service.data.copy() @@ -167,7 +175,7 @@ async def async_setup(hass, config): for sens in sensor: side = sens.split("_")[1] userid = eight.fetch_userid(side) - usrobj = eight.users[userid] + usrobj: EightUser = eight.users[userid] await usrobj.set_heating_level(target, duration) await heat_coordinator.async_request_refresh() @@ -183,7 +191,7 @@ async def async_setup(hass, config): class EightSleepHeatDataCoordinator(DataUpdateCoordinator): """Class to retrieve heat data from Eight Sleep.""" - def __init__(self, hass, api): + def __init__(self, hass: HomeAssistant, api: EightSleep) -> None: """Initialize coordinator.""" self.api = api super().__init__( @@ -198,7 +206,7 @@ class EightSleepHeatDataCoordinator(DataUpdateCoordinator): class EightSleepUserDataCoordinator(DataUpdateCoordinator): """Class to retrieve user data from Eight Sleep.""" - def __init__(self, hass, api): + def __init__(self, hass: HomeAssistant, api: EightSleep) -> None: """Initialize coordinator.""" self.api = api super().__init__( @@ -213,7 +221,7 @@ class EightSleepUserDataCoordinator(DataUpdateCoordinator): class EightSleepEntity(CoordinatorEntity): """The Eight Sleep device entity.""" - def __init__(self, coordinator, eight): + def __init__(self, coordinator: DataUpdateCoordinator, eight: EightSleep) -> None: """Initialize the data object.""" super().__init__(coordinator) self._eight = eight diff --git a/homeassistant/components/eight_sleep/binary_sensor.py b/homeassistant/components/eight_sleep/binary_sensor.py index ca8a10b0f93..5b6e1f6a9c3 100644 --- a/homeassistant/components/eight_sleep/binary_sensor.py +++ b/homeassistant/components/eight_sleep/binary_sensor.py @@ -1,11 +1,17 @@ """Support for Eight Sleep binary sensors.""" import logging +from pyeight.eight import EightSleep +from pyeight.user import EightUser + from homeassistant.components.binary_sensor import ( DEVICE_CLASS_OCCUPANCY, BinarySensorEntity, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import ( CONF_BINARY_SENSORS, @@ -19,7 +25,12 @@ from . import ( _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType = None, +) -> None: """Set up the eight sleep binary sensor.""" if discovery_info is None: return @@ -40,7 +51,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class EightHeatSensor(EightSleepEntity, BinarySensorEntity): """Representation of a Eight Sleep heat-based sensor.""" - def __init__(self, name, coordinator, eight, sensor): + def __init__( + self, + name: str, + coordinator: DataUpdateCoordinator, + eight: EightSleep, + sensor: str, + ) -> None: """Initialize the sensor.""" super().__init__(coordinator, eight) @@ -50,7 +67,7 @@ class EightHeatSensor(EightSleepEntity, BinarySensorEntity): self._side = self._sensor.split("_")[0] self._userid = self._eight.fetch_userid(self._side) - self._usrobj = self._eight.users[self._userid] + self._usrobj: EightUser = self._eight.users[self._userid] self._attr_name = f"{name} {self._mapped_name}" self._attr_device_class = DEVICE_CLASS_OCCUPANCY @@ -63,12 +80,12 @@ class EightHeatSensor(EightSleepEntity, BinarySensorEntity): ) @property - def is_on(self): + def is_on(self) -> bool: """Return true if the binary sensor is on.""" - return self._state + return bool(self._state) @callback - def _handle_coordinator_update(self): + def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" self._state = self._usrobj.bed_presence super()._handle_coordinator_update() diff --git a/homeassistant/components/eight_sleep/sensor.py b/homeassistant/components/eight_sleep/sensor.py index 0e84eea64f6..c7c58c05e7d 100644 --- a/homeassistant/components/eight_sleep/sensor.py +++ b/homeassistant/components/eight_sleep/sensor.py @@ -1,5 +1,12 @@ """Support for Eight Sleep sensors.""" +from __future__ import annotations + +from collections.abc import Mapping import logging +from typing import Any + +from pyeight.eight import EightSleep +from pyeight.user import EightUser from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( @@ -8,7 +15,9 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import ( CONF_SENSORS, @@ -18,6 +27,8 @@ from . import ( DATA_USER, NAME_MAP, EightSleepEntity, + EightSleepHeatDataCoordinator, + EightSleepUserDataCoordinator, ) ATTR_ROOM_TEMP = "Room Temperature" @@ -48,7 +59,12 @@ ATTR_FIT_WAKEUP_SCORE = "Fitness Wakeup Score" _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType = None, +) -> None: """Set up the eight sleep sensors.""" if discovery_info is None: return @@ -56,15 +72,15 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= name = "Eight" sensors = discovery_info[CONF_SENSORS] eight = hass.data[DATA_EIGHT][DATA_API] - heat_coordinator = hass.data[DATA_EIGHT][DATA_HEAT] - user_coordinator = hass.data[DATA_EIGHT][DATA_USER] + heat_coordinator: EightSleepHeatDataCoordinator = hass.data[DATA_EIGHT][DATA_HEAT] + user_coordinator: EightSleepUserDataCoordinator = hass.data[DATA_EIGHT][DATA_USER] if hass.config.units.is_metric: units = "si" else: units = "us" - all_sensors = [] + all_sensors: list[EightSleepEntity] = [] for sensor in sensors: if "bed_state" in sensor: @@ -84,7 +100,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class EightHeatSensor(EightSleepEntity, SensorEntity): """Representation of an eight sleep heat-based sensor.""" - def __init__(self, name, coordinator, eight, sensor): + def __init__( + self, + name: str, + coordinator: EightSleepHeatDataCoordinator, + eight: EightSleep, + sensor: str, + ) -> None: """Initialize the sensor.""" super().__init__(coordinator, eight) @@ -95,7 +117,7 @@ class EightHeatSensor(EightSleepEntity, SensorEntity): self._side = self._sensor.split("_")[0] self._userid = self._eight.fetch_userid(self._side) - self._usrobj = self._eight.users[self._userid] + self._usrobj: EightUser = self._eight.users[self._userid] _LOGGER.debug( "Heat Sensor: %s, Side: %s, User: %s", @@ -105,29 +127,29 @@ class EightHeatSensor(EightSleepEntity, SensorEntity): ) @property - def name(self): + def name(self) -> str: """Return the name of the sensor, if any.""" return self._name @property - def native_value(self): + def native_value(self) -> str | None: """Return the state of the sensor.""" return self._state @property - def native_unit_of_measurement(self): + def native_unit_of_measurement(self) -> str: """Return the unit the value is expressed in.""" return PERCENTAGE @callback - def _handle_coordinator_update(self): + def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" _LOGGER.debug("Updating Heat sensor: %s", self._sensor) self._state = self._usrobj.heating_level super()._handle_coordinator_update() @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> Mapping[str, Any]: """Return device state attributes.""" return { ATTR_TARGET_HEAT: self._usrobj.target_heating_level, @@ -139,7 +161,14 @@ class EightHeatSensor(EightSleepEntity, SensorEntity): class EightUserSensor(EightSleepEntity, SensorEntity): """Representation of an eight sleep user-based sensor.""" - def __init__(self, name, coordinator, eight, sensor, units): + def __init__( + self, + name: str, + coordinator: EightSleepUserDataCoordinator, + eight: EightSleep, + sensor: str, + units: str, + ) -> None: """Initialize the sensor.""" super().__init__(coordinator, eight) @@ -153,7 +182,7 @@ class EightUserSensor(EightSleepEntity, SensorEntity): self._side = self._sensor.split("_", 1)[0] self._userid = self._eight.fetch_userid(self._side) - self._usrobj = self._eight.users[self._userid] + self._usrobj: EightUser = self._eight.users[self._userid] _LOGGER.debug( "User Sensor: %s, Side: %s, User: %s", @@ -163,17 +192,17 @@ class EightUserSensor(EightSleepEntity, SensorEntity): ) @property - def name(self): + def name(self) -> str: """Return the name of the sensor, if any.""" return self._name @property - def native_value(self): + def native_value(self) -> str | None: """Return the state of the sensor.""" return self._state @property - def native_unit_of_measurement(self): + def native_unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" if ( "current_sleep" in self._sensor @@ -188,14 +217,14 @@ class EightUserSensor(EightSleepEntity, SensorEntity): return None @property - def device_class(self): + def device_class(self) -> str | None: """Return the class of this device, from component DEVICE_CLASSES.""" if "bed_temp" in self._sensor: return DEVICE_CLASS_TEMPERATURE return None @callback - def _handle_coordinator_update(self): + def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" _LOGGER.debug("Updating User sensor: %s", self._sensor) if "current" in self._sensor: @@ -223,7 +252,7 @@ class EightUserSensor(EightSleepEntity, SensorEntity): super()._handle_coordinator_update() @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return device state attributes.""" if self._attr is None: # Skip attributes if sensor type doesn't support @@ -313,7 +342,14 @@ class EightUserSensor(EightSleepEntity, SensorEntity): class EightRoomSensor(EightSleepEntity, SensorEntity): """Representation of an eight sleep room sensor.""" - def __init__(self, name, coordinator, eight, sensor, units): + def __init__( + self, + name: str, + coordinator: EightSleepUserDataCoordinator, + eight: EightSleep, + sensor: str, + units: str, + ) -> None: """Initialize the sensor.""" super().__init__(coordinator, eight) @@ -325,17 +361,17 @@ class EightRoomSensor(EightSleepEntity, SensorEntity): self._units = units @property - def name(self): + def name(self) -> str: """Return the name of the sensor, if any.""" return self._name @property - def native_value(self): + def native_value(self) -> str | None: """Return the state of the sensor.""" return self._state @callback - def _handle_coordinator_update(self): + def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" _LOGGER.debug("Updating Room sensor: %s", self._sensor) temp = self._eight.room_temperature() @@ -349,13 +385,13 @@ class EightRoomSensor(EightSleepEntity, SensorEntity): super()._handle_coordinator_update() @property - def native_unit_of_measurement(self): + def native_unit_of_measurement(self) -> str: """Return the unit the value is expressed in.""" if self._units == "si": return TEMP_CELSIUS return TEMP_FAHRENHEIT @property - def device_class(self): + def device_class(self) -> str: """Return the class of this device, from component DEVICE_CLASSES.""" return DEVICE_CLASS_TEMPERATURE From bd5c13167587ab3f24ff0da4cf1e714c976f570e Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Tue, 26 Oct 2021 09:02:49 +0200 Subject: [PATCH 0853/1038] Add all kraken entities on startup (#58027) * Add all kraken entities on startup * Add _async_add_kraken_sensors * Add test sensors available after restart * Add test for config update * Update tests/components/kraken/test_sensor.py Co-authored-by: Allen Porter Co-authored-by: Allen Porter --- homeassistant/components/kraken/sensor.py | 35 +++++---- tests/components/kraken/test_sensor.py | 86 +++++++++++++++++++++++ 2 files changed, 107 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/kraken/sensor.py b/homeassistant/components/kraken/sensor.py index d2b2cfeae70..f18d0e78d94 100644 --- a/homeassistant/components/kraken/sensor.py +++ b/homeassistant/components/kraken/sensor.py @@ -33,8 +33,26 @@ async def async_setup_entry( ) -> None: """Add kraken entities from a config_entry.""" + def _async_add_kraken_sensors(tracked_asset_pairs: list[str]) -> None: + entities = [] + for tracked_asset_pair in tracked_asset_pairs: + entities.extend( + [ + KrakenSensor( + hass.data[DOMAIN], + tracked_asset_pair, + description, + ) + for description in SENSOR_TYPES + ] + ) + async_add_entities(entities, True) + + _async_add_kraken_sensors(config_entry.options[CONF_TRACKED_ASSET_PAIRS]) + @callback def async_update_sensors(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Add or remove sensors for configured tracked asset pairs.""" dev_reg = device_registry.async_get(hass) existing_devices = { @@ -44,7 +62,7 @@ async def async_setup_entry( ) } - entities = [] + asset_pairs_to_add = [] for tracked_asset_pair in config_entry.options[CONF_TRACKED_ASSET_PAIRS]: # Only create new devices if ( @@ -52,24 +70,13 @@ async def async_setup_entry( ) in existing_devices: existing_devices.pop(device_name) else: - entities.extend( - [ - KrakenSensor( - hass.data[DOMAIN], - tracked_asset_pair, - description, - ) - for description in SENSOR_TYPES - ] - ) - async_add_entities(entities, True) + asset_pairs_to_add.append(tracked_asset_pair) + _async_add_kraken_sensors(asset_pairs_to_add) # Remove devices for asset pairs which are no longer tracked for device_id in existing_devices.values(): dev_reg.async_remove_device(device_id) - async_update_sensors(hass, config_entry) - config_entry.async_on_unload( async_dispatcher_connect( hass, diff --git a/tests/components/kraken/test_sensor.py b/tests/components/kraken/test_sensor.py index 110a944a4d5..ca98251f157 100644 --- a/tests/components/kraken/test_sensor.py +++ b/tests/components/kraken/test_sensor.py @@ -228,6 +228,92 @@ async def test_sensor(hass): assert xbt_usd_opening_price_today.state == "0.0003513" +async def test_sensors_available_after_restart(hass): + """Test that all sensors are added again after a restart.""" + utcnow = dt_util.utcnow() + # Patching 'utcnow' to gain more control over the timed update. + with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( + "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", + return_value=TRADEABLE_ASSET_PAIR_RESPONSE, + ), patch( + "pykrakenapi.KrakenAPI.get_ticker_information", + return_value=TICKER_INFORMATION_RESPONSE, + ): + entry = MockConfigEntry( + domain=DOMAIN, + options={ + CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL, + CONF_TRACKED_ASSET_PAIRS: [DEFAULT_TRACKED_ASSET_PAIR], + }, + ) + + device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, "XBT_USD")}, + name="XBT USD", + manufacturer="Kraken.com", + entry_type="service", + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + sensor = hass.states.get("sensor.xbt_usd_ask") + assert sensor.state == "0.0003494" + + +async def test_sensors_added_after_config_update(hass): + """Test that sensors are added when another tracked asset pair is added.""" + utcnow = dt_util.utcnow() + # Patching 'utcnow' to gain more control over the timed update. + with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( + "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", + return_value=TRADEABLE_ASSET_PAIR_RESPONSE, + ), patch( + "pykrakenapi.KrakenAPI.get_ticker_information", + return_value=TICKER_INFORMATION_RESPONSE, + ): + entry = MockConfigEntry( + domain=DOMAIN, + options={ + CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL, + CONF_TRACKED_ASSET_PAIRS: [DEFAULT_TRACKED_ASSET_PAIR], + }, + ) + + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + assert hass.states.get("sensor.xbt_usd_ask") + assert not hass.states.get("sensor.ada_xbt_ask") + + hass.config_entries.async_update_entry( + entry, + options={ + CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL, + CONF_TRACKED_ASSET_PAIRS: [DEFAULT_TRACKED_ASSET_PAIR, "ADA/XBT"], + }, + ) + async_fire_time_changed( + hass, utcnow + timedelta(seconds=DEFAULT_SCAN_INTERVAL * 2) + ) + await hass.async_block_till_done() + + assert hass.states.get("sensor.ada_xbt_ask") + + async def test_missing_pair_marks_sensor_unavailable(hass): """Test that a missing tradable asset pair marks the sensor unavailable.""" utcnow = dt_util.utcnow() From 2f346a8048bc2d00ed17ef1be59d35452e1dd4b2 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Tue, 26 Oct 2021 09:04:19 +0200 Subject: [PATCH 0854/1038] Add speed & intensity controls to wled (#56862) Co-authored-by: Franck Nijhof --- homeassistant/components/wled/__init__.py | 3 +- homeassistant/components/wled/number.py | 133 ++++++++++ tests/components/wled/test_number.py | 302 ++++++++++++++++++++++ 3 files changed, 437 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/wled/number.py create mode 100644 tests/components/wled/test_number.py diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index 29c6b98b381..e7697676014 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN from homeassistant.components.select import DOMAIN as SELECT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN @@ -11,7 +12,7 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN from .coordinator import WLEDDataUpdateCoordinator -PLATFORMS = (LIGHT_DOMAIN, SELECT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN) +PLATFORMS = (LIGHT_DOMAIN, SELECT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN, NUMBER_DOMAIN) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/wled/number.py b/homeassistant/components/wled/number.py new file mode 100644 index 00000000000..a51eb915ba6 --- /dev/null +++ b/homeassistant/components/wled/number.py @@ -0,0 +1,133 @@ +"""Support for LED numbers.""" +from __future__ import annotations + +from functools import partial + +from homeassistant.components.number import NumberEntity, NumberEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_CONFIG +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ATTR_INTENSITY, ATTR_SPEED, DOMAIN +from .coordinator import WLEDDataUpdateCoordinator +from .helpers import wled_exception_handler +from .models import WLEDEntity + +PARALLEL_UPDATES = 1 + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up WLED number based on a config entry.""" + coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + update_segments = partial( + async_update_segments, + coordinator, + set(), + async_add_entities, + ) + coordinator.async_add_listener(update_segments) + update_segments() + + +NUMBERS = [ + NumberEntityDescription( + key=ATTR_SPEED, + name="Speed", + icon="mdi:speedometer", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + NumberEntityDescription( + key=ATTR_INTENSITY, + name="Intensity", + entity_category=ENTITY_CATEGORY_CONFIG, + ), +] + + +class WLEDNumber(WLEDEntity, NumberEntity): + """Defines a WLED speed number.""" + + _attr_step = 1 + _attr_min_value = 0 + _attr_max_value = 255 + + def __init__( + self, + coordinator: WLEDDataUpdateCoordinator, + segment: int, + description: NumberEntityDescription, + ) -> None: + """Initialize WLED .""" + super().__init__(coordinator=coordinator) + self.entity_description = description + + # Segment 0 uses a simpler name, which is more natural for when using + # a single segment / using WLED with one big LED strip. + self._attr_name = ( + f"{coordinator.data.info.name} Segment {segment} {description.name}" + ) + if segment == 0: + self._attr_name = f"{coordinator.data.info.name} {description.name}" + + self._attr_unique_id = ( + f"{coordinator.data.info.mac_address}_{description.key}_{segment}" + ) + self._segment = segment + + @property + def available(self) -> bool: + """Return True if entity is available.""" + try: + self.coordinator.data.state.segments[self._segment] + except IndexError: + return False + + return super().available + + @property + def value(self) -> float | None: + """Return the current WLED segment number value.""" + return getattr( + self.coordinator.data.state.segments[self._segment], + self.entity_description.key, + ) + + @wled_exception_handler + async def async_set_value(self, value: float) -> None: + """Set the WLED segment value.""" + key = self.entity_description.key + if key == ATTR_SPEED: + await self.coordinator.wled.segment( + segment_id=self._segment, speed=int(value) + ) + elif key == ATTR_INTENSITY: + return await self.coordinator.wled.segment( + segment_id=self._segment, intensity=int(value) + ) + + +@callback +def async_update_segments( + coordinator: WLEDDataUpdateCoordinator, + current_ids: set[int], + async_add_entities, +) -> None: + """Update segments.""" + segment_ids = {segment.segment_id for segment in coordinator.data.state.segments} + + new_entities = [] + + # Process new segments, add them to Home Assistant + for segment_id in segment_ids - current_ids: + current_ids.add(segment_id) + for desc in NUMBERS: + new_entities.append(WLEDNumber(coordinator, segment_id, desc)) + + if new_entities: + async_add_entities(new_entities) diff --git a/tests/components/wled/test_number.py b/tests/components/wled/test_number.py new file mode 100644 index 00000000000..e4b8958b077 --- /dev/null +++ b/tests/components/wled/test_number.py @@ -0,0 +1,302 @@ +"""Tests for the WLED number platform.""" +import json +from unittest.mock import MagicMock + +import pytest +from wled import Device as WLEDDevice, WLEDConnectionError, WLEDError + +from homeassistant.components.number import ATTR_MAX, ATTR_MIN, DOMAIN as NUMBER_DOMAIN +from homeassistant.components.number.const import ( + ATTR_STEP, + ATTR_VALUE, + SERVICE_SET_VALUE, +) +from homeassistant.components.wled.const import SCAN_INTERVAL +from homeassistant.const import ATTR_ENTITY_ID, ATTR_ICON, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +import homeassistant.util.dt as dt_util + +from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture + + +async def test_speed_state( + hass: HomeAssistant, init_integration: MockConfigEntry +) -> None: + """Test the creation and values of the WLED numbers.""" + entity_registry = er.async_get(hass) + + # First segment of the strip + state = hass.states.get("number.wled_rgb_light_segment_1_speed") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:speedometer" + assert state.attributes.get(ATTR_MAX) == 255 + assert state.attributes.get(ATTR_MIN) == 0 + assert state.attributes.get(ATTR_STEP) == 1 + assert state.state == "16" + + entry = entity_registry.async_get("number.wled_rgb_light_segment_1_speed") + assert entry + assert entry.unique_id == "aabbccddeeff_speed_1" + + +async def test_speed_segment_change_state( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, +) -> None: + """Test the value change of the WLED segments.""" + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: "number.wled_rgb_light_segment_1_speed", + ATTR_VALUE: 42, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.segment.call_count == 1 + mock_wled.segment.assert_called_with( + segment_id=1, + speed=42, + ) + + +@pytest.mark.parametrize("mock_wled", ["wled/rgb_single_segment.json"], indirect=True) +async def test_speed_dynamically_handle_segments( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, +) -> None: + """Test if a new/deleted segment is dynamically added/removed.""" + segment0 = hass.states.get("number.wled_rgb_light_speed") + segment1 = hass.states.get("number.wled_rgb_light_segment_1_speed") + assert segment0 + assert segment0.state == "32" + assert not segment1 + + # Test adding a segment dynamically... + return_value = mock_wled.update.return_value + mock_wled.update.return_value = WLEDDevice( + json.loads(load_fixture("wled/rgb.json")) + ) + + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + segment0 = hass.states.get("number.wled_rgb_light_speed") + segment1 = hass.states.get("number.wled_rgb_light_segment_1_speed") + assert segment0 + assert segment0.state == "32" + assert segment1 + assert segment1.state == "16" + + # Test remove segment again... + mock_wled.update.return_value = return_value + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + segment0 = hass.states.get("number.wled_rgb_light_speed") + segment1 = hass.states.get("number.wled_rgb_light_segment_1_speed") + assert segment0 + assert segment0.state == "32" + assert segment1 + assert segment1.state == STATE_UNAVAILABLE + + +async def test_speed_error( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test error handling of the WLED numbers.""" + mock_wled.segment.side_effect = WLEDError + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: "number.wled_rgb_light_segment_1_speed", + ATTR_VALUE: 42, + }, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("number.wled_rgb_light_segment_1_speed") + assert state + assert state.state == "16" + assert "Invalid response from API" in caplog.text + assert mock_wled.segment.call_count == 1 + mock_wled.segment.assert_called_with(segment_id=1, speed=42) + + +async def test_speed_connection_error( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test error handling of the WLED numbers.""" + mock_wled.segment.side_effect = WLEDConnectionError + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: "number.wled_rgb_light_segment_1_speed", + ATTR_VALUE: 42, + }, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("number.wled_rgb_light_segment_1_speed") + assert state + assert state.state == STATE_UNAVAILABLE + assert "Error communicating with API" in caplog.text + assert mock_wled.segment.call_count == 1 + mock_wled.segment.assert_called_with(segment_id=1, speed=42) + + +async def test_intensity_state( + hass: HomeAssistant, init_integration: MockConfigEntry +) -> None: + """Test the creation and values of the WLED numbers.""" + entity_registry = er.async_get(hass) + + # First segment of the strip + state = hass.states.get("number.wled_rgb_light_segment_1_intensity") + assert state + assert state.attributes.get(ATTR_ICON) is None + assert state.attributes.get(ATTR_MAX) == 255 + assert state.attributes.get(ATTR_MIN) == 0 + assert state.attributes.get(ATTR_STEP) == 1 + assert state.state == "64" + + entry = entity_registry.async_get("number.wled_rgb_light_segment_1_intensity") + assert entry + assert entry.unique_id == "aabbccddeeff_intensity_1" + + +async def test_intensity_segment_change_state( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, +) -> None: + """Test the value change of the WLED segments.""" + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: "number.wled_rgb_light_segment_1_intensity", + ATTR_VALUE: 128, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.segment.call_count == 1 + mock_wled.segment.assert_called_with( + segment_id=1, + intensity=128, + ) + + +@pytest.mark.parametrize("mock_wled", ["wled/rgb_single_segment.json"], indirect=True) +async def test_intensity_dynamically_handle_segments( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, +) -> None: + """Test if a new/deleted segment is dynamically added/removed.""" + segment0 = hass.states.get("number.wled_rgb_light_intensity") + segment1 = hass.states.get("number.wled_rgb_light_segment_1_intensity") + assert segment0 + assert segment0.state == "128" + assert not segment1 + + # Test adding a segment dynamically... + return_value = mock_wled.update.return_value + mock_wled.update.return_value = WLEDDevice( + json.loads(load_fixture("wled/rgb.json")) + ) + + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + segment0 = hass.states.get("number.wled_rgb_light_intensity") + segment1 = hass.states.get("number.wled_rgb_light_segment_1_intensity") + assert segment0 + assert segment0.state == "128" + assert segment1 + assert segment1.state == "64" + + # Test remove segment again... + mock_wled.update.return_value = return_value + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + segment0 = hass.states.get("number.wled_rgb_light_intensity") + segment1 = hass.states.get("number.wled_rgb_light_segment_1_intensity") + assert segment0 + assert segment0.state == "128" + assert segment1 + assert segment1.state == STATE_UNAVAILABLE + + +async def test_intensity_error( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test error handling of the WLED numbers.""" + mock_wled.segment.side_effect = WLEDError + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: "number.wled_rgb_light_segment_1_intensity", + ATTR_VALUE: 21, + }, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("number.wled_rgb_light_segment_1_intensity") + assert state + assert state.state == "64" + assert "Invalid response from API" in caplog.text + assert mock_wled.segment.call_count == 1 + mock_wled.segment.assert_called_with(segment_id=1, intensity=21) + + +async def test_intensity_connection_error( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test error handling of the WLED numbers.""" + mock_wled.segment.side_effect = WLEDConnectionError + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: "number.wled_rgb_light_segment_1_intensity", + ATTR_VALUE: 128, + }, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("number.wled_rgb_light_segment_1_intensity") + assert state + assert state.state == STATE_UNAVAILABLE + assert "Error communicating with API" in caplog.text + assert mock_wled.segment.call_count == 1 + mock_wled.segment.assert_called_with(segment_id=1, intensity=128) From f594bc353be32445a95f3fd82b4f6bc561856954 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 26 Oct 2021 10:26:50 +0200 Subject: [PATCH 0855/1038] Add support for external statistics (#56607) * Support external statistics * Update tests * Lint * Adjust code after rebase * Separate external statistic_id with :, add name to metadata * Adjust tests * Simplify get_metadata_with_session * Address review comments * Allow updating external statistics * Validate input * Adjust tests after rebase * Pylint * Adjust tests * Improve test coverage --- homeassistant/components/recorder/__init__.py | 24 +- .../components/recorder/migration.py | 4 + homeassistant/components/recorder/models.py | 28 +-- .../components/recorder/statistics.py | 210 +++++++++++++--- homeassistant/components/sensor/recorder.py | 18 +- tests/components/energy/test_sensor.py | 12 +- tests/components/history/test_init.py | 21 +- tests/components/recorder/test_statistics.py | 177 ++++++++++++- .../components/recorder/test_websocket_api.py | 14 +- tests/components/sensor/test_recorder.py | 237 +++++++++++++++--- 10 files changed, 634 insertions(+), 111 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 83652b7864c..465209c7ed7 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Callable, Iterable import concurrent.futures from datetime import datetime, timedelta import logging @@ -366,6 +366,13 @@ class StatisticsTask(NamedTuple): start: datetime +class ExternalStatisticsTask(NamedTuple): + """An object to insert into the recorder queue to run an external statistics task.""" + + metadata: dict + statistics: Iterable[dict] + + class WaitTask: """An object to insert into the recorder queue to tell it set the _queue_watch event.""" @@ -597,6 +604,11 @@ class Recorder(threading.Thread): """Update statistics metadata for a statistic_id.""" self.queue.put(UpdateStatisticsMetadataTask(statistic_id, unit_of_measurement)) + @callback + def async_external_statistics(self, metadata, stats): + """Schedule external statistics.""" + self.queue.put(ExternalStatisticsTask(metadata, stats)) + @callback def _async_setup_periodic_tasks(self): """Prepare periodic tasks.""" @@ -776,6 +788,13 @@ class Recorder(threading.Thread): # Schedule a new statistics task if this one didn't finish self.queue.put(StatisticsTask(start)) + def _run_external_statistics(self, metadata, stats): + """Run statistics task.""" + if statistics.add_external_statistics(self, metadata, stats): + return + # Schedule a new statistics task if this one didn't finish + self.queue.put(StatisticsTask(metadata, stats)) + def _process_one_event(self, event): """Process one event.""" if isinstance(event, PurgeTask): @@ -798,6 +817,9 @@ class Recorder(threading.Thread): self, event.statistic_id, event.unit_of_measurement ) return + if isinstance(event, ExternalStatisticsTask): + self._run_external_statistics(event.metadata, event.statistics) + return if isinstance(event, WaitTask): self._queue_watch.set() return diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index fec2e1e962c..508476e0c2b 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -579,6 +579,10 @@ def _apply_update(instance, session, new_version, old_version): # noqa: C901 sum=last_statistic.sum, ) ) + elif new_version == 23: + # Add name column to StatisticsMeta + _add_columns(session, "statistics_meta", ["name VARCHAR(255)"]) + else: raise ValueError(f"No schema migration defined for version {new_version}") diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index a43c7781c8d..6998c8e5f53 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -1,7 +1,6 @@ """Models for SQLAlchemy.""" from __future__ import annotations -from collections.abc import Iterable from datetime import datetime, timedelta import json import logging @@ -41,7 +40,7 @@ import homeassistant.util.dt as dt_util # pylint: disable=invalid-name Base = declarative_base() -SCHEMA_VERSION = 22 +SCHEMA_VERSION = 23 _LOGGER = logging.getLogger(__name__) @@ -231,7 +230,7 @@ class StatisticResult(TypedDict): """ meta: StatisticMetaData - stat: Iterable[StatisticData] + stat: StatisticData class StatisticDataBase(TypedDict): @@ -310,10 +309,12 @@ class StatisticsShortTerm(Base, StatisticsBase): # type: ignore class StatisticMetaData(TypedDict): """Statistic meta data class.""" - statistic_id: str - unit_of_measurement: str | None has_mean: bool has_sum: bool + name: str | None + source: str + statistic_id: str + unit_of_measurement: str | None class StatisticsMeta(Base): # type: ignore @@ -329,23 +330,12 @@ class StatisticsMeta(Base): # type: ignore unit_of_measurement = Column(String(255)) has_mean = Column(Boolean) has_sum = Column(Boolean) + name = Column(String(255)) @staticmethod - def from_meta( - source: str, - statistic_id: str, - unit_of_measurement: str | None, - has_mean: bool, - has_sum: bool, - ) -> StatisticsMeta: + def from_meta(meta: StatisticMetaData) -> StatisticsMeta: """Create object from meta data.""" - return StatisticsMeta( - source=source, - statistic_id=statistic_id, - unit_of_measurement=unit_of_measurement, - has_mean=has_mean, - has_sum=has_sum, - ) + return StatisticsMeta(**meta) class RecorderRuns(Base): # type: ignore diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 374345c8303..40470c2346d 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -7,6 +7,7 @@ import dataclasses from datetime import datetime, timedelta from itertools import chain, groupby import logging +import re from statistics import mean from typing import TYPE_CHECKING, Any, Literal @@ -23,6 +24,7 @@ from homeassistant.const import ( VOLUME_CUBIC_METERS, ) from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry import homeassistant.util.dt as dt_util import homeassistant.util.pressure as pressure_util @@ -30,7 +32,7 @@ import homeassistant.util.temperature as temperature_util from homeassistant.util.unit_system import UnitSystem import homeassistant.util.volume as volume_util -from .const import DOMAIN +from .const import DATA_INSTANCE, DOMAIN from .models import ( StatisticData, StatisticMetaData, @@ -100,9 +102,11 @@ QUERY_STATISTICS_SUMMARY_SUM_LEGACY = [ QUERY_STATISTIC_META = [ StatisticsMeta.id, StatisticsMeta.statistic_id, + StatisticsMeta.source, StatisticsMeta.unit_of_measurement, StatisticsMeta.has_mean, StatisticsMeta.has_sum, + StatisticsMeta.name, ] QUERY_STATISTIC_META_ID = [ @@ -138,6 +142,22 @@ UNIT_CONVERSIONS = { _LOGGER = logging.getLogger(__name__) +def split_statistic_id(entity_id: str) -> list[str]: + """Split a state entity ID into domain and object ID.""" + return entity_id.split(":", 1) + + +VALID_STATISTIC_ID = re.compile(r"^(?!.+__)(?!_)[\da-z_]+(? bool: + """Test if a statistic ID is a valid format. + + Format: : where both are slugs. + """ + return VALID_STATISTIC_ID.match(statistic_id) is not None + + @dataclasses.dataclass class ValidationIssue: """Error or warning message.""" @@ -208,10 +228,7 @@ def _update_or_add_metadata( hass, session, statistic_ids=[statistic_id] ) if not old_metadata_dict: - unit = new_metadata["unit_of_measurement"] - has_mean = new_metadata["has_mean"] - has_sum = new_metadata["has_sum"] - meta = StatisticsMeta.from_meta(DOMAIN, statistic_id, unit, has_mean, has_sum) + meta = StatisticsMeta.from_meta(new_metadata) session.add(meta) session.flush() # Flush to get the metadata id assigned _LOGGER.debug( @@ -397,15 +414,12 @@ def compile_statistics(instance: Recorder, start: datetime) -> bool: with session_scope(session=instance.get_session()) as session: # type: ignore for stats in platform_stats: metadata_id = _update_or_add_metadata(instance.hass, session, stats["meta"]) - for stat in stats["stat"]: - try: - session.add(StatisticsShortTerm.from_stats(metadata_id, stat)) - except SQLAlchemyError: - _LOGGER.exception( - "Unexpected exception when inserting statistics %s:%s ", - metadata_id, - stats, - ) + _insert_statistics( + session, + StatisticsShortTerm, + metadata_id, + stats["stat"], + ) if start.minute == 55: # A full hour is ready, summarize it @@ -416,6 +430,50 @@ def compile_statistics(instance: Recorder, start: datetime) -> bool: return True +def _insert_statistics( + session: scoped_session, + table: type[Statistics | StatisticsShortTerm], + metadata_id: int, + statistic: StatisticData, +) -> None: + """Insert statistics in the database.""" + try: + session.add(table.from_stats(metadata_id, statistic)) + except SQLAlchemyError: + _LOGGER.exception( + "Unexpected exception when inserting statistics %s:%s ", + metadata_id, + statistic, + ) + + +def _update_statistics( + session: scoped_session, + table: type[Statistics | StatisticsShortTerm], + stat_id: int, + statistic: StatisticData, +) -> None: + """Insert statistics in the database.""" + try: + session.query(table).filter_by(id=stat_id).update( + { + table.mean: statistic["mean"], + table.min: statistic["min"], + table.max: statistic["max"], + table.last_reset: statistic["last_reset"], + table.state: statistic["state"], + table.sum: statistic["sum"], + }, + synchronize_session=False, + ) + except SQLAlchemyError: + _LOGGER.exception( + "Unexpected exception when updating statistics %s:%s ", + id, + statistic, + ) + + def get_metadata_with_session( hass: HomeAssistant, session: scoped_session, @@ -426,24 +484,12 @@ def get_metadata_with_session( ) -> dict[str, tuple[int, StatisticMetaData]]: """Fetch meta data. - Returns a dict of (metadata_id, StatisticMetaData) indexed by statistic_id. + Returns a dict of (metadata_id, StatisticMetaData) tuples indexed by statistic_id. If statistic_ids is given, fetch metadata only for the listed statistics_ids. If statistic_type is given, fetch metadata only for statistic_ids supporting it. """ - def _meta(metas: list, wanted_metadata_id: str) -> StatisticMetaData | None: - meta: StatisticMetaData | None = None - for metadata_id, statistic_id, unit, has_mean, has_sum in metas: - if metadata_id == wanted_metadata_id: - meta = { - "statistic_id": statistic_id, - "unit_of_measurement": unit, - "has_mean": has_mean, - "has_sum": has_sum, - } - return meta - # Fetch metatadata from the database baked_query = hass.data[STATISTICS_META_BAKERY]( lambda session: session.query(*QUERY_STATISTIC_META) @@ -468,14 +514,20 @@ def get_metadata_with_session( if not result: return {} - metadata_ids = [metadata[0] for metadata in result] - # Prepare the result dict - metadata: dict[str, tuple[int, StatisticMetaData]] = {} - for _id in metadata_ids: - meta = _meta(result, _id) - if meta: - metadata[meta["statistic_id"]] = (_id, meta) - return metadata + return { + meta["statistic_id"]: ( + meta["id"], + { + "source": meta["source"], + "statistic_id": meta["statistic_id"], + "unit_of_measurement": meta["unit_of_measurement"], + "has_mean": meta["has_mean"], + "has_sum": meta["has_sum"], + "name": meta["name"], + }, + ) + for meta in result + } def get_metadata( @@ -553,7 +605,11 @@ def list_statistic_ids( meta["unit_of_measurement"] = unit statistic_ids = { - meta["statistic_id"]: meta["unit_of_measurement"] + meta["statistic_id"]: { + "name": meta["name"], + "source": meta["source"], + "unit_of_measurement": meta["unit_of_measurement"], + } for _, meta in metadata.values() } @@ -563,19 +619,25 @@ def list_statistic_ids( continue platform_statistic_ids = platform.list_statistic_ids(hass, statistic_type) - for statistic_id, unit in platform_statistic_ids.items(): + for statistic_id, info in platform_statistic_ids.items(): + unit = info["unit_of_measurement"] if unit is not None: # Display unit according to user settings unit = _configured_unit(unit, units) - platform_statistic_ids[statistic_id] = unit + platform_statistic_ids[statistic_id]["unit_of_measurement"] = unit for key, value in platform_statistic_ids.items(): statistic_ids.setdefault(key, value) - # Return a map of statistic_id to unit_of_measurement + # Return a list of statistic_id + metadata return [ - {"statistic_id": _id, "unit_of_measurement": unit} - for _id, unit in statistic_ids.items() + { + "statistic_id": _id, + "name": info.get("name"), + "source": info["source"], + "unit_of_measurement": info["unit_of_measurement"], + } + for _id, info in statistic_ids.items() ] @@ -919,3 +981,69 @@ def validate_statistics(hass: HomeAssistant) -> dict[str, list[ValidationIssue]] continue platform_validation.update(platform.validate_statistics(hass)) return platform_validation + + +def _statistics_exists( + session: scoped_session, + table: type[Statistics | StatisticsShortTerm], + metadata_id: int, + start: datetime, +) -> int | None: + """Return id if a statistics entry already exists.""" + result = ( + session.query(table.id) + .filter(table.metadata_id == metadata_id and table.start == start) + .first() + ) + return result["id"] if result else None + + +@callback +def async_add_external_statistics( + hass: HomeAssistant, + metadata: StatisticMetaData, + statistics: Iterable[StatisticData], +) -> None: + """Add hourly statistics from an external source. + + This inserts an add_external_statistics job in the recorder's queue. + """ + # The statistic_id has same limitations as an entity_id, but with a ':' as separator + if not valid_statistic_id(metadata["statistic_id"]): + raise HomeAssistantError("Invalid statistic_id") + + # The source must not be empty and must be aligned with the statistic_id + domain, _object_id = split_statistic_id(metadata["statistic_id"]) + if not metadata["source"] or metadata["source"] != domain: + raise HomeAssistantError("Invalid source") + + for statistic in statistics: + start = statistic["start"] + if start.tzinfo is None or start.tzinfo.utcoffset(start) is None: + raise HomeAssistantError("Naive timestamp") + if start.minute != 0 or start.second != 0 or start.microsecond != 0: + raise HomeAssistantError("Invalid timestamp") + statistic["start"] = dt_util.as_utc(start) + + # Insert job in recorder's queue + hass.data[DATA_INSTANCE].async_external_statistics(metadata, statistics) + + +@retryable_database_job("statistics") +def add_external_statistics( + instance: Recorder, + metadata: StatisticMetaData, + statistics: Iterable[StatisticData], +) -> bool: + """Process an add_statistics job.""" + with session_scope(session=instance.get_session()) as session: # type: ignore + metadata_id = _update_or_add_metadata(instance.hass, session, metadata) + for stat in statistics: + if stat_id := _statistics_exists( + session, Statistics, metadata_id, stat["start"] + ): + _update_statistics(session, Statistics, stat_id, stat) + else: + _insert_statistics(session, Statistics, metadata_id, stat) + + return True diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 9422e51f5a6..8bddc74693e 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -490,10 +490,12 @@ def _compile_statistics( # noqa: C901 # Set meta data meta: StatisticMetaData = { - "statistic_id": entity_id, - "unit_of_measurement": unit, "has_mean": "mean" in wanted_statistics[entity_id], "has_sum": "sum" in wanted_statistics[entity_id], + "name": None, + "source": RECORDER_DOMAIN, + "statistic_id": entity_id, + "unit_of_measurement": unit, } # Make calculations @@ -606,7 +608,7 @@ def _compile_statistics( # noqa: C901 stat["sum"] = _sum stat["state"] = new_state - result.append({"meta": meta, "stat": (stat,)}) + result.append({"meta": meta, "stat": stat}) return result @@ -638,14 +640,20 @@ def list_statistic_ids(hass: HomeAssistant, statistic_type: str | None = None) - continue if device_class not in UNIT_CONVERSIONS: - statistic_ids[state.entity_id] = native_unit + statistic_ids[state.entity_id] = { + "source": RECORDER_DOMAIN, + "unit_of_measurement": native_unit, + } continue if native_unit not in UNIT_CONVERSIONS[device_class]: continue statistics_unit = DEVICE_CLASS_UNITS[device_class] - statistic_ids[state.entity_id] = statistics_unit + statistic_ids[state.entity_id] = { + "source": RECORDER_DOMAIN, + "unit_of_measurement": statistics_unit, + } return statistic_ids diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index dc9b28b55b9..3d0eb5b1318 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -232,7 +232,7 @@ async def test_cost_sensor_price_entity_total_increasing( await async_wait_recording_done_without_instance(hass) all_statistics = await hass.loop.run_in_executor(None, _compile_statistics, hass) statistics = get_statistics_for_entity(all_statistics, cost_sensor_entity_id) - assert statistics["stat"][0]["sum"] == 19.0 + assert statistics["stat"]["sum"] == 19.0 # Energy sensor has a small dip, no reset should be detected hass.states.async_set( @@ -272,7 +272,7 @@ async def test_cost_sensor_price_entity_total_increasing( await async_wait_recording_done_without_instance(hass) all_statistics = await hass.loop.run_in_executor(None, _compile_statistics, hass) statistics = get_statistics_for_entity(all_statistics, cost_sensor_entity_id) - assert statistics["stat"][0]["sum"] == 38.0 + assert statistics["stat"]["sum"] == 38.0 @pytest.mark.parametrize("initial_energy,initial_cost", [(0, "0.0"), (None, "unknown")]) @@ -437,7 +437,7 @@ async def test_cost_sensor_price_entity_total( await async_wait_recording_done_without_instance(hass) all_statistics = await hass.loop.run_in_executor(None, _compile_statistics, hass) statistics = get_statistics_for_entity(all_statistics, cost_sensor_entity_id) - assert statistics["stat"][0]["sum"] == 19.0 + assert statistics["stat"]["sum"] == 19.0 # Energy sensor has a small dip hass.states.async_set( @@ -478,7 +478,7 @@ async def test_cost_sensor_price_entity_total( await async_wait_recording_done_without_instance(hass) all_statistics = await hass.loop.run_in_executor(None, _compile_statistics, hass) statistics = get_statistics_for_entity(all_statistics, cost_sensor_entity_id) - assert statistics["stat"][0]["sum"] == 38.0 + assert statistics["stat"]["sum"] == 38.0 @pytest.mark.parametrize("initial_energy,initial_cost", [(0, "0.0"), (None, "unknown")]) @@ -642,7 +642,7 @@ async def test_cost_sensor_price_entity_total_no_reset( await async_wait_recording_done_without_instance(hass) all_statistics = await hass.loop.run_in_executor(None, _compile_statistics, hass) statistics = get_statistics_for_entity(all_statistics, cost_sensor_entity_id) - assert statistics["stat"][0]["sum"] == 19.0 + assert statistics["stat"]["sum"] == 19.0 # Energy sensor has a small dip hass.states.async_set( @@ -659,7 +659,7 @@ async def test_cost_sensor_price_entity_total_no_reset( await async_wait_recording_done_without_instance(hass) all_statistics = await hass.loop.run_in_executor(None, _compile_statistics, hass) statistics = get_statistics_for_entity(all_statistics, cost_sensor_entity_id) - assert statistics["stat"][0]["sum"] == 18.0 + assert statistics["stat"]["sum"] == 18.0 async def test_cost_sensor_handle_wh(hass, hass_storage) -> None: diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 422bccf100d..18fa3dc7625 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -1010,7 +1010,12 @@ async def test_list_statistic_ids(hass, hass_ws_client, units, attributes, unit) response = await client.receive_json() assert response["success"] assert response["result"] == [ - {"statistic_id": "sensor.test", "unit_of_measurement": unit} + { + "statistic_id": "sensor.test", + "name": None, + "source": "recorder", + "unit_of_measurement": unit, + } ] hass.data[recorder.DATA_INSTANCE].do_adhoc_statistics(start=now) @@ -1023,7 +1028,12 @@ async def test_list_statistic_ids(hass, hass_ws_client, units, attributes, unit) response = await client.receive_json() assert response["success"] assert response["result"] == [ - {"statistic_id": "sensor.test", "unit_of_measurement": unit} + { + "statistic_id": "sensor.test", + "name": None, + "source": "recorder", + "unit_of_measurement": unit, + } ] await client.send_json( @@ -1038,7 +1048,12 @@ async def test_list_statistic_ids(hass, hass_ws_client, units, attributes, unit) response = await client.receive_json() assert response["success"] assert response["result"] == [ - {"statistic_id": "sensor.test", "unit_of_measurement": unit} + { + "statistic_id": "sensor.test", + "name": None, + "source": "recorder", + "unit_of_measurement": unit, + } ] await client.send_json( diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index d3496407949..4682b7fe482 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -13,10 +13,14 @@ from homeassistant.components.recorder.models import ( process_timestamp_to_utc_isoformat, ) from homeassistant.components.recorder.statistics import ( + async_add_external_statistics, get_last_statistics, + get_metadata, + list_statistic_ids, statistics_during_period, ) from homeassistant.const import TEMP_CELSIUS +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util @@ -117,7 +121,7 @@ def mock_sensor_statistics(): "has_mean": True, "has_sum": False, }, - "stat": ({"start": start},), + "stat": {"start": start}, } def get_fake_stats(_hass, start, _end): @@ -301,6 +305,177 @@ def test_statistics_duplicated(hass_recorder, caplog): caplog.clear() +def test_external_statistics(hass_recorder, caplog): + """Test inserting external statistics.""" + hass = hass_recorder() + wait_recording_done(hass) + assert "Compiling statistics for" not in caplog.text + assert "Statistics already compiled" not in caplog.text + + zero = dt_util.utcnow() + period1 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) + + external_statistics = { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 2, + } + + external_metadata = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import", + "unit_of_measurement": "kWh", + } + + async_add_external_statistics(hass, external_metadata, (external_statistics,)) + wait_recording_done(hass) + stats = statistics_during_period(hass, zero, period="hour") + assert stats == { + "test:total_energy_import": [ + { + "statistic_id": "test:total_energy_import", + "start": period1.isoformat(), + "end": (period1 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(0.0), + "sum": approx(2.0), + } + ] + } + statistic_ids = list_statistic_ids(hass) + assert statistic_ids == [ + { + "statistic_id": "test:total_energy_import", + "name": "Total imported energy", + "source": "test", + "unit_of_measurement": "kWh", + } + ] + metadata = get_metadata(hass, statistic_ids=("test:total_energy_import",)) + assert metadata == { + "test:total_energy_import": ( + 1, + { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import", + "unit_of_measurement": "kWh", + }, + ) + } + + # Update the previously inserted statistics + external_statistics = { + "start": period1, + "max": 1, + "mean": 2, + "min": 3, + "last_reset": None, + "state": 4, + "sum": 5, + } + async_add_external_statistics(hass, external_metadata, (external_statistics,)) + wait_recording_done(hass) + stats = statistics_during_period(hass, zero, period="hour") + assert stats == { + "test:total_energy_import": [ + { + "statistic_id": "test:total_energy_import", + "start": period1.isoformat(), + "end": (period1 + timedelta(hours=1)).isoformat(), + "max": approx(1.0), + "mean": approx(2.0), + "min": approx(3.0), + "last_reset": None, + "state": approx(4.0), + "sum": approx(5.0), + } + ] + } + + +def test_external_statistics_errors(hass_recorder, caplog): + """Test validation of external statistics.""" + hass = hass_recorder() + wait_recording_done(hass) + assert "Compiling statistics for" not in caplog.text + assert "Statistics already compiled" not in caplog.text + + zero = dt_util.utcnow() + period1 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) + + _external_statistics = { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 2, + } + + _external_metadata = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import", + "unit_of_measurement": "kWh", + } + + # Attempt to insert statistics for an entity + external_metadata = { + **_external_metadata, + "statistic_id": "sensor.total_energy_import", + } + external_statistics = {**_external_statistics} + with pytest.raises(HomeAssistantError): + async_add_external_statistics(hass, external_metadata, (external_statistics,)) + wait_recording_done(hass) + assert statistics_during_period(hass, zero, period="hour") == {} + assert list_statistic_ids(hass) == [] + assert get_metadata(hass, statistic_ids=("sensor.total_energy_import",)) == {} + + # Attempt to insert statistics for the wrong domain + external_metadata = {**_external_metadata, "source": "other"} + external_statistics = {**_external_statistics} + with pytest.raises(HomeAssistantError): + async_add_external_statistics(hass, external_metadata, (external_statistics,)) + wait_recording_done(hass) + assert statistics_during_period(hass, zero, period="hour") == {} + assert list_statistic_ids(hass) == [] + assert get_metadata(hass, statistic_ids=("test:total_energy_import",)) == {} + + # Attempt to insert statistics for an naive starting time + external_metadata = {**_external_metadata} + external_statistics = { + **_external_statistics, + "start": period1.replace(tzinfo=None), + } + with pytest.raises(HomeAssistantError): + async_add_external_statistics(hass, external_metadata, (external_statistics,)) + wait_recording_done(hass) + assert statistics_during_period(hass, zero, period="hour") == {} + assert list_statistic_ids(hass) == [] + assert get_metadata(hass, statistic_ids=("test:total_energy_import",)) == {} + + # Attempt to insert statistics for an invalid starting time + external_metadata = {**_external_metadata} + external_statistics = {**_external_statistics, "start": period1.replace(minute=1)} + with pytest.raises(HomeAssistantError): + async_add_external_statistics(hass, external_metadata, (external_statistics,)) + wait_recording_done(hass) + assert statistics_during_period(hass, zero, period="hour") == {} + assert list_statistic_ids(hass) == [] + assert get_metadata(hass, statistic_ids=("test:total_energy_import",)) == {} + + def record_states(hass): """Record some test states. diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index e60659aaab2..d52393fb693 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -206,7 +206,12 @@ async def test_update_statistics_metadata(hass, hass_ws_client, new_unit): response = await client.receive_json() assert response["success"] assert response["result"] == [ - {"statistic_id": "sensor.test", "unit_of_measurement": "W"} + { + "statistic_id": "sensor.test", + "name": None, + "source": "recorder", + "unit_of_measurement": "W", + } ] await client.send_json( @@ -225,5 +230,10 @@ async def test_update_statistics_metadata(hass, hass_ws_client, new_unit): response = await client.receive_json() assert response["success"] assert response["result"] == [ - {"statistic_id": "sensor.test", "unit_of_measurement": new_unit} + { + "statistic_id": "sensor.test", + "name": None, + "source": "recorder", + "unit_of_measurement": new_unit, + } ] diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index aea53597c36..2da1a203dfd 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -112,7 +112,12 @@ def test_compile_hourly_statistics( wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ - {"statistic_id": "sensor.test1", "unit_of_measurement": native_unit} + { + "statistic_id": "sensor.test1", + "name": None, + "source": "recorder", + "unit_of_measurement": native_unit, + } ] stats = statistics_during_period(hass, zero, period="5minute") assert stats == { @@ -170,7 +175,12 @@ def test_compile_hourly_statistics_purged_state_changes( wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ - {"statistic_id": "sensor.test1", "unit_of_measurement": native_unit} + { + "statistic_id": "sensor.test1", + "name": None, + "source": "recorder", + "unit_of_measurement": native_unit, + } ] stats = statistics_during_period(hass, zero, period="5minute") assert stats == { @@ -231,9 +241,24 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ - {"statistic_id": "sensor.test1", "unit_of_measurement": "°C"}, - {"statistic_id": "sensor.test6", "unit_of_measurement": "°C"}, - {"statistic_id": "sensor.test7", "unit_of_measurement": "°C"}, + { + "statistic_id": "sensor.test1", + "name": None, + "source": "recorder", + "unit_of_measurement": "°C", + }, + { + "statistic_id": "sensor.test6", + "name": None, + "source": "recorder", + "unit_of_measurement": "°C", + }, + { + "statistic_id": "sensor.test7", + "name": None, + "source": "recorder", + "unit_of_measurement": "°C", + }, ] stats = statistics_during_period(hass, zero, period="5minute") assert stats == { @@ -334,7 +359,12 @@ def test_compile_hourly_sum_statistics_amount( wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ - {"statistic_id": "sensor.test1", "unit_of_measurement": display_unit} + { + "statistic_id": "sensor.test1", + "name": None, + "source": "recorder", + "unit_of_measurement": display_unit, + } ] stats = statistics_during_period(hass, period0, period="5minute") expected_stats = { @@ -471,7 +501,12 @@ def test_compile_hourly_sum_statistics_amount_reset_every_state_change( wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ - {"statistic_id": "sensor.test1", "unit_of_measurement": native_unit} + { + "statistic_id": "sensor.test1", + "name": None, + "source": "recorder", + "unit_of_measurement": native_unit, + } ] stats = statistics_during_period(hass, zero, period="5minute") assert stats == { @@ -555,7 +590,12 @@ def test_compile_hourly_sum_statistics_amount_invalid_last_reset( wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ - {"statistic_id": "sensor.test1", "unit_of_measurement": native_unit} + { + "statistic_id": "sensor.test1", + "name": None, + "source": "recorder", + "unit_of_measurement": native_unit, + } ] stats = statistics_during_period(hass, zero, period="5minute") assert stats == { @@ -623,7 +663,12 @@ def test_compile_hourly_sum_statistics_nan_inf_state( wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ - {"statistic_id": "sensor.test1", "unit_of_measurement": native_unit} + { + "statistic_id": "sensor.test1", + "name": None, + "source": "recorder", + "unit_of_measurement": native_unit, + } ] stats = statistics_during_period(hass, zero, period="5minute") assert stats == { @@ -729,6 +774,8 @@ def test_compile_hourly_sum_statistics_negative_state( wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert { + "name": None, + "source": "recorder", "statistic_id": entity_id, "unit_of_measurement": native_unit, } in statistic_ids @@ -802,7 +849,12 @@ def test_compile_hourly_sum_statistics_total_no_reset( wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ - {"statistic_id": "sensor.test1", "unit_of_measurement": native_unit} + { + "statistic_id": "sensor.test1", + "name": None, + "source": "recorder", + "unit_of_measurement": native_unit, + } ] stats = statistics_during_period(hass, period0, period="5minute") assert stats == { @@ -888,7 +940,12 @@ def test_compile_hourly_sum_statistics_total_increasing( wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ - {"statistic_id": "sensor.test1", "unit_of_measurement": native_unit} + { + "statistic_id": "sensor.test1", + "name": None, + "source": "recorder", + "unit_of_measurement": native_unit, + } ] stats = statistics_during_period(hass, period0, period="5minute") assert stats == { @@ -984,7 +1041,12 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip( ) in caplog.text statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ - {"statistic_id": "sensor.test1", "unit_of_measurement": native_unit} + { + "statistic_id": "sensor.test1", + "name": None, + "source": "recorder", + "unit_of_measurement": native_unit, + } ] stats = statistics_during_period(hass, period0, period="5minute") assert stats == { @@ -1077,7 +1139,12 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ - {"statistic_id": "sensor.test1", "unit_of_measurement": "kWh"} + { + "statistic_id": "sensor.test1", + "name": None, + "source": "recorder", + "unit_of_measurement": "kWh", + } ] stats = statistics_during_period(hass, period0, period="5minute") assert stats == { @@ -1164,9 +1231,24 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ - {"statistic_id": "sensor.test1", "unit_of_measurement": "kWh"}, - {"statistic_id": "sensor.test2", "unit_of_measurement": "kWh"}, - {"statistic_id": "sensor.test3", "unit_of_measurement": "kWh"}, + { + "statistic_id": "sensor.test1", + "name": None, + "source": "recorder", + "unit_of_measurement": "kWh", + }, + { + "statistic_id": "sensor.test2", + "name": None, + "source": "recorder", + "unit_of_measurement": "kWh", + }, + { + "statistic_id": "sensor.test3", + "name": None, + "source": "recorder", + "unit_of_measurement": "kWh", + }, ] stats = statistics_during_period(hass, period0, period="5minute") assert stats == { @@ -1476,13 +1558,23 @@ def test_list_statistic_ids( hass.states.set("sensor.test1", 0, attributes=attributes) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ - {"statistic_id": "sensor.test1", "unit_of_measurement": native_unit} + { + "statistic_id": "sensor.test1", + "name": None, + "source": "recorder", + "unit_of_measurement": native_unit, + }, ] for stat_type in ["mean", "sum", "dogs"]: statistic_ids = list_statistic_ids(hass, statistic_type=stat_type) if statistic_type == stat_type: assert statistic_ids == [ - {"statistic_id": "sensor.test1", "unit_of_measurement": native_unit} + { + "statistic_id": "sensor.test1", + "name": None, + "source": "recorder", + "unit_of_measurement": native_unit, + }, ] else: assert statistic_ids == [] @@ -1554,7 +1646,12 @@ def test_compile_hourly_statistics_changing_units_1( assert "does not match the unit of already compiled" not in caplog.text statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ - {"statistic_id": "sensor.test1", "unit_of_measurement": native_unit} + { + "statistic_id": "sensor.test1", + "name": None, + "source": "recorder", + "unit_of_measurement": native_unit, + }, ] stats = statistics_during_period(hass, zero, period="5minute") assert stats == { @@ -1581,7 +1678,12 @@ def test_compile_hourly_statistics_changing_units_1( ) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ - {"statistic_id": "sensor.test1", "unit_of_measurement": native_unit} + { + "statistic_id": "sensor.test1", + "name": None, + "source": "recorder", + "unit_of_measurement": native_unit, + }, ] stats = statistics_during_period(hass, zero, period="5minute") assert stats == { @@ -1639,7 +1741,12 @@ def test_compile_hourly_statistics_changing_units_2( assert "and matches the unit of already compiled statistics" not in caplog.text statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ - {"statistic_id": "sensor.test1", "unit_of_measurement": "cats"} + { + "statistic_id": "sensor.test1", + "name": None, + "source": "recorder", + "unit_of_measurement": "cats", + }, ] stats = statistics_during_period(hass, zero, period="5minute") assert stats == {} @@ -1687,7 +1794,12 @@ def test_compile_hourly_statistics_changing_units_3( assert "does not match the unit of already compiled" not in caplog.text statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ - {"statistic_id": "sensor.test1", "unit_of_measurement": native_unit} + { + "statistic_id": "sensor.test1", + "name": None, + "source": "recorder", + "unit_of_measurement": native_unit, + }, ] stats = statistics_during_period(hass, zero, period="5minute") assert stats == { @@ -1712,7 +1824,12 @@ def test_compile_hourly_statistics_changing_units_3( assert f"matches the unit of already compiled statistics ({unit})" in caplog.text statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ - {"statistic_id": "sensor.test1", "unit_of_measurement": native_unit} + { + "statistic_id": "sensor.test1", + "name": None, + "source": "recorder", + "unit_of_measurement": native_unit, + }, ] stats = statistics_during_period(hass, zero, period="5minute") assert stats == { @@ -1760,7 +1877,12 @@ def test_compile_hourly_statistics_changing_device_class_1( assert "does not match the unit of already compiled" not in caplog.text statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ - {"statistic_id": "sensor.test1", "unit_of_measurement": state_unit} + { + "statistic_id": "sensor.test1", + "name": None, + "source": "recorder", + "unit_of_measurement": state_unit, + }, ] stats = statistics_during_period(hass, zero, period="5minute") assert stats == { @@ -1801,7 +1923,12 @@ def test_compile_hourly_statistics_changing_device_class_1( ) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ - {"statistic_id": "sensor.test1", "unit_of_measurement": state_unit} + { + "statistic_id": "sensor.test1", + "name": None, + "source": "recorder", + "unit_of_measurement": state_unit, + }, ] stats = statistics_during_period(hass, zero, period="5minute") assert stats == { @@ -1850,7 +1977,12 @@ def test_compile_hourly_statistics_changing_device_class_2( assert "does not match the unit of already compiled" not in caplog.text statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ - {"statistic_id": "sensor.test1", "unit_of_measurement": statistic_unit} + { + "statistic_id": "sensor.test1", + "name": None, + "source": "recorder", + "unit_of_measurement": statistic_unit, + }, ] stats = statistics_during_period(hass, zero, period="5minute") assert stats == { @@ -1891,7 +2023,12 @@ def test_compile_hourly_statistics_changing_device_class_2( ) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ - {"statistic_id": "sensor.test1", "unit_of_measurement": statistic_unit} + { + "statistic_id": "sensor.test1", + "name": None, + "source": "recorder", + "unit_of_measurement": statistic_unit, + }, ] stats = statistics_during_period(hass, zero, period="5minute") assert stats == { @@ -1943,7 +2080,12 @@ def test_compile_hourly_statistics_changing_statistics( wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ - {"statistic_id": "sensor.test1", "unit_of_measurement": None} + { + "statistic_id": "sensor.test1", + "name": None, + "source": "recorder", + "unit_of_measurement": None, + }, ] metadata = get_metadata(hass, statistic_ids=("sensor.test1",)) assert metadata == { @@ -1952,6 +2094,8 @@ def test_compile_hourly_statistics_changing_statistics( { "has_mean": True, "has_sum": False, + "name": None, + "source": "recorder", "statistic_id": "sensor.test1", "unit_of_measurement": None, }, @@ -1968,7 +2112,12 @@ def test_compile_hourly_statistics_changing_statistics( wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ - {"statistic_id": "sensor.test1", "unit_of_measurement": None} + { + "statistic_id": "sensor.test1", + "name": None, + "source": "recorder", + "unit_of_measurement": None, + }, ] metadata = get_metadata(hass, statistic_ids=("sensor.test1",)) assert metadata == { @@ -1977,6 +2126,8 @@ def test_compile_hourly_statistics_changing_statistics( { "has_mean": False, "has_sum": True, + "name": None, + "source": "recorder", "statistic_id": "sensor.test1", "unit_of_measurement": None, }, @@ -2155,10 +2306,30 @@ def test_compile_statistics_hourly_daily_monthly_summary( statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ - {"statistic_id": "sensor.test1", "unit_of_measurement": "%"}, - {"statistic_id": "sensor.test2", "unit_of_measurement": "%"}, - {"statistic_id": "sensor.test3", "unit_of_measurement": "%"}, - {"statistic_id": "sensor.test4", "unit_of_measurement": "EUR"}, + { + "statistic_id": "sensor.test1", + "name": None, + "source": "recorder", + "unit_of_measurement": "%", + }, + { + "statistic_id": "sensor.test2", + "name": None, + "source": "recorder", + "unit_of_measurement": "%", + }, + { + "statistic_id": "sensor.test3", + "name": None, + "source": "recorder", + "unit_of_measurement": "%", + }, + { + "statistic_id": "sensor.test4", + "name": None, + "source": "recorder", + "unit_of_measurement": "EUR", + }, ] stats = statistics_during_period(hass, zero, period="5minute") From def7c80e71e387b39b5ca2a14dc7a4f01559a07c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 26 Oct 2021 03:32:49 -0500 Subject: [PATCH 0856/1038] Add support for fan groups (#57941) * Add support for fan groups * dry * dry * fix refactor error * tweaks * wip * tweaks * tweaks * fix * fixes * coverage * tweaks --- homeassistant/components/group/__init__.py | 2 +- homeassistant/components/group/fan.py | 284 +++++++++++ homeassistant/components/group/util.py | 32 +- tests/components/group/test_fan.py | 508 ++++++++++++++++++++ tests/fixtures/group/fan_configuration.yaml | 13 + 5 files changed, 836 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/group/fan.py create mode 100644 tests/components/group/test_fan.py create mode 100644 tests/fixtures/group/fan_configuration.yaml diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index b06c25f48c9..523b45a94f7 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -56,7 +56,7 @@ ATTR_ALL = "all" SERVICE_SET = "set" SERVICE_REMOVE = "remove" -PLATFORMS = ["light", "cover", "notify", "binary_sensor"] +PLATFORMS = ["light", "cover", "notify", "fan", "binary_sensor"] REG_KEY = f"{DOMAIN}_registry" diff --git a/homeassistant/components/group/fan.py b/homeassistant/components/group/fan.py new file mode 100644 index 00000000000..d36fcc39f43 --- /dev/null +++ b/homeassistant/components/group/fan.py @@ -0,0 +1,284 @@ +"""This platform allows several fans to be grouped into one fan.""" +from __future__ import annotations + +from functools import reduce +import logging +from operator import ior +from typing import Any + +import voluptuous as vol + +from homeassistant.components.fan import ( + ATTR_DIRECTION, + ATTR_OSCILLATING, + ATTR_PERCENTAGE, + ATTR_PERCENTAGE_STEP, + DOMAIN, + PLATFORM_SCHEMA, + SERVICE_OSCILLATE, + SERVICE_SET_DIRECTION, + SERVICE_SET_PERCENTAGE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SUPPORT_DIRECTION, + SUPPORT_OSCILLATE, + SUPPORT_SET_SPEED, + FanEntity, +) +from homeassistant.const import ( + ATTR_ASSUMED_STATE, + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + CONF_ENTITIES, + CONF_NAME, + CONF_UNIQUE_ID, + STATE_ON, +) +from homeassistant.core import CoreState, Event, HomeAssistant, State +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.typing import ConfigType + +from . import GroupEntity +from .util import ( + attribute_equal, + most_frequent_attribute, + reduce_attribute, + states_equal, +) + +SUPPORTED_FLAGS = {SUPPORT_SET_SPEED, SUPPORT_DIRECTION, SUPPORT_OSCILLATE} + +DEFAULT_NAME = "Fan Group" + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNIQUE_ID): cv.string, + } +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: dict[str, Any] | None = None, +) -> None: + """Set up the Group Cover platform.""" + async_add_entities( + [FanGroup(config.get(CONF_UNIQUE_ID), config[CONF_NAME], config[CONF_ENTITIES])] + ) + + +class FanGroup(GroupEntity, FanEntity): + """Representation of a FanGroup.""" + + _attr_assumed_state: bool = True + + def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> None: + """Initialize a FanGroup entity.""" + self._entities = entities + self._fans: dict[int, set[str]] = {flag: set() for flag in SUPPORTED_FLAGS} + self._percentage = None + self._oscillating = None + self._direction = None + self._supported_features = 0 + self._speed_count = 100 + self._is_on = False + self._attr_name = name + self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entities} + self._attr_unique_id = unique_id + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return self._supported_features + + @property + def speed_count(self) -> int: + """Return the number of speeds the fan supports.""" + return self._speed_count + + @property + def is_on(self) -> bool: + """Return true if the entity is on.""" + return self._is_on + + @property + def percentage(self) -> int | None: + """Return the current speed as a percentage.""" + return self._percentage + + @property + def current_direction(self) -> str | None: + """Return the current direction of the fan.""" + return self._direction + + @property + def oscillating(self) -> bool | None: + """Return whether or not the fan is currently oscillating.""" + return self._oscillating + + async def _update_supported_features_event(self, event: Event) -> None: + self.async_set_context(event.context) + if (entity := event.data.get("entity_id")) is not None: + await self.async_update_supported_features( + entity, event.data.get("new_state") + ) + + async def async_update_supported_features( + self, + entity_id: str, + new_state: State | None, + update_state: bool = True, + ) -> None: + """Update dictionaries with supported features.""" + if not new_state: + for values in self._fans.values(): + values.discard(entity_id) + else: + features = new_state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + for feature in SUPPORTED_FLAGS: + if features & feature: + self._fans[feature].add(entity_id) + else: + self._fans[feature].discard(entity_id) + + if update_state: + await self.async_defer_or_update_ha_state() + + async def async_added_to_hass(self) -> None: + """Register listeners.""" + for entity_id in self._entities: + if (new_state := self.hass.states.get(entity_id)) is None: + continue + await self.async_update_supported_features( + entity_id, new_state, update_state=False + ) + self.async_on_remove( + async_track_state_change_event( + self.hass, self._entities, self._update_supported_features_event + ) + ) + + if self.hass.state == CoreState.running: + await self.async_update() + return + await super().async_added_to_hass() + + async def async_set_percentage(self, percentage: int) -> None: + """Set the speed of the fan, as a percentage.""" + if percentage == 0: + await self.async_turn_off() + await self._async_call_supported_entities( + SERVICE_SET_PERCENTAGE, SUPPORT_SET_SPEED, {ATTR_PERCENTAGE: percentage} + ) + + async def async_oscillate(self, oscillating: bool) -> None: + """Oscillate the fan.""" + await self._async_call_supported_entities( + SERVICE_OSCILLATE, SUPPORT_OSCILLATE, {ATTR_OSCILLATING: oscillating} + ) + + async def async_set_direction(self, direction: str) -> None: + """Set the direction of the fan.""" + await self._async_call_supported_entities( + SERVICE_SET_DIRECTION, SUPPORT_DIRECTION, {ATTR_DIRECTION: direction} + ) + + async def async_turn_on( + self, + speed: str | None = None, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, + ) -> None: + """Turn on the fan.""" + if percentage is not None: + await self.async_set_percentage(percentage) + return + await self._async_call_all_entities(SERVICE_TURN_ON) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the fans off.""" + await self._async_call_all_entities(SERVICE_TURN_OFF) + + async def _async_call_supported_entities( + self, service: str, support_flag: int, data: dict[str, Any] + ) -> None: + """Call a service with all entities.""" + await self.hass.services.async_call( + DOMAIN, + service, + {**data, ATTR_ENTITY_ID: self._fans[support_flag]}, + blocking=True, + context=self._context, + ) + + async def _async_call_all_entities(self, service: str) -> None: + """Call a service with all entities.""" + await self.hass.services.async_call( + DOMAIN, + service, + {ATTR_ENTITY_ID: self._entities}, + blocking=True, + context=self._context, + ) + + def _async_states_by_support_flag(self, flag: int) -> list[State]: + """Return all the entity states for a supported flag.""" + states: list[State] = list( + filter(None, [self.hass.states.get(x) for x in self._fans[flag]]) + ) + return states + + def _set_attr_most_frequent(self, attr: str, flag: int, entity_attr: str) -> None: + """Set an attribute based on most frequent supported entities attributes.""" + states = self._async_states_by_support_flag(flag) + setattr(self, attr, most_frequent_attribute(states, entity_attr)) + self._attr_assumed_state |= not attribute_equal(states, entity_attr) + + async def async_update(self) -> None: + """Update state and attributes.""" + self._attr_assumed_state = False + + on_states: list[State] = list( + filter(None, [self.hass.states.get(x) for x in self._entities]) + ) + self._is_on = any(state.state == STATE_ON for state in on_states) + self._attr_assumed_state |= not states_equal(on_states) + + percentage_states = self._async_states_by_support_flag(SUPPORT_SET_SPEED) + self._percentage = reduce_attribute(percentage_states, ATTR_PERCENTAGE) + self._attr_assumed_state |= not attribute_equal( + percentage_states, ATTR_PERCENTAGE + ) + if ( + percentage_states + and percentage_states[0].attributes.get(ATTR_PERCENTAGE_STEP) + and attribute_equal(percentage_states, ATTR_PERCENTAGE_STEP) + ): + self._speed_count = ( + round(100 / percentage_states[0].attributes[ATTR_PERCENTAGE_STEP]) + or 100 + ) + else: + self._speed_count = 100 + + self._set_attr_most_frequent( + "_oscillating", SUPPORT_OSCILLATE, ATTR_OSCILLATING + ) + self._set_attr_most_frequent("_direction", SUPPORT_DIRECTION, ATTR_DIRECTION) + + self._supported_features = reduce( + ior, [feature for feature in SUPPORTED_FLAGS if self._fans[feature]], 0 + ) + self._attr_assumed_state |= any( + state.attributes.get(ATTR_ASSUMED_STATE) for state in on_states + ) diff --git a/homeassistant/components/group/util.py b/homeassistant/components/group/util.py index d1e40f616d4..da67e071f27 100644 --- a/homeassistant/components/group/util.py +++ b/homeassistant/components/group/util.py @@ -15,6 +15,12 @@ def find_state_attributes(states: list[State], key: str) -> Iterator[Any]: yield value +def find_state(states: list[State]) -> Iterator[Any]: + """Find state from states.""" + for state in states: + yield state.state + + def mean_int(*args: Any) -> int: """Return the mean of the supplied values.""" return int(sum(args) / len(args)) @@ -30,8 +36,30 @@ def attribute_equal(states: list[State], key: str) -> bool: Note: Returns True if no matching attribute is found. """ - attrs = find_state_attributes(states, key) - grp = groupby(attrs) + return _values_equal(find_state_attributes(states, key)) + + +def most_frequent_attribute(states: list[State], key: str) -> Any | None: + """Find attributes with matching key from states.""" + if attrs := list(find_state_attributes(states, key)): + return max(set(attrs), key=attrs.count) + return None + + +def states_equal(states: list[State]) -> bool: + """Return True if all states are equal. + + Note: Returns True if no matching attribute is found. + """ + return _values_equal(find_state(states)) + + +def _values_equal(values: Iterator[Any]) -> bool: + """Return True if all values are equal. + + Note: Returns True if no matching attribute is found. + """ + grp = groupby(values) return bool(next(grp, True) and not next(grp, False)) diff --git a/tests/components/group/test_fan.py b/tests/components/group/test_fan.py new file mode 100644 index 00000000000..10770e3de06 --- /dev/null +++ b/tests/components/group/test_fan.py @@ -0,0 +1,508 @@ +"""The tests for the group fan platform.""" +from os import path +from unittest.mock import patch + +import pytest + +from homeassistant import config as hass_config +from homeassistant.components.fan import ( + ATTR_DIRECTION, + ATTR_OSCILLATING, + ATTR_PERCENTAGE, + ATTR_PERCENTAGE_STEP, + DIRECTION_FORWARD, + DIRECTION_REVERSE, + DOMAIN, + SERVICE_OSCILLATE, + SERVICE_SET_DIRECTION, + SERVICE_SET_PERCENTAGE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SUPPORT_DIRECTION, + SUPPORT_OSCILLATE, + SUPPORT_SET_SPEED, +) +from homeassistant.components.group import SERVICE_RELOAD +from homeassistant.components.group.fan import DEFAULT_NAME +from homeassistant.const import ( + ATTR_ASSUMED_STATE, + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_SUPPORTED_FEATURES, + CONF_ENTITIES, + CONF_UNIQUE_ID, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import CoreState +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + +from tests.common import assert_setup_component + +FAN_GROUP = "fan.fan_group" + +MISSING_FAN_ENTITY_ID = "fan.missing" +LIVING_ROOM_FAN_ENTITY_ID = "fan.living_room_fan" +PERCENTAGE_FULL_FAN_ENTITY_ID = "fan.percentage_full_fan" +CEILING_FAN_ENTITY_ID = "fan.ceiling_fan" +PERCENTAGE_LIMITED_FAN_ENTITY_ID = "fan.percentage_limited_fan" + +FULL_FAN_ENTITY_IDS = [LIVING_ROOM_FAN_ENTITY_ID, PERCENTAGE_FULL_FAN_ENTITY_ID] +LIMITED_FAN_ENTITY_IDS = [CEILING_FAN_ENTITY_ID, PERCENTAGE_LIMITED_FAN_ENTITY_ID] + + +FULL_SUPPORT_FEATURES = SUPPORT_SET_SPEED | SUPPORT_DIRECTION | SUPPORT_OSCILLATE + + +CONFIG_MISSING_FAN = { + DOMAIN: [ + {"platform": "demo"}, + { + "platform": "group", + CONF_ENTITIES: [ + MISSING_FAN_ENTITY_ID, + *FULL_FAN_ENTITY_IDS, + *LIMITED_FAN_ENTITY_IDS, + ], + }, + ] +} + +CONFIG_FULL_SUPPORT = { + DOMAIN: [ + {"platform": "demo"}, + { + "platform": "group", + CONF_ENTITIES: [*FULL_FAN_ENTITY_IDS], + }, + ] +} + +CONFIG_LIMITED_SUPPORT = { + DOMAIN: [ + { + "platform": "group", + CONF_ENTITIES: [*LIMITED_FAN_ENTITY_IDS], + }, + ] +} + + +CONFIG_ATTRIBUTES = { + DOMAIN: { + "platform": "group", + CONF_ENTITIES: [*FULL_FAN_ENTITY_IDS, *LIMITED_FAN_ENTITY_IDS], + CONF_UNIQUE_ID: "unique_identifier", + } +} + + +@pytest.fixture +async def setup_comp(hass, config_count): + """Set up group fan component.""" + config, count = config_count + with assert_setup_component(count, DOMAIN): + await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + +@pytest.mark.parametrize("config_count", [(CONFIG_ATTRIBUTES, 1)]) +async def test_state(hass, setup_comp): + """Test handling of state.""" + state = hass.states.get(FAN_GROUP) + # No entity has a valid state -> group state off + assert state.state == STATE_OFF + assert state.attributes[ATTR_FRIENDLY_NAME] == DEFAULT_NAME + assert state.attributes[ATTR_ENTITY_ID] == [ + *FULL_FAN_ENTITY_IDS, + *LIMITED_FAN_ENTITY_IDS, + ] + assert ATTR_ASSUMED_STATE not in state.attributes + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + # Set all entities as on -> group state on + hass.states.async_set(CEILING_FAN_ENTITY_ID, STATE_ON, {}) + hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, STATE_ON, {}) + hass.states.async_set(PERCENTAGE_FULL_FAN_ENTITY_ID, STATE_ON, {}) + hass.states.async_set(PERCENTAGE_LIMITED_FAN_ENTITY_ID, STATE_ON, {}) + await hass.async_block_till_done() + state = hass.states.get(FAN_GROUP) + assert state.state == STATE_ON + + # Set all entities as off -> group state off + hass.states.async_set(CEILING_FAN_ENTITY_ID, STATE_OFF, {}) + hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, STATE_OFF, {}) + hass.states.async_set(PERCENTAGE_FULL_FAN_ENTITY_ID, STATE_OFF, {}) + hass.states.async_set(PERCENTAGE_LIMITED_FAN_ENTITY_ID, STATE_OFF, {}) + await hass.async_block_till_done() + state = hass.states.get(FAN_GROUP) + assert state.state == STATE_OFF + + # Set first entity as on -> group state on + hass.states.async_set(CEILING_FAN_ENTITY_ID, STATE_ON, {}) + hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, STATE_OFF, {}) + hass.states.async_set(PERCENTAGE_FULL_FAN_ENTITY_ID, STATE_OFF, {}) + hass.states.async_set(PERCENTAGE_LIMITED_FAN_ENTITY_ID, STATE_OFF, {}) + await hass.async_block_till_done() + state = hass.states.get(FAN_GROUP) + assert state.state == STATE_ON + + # Set last entity as on -> group state on + hass.states.async_set(CEILING_FAN_ENTITY_ID, STATE_OFF, {}) + hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, STATE_OFF, {}) + hass.states.async_set(PERCENTAGE_FULL_FAN_ENTITY_ID, STATE_OFF, {}) + hass.states.async_set(PERCENTAGE_LIMITED_FAN_ENTITY_ID, STATE_ON, {}) + await hass.async_block_till_done() + state = hass.states.get(FAN_GROUP) + assert state.state == STATE_ON + + # now remove an entity + hass.states.async_remove(PERCENTAGE_LIMITED_FAN_ENTITY_ID) + await hass.async_block_till_done() + state = hass.states.get(FAN_GROUP) + assert state.state == STATE_OFF + assert ATTR_ASSUMED_STATE not in state.attributes + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + # Test entity registry integration + entity_registry = er.async_get(hass) + entry = entity_registry.async_get(FAN_GROUP) + assert entry + assert entry.unique_id == "unique_identifier" + + +@pytest.mark.parametrize("config_count", [(CONFIG_ATTRIBUTES, 1)]) +async def test_attributes(hass, setup_comp): + """Test handling of state attributes.""" + state = hass.states.get(FAN_GROUP) + assert state.state == STATE_OFF + assert state.attributes[ATTR_FRIENDLY_NAME] == DEFAULT_NAME + assert state.attributes[ATTR_ENTITY_ID] == [ + *FULL_FAN_ENTITY_IDS, + *LIMITED_FAN_ENTITY_IDS, + ] + assert ATTR_ASSUMED_STATE not in state.attributes + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + hass.states.async_set(CEILING_FAN_ENTITY_ID, STATE_ON, {}) + hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, STATE_ON, {}) + hass.states.async_set(PERCENTAGE_FULL_FAN_ENTITY_ID, STATE_ON, {}) + hass.states.async_set(PERCENTAGE_LIMITED_FAN_ENTITY_ID, STATE_ON, {}) + await hass.async_block_till_done() + state = hass.states.get(FAN_GROUP) + assert state.state == STATE_ON + + # Add Entity that supports speed + hass.states.async_set( + CEILING_FAN_ENTITY_ID, + STATE_ON, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_SET_SPEED, + ATTR_PERCENTAGE: 50, + }, + ) + await hass.async_block_till_done() + + state = hass.states.get(FAN_GROUP) + assert state.state == STATE_ON + assert ATTR_ASSUMED_STATE not in state.attributes + assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_SET_SPEED + assert ATTR_PERCENTAGE in state.attributes + assert state.attributes[ATTR_PERCENTAGE] == 50 + assert ATTR_ASSUMED_STATE not in state.attributes + + # Add Entity that supports + # ### Test assumed state ### + # ########################## + + # Add Entity with a different speed should set assumed state + hass.states.async_set( + PERCENTAGE_LIMITED_FAN_ENTITY_ID, + STATE_ON, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_SET_SPEED, + ATTR_PERCENTAGE: 75, + }, + ) + await hass.async_block_till_done() + + state = hass.states.get(FAN_GROUP) + assert state.state == STATE_ON + assert state.attributes[ATTR_ASSUMED_STATE] is True + assert state.attributes[ATTR_PERCENTAGE] == int((50 + 75) / 2) + + +@pytest.mark.parametrize("config_count", [(CONFIG_FULL_SUPPORT, 2)]) +async def test_direction_oscillating(hass, setup_comp): + """Test handling of direction and oscillating attributes.""" + + hass.states.async_set( + LIVING_ROOM_FAN_ENTITY_ID, + STATE_ON, + { + ATTR_SUPPORTED_FEATURES: FULL_SUPPORT_FEATURES, + ATTR_OSCILLATING: True, + ATTR_DIRECTION: DIRECTION_FORWARD, + ATTR_PERCENTAGE: 50, + }, + ) + hass.states.async_set( + PERCENTAGE_FULL_FAN_ENTITY_ID, + STATE_ON, + { + ATTR_SUPPORTED_FEATURES: FULL_SUPPORT_FEATURES, + ATTR_OSCILLATING: True, + ATTR_DIRECTION: DIRECTION_FORWARD, + ATTR_PERCENTAGE: 50, + }, + ) + await hass.async_block_till_done() + + state = hass.states.get(FAN_GROUP) + assert state.state == STATE_ON + assert state.attributes[ATTR_FRIENDLY_NAME] == DEFAULT_NAME + assert state.attributes[ATTR_ENTITY_ID] == [*FULL_FAN_ENTITY_IDS] + assert ATTR_ASSUMED_STATE not in state.attributes + assert state.attributes[ATTR_SUPPORTED_FEATURES] == FULL_SUPPORT_FEATURES + assert ATTR_PERCENTAGE in state.attributes + assert state.attributes[ATTR_PERCENTAGE] == 50 + assert state.attributes[ATTR_OSCILLATING] is True + assert state.attributes[ATTR_DIRECTION] == DIRECTION_FORWARD + assert ATTR_ASSUMED_STATE not in state.attributes + + # Add Entity that supports + # ### Test assumed state ### + # ########################## + + # Add Entity with a different direction should set assumed state + hass.states.async_set( + PERCENTAGE_FULL_FAN_ENTITY_ID, + STATE_ON, + { + ATTR_SUPPORTED_FEATURES: FULL_SUPPORT_FEATURES, + ATTR_OSCILLATING: True, + ATTR_DIRECTION: DIRECTION_REVERSE, + ATTR_PERCENTAGE: 50, + }, + ) + await hass.async_block_till_done() + + state = hass.states.get(FAN_GROUP) + assert state.state == STATE_ON + assert state.attributes[ATTR_ASSUMED_STATE] is True + assert ATTR_PERCENTAGE in state.attributes + assert state.attributes[ATTR_PERCENTAGE] == 50 + assert state.attributes[ATTR_OSCILLATING] is True + assert ATTR_ASSUMED_STATE in state.attributes + + # Now that everything is the same, no longer assumed state + + hass.states.async_set( + LIVING_ROOM_FAN_ENTITY_ID, + STATE_ON, + { + ATTR_SUPPORTED_FEATURES: FULL_SUPPORT_FEATURES, + ATTR_OSCILLATING: True, + ATTR_DIRECTION: DIRECTION_REVERSE, + ATTR_PERCENTAGE: 50, + }, + ) + await hass.async_block_till_done() + + state = hass.states.get(FAN_GROUP) + assert state.state == STATE_ON + assert ATTR_PERCENTAGE in state.attributes + assert state.attributes[ATTR_PERCENTAGE] == 50 + assert state.attributes[ATTR_OSCILLATING] is True + assert state.attributes[ATTR_DIRECTION] == DIRECTION_REVERSE + assert ATTR_ASSUMED_STATE not in state.attributes + + hass.states.async_set( + LIVING_ROOM_FAN_ENTITY_ID, + STATE_ON, + { + ATTR_SUPPORTED_FEATURES: FULL_SUPPORT_FEATURES, + ATTR_OSCILLATING: False, + ATTR_DIRECTION: DIRECTION_FORWARD, + ATTR_PERCENTAGE: 50, + }, + ) + hass.states.async_set( + PERCENTAGE_FULL_FAN_ENTITY_ID, + STATE_ON, + { + ATTR_SUPPORTED_FEATURES: FULL_SUPPORT_FEATURES, + ATTR_OSCILLATING: False, + ATTR_DIRECTION: DIRECTION_FORWARD, + ATTR_PERCENTAGE: 50, + }, + ) + await hass.async_block_till_done() + + state = hass.states.get(FAN_GROUP) + assert state.state == STATE_ON + assert ATTR_PERCENTAGE in state.attributes + assert state.attributes[ATTR_PERCENTAGE] == 50 + assert state.attributes[ATTR_OSCILLATING] is False + assert state.attributes[ATTR_DIRECTION] == DIRECTION_FORWARD + assert ATTR_ASSUMED_STATE not in state.attributes + + +@pytest.mark.parametrize("config_count", [(CONFIG_MISSING_FAN, 2)]) +async def test_state_missing_entity_id(hass, setup_comp): + """Test we can still setup with a missing entity id.""" + state = hass.states.get(FAN_GROUP) + await hass.async_block_till_done() + assert state.state == STATE_OFF + + +async def test_setup_before_started(hass): + """Test we can setup before starting.""" + hass.state = CoreState.stopped + assert await async_setup_component(hass, DOMAIN, CONFIG_MISSING_FAN) + + await hass.async_block_till_done() + await hass.async_start() + + await hass.async_block_till_done() + assert hass.states.get(FAN_GROUP).state == STATE_OFF + + +@pytest.mark.parametrize("config_count", [(CONFIG_MISSING_FAN, 2)]) +async def test_reload(hass, setup_comp): + """Test the ability to reload fans.""" + await hass.async_block_till_done() + await hass.async_start() + + await hass.async_block_till_done() + assert hass.states.get(FAN_GROUP).state == STATE_OFF + + yaml_path = path.join( + _get_fixtures_base_path(), + "fixtures", + "group/fan_configuration.yaml", + ) + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + "group", + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.get(FAN_GROUP) is None + assert hass.states.get("fan.upstairs_fans") is not None + + +def _get_fixtures_base_path(): + return path.dirname(path.dirname(path.dirname(__file__))) + + +@pytest.mark.parametrize("config_count", [(CONFIG_FULL_SUPPORT, 2)]) +async def test_service_calls(hass, setup_comp): + """Test calling services.""" + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: FAN_GROUP}, blocking=True + ) + assert hass.states.get(LIVING_ROOM_FAN_ENTITY_ID).state == STATE_ON + assert hass.states.get(PERCENTAGE_FULL_FAN_ENTITY_ID).state == STATE_ON + assert hass.states.get(FAN_GROUP).state == STATE_ON + + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: FAN_GROUP, ATTR_PERCENTAGE: 66}, + blocking=True, + ) + living_room_fan_state = hass.states.get(LIVING_ROOM_FAN_ENTITY_ID) + assert living_room_fan_state.attributes[ATTR_PERCENTAGE] == 66 + percentage_full_fan_state = hass.states.get(PERCENTAGE_FULL_FAN_ENTITY_ID) + assert percentage_full_fan_state.attributes[ATTR_PERCENTAGE] == 66 + fan_group_state = hass.states.get(FAN_GROUP) + assert fan_group_state.attributes[ATTR_PERCENTAGE] == 66 + assert fan_group_state.attributes[ATTR_PERCENTAGE_STEP] == 100 / 3 + + await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: FAN_GROUP}, blocking=True + ) + assert hass.states.get(LIVING_ROOM_FAN_ENTITY_ID).state == STATE_OFF + assert hass.states.get(PERCENTAGE_FULL_FAN_ENTITY_ID).state == STATE_OFF + assert hass.states.get(FAN_GROUP).state == STATE_OFF + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_PERCENTAGE, + {ATTR_ENTITY_ID: FAN_GROUP, ATTR_PERCENTAGE: 100}, + blocking=True, + ) + living_room_fan_state = hass.states.get(LIVING_ROOM_FAN_ENTITY_ID) + assert living_room_fan_state.attributes[ATTR_PERCENTAGE] == 100 + percentage_full_fan_state = hass.states.get(PERCENTAGE_FULL_FAN_ENTITY_ID) + assert percentage_full_fan_state.attributes[ATTR_PERCENTAGE] == 100 + fan_group_state = hass.states.get(FAN_GROUP) + assert fan_group_state.attributes[ATTR_PERCENTAGE] == 100 + + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: FAN_GROUP, ATTR_PERCENTAGE: 0}, + blocking=True, + ) + assert hass.states.get(LIVING_ROOM_FAN_ENTITY_ID).state == STATE_OFF + assert hass.states.get(PERCENTAGE_FULL_FAN_ENTITY_ID).state == STATE_OFF + assert hass.states.get(FAN_GROUP).state == STATE_OFF + + await hass.services.async_call( + DOMAIN, + SERVICE_OSCILLATE, + {ATTR_ENTITY_ID: FAN_GROUP, ATTR_OSCILLATING: True}, + blocking=True, + ) + living_room_fan_state = hass.states.get(LIVING_ROOM_FAN_ENTITY_ID) + assert living_room_fan_state.attributes[ATTR_OSCILLATING] is True + percentage_full_fan_state = hass.states.get(PERCENTAGE_FULL_FAN_ENTITY_ID) + assert percentage_full_fan_state.attributes[ATTR_OSCILLATING] is True + fan_group_state = hass.states.get(FAN_GROUP) + assert fan_group_state.attributes[ATTR_OSCILLATING] is True + + await hass.services.async_call( + DOMAIN, + SERVICE_OSCILLATE, + {ATTR_ENTITY_ID: FAN_GROUP, ATTR_OSCILLATING: False}, + blocking=True, + ) + living_room_fan_state = hass.states.get(LIVING_ROOM_FAN_ENTITY_ID) + assert living_room_fan_state.attributes[ATTR_OSCILLATING] is False + percentage_full_fan_state = hass.states.get(PERCENTAGE_FULL_FAN_ENTITY_ID) + assert percentage_full_fan_state.attributes[ATTR_OSCILLATING] is False + fan_group_state = hass.states.get(FAN_GROUP) + assert fan_group_state.attributes[ATTR_OSCILLATING] is False + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_DIRECTION, + {ATTR_ENTITY_ID: FAN_GROUP, ATTR_DIRECTION: DIRECTION_FORWARD}, + blocking=True, + ) + living_room_fan_state = hass.states.get(LIVING_ROOM_FAN_ENTITY_ID) + assert living_room_fan_state.attributes[ATTR_DIRECTION] == DIRECTION_FORWARD + percentage_full_fan_state = hass.states.get(PERCENTAGE_FULL_FAN_ENTITY_ID) + assert percentage_full_fan_state.attributes[ATTR_DIRECTION] == DIRECTION_FORWARD + fan_group_state = hass.states.get(FAN_GROUP) + assert fan_group_state.attributes[ATTR_DIRECTION] == DIRECTION_FORWARD + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_DIRECTION, + {ATTR_ENTITY_ID: FAN_GROUP, ATTR_DIRECTION: DIRECTION_REVERSE}, + blocking=True, + ) + living_room_fan_state = hass.states.get(LIVING_ROOM_FAN_ENTITY_ID) + assert living_room_fan_state.attributes[ATTR_DIRECTION] == DIRECTION_REVERSE + percentage_full_fan_state = hass.states.get(PERCENTAGE_FULL_FAN_ENTITY_ID) + assert percentage_full_fan_state.attributes[ATTR_DIRECTION] == DIRECTION_REVERSE + fan_group_state = hass.states.get(FAN_GROUP) + assert fan_group_state.attributes[ATTR_DIRECTION] == DIRECTION_REVERSE diff --git a/tests/fixtures/group/fan_configuration.yaml b/tests/fixtures/group/fan_configuration.yaml new file mode 100644 index 00000000000..0a33d1819b6 --- /dev/null +++ b/tests/fixtures/group/fan_configuration.yaml @@ -0,0 +1,13 @@ +fan: + - platform: group + name: Upstairs Fans + entities: + - fan.living_room_fan + - fan.percentage_full_fan + +notify: + - platform: group + name: new_group_notify + services: + - service: demo1 + - service: demo2 From d16304a20114444641f344a27c23daf77301100d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 26 Oct 2021 10:34:02 +0200 Subject: [PATCH 0857/1038] Allow homeassistant prefix for device info configuration url (#58414) * Allow panel prefix for device info configuration url * Add to add-ons * Use homeassistant as the prefix * Update homeassistant/components/hassio/__init__.py Co-authored-by: Paulus Schoutsen Co-authored-by: Paulus Schoutsen --- homeassistant/components/hassio/__init__.py | 1 + homeassistant/helpers/entity_platform.py | 16 +++++--- tests/helpers/test_entity_platform.py | 44 +++++++++++++++++++++ 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index d424722b4db..0f8ff5441f3 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -661,6 +661,7 @@ def async_register_addons_in_dev_reg( sw_version=addon[ATTR_VERSION], name=addon[ATTR_NAME], entry_type=ATTR_SERVICE, + configuration_url=f"homeassistant://hassio/addon/{addon[ATTR_SLUG]}", ) if manufacturer := addon.get(ATTR_REPOSITORY) or addon.get(ATTR_URL): params[ATTR_MANUFACTURER] = manufacturer diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index ccc9b1663a8..1864111c0ed 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -8,6 +8,7 @@ from datetime import datetime, timedelta from logging import Logger, getLogger from types import ModuleType from typing import TYPE_CHECKING, Any, Protocol +from urllib.parse import urlparse import voluptuous as vol @@ -476,14 +477,17 @@ class EntityPlatform: processed_dev_info[key] = device_info[key] # type: ignore[misc] if "configuration_url" in device_info: - try: - processed_dev_info["configuration_url"] = cv.url( - device_info["configuration_url"] - ) - except vol.Invalid: + configuration_url = str(device_info["configuration_url"]) + if urlparse(configuration_url).scheme in [ + "http", + "https", + "homeassistant", + ]: + processed_dev_info["configuration_url"] = configuration_url + else: _LOGGER.warning( "Ignoring invalid device configuration_url '%s'", - device_info["configuration_url"], + configuration_url, ) try: diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 9f213801355..636ce7a764b 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -967,6 +967,50 @@ async def test_device_info_invalid_url(hass, caplog): ) +async def test_device_info_homeassistant_url(hass, caplog): + """Test device info with homeassistant URL.""" + registry = dr.async_get(hass) + registry.async_get_or_create( + config_entry_id="123", + connections=set(), + identifiers={("mqtt", "via-id")}, + manufacturer="manufacturer", + model="via", + ) + + async def async_setup_entry(hass, config_entry, async_add_entities): + """Mock setup entry method.""" + async_add_entities( + [ + # Valid device info, with homeassistant url + MockEntity( + unique_id="qwer", + device_info={ + "identifiers": {("mqtt", "1234")}, + "configuration_url": "homeassistant://config/mqtt", + }, + ), + ] + ) + return True + + platform = MockPlatform(async_setup_entry=async_setup_entry) + config_entry = MockConfigEntry(entry_id="super-mock-id") + entity_platform = MockEntityPlatform( + hass, platform_name=config_entry.domain, platform=platform + ) + + assert await entity_platform.async_setup_entry(config_entry) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids()) == 1 + + device = registry.async_get_device({("mqtt", "1234")}) + assert device is not None + assert device.identifiers == {("mqtt", "1234")} + assert device.configuration_url == "homeassistant://config/mqtt" + + async def test_entity_disabled_by_integration(hass): """Test entity disabled by integration.""" component = EntityComponent(_LOGGER, DOMAIN, hass, timedelta(seconds=20)) From d0cc2a530a21c593d69e66f53e4f7c420322cc8b Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 26 Oct 2021 10:42:42 +0200 Subject: [PATCH 0858/1038] Remove redundant value test in KNX Number entity (#58455) * remove redundant test for out of bound value * increase test coverage for number --- homeassistant/components/knx/number.py | 5 -- tests/components/knx/test_number.py | 91 ++++++++++++++++++++++++-- 2 files changed, 84 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/knx/number.py b/homeassistant/components/knx/number.py index 2fc8e1af244..659c334fd4a 100644 --- a/homeassistant/components/knx/number.py +++ b/homeassistant/components/knx/number.py @@ -97,9 +97,4 @@ class KNXNumber(KnxEntity, NumberEntity, RestoreEntity): async def async_set_value(self, value: float) -> None: """Set new value.""" - if value < self.min_value or value > self.max_value: - raise ValueError( - f"Invalid value for {self.entity_id}: {value} " - f"(range {self.min_value} - {self.max_value})" - ) await self._device.set(value) diff --git a/tests/components/knx/test_number.py b/tests/components/knx/test_number.py index 1a2044b166c..d14f01ee5fe 100644 --- a/tests/components/knx/test_number.py +++ b/tests/components/knx/test_number.py @@ -1,23 +1,100 @@ """Test KNX number.""" -from homeassistant.components.knx.const import KNX_ADDRESS +from unittest.mock import patch + +import pytest + +from homeassistant.components.knx.const import CONF_RESPOND_TO_READ, KNX_ADDRESS from homeassistant.components.knx.schema import NumberSchema from homeassistant.const import CONF_NAME, CONF_TYPE -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, State from .conftest import KNXTestKit -async def test_number_unit_of_measurement(hass: HomeAssistant, knx: KNXTestKit): - """Test simple KNX number.""" +async def test_number_set_value(hass: HomeAssistant, knx: KNXTestKit): + """Test KNX number with passive_address and respond_to_read restoring state.""" test_address = "1/1/1" await knx.setup_integration( { NumberSchema.PLATFORM_NAME: { CONF_NAME: "test", KNX_ADDRESS: test_address, - CONF_TYPE: "illuminance", + CONF_TYPE: "percent", } } ) - assert len(hass.states.async_all()) == 1 - assert hass.states.get("number.test").attributes.get("unit_of_measurement") == "lx" + # set value + await hass.services.async_call( + "number", + "set_value", + {"entity_id": "number.test", "value": 4.0}, + blocking=True, + ) + await knx.assert_write(test_address, (0x0A,)) + state = hass.states.get("number.test") + assert state.state == "4" + assert state.attributes.get("unit_of_measurement") == "%" + + # set value out of range + with pytest.raises(ValueError): + await hass.services.async_call( + "number", + "set_value", + {"entity_id": "number.test", "value": 101.0}, + blocking=True, + ) + with pytest.raises(ValueError): + await hass.services.async_call( + "number", + "set_value", + {"entity_id": "number.test", "value": -1}, + blocking=True, + ) + await knx.assert_no_telegram() + state = hass.states.get("number.test") + assert state.state == "4" + + # update from KNX + await knx.receive_write(test_address, (0xE6,)) + state = hass.states.get("number.test") + assert state.state == "90" + + +async def test_number_restore_and_respond(hass: HomeAssistant, knx: KNXTestKit): + """Test KNX number with passive_address and respond_to_read restoring state.""" + test_address = "1/1/1" + test_passive_address = "3/3/3" + fake_state = State("number.test", "160") + + with patch( + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", + return_value=fake_state, + ): + await knx.setup_integration( + { + NumberSchema.PLATFORM_NAME: { + CONF_NAME: "test", + KNX_ADDRESS: [test_address, test_passive_address], + CONF_RESPOND_TO_READ: True, + CONF_TYPE: "illuminance", + } + } + ) + # restored state - doesn't send telegram + state = hass.states.get("number.test") + assert state.state == "160.0" + assert state.attributes.get("unit_of_measurement") == "lx" + await knx.assert_telegram_count(0) + + # respond with restored state + await knx.receive_read(test_address) + await knx.assert_response(test_address, (0x1F, 0xD0)) + + # don't respond to passive address + await knx.receive_read(test_passive_address) + await knx.assert_no_telegram() + + # update from KNX passive address + await knx.receive_write(test_passive_address, (0x4E, 0xDE)) + state = hass.states.get("number.test") + assert state.state == "9000.96" From 00377a926e0d4dae6df000937f4c5f5887a7bd1e Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Tue, 26 Oct 2021 10:53:13 +0200 Subject: [PATCH 0859/1038] Fix velbus climate (#58408) * Initial work on velbus climate fixes home-assistant/core#58382 * Clean up the code, fixed the preset_mode * Fix climate havc mode return value --- homeassistant/components/velbus/climate.py | 56 ++++++++++++------- homeassistant/components/velbus/const.py | 27 +++++++-- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 60 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/velbus/climate.py b/homeassistant/components/velbus/climate.py index 318dff463f0..5c18243e371 100644 --- a/homeassistant/components/velbus/climate.py +++ b/homeassistant/components/velbus/climate.py @@ -1,13 +1,16 @@ """Support for Velbus thermostat.""" +from __future__ import annotations + from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, + SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from . import VelbusEntity -from .const import DOMAIN +from .const import DOMAIN, PRESET_MODES async def async_setup_entry(hass, entry, async_add_entities): @@ -24,47 +27,60 @@ class VelbusClimate(VelbusEntity, ClimateEntity): """Representation of a Velbus thermostat.""" @property - def supported_features(self): + def supported_features(self) -> int: """Return the list off supported features.""" - return SUPPORT_TARGET_TEMPERATURE + return SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit.""" return TEMP_CELSIUS @property - def current_temperature(self): + def current_temperature(self) -> int | None: """Return the current temperature.""" return self._channel.get_state() @property - def hvac_mode(self): - """Return hvac operation ie. heat, cool mode. - - Need to be one of HVAC_MODE_*. - """ + def hvac_mode(self) -> str: + """Return hvac operation ie. heat, cool mode.""" return HVAC_MODE_HEAT @property - def hvac_modes(self): - """Return the list of available hvac operation modes. - - Need to be a subset of HVAC_MODES. - """ + def hvac_modes(self) -> list[str]: + """Return the list of available hvac operation modes.""" return [HVAC_MODE_HEAT] @property - def target_temperature(self): + def target_temperature(self) -> int | None: """Return the temperature we try to reach.""" return self._channel.get_climate_target() - def set_temperature(self, **kwargs): + @property + def preset_modes(self) -> list[str] | None: + """Return a list of all possible presets.""" + return list(PRESET_MODES.keys()) + + @property + def preset_mode(self) -> str | None: + """Return the current Preset for this channel.""" + return next( + ( + key + for key, val in PRESET_MODES.items() + if val == self._channel.get_climate_preset() + ), + None, + ) + + async def async_set_temperature(self, **kwargs) -> None: """Set new target temperatures.""" if (temp := kwargs.get(ATTR_TEMPERATURE)) is None: return - self._channel.set_temp(temp) + await self._channel.set_temp(temp) self.schedule_update_ha_state() - def set_hvac_mode(self, hvac_mode): - """Set new target hvac mode.""" + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the new preset mode.""" + await self._channel.set_preset(PRESET_MODES[preset_mode]) + self.async_write_ha_state() diff --git a/homeassistant/components/velbus/const.py b/homeassistant/components/velbus/const.py index 69c0c926136..d295c725d21 100644 --- a/homeassistant/components/velbus/const.py +++ b/homeassistant/components/velbus/const.py @@ -1,10 +1,25 @@ """Const for Velbus.""" +from typing import Final -DOMAIN = "velbus" +from homeassistant.components.climate.const import ( + PRESET_AWAY, + PRESET_COMFORT, + PRESET_ECO, + PRESET_HOME, +) -CONF_INTERFACE = "interface" -CONF_MEMO_TEXT = "memo_text" +DOMAIN: Final = "velbus" -SERVICE_SCAN = "scan" -SERVICE_SYNC = "sync_clock" -SERVICE_SET_MEMO_TEXT = "set_memo_text" +CONF_INTERFACE: Final = "interface" +CONF_MEMO_TEXT: Final = "memo_text" + +SERVICE_SCAN: Final = "scan" +SERVICE_SYNC: Final = "sync_clock" +SERVICE_SET_MEMO_TEXT: Final = "set_memo_text" + +PRESET_MODES: Final = { + PRESET_ECO: "safe", + PRESET_AWAY: "night", + PRESET_HOME: "day", + PRESET_COMFORT: "comfort", +} diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 8c84c3944f8..4541b428a4a 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -2,7 +2,7 @@ "domain": "velbus", "name": "Velbus", "documentation": "https://www.home-assistant.io/integrations/velbus", - "requirements": ["velbus-aio==2021.10.6"], + "requirements": ["velbus-aio==2021.10.7"], "config_flow": true, "codeowners": ["@Cereal2nd", "@brefra"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 6ee4e154dde..54555279d2f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2360,7 +2360,7 @@ uvcclient==0.11.0 vallox-websocket-api==2.8.1 # homeassistant.components.velbus -velbus-aio==2021.10.6 +velbus-aio==2021.10.7 # homeassistant.components.venstar venstarcolortouch==0.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d153bef0caa..215ed38842d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1364,7 +1364,7 @@ url-normalize==1.4.1 uvcclient==0.11.0 # homeassistant.components.velbus -velbus-aio==2021.10.6 +velbus-aio==2021.10.7 # homeassistant.components.venstar venstarcolortouch==0.14 From 486866b576c4a946a6247c1e8a39d8745cf7869f Mon Sep 17 00:00:00 2001 From: Andre Richter Date: Tue, 26 Oct 2021 10:55:29 +0200 Subject: [PATCH 0860/1038] Use NamedTuple in Vallox service_to_method mapping (#58361) --- homeassistant/components/vallox/__init__.py | 60 ++++++++++++--------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py index cb9a3478839..63b594a5bf2 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations from dataclasses import dataclass, field import ipaddress import logging -from typing import Any +from typing import Any, NamedTuple from vallox_websocket_api import PROFILE as VALLOX_PROFILE, Vallox from vallox_websocket_api.exceptions import ValloxApiException @@ -63,28 +63,36 @@ SERVICE_SCHEMA_SET_PROFILE_FAN_SPEED = vol.Schema( } ) + +class ServiceMethodDetails(NamedTuple): + """Details for SERVICE_TO_METHOD mapping.""" + + method: str + schema: vol.Schema + + SERVICE_SET_PROFILE = "set_profile" SERVICE_SET_PROFILE_FAN_SPEED_HOME = "set_profile_fan_speed_home" SERVICE_SET_PROFILE_FAN_SPEED_AWAY = "set_profile_fan_speed_away" SERVICE_SET_PROFILE_FAN_SPEED_BOOST = "set_profile_fan_speed_boost" SERVICE_TO_METHOD = { - SERVICE_SET_PROFILE: { - "method": "async_set_profile", - "schema": SERVICE_SCHEMA_SET_PROFILE, - }, - SERVICE_SET_PROFILE_FAN_SPEED_HOME: { - "method": "async_set_profile_fan_speed_home", - "schema": SERVICE_SCHEMA_SET_PROFILE_FAN_SPEED, - }, - SERVICE_SET_PROFILE_FAN_SPEED_AWAY: { - "method": "async_set_profile_fan_speed_away", - "schema": SERVICE_SCHEMA_SET_PROFILE_FAN_SPEED, - }, - SERVICE_SET_PROFILE_FAN_SPEED_BOOST: { - "method": "async_set_profile_fan_speed_boost", - "schema": SERVICE_SCHEMA_SET_PROFILE_FAN_SPEED, - }, + SERVICE_SET_PROFILE: ServiceMethodDetails( + method="async_set_profile", + schema=SERVICE_SCHEMA_SET_PROFILE, + ), + SERVICE_SET_PROFILE_FAN_SPEED_HOME: ServiceMethodDetails( + method="async_set_profile_fan_speed_home", + schema=SERVICE_SCHEMA_SET_PROFILE_FAN_SPEED, + ), + SERVICE_SET_PROFILE_FAN_SPEED_AWAY: ServiceMethodDetails( + method="async_set_profile_fan_speed_away", + schema=SERVICE_SCHEMA_SET_PROFILE_FAN_SPEED, + ), + SERVICE_SET_PROFILE_FAN_SPEED_BOOST: ServiceMethodDetails( + method="async_set_profile_fan_speed_boost", + schema=SERVICE_SCHEMA_SET_PROFILE_FAN_SPEED, + ), } @@ -143,10 +151,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) service_handler = ValloxServiceHandler(client, coordinator) - for vallox_service, method in SERVICE_TO_METHOD.items(): - schema = method["schema"] + for vallox_service, service_details in SERVICE_TO_METHOD.items(): hass.services.async_register( - DOMAIN, vallox_service, service_handler.async_handle, schema=schema + DOMAIN, + vallox_service, + service_handler.async_handle, + schema=service_details.schema, ) hass.data[DOMAIN] = {"client": client, "coordinator": coordinator, "name": name} @@ -248,17 +258,17 @@ class ValloxServiceHandler: async def async_handle(self, call: ServiceCall) -> None: """Dispatch a service call.""" - method = SERVICE_TO_METHOD.get(call.service) + service_details = SERVICE_TO_METHOD.get(call.service) params = call.data.copy() - if method is None: + if service_details is None: return - if not hasattr(self, method["method"]): - _LOGGER.error("Service not implemented: %s", method["method"]) + if not hasattr(self, service_details.method): + _LOGGER.error("Service not implemented: %s", service_details.method) return - result = await getattr(self, method["method"])(**params) + result = await getattr(self, service_details.method)(**params) # This state change affects other entities like sensors. Force an immediate update that can # be observed by all parties involved. From fd45a07677495d2c92467011e93b3e0492185ed2 Mon Sep 17 00:00:00 2001 From: Peter Nijssen Date: Tue, 26 Oct 2021 11:05:03 +0200 Subject: [PATCH 0861/1038] Automatic spider supported fan speed and hvac (#58308) * Automatic fill of supported fan speed and hvac Automatic fill of supported fan speed and hvac * Update manifest of spiderpy to 1.5.0 Update manifest of spiderpy to 1.5.0 * Update spiderpy version to 1.5.0 in requirements files * Code formatted using Black * Move support fan and hvac values into a class variable * Move convert to HA value to hvac_modes method * Log a warning for any invalid operation mode * Update homeassistant/components/spider/climate.py Update as suggested by @mivn23 Co-authored-by: mvn23 * PR feedback update + dependency update * Remove logging Co-authored-by: Bennert Co-authored-by: mvn23 --- homeassistant/components/spider/climate.py | 13 +++++++------ homeassistant/components/spider/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/spider/climate.py b/homeassistant/components/spider/climate.py index 52146518ee3..fbae603a239 100644 --- a/homeassistant/components/spider/climate.py +++ b/homeassistant/components/spider/climate.py @@ -11,10 +11,6 @@ from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from .const import DOMAIN -SUPPORT_FAN = ["Auto", "Low", "Medium", "High", "Boost 10", "Boost 20", "Boost 30"] - -SUPPORT_HVAC = [HVAC_MODE_HEAT, HVAC_MODE_COOL] - HA_STATE_TO_SPIDER = { HVAC_MODE_COOL: "Cool", HVAC_MODE_HEAT: "Heat", @@ -43,6 +39,11 @@ class SpiderThermostat(ClimateEntity): """Initialize the thermostat.""" self.api = api self.thermostat = thermostat + self.support_fan = thermostat.fan_speed_values + self.support_hvac = [] + for operation_value in thermostat.operation_values: + if operation_value in SPIDER_STATE_TO_HA: + self.support_hvac.append(SPIDER_STATE_TO_HA[operation_value]) @property def device_info(self): @@ -109,7 +110,7 @@ class SpiderThermostat(ClimateEntity): @property def hvac_modes(self): """Return the list of available operation modes.""" - return SUPPORT_HVAC + return self.support_hvac def set_temperature(self, **kwargs): """Set new target temperature.""" @@ -134,7 +135,7 @@ class SpiderThermostat(ClimateEntity): @property def fan_modes(self): """List of available fan modes.""" - return SUPPORT_FAN + return self.support_fan def update(self): """Get the latest data.""" diff --git a/homeassistant/components/spider/manifest.json b/homeassistant/components/spider/manifest.json index d25d2c97901..b80fa0926cd 100644 --- a/homeassistant/components/spider/manifest.json +++ b/homeassistant/components/spider/manifest.json @@ -2,8 +2,8 @@ "domain": "spider", "name": "Itho Daalderop Spider", "documentation": "https://www.home-assistant.io/integrations/spider", - "requirements": ["spiderpy==1.4.3"], + "requirements": ["spiderpy==1.6.1"], "codeowners": ["@peternijssen"], "config_flow": true, "iot_class": "cloud_polling" -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 54555279d2f..125337ccf19 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2205,7 +2205,7 @@ speak2mary==1.4.0 speedtest-cli==2.1.3 # homeassistant.components.spider -spiderpy==1.4.3 +spiderpy==1.6.1 # homeassistant.components.spotify spotipy==2.18.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 215ed38842d..13ddf117d4f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1281,7 +1281,7 @@ speak2mary==1.4.0 speedtest-cli==2.1.3 # homeassistant.components.spider -spiderpy==1.4.3 +spiderpy==1.6.1 # homeassistant.components.spotify spotipy==2.18.0 From 339d0419252439901337b43adb956cc5d31c0cbf Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Tue, 26 Oct 2021 12:45:42 +0200 Subject: [PATCH 0862/1038] Address late review of velbus (#58463) --- homeassistant/components/velbus/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/velbus/climate.py b/homeassistant/components/velbus/climate.py index 5c18243e371..6bc848a92ab 100644 --- a/homeassistant/components/velbus/climate.py +++ b/homeassistant/components/velbus/climate.py @@ -59,7 +59,7 @@ class VelbusClimate(VelbusEntity, ClimateEntity): @property def preset_modes(self) -> list[str] | None: """Return a list of all possible presets.""" - return list(PRESET_MODES.keys()) + return list(PRESET_MODES) @property def preset_mode(self) -> str | None: @@ -78,7 +78,7 @@ class VelbusClimate(VelbusEntity, ClimateEntity): if (temp := kwargs.get(ATTR_TEMPERATURE)) is None: return await self._channel.set_temp(temp) - self.schedule_update_ha_state() + self.async_write_ha_state() async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the new preset mode.""" From b0e1bab58b6c4d662a4b3ed88554180db645e1fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 26 Oct 2021 14:13:27 +0300 Subject: [PATCH 0863/1038] Use http.HTTPStatus in util.aiohttp (#58456) --- homeassistant/util/aiohttp.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/util/aiohttp.py b/homeassistant/util/aiohttp.py index 7d14ec252d9..b23e5cf29e8 100644 --- a/homeassistant/util/aiohttp.py +++ b/homeassistant/util/aiohttp.py @@ -1,6 +1,7 @@ """Utilities to help with aiohttp.""" from __future__ import annotations +from http import HTTPStatus import io import json from typing import Any @@ -8,8 +9,6 @@ from urllib.parse import parse_qsl from multidict import CIMultiDict, MultiDict -from homeassistant.const import HTTP_OK - class MockStreamReader: """Small mock to imitate stream reader.""" @@ -35,7 +34,7 @@ class MockRequest: content: bytes, mock_source: str, method: str = "GET", - status: int = HTTP_OK, + status: int = HTTPStatus.OK, headers: dict[str, str] | None = None, query_string: str | None = None, url: str = "", From 6b9fb4bda34e28770efa51d7431d17d8cfa964cc Mon Sep 17 00:00:00 2001 From: Brent Petit Date: Tue, 26 Oct 2021 06:37:43 -0500 Subject: [PATCH 0864/1038] Clean up rounding in Ecobee integration (#56319) --- homeassistant/components/ecobee/climate.py | 24 ++++++++++++++-------- tests/components/ecobee/test_climate.py | 4 ++-- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index bc5f74c5022..473ba0cdd58 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -32,6 +32,7 @@ from homeassistant.components.climate.const import ( from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, + PRECISION_HALVES, PRECISION_TENTHS, STATE_ON, TEMP_FAHRENHEIT, @@ -394,24 +395,29 @@ class Thermostat(ClimateEntity): return PRECISION_TENTHS @property - def current_temperature(self): + def current_temperature(self) -> float: """Return the current temperature.""" return self.thermostat["runtime"]["actualTemperature"] / 10.0 @property - def target_temperature_low(self): + def target_temperature_low(self) -> float | None: """Return the lower bound temperature we try to reach.""" if self.hvac_mode == HVAC_MODE_HEAT_COOL: - return round(self.thermostat["runtime"]["desiredHeat"] / 10.0) + return self.thermostat["runtime"]["desiredHeat"] / 10.0 return None @property - def target_temperature_high(self): + def target_temperature_high(self) -> float | None: """Return the upper bound temperature we try to reach.""" if self.hvac_mode == HVAC_MODE_HEAT_COOL: - return round(self.thermostat["runtime"]["desiredCool"] / 10.0) + return self.thermostat["runtime"]["desiredCool"] / 10.0 return None + @property + def target_temperature_step(self) -> float: + """Set target temperature step to halves.""" + return PRECISION_HALVES + @property def has_humidifier_control(self): """Return true if humidifier connected to thermostat and set to manual/on mode.""" @@ -438,14 +444,14 @@ class Thermostat(ClimateEntity): return DEFAULT_MAX_HUMIDITY @property - def target_temperature(self): + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" if self.hvac_mode == HVAC_MODE_HEAT_COOL: return None if self.hvac_mode == HVAC_MODE_HEAT: - return round(self.thermostat["runtime"]["desiredHeat"] / 10.0) + return self.thermostat["runtime"]["desiredHeat"] / 10.0 if self.hvac_mode == HVAC_MODE_COOL: - return round(self.thermostat["runtime"]["desiredCool"] / 10.0) + return self.thermostat["runtime"]["desiredCool"] / 10.0 return None @property @@ -682,7 +688,7 @@ class Thermostat(ClimateEntity): heat_temp = temp cool_temp = temp else: - delta = self.thermostat["settings"]["heatCoolMinDelta"] / 10 + delta = self.thermostat["settings"]["heatCoolMinDelta"] / 10.0 heat_temp = temp - delta cool_temp = temp + delta self.set_auto_temp_hold(heat_temp, cool_temp) diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index 195c086adda..da4cc6cf455 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -87,14 +87,14 @@ async def test_target_temperature_low(ecobee_fixture, thermostat): """Test target low temperature.""" assert thermostat.target_temperature_low == 40 ecobee_fixture["runtime"]["desiredHeat"] = 502 - assert thermostat.target_temperature_low == 50 + assert thermostat.target_temperature_low == 50.2 async def test_target_temperature_high(ecobee_fixture, thermostat): """Test target high temperature.""" assert thermostat.target_temperature_high == 20 ecobee_fixture["runtime"]["desiredCool"] = 679 - assert thermostat.target_temperature_high == 68 + assert thermostat.target_temperature_high == 67.9 async def test_target_temperature(ecobee_fixture, thermostat): From abf6edea6aaf5bdeb3205240bbf8c790e8058810 Mon Sep 17 00:00:00 2001 From: Lukas Date: Tue, 26 Oct 2021 13:39:34 +0200 Subject: [PATCH 0865/1038] Add device_class and state_class as optional attributes to the scrape sensor, to support statistics (#58164) --- homeassistant/components/scrape/sensor.py | 57 +++++++++++++++++------ 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index 2e118bf744f..330df3dd245 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -6,9 +6,16 @@ import httpx import voluptuous as vol from homeassistant.components.rest.data import RestData -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + CONF_STATE_CLASS, + DEVICE_CLASSES_SCHEMA, + PLATFORM_SCHEMA, + STATE_CLASSES_SCHEMA, + SensorEntity, +) from homeassistant.const import ( CONF_AUTHENTICATION, + CONF_DEVICE_CLASS, CONF_HEADERS, CONF_NAME, CONF_PASSWORD, @@ -45,6 +52,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, @@ -64,6 +73,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= attr = config.get(CONF_ATTR) index = config.get(CONF_INDEX) unit = config.get(CONF_UNIT_OF_MEASUREMENT) + device_class = config.get(CONF_DEVICE_CLASS) + state_class = config.get(CONF_STATE_CLASS) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) @@ -84,33 +95,49 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= raise PlatformNotReady async_add_entities( - [ScrapeSensor(rest, name, select, attr, index, value_template, unit)], True + [ + ScrapeSensor( + rest, + name, + select, + attr, + index, + value_template, + unit, + device_class, + state_class, + ) + ], + True, ) class ScrapeSensor(SensorEntity): """Representation of a web scrape sensor.""" - def __init__(self, rest, name, select, attr, index, value_template, unit): + def __init__( + self, + rest, + name, + select, + attr, + index, + value_template, + unit, + device_class, + state_class, + ): """Initialize a web scrape sensor.""" self.rest = rest - self._name = name self._state = None self._select = select self._attr = attr self._index = index self._value_template = value_template - self._unit_of_measurement = unit - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def native_unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit_of_measurement + self._attr_name = name + self._attr_native_unit_of_measurement = unit + self._attr_device_class = device_class + self._attr_state_class = state_class @property def native_value(self): From e9ba5f3b4bd5cfff4ac43c9914c0ba4908c0c435 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 26 Oct 2021 13:41:59 +0200 Subject: [PATCH 0866/1038] Warn when recorder connects to an unsupported database (#58161) --- homeassistant/components/recorder/util.py | 127 ++++++++++- tests/components/recorder/test_util.py | 257 +++++++++++++++++++++- 2 files changed, 371 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 8277f86e9f9..8658c7b3677 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -7,9 +7,15 @@ from datetime import timedelta import functools import logging import os +import re import time from typing import TYPE_CHECKING +from awesomeversion import ( + AwesomeVersion, + AwesomeVersionException, + AwesomeVersionStrategy, +) from sqlalchemy import text from sqlalchemy.exc import OperationalError, SQLAlchemyError from sqlalchemy.orm.session import Session @@ -39,6 +45,14 @@ RETRIES = 3 QUERY_RETRY_WAIT = 0.1 SQLITE3_POSTFIXES = ["", "-wal", "-shm"] +MIN_VERSION_MARIA_DB = AwesomeVersion("10.3.0", AwesomeVersionStrategy.SIMPLEVER) +MIN_VERSION_MARIA_DB_ROWNUM = AwesomeVersion("10.2.0", AwesomeVersionStrategy.SIMPLEVER) +MIN_VERSION_MYSQL = AwesomeVersion("8.0.0", AwesomeVersionStrategy.SIMPLEVER) +MIN_VERSION_MYSQL_ROWNUM = AwesomeVersion("5.8.0", AwesomeVersionStrategy.SIMPLEVER) +MIN_VERSION_PGSQL = AwesomeVersion(120000, AwesomeVersionStrategy.BUILDVER) +MIN_VERSION_SQLITE = AwesomeVersion("3.32.1", AwesomeVersionStrategy.SIMPLEVER) +MIN_VERSION_SQLITE_ROWNUM = AwesomeVersion("3.25.0", 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 # should do a check on the sqlite3 database. @@ -275,6 +289,55 @@ def query_on_connection(dbapi_connection, statement): return result +def _warn_unsupported_dialect(dialect): + """Warn about unsupported database version.""" + _LOGGER.warning( + "Database %s is not supported; Home Assistant supports %s. " + "Starting with Home Assistant 2022.2 this will prevent the recorder from " + "starting. Please migrate your database to a supported software before then", + dialect, + "MariaDB ≥ 10.3, MySQL ≥ 8.0, PostgreSQL ≥ 12, SQLite ≥ 3.32.1", + ) + + +def _warn_unsupported_version(server_version, dialect, minimum_version): + """Warn about unsupported database version.""" + _LOGGER.warning( + "Version %s of %s is not supported; minimum supported version is %s. " + "Starting with Home Assistant 2022.2 this will prevent the recorder from " + "starting. Please upgrade your database software before then", + server_version, + dialect, + minimum_version, + ) + + +def _extract_version_from_server_response(server_response): + """Attempt to extract version from server response.""" + try: + return AwesomeVersion( + server_response, + ensure_strategy=AwesomeVersionStrategy.SIMPLEVER, + find_first_match=True, + ) + except AwesomeVersionException: + return None + + +def _pgsql_numerical_version_to_string(version_num): + """Convert numerical PostgreSQL version to string.""" + if version_num < 100000: + major = version_num // 10000 + minor = version_num % 10000 // 100 + patch = version_num % 100 + return f"{major}.{minor}.{patch}" + + # version 10+ + major = version_num // 10000 + patch = version_num % 10000 + return f"{major}.{patch}" + + def setup_connection_for_dialect( instance, dialect_name, dbapi_connection, first_connection ): @@ -292,12 +355,17 @@ def setup_connection_for_dialect( # instead of every time we open the sqlite connection # as its persistent and isn't free to call every time. result = query_on_connection(dbapi_connection, "SELECT sqlite_version()") - version = result[0][0] - major, minor, _patch = version.split(".", 2) - if int(major) == 3 and int(minor) < 25: + version_string = result[0][0] + version = _extract_version_from_server_response(version_string) + + if version and version < MIN_VERSION_SQLITE_ROWNUM: instance._db_supports_row_number = ( # pylint: disable=[protected-access] False ) + if not version or version < MIN_VERSION_SQLITE: + _warn_unsupported_version( + version or version_string, "SQLite", MIN_VERSION_SQLITE + ) # approximately 8MiB of memory execute_on_connection(dbapi_connection, "PRAGMA cache_size = -8192") @@ -305,18 +373,55 @@ def setup_connection_for_dialect( # enable support for foreign keys execute_on_connection(dbapi_connection, "PRAGMA foreign_keys=ON") - if dialect_name == "mysql": + elif dialect_name == "mysql": execute_on_connection(dbapi_connection, "SET session wait_timeout=28800") if first_connection: result = query_on_connection(dbapi_connection, "SELECT VERSION()") - version = result[0][0] - major, minor, _patch = version.split(".", 2) - if (int(major) == 5 and int(minor) < 8) or ( - int(major) == 10 and int(minor) < 2 - ): - instance._db_supports_row_number = ( # pylint: disable=[protected-access] - False + version_string = result[0][0] + version = _extract_version_from_server_response(version_string) + is_maria_db = re.search("MariaDb", version_string, re.IGNORECASE) + + if is_maria_db: + if version and version < MIN_VERSION_MARIA_DB_ROWNUM: + instance._db_supports_row_number = ( # pylint: disable=[protected-access] + False + ) + if not version or version < MIN_VERSION_MARIA_DB: + _warn_unsupported_version( + version or version_string, "MariaDB", MIN_VERSION_MARIA_DB + ) + else: + if version and version < MIN_VERSION_MYSQL_ROWNUM: + instance._db_supports_row_number = ( # pylint: disable=[protected-access] + False + ) + if not version or version < MIN_VERSION_MYSQL: + _warn_unsupported_version( + version or version_string, "MySQL", MIN_VERSION_MYSQL + ) + + elif dialect_name == "postgresql": + if first_connection: + # server_version_num was added in 2006 + result = query_on_connection(dbapi_connection, "SHOW server_version_num") + version_string = result[0][0] + try: + version = AwesomeVersion( + version_string, AwesomeVersionStrategy.BUILDVER ) + except AwesomeVersionException: + version = None + if not version or version < MIN_VERSION_PGSQL: + if version: + version_string = _pgsql_numerical_version_to_string(int(version)) + _warn_unsupported_version( + version_string, + "PostgreSQL", + _pgsql_numerical_version_to_string(int(MIN_VERSION_PGSQL)), + ) + + else: + _warn_unsupported_dialect(dialect_name) def end_incomplete_runs(session, start_time): diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index ff690e24279..ced614ee2fa 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -125,8 +125,8 @@ async def test_last_run_was_recently_clean(hass): @pytest.mark.parametrize( "mysql_version, db_supports_row_number", [ - ("10.2.0", True), - ("10.1.0", False), + ("10.2.0-MariaDB", True), + ("10.1.0-MariaDB", False), ("5.8.0", True), ("5.7.0", False), ], @@ -207,6 +207,259 @@ def test_setup_connection_for_dialect_sqlite(sqlite_version, db_supports_row_num assert instance_mock._db_supports_row_number == db_supports_row_number +@pytest.mark.parametrize( + "mysql_version,message", + [ + ( + "10.2.0-MariaDB", + "Version 10.2.0 of MariaDB is not supported; minimum supported version is 10.3.0.", + ), + ( + "5.7.26-0ubuntu0.18.04.1", + "Version 5.7.26 of MySQL is not supported; minimum supported version is 8.0.0.", + ), + ( + "some_random_response", + "Version some_random_response of MySQL is not supported; minimum supported version is 8.0.0.", + ), + ], +) +def test_warn_outdated_mysql(caplog, mysql_version, message): + """Test setting up the connection for an outdated mysql version.""" + instance_mock = MagicMock(_db_supports_row_number=True) + execute_args = [] + close_mock = MagicMock() + + def execute_mock(statement): + nonlocal execute_args + execute_args.append(statement) + + def fetchall_mock(): + nonlocal execute_args + if execute_args[-1] == "SELECT VERSION()": + return [[mysql_version]] + return None + + def _make_cursor_mock(*_): + return MagicMock(execute=execute_mock, close=close_mock, fetchall=fetchall_mock) + + dbapi_connection = MagicMock(cursor=_make_cursor_mock) + + util.setup_connection_for_dialect(instance_mock, "mysql", dbapi_connection, True) + + assert message in caplog.text + + +@pytest.mark.parametrize( + "mysql_version", + [ + ("10.3.0"), + ("8.0.0"), + ], +) +def test_supported_mysql(caplog, mysql_version): + """Test setting up the connection for a supported mysql version.""" + instance_mock = MagicMock(_db_supports_row_number=True) + execute_args = [] + close_mock = MagicMock() + + def execute_mock(statement): + nonlocal execute_args + execute_args.append(statement) + + def fetchall_mock(): + nonlocal execute_args + if execute_args[-1] == "SELECT VERSION()": + return [[mysql_version]] + return None + + def _make_cursor_mock(*_): + return MagicMock(execute=execute_mock, close=close_mock, fetchall=fetchall_mock) + + dbapi_connection = MagicMock(cursor=_make_cursor_mock) + + util.setup_connection_for_dialect(instance_mock, "mysql", dbapi_connection, True) + + assert "minimum supported version" not in caplog.text + + +@pytest.mark.parametrize( + "pgsql_version,message", + [ + ( + "110013", + "Version 11.13 of PostgreSQL is not supported; minimum supported version is 12.0.", + ), + ( + "90210", + "Version 9.2.10 of PostgreSQL is not supported; minimum supported version is 12.0.", + ), + ( + "unexpected", + "Version unexpected of PostgreSQL is not supported; minimum supported version is 12.0.", + ), + ], +) +def test_warn_outdated_pgsql(caplog, pgsql_version, message): + """Test setting up the connection for an outdated PostgreSQL version.""" + instance_mock = MagicMock(_db_supports_row_number=True) + execute_args = [] + close_mock = MagicMock() + + def execute_mock(statement): + nonlocal execute_args + execute_args.append(statement) + + def fetchall_mock(): + nonlocal execute_args + if execute_args[-1] == "SHOW server_version_num": + return [[pgsql_version]] + return None + + def _make_cursor_mock(*_): + return MagicMock(execute=execute_mock, close=close_mock, fetchall=fetchall_mock) + + dbapi_connection = MagicMock(cursor=_make_cursor_mock) + + util.setup_connection_for_dialect( + instance_mock, "postgresql", dbapi_connection, True + ) + + assert message in caplog.text + + +@pytest.mark.parametrize( + "pgsql_version", + [ + (130000), + ], +) +def test_supported_pgsql(caplog, pgsql_version): + """Test setting up the connection for a supported PostgreSQL version.""" + instance_mock = MagicMock(_db_supports_row_number=True) + execute_args = [] + close_mock = MagicMock() + + def execute_mock(statement): + nonlocal execute_args + execute_args.append(statement) + + def fetchall_mock(): + nonlocal execute_args + if execute_args[-1] == "SHOW server_version_num": + return [[pgsql_version]] + return None + + def _make_cursor_mock(*_): + return MagicMock(execute=execute_mock, close=close_mock, fetchall=fetchall_mock) + + dbapi_connection = MagicMock(cursor=_make_cursor_mock) + + util.setup_connection_for_dialect( + instance_mock, "postgresql", dbapi_connection, True + ) + + assert "minimum supported version" not in caplog.text + + +@pytest.mark.parametrize( + "sqlite_version,message", + [ + ( + "3.32.0", + "Version 3.32.0 of SQLite is not supported; minimum supported version is 3.32.1.", + ), + ( + "3.31.0", + "Version 3.31.0 of SQLite is not supported; minimum supported version is 3.32.1.", + ), + ( + "2.0.0", + "Version 2.0.0 of SQLite is not supported; minimum supported version is 3.32.1.", + ), + ( + "dogs", + "Version dogs of SQLite is not supported; minimum supported version is 3.32.1.", + ), + ], +) +def test_warn_outdated_sqlite(caplog, sqlite_version, message): + """Test setting up the connection for an outdated sqlite version.""" + instance_mock = MagicMock(_db_supports_row_number=True) + execute_args = [] + close_mock = MagicMock() + + def execute_mock(statement): + nonlocal execute_args + execute_args.append(statement) + + def fetchall_mock(): + nonlocal execute_args + if execute_args[-1] == "SELECT sqlite_version()": + return [[sqlite_version]] + return None + + def _make_cursor_mock(*_): + return MagicMock(execute=execute_mock, close=close_mock, fetchall=fetchall_mock) + + dbapi_connection = MagicMock(cursor=_make_cursor_mock) + + util.setup_connection_for_dialect(instance_mock, "sqlite", dbapi_connection, True) + + assert message in caplog.text + + +@pytest.mark.parametrize( + "sqlite_version", + [ + ("3.32.1"), + ("3.33.0"), + ], +) +def test_supported_sqlite(caplog, sqlite_version): + """Test setting up the connection for a supported sqlite version.""" + instance_mock = MagicMock(_db_supports_row_number=True) + execute_args = [] + close_mock = MagicMock() + + def execute_mock(statement): + nonlocal execute_args + execute_args.append(statement) + + def fetchall_mock(): + nonlocal execute_args + if execute_args[-1] == "SELECT sqlite_version()": + return [[sqlite_version]] + return None + + def _make_cursor_mock(*_): + return MagicMock(execute=execute_mock, close=close_mock, fetchall=fetchall_mock) + + dbapi_connection = MagicMock(cursor=_make_cursor_mock) + + util.setup_connection_for_dialect(instance_mock, "sqlite", dbapi_connection, True) + + assert "minimum supported version" not in caplog.text + + +@pytest.mark.parametrize( + "dialect,message", + [ + ("mssql", "Database mssql is not supported"), + ("oracle", "Database oracle is not supported"), + ("some_db", "Database some_db is not supported"), + ], +) +def test_warn_unsupported_dialect(caplog, dialect, message): + """Test setting up the connection for an outdated sqlite version.""" + instance_mock = MagicMock() + dbapi_connection = MagicMock() + + util.setup_connection_for_dialect(instance_mock, dialect, dbapi_connection, True) + + assert message in caplog.text + + def test_basic_sanity_check(hass_recorder): """Test the basic sanity checks with a missing table.""" hass = hass_recorder() From ac4496b98547edaf1e9eb61a77bb6a9242a166a0 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 26 Oct 2021 13:43:54 +0200 Subject: [PATCH 0867/1038] Use async_on_unload in Netatmo (#58461) --- homeassistant/components/netatmo/__init__.py | 2 -- homeassistant/components/netatmo/camera.py | 2 +- homeassistant/components/netatmo/climate.py | 4 ++-- homeassistant/components/netatmo/data_handler.py | 13 ++++--------- homeassistant/components/netatmo/light.py | 2 +- .../components/netatmo/netatmo_entity_base.py | 6 +----- homeassistant/components/netatmo/select.py | 2 +- homeassistant/components/netatmo/sensor.py | 2 +- 8 files changed, 11 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 76a5eeb9c86..bb522291d19 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -229,8 +229,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.data[DOMAIN][entry.entry_id][AUTH].async_dropwebhook() _LOGGER.info("Unregister Netatmo webhook") - await hass.data[DOMAIN][entry.entry_id][DATA_HANDLER].async_cleanup() - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 7e2ea494604..6cb7457f8f6 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -152,7 +152,7 @@ class NetatmoCamera(NetatmoBase, Camera): await super().async_added_to_hass() for event_type in (EVENT_TYPE_LIGHT_MODE, EVENT_TYPE_OFF, EVENT_TYPE_ON): - self._listeners.append( + self.data_handler.config_entry.async_on_unload( async_dispatcher_connect( self.hass, f"signal-{DOMAIN}-webhook-{event_type}", diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 4a43267852f..145735f4c95 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -244,7 +244,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): EVENT_TYPE_CANCEL_SET_POINT, EVENT_TYPE_SCHEDULE, ): - self._listeners.append( + self.data_handler.config_entry.async_on_unload( async_dispatcher_connect( self.hass, f"signal-{DOMAIN}-webhook-{event_type}", @@ -485,7 +485,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): return self._room_status = self._home_status.rooms.get(self._id) - self._room_data = self._data.rooms.get(self._home_id, {}).get(self._id) + self._room_data = self._data.rooms.get(self._home_id, {}).get(self._id, {}) if not self._room_status or not self._room_data: if self._connected: diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 128a3174b9d..8cd0f2047ed 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -70,11 +70,11 @@ class NetatmoDataClass: class NetatmoDataHandler: """Manages the Netatmo data handling.""" - def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Initialize self.""" self.hass = hass - self._auth = hass.data[DOMAIN][entry.entry_id][AUTH] - self.listeners: list[CALLBACK_TYPE] = [] + self.config_entry = config_entry + self._auth = hass.data[DOMAIN][config_entry.entry_id][AUTH] self.data_classes: dict = {} self.data: dict = {} self._queue: deque = deque() @@ -87,7 +87,7 @@ class NetatmoDataHandler: self.hass, self.async_update, timedelta(seconds=SCAN_INTERVAL) ) - self.listeners.append( + self.config_entry.async_on_unload( async_dispatcher_connect( self.hass, f"signal-{DOMAIN}-webhook-None", @@ -121,11 +121,6 @@ class NetatmoDataHandler: self.data_classes[data_class_entry].next_scan = time() self._queue.rotate(-(self._queue.index(self.data_classes[data_class_entry]))) - async def async_cleanup(self) -> None: - """Clean up the Netatmo data handler.""" - for listener in self.listeners: - listener() - async def handle_event(self, event: dict) -> None: """Handle webhook events.""" if event["data"][WEBHOOK_PUSH_TYPE] == WEBHOOK_ACTIVATION: diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index cb52271dbf5..9a03a3fa848 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -99,7 +99,7 @@ class NetatmoLight(NetatmoBase, LightEntity): """Entity created.""" await super().async_added_to_hass() - self._listeners.append( + self.data_handler.config_entry.async_on_unload( async_dispatcher_connect( self.hass, f"signal-{DOMAIN}-webhook-{EVENT_TYPE_LIGHT_MODE}", diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py index 1704fadedca..5a497275eaf 100644 --- a/homeassistant/components/netatmo/netatmo_entity_base.py +++ b/homeassistant/components/netatmo/netatmo_entity_base.py @@ -2,7 +2,7 @@ from __future__ import annotations from homeassistant.const import ATTR_ATTRIBUTION -from homeassistant.core import CALLBACK_TYPE, callback +from homeassistant.core import callback from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ( @@ -23,7 +23,6 @@ class NetatmoBase(Entity): """Set up Netatmo entity base.""" self.data_handler = data_handler self._data_classes: list[dict] = [] - self._listeners: list[CALLBACK_TYPE] = [] self._device_name: str = "" self._id: str = "" @@ -76,9 +75,6 @@ class NetatmoBase(Entity): """Run when entity will be removed from hass.""" await super().async_will_remove_from_hass() - for listener in self._listeners: - listener() - for data_class in self._data_classes: await self.data_handler.unregister_data_class( data_class[SIGNAL_NAME], self.async_update_callback diff --git a/homeassistant/components/netatmo/select.py b/homeassistant/components/netatmo/select.py index 1f4c60b9dbc..f5ab43bbd12 100644 --- a/homeassistant/components/netatmo/select.py +++ b/homeassistant/components/netatmo/select.py @@ -102,7 +102,7 @@ class NetatmoScheduleSelect(NetatmoBase, SelectEntity): await super().async_added_to_hass() for event_type in (EVENT_TYPE_SCHEDULE,): - self._listeners.append( + self.data_handler.config_entry.async_on_unload( async_dispatcher_connect( self.hass, f"signal-{DOMAIN}-webhook-{event_type}", diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 995cd6fa9e0..5e7d5ae7893 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -705,7 +705,7 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): await super().async_added_to_hass() assert self.device_info and "name" in self.device_info - self.data_handler.listeners.append( + self.data_handler.config_entry.async_on_unload( async_dispatcher_connect( self.hass, f"netatmo-config-{self.device_info['name']}", From ac5e32d6487d603262b7b2b733972da1a2df2e23 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 26 Oct 2021 14:05:45 +0200 Subject: [PATCH 0868/1038] Corrections for external statistics (#58469) --- .../components/recorder/statistics.py | 18 ++--- tests/components/recorder/test_statistics.py | 76 ++++++++++++++++++- 2 files changed, 81 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 40470c2346d..edac688bdd1 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -182,8 +182,8 @@ def async_setup(hass: HomeAssistant) -> None: entity_id = event.data["entity_id"] with session_scope(hass=hass) as session: session.query(StatisticsMeta).filter( - StatisticsMeta.statistic_id == old_entity_id - and StatisticsMeta.source == DOMAIN + (StatisticsMeta.statistic_id == old_entity_id) + & (StatisticsMeta.source == DOMAIN) ).update({StatisticsMeta.statistic_id: entity_id}) @callback @@ -457,12 +457,12 @@ def _update_statistics( try: session.query(table).filter_by(id=stat_id).update( { - table.mean: statistic["mean"], - table.min: statistic["min"], - table.max: statistic["max"], - table.last_reset: statistic["last_reset"], - table.state: statistic["state"], - table.sum: statistic["sum"], + table.mean: statistic.get("mean"), + table.min: statistic.get("min"), + table.max: statistic.get("max"), + table.last_reset: statistic.get("last_reset"), + table.state: statistic.get("state"), + table.sum: statistic.get("sum"), }, synchronize_session=False, ) @@ -992,7 +992,7 @@ def _statistics_exists( """Return id if a statistics entry already exists.""" result = ( session.query(table.id) - .filter(table.metadata_id == metadata_id and table.start == start) + .filter((table.metadata_id == metadata_id) & (table.start == start)) .first() ) return result["id"] if result else None diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 4682b7fe482..5b93d1f567d 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -314,13 +314,20 @@ def test_external_statistics(hass_recorder, caplog): zero = dt_util.utcnow() period1 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) + period2 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=2) - external_statistics = { + external_statistics1 = { "start": period1, "last_reset": None, "state": 0, "sum": 2, } + external_statistics2 = { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 3, + } external_metadata = { "has_mean": False, @@ -331,7 +338,9 @@ def test_external_statistics(hass_recorder, caplog): "unit_of_measurement": "kWh", } - async_add_external_statistics(hass, external_metadata, (external_statistics,)) + async_add_external_statistics( + hass, external_metadata, (external_statistics1, external_statistics2) + ) wait_recording_done(hass) stats = statistics_during_period(hass, zero, period="hour") assert stats == { @@ -346,7 +355,18 @@ def test_external_statistics(hass_recorder, caplog): "last_reset": None, "state": approx(0.0), "sum": approx(2.0), - } + }, + { + "statistic_id": "test:total_energy_import", + "start": period2.isoformat(), + "end": (period2 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(1.0), + "sum": approx(3.0), + }, ] } statistic_ids = list_statistic_ids(hass) @@ -373,6 +393,43 @@ def test_external_statistics(hass_recorder, caplog): ) } + # Update the previously inserted statistics + external_statistics = { + "start": period1, + "last_reset": None, + "state": 5, + "sum": 6, + } + async_add_external_statistics(hass, external_metadata, (external_statistics,)) + wait_recording_done(hass) + stats = statistics_during_period(hass, zero, period="hour") + assert stats == { + "test:total_energy_import": [ + { + "statistic_id": "test:total_energy_import", + "start": period1.isoformat(), + "end": (period1 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(5.0), + "sum": approx(6.0), + }, + { + "statistic_id": "test:total_energy_import", + "start": period2.isoformat(), + "end": (period2 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(1.0), + "sum": approx(3.0), + }, + ] + } + # Update the previously inserted statistics external_statistics = { "start": period1, @@ -398,7 +455,18 @@ def test_external_statistics(hass_recorder, caplog): "last_reset": None, "state": approx(4.0), "sum": approx(5.0), - } + }, + { + "statistic_id": "test:total_energy_import", + "start": period2.isoformat(), + "end": (period2 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(1.0), + "sum": approx(3.0), + }, ] } From 77d02d08bc3870403ea306883fae11ad83fd220f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 26 Oct 2021 05:47:07 -0700 Subject: [PATCH 0869/1038] Validate device automation capablities WS calls (#58444) --- .../components/device_automation/__init__.py | 8 ++++++-- tests/components/device_automation/test_init.py | 12 ++++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index e3aca4884b5..74582f0f77b 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -316,7 +316,9 @@ async def websocket_device_automation_get_action_capabilities(hass, connection, @websocket_api.websocket_command( { vol.Required("type"): "device_automation/condition/capabilities", - vol.Required("condition"): dict, + vol.Required("condition"): cv.DEVICE_CONDITION_BASE_SCHEMA.extend( + {}, extra=vol.ALLOW_EXTRA + ), } ) @websocket_api.async_response @@ -333,7 +335,9 @@ async def websocket_device_automation_get_condition_capabilities(hass, connectio @websocket_api.websocket_command( { vol.Required("type"): "device_automation/trigger/capabilities", - vol.Required("trigger"): dict, + vol.Required("trigger"): DEVICE_TRIGGER_BASE_SCHEMA.extend( + {}, extra=vol.ALLOW_EXTRA + ), } ) @websocket_api.async_response diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 93d64e97959..563611b99ad 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -341,7 +341,7 @@ async def test_websocket_get_bad_condition_capabilities( { "id": 1, "type": "device_automation/condition/capabilities", - "condition": {"domain": "beer"}, + "condition": {"condition": "device", "domain": "beer", "device_id": "1234"}, } ) msg = await client.receive_json() @@ -364,7 +364,11 @@ async def test_websocket_get_no_condition_capabilities( { "id": 1, "type": "device_automation/condition/capabilities", - "condition": {"domain": "deconz"}, + "condition": { + "condition": "device", + "domain": "deconz", + "device_id": "abcd", + }, } ) msg = await client.receive_json() @@ -531,7 +535,7 @@ async def test_websocket_get_bad_trigger_capabilities( { "id": 1, "type": "device_automation/trigger/capabilities", - "trigger": {"domain": "beer"}, + "trigger": {"platform": "device", "domain": "beer", "device_id": "abcd"}, } ) msg = await client.receive_json() @@ -554,7 +558,7 @@ async def test_websocket_get_no_trigger_capabilities( { "id": 1, "type": "device_automation/trigger/capabilities", - "trigger": {"domain": "deconz"}, + "trigger": {"platform": "device", "domain": "deconz", "device_id": "abcd"}, } ) msg = await client.receive_json() From d49c5d511b2fc03d66c88d8907dc88e6a9b20514 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 26 Oct 2021 05:55:03 -0700 Subject: [PATCH 0870/1038] Add entity_category to SmartThings sensors (#58375) --- .../components/smartthings/binary_sensor.py | 9 + .../components/smartthings/sensor.py | 232 +++++++++++++++--- .../smartthings/test_binary_sensor.py | 26 +- tests/components/smartthings/test_sensor.py | 4 + 4 files changed, 234 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/smartthings/binary_sensor.py b/homeassistant/components/smartthings/binary_sensor.py index 74eb253ebbb..79b4176d9ea 100644 --- a/homeassistant/components/smartthings/binary_sensor.py +++ b/homeassistant/components/smartthings/binary_sensor.py @@ -15,6 +15,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_SOUND, BinarySensorEntity, ) +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC from . import SmartThingsEntity from .const import DATA_BROKERS, DOMAIN @@ -41,6 +42,9 @@ ATTRIB_TO_CLASS = { Attribute.valve: DEVICE_CLASS_OPENING, Attribute.water: DEVICE_CLASS_MOISTURE, } +ATTRIB_TO_ENTTIY_CATEGORY = { + Attribute.tamper: ENTITY_CATEGORY_DIAGNOSTIC, +} async def async_setup_entry(hass, config_entry, async_add_entities): @@ -88,3 +92,8 @@ class SmartThingsBinarySensor(SmartThingsEntity, BinarySensorEntity): def device_class(self): """Return the class of this device.""" return ATTRIB_TO_CLASS[self._attribute] + + @property + def entity_category(self): + """Return the entity category of this device.""" + return ATTRIB_TO_ENTTIY_CATEGORY.get(self._attribute) diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index f5ab5562229..eab840bd629 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -28,6 +28,8 @@ from homeassistant.const import ( DEVICE_CLASS_VOLTAGE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, + ENTITY_CATEGORY_CONFIG, + ENTITY_CATEGORY_DIAGNOSTIC, LIGHT_LUX, MASS_KILOGRAMS, PERCENTAGE, @@ -40,22 +42,54 @@ from homeassistant.const import ( from . import SmartThingsEntity from .const import DATA_BROKERS, DOMAIN -Map = namedtuple("map", "attribute name default_unit device_class state_class") +Map = namedtuple( + "map", "attribute name default_unit device_class state_class entity_category" +) CAPABILITY_TO_SENSORS = { Capability.activity_lighting_mode: [ - Map(Attribute.lighting_mode, "Activity Lighting Mode", None, None, None) + Map( + Attribute.lighting_mode, + "Activity Lighting Mode", + None, + None, + None, + ENTITY_CATEGORY_CONFIG, + ) ], Capability.air_conditioner_mode: [ - Map(Attribute.air_conditioner_mode, "Air Conditioner Mode", None, None, None) + Map( + Attribute.air_conditioner_mode, + "Air Conditioner Mode", + None, + None, + None, + ENTITY_CATEGORY_CONFIG, + ) ], Capability.air_quality_sensor: [ - Map(Attribute.air_quality, "Air Quality", "CAQI", None, STATE_CLASS_MEASUREMENT) + Map( + Attribute.air_quality, + "Air Quality", + "CAQI", + None, + STATE_CLASS_MEASUREMENT, + None, + ) + ], + Capability.alarm: [Map(Attribute.alarm, "Alarm", None, None, None, None)], + Capability.audio_volume: [ + Map(Attribute.volume, "Volume", PERCENTAGE, None, None, None) ], - Capability.alarm: [Map(Attribute.alarm, "Alarm", None, None, None)], - Capability.audio_volume: [Map(Attribute.volume, "Volume", PERCENTAGE, None, None)], Capability.battery: [ - Map(Attribute.battery, "Battery", PERCENTAGE, DEVICE_CLASS_BATTERY, None) + Map( + Attribute.battery, + "Battery", + PERCENTAGE, + DEVICE_CLASS_BATTERY, + None, + ENTITY_CATEGORY_DIAGNOSTIC, + ) ], Capability.body_mass_index_measurement: [ Map( @@ -64,6 +98,7 @@ CAPABILITY_TO_SENSORS = { f"{MASS_KILOGRAMS}/{AREA_SQUARE_METERS}", None, STATE_CLASS_MEASUREMENT, + None, ) ], Capability.body_weight_measurement: [ @@ -73,6 +108,7 @@ CAPABILITY_TO_SENSORS = { MASS_KILOGRAMS, None, STATE_CLASS_MEASUREMENT, + None, ) ], Capability.carbon_dioxide_measurement: [ @@ -82,10 +118,18 @@ CAPABILITY_TO_SENSORS = { CONCENTRATION_PARTS_PER_MILLION, DEVICE_CLASS_CO2, STATE_CLASS_MEASUREMENT, + None, ) ], Capability.carbon_monoxide_detector: [ - Map(Attribute.carbon_monoxide, "Carbon Monoxide Detector", None, None, None) + Map( + Attribute.carbon_monoxide, + "Carbon Monoxide Detector", + None, + None, + None, + None, + ) ], Capability.carbon_monoxide_measurement: [ Map( @@ -94,29 +138,50 @@ CAPABILITY_TO_SENSORS = { CONCENTRATION_PARTS_PER_MILLION, DEVICE_CLASS_CO, STATE_CLASS_MEASUREMENT, + None, ) ], Capability.dishwasher_operating_state: [ - Map(Attribute.machine_state, "Dishwasher Machine State", None, None, None), - Map(Attribute.dishwasher_job_state, "Dishwasher Job State", None, None, None), + Map( + Attribute.machine_state, "Dishwasher Machine State", None, None, None, None + ), + Map( + Attribute.dishwasher_job_state, + "Dishwasher Job State", + None, + None, + None, + None, + ), Map( Attribute.completion_time, "Dishwasher Completion Time", None, DEVICE_CLASS_TIMESTAMP, None, + None, ), ], - Capability.dryer_mode: [Map(Attribute.dryer_mode, "Dryer Mode", None, None, None)], + Capability.dryer_mode: [ + Map( + Attribute.dryer_mode, + "Dryer Mode", + None, + None, + None, + ENTITY_CATEGORY_CONFIG, + ) + ], Capability.dryer_operating_state: [ - Map(Attribute.machine_state, "Dryer Machine State", None, None, None), - Map(Attribute.dryer_job_state, "Dryer Job State", None, None, None), + Map(Attribute.machine_state, "Dryer Machine State", None, None, None, None), + Map(Attribute.dryer_job_state, "Dryer Job State", None, None, None, None), Map( Attribute.completion_time, "Dryer Completion Time", None, DEVICE_CLASS_TIMESTAMP, None, + None, ), ], Capability.dust_sensor: [ @@ -126,8 +191,16 @@ CAPABILITY_TO_SENSORS = { None, None, STATE_CLASS_MEASUREMENT, + None, + ), + Map( + Attribute.dust_level, + "Dust Level", + None, + None, + STATE_CLASS_MEASUREMENT, + None, ), - Map(Attribute.dust_level, "Dust Level", None, None, STATE_CLASS_MEASUREMENT), ], Capability.energy_meter: [ Map( @@ -136,6 +209,7 @@ CAPABILITY_TO_SENSORS = { ENERGY_KILO_WATT_HOUR, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING, + None, ) ], Capability.equivalent_carbon_dioxide_measurement: [ @@ -145,6 +219,7 @@ CAPABILITY_TO_SENSORS = { CONCENTRATION_PARTS_PER_MILLION, None, STATE_CLASS_MEASUREMENT, + None, ) ], Capability.formaldehyde_measurement: [ @@ -154,6 +229,7 @@ CAPABILITY_TO_SENSORS = { CONCENTRATION_PARTS_PER_MILLION, None, STATE_CLASS_MEASUREMENT, + None, ) ], Capability.gas_meter: [ @@ -163,14 +239,18 @@ CAPABILITY_TO_SENSORS = { ENERGY_KILO_WATT_HOUR, None, STATE_CLASS_MEASUREMENT, + None, + ), + Map( + Attribute.gas_meter_calorific, "Gas Meter Calorific", None, None, None, None ), - Map(Attribute.gas_meter_calorific, "Gas Meter Calorific", None, None, None), Map( Attribute.gas_meter_time, "Gas Meter Time", None, DEVICE_CLASS_TIMESTAMP, None, + None, ), Map( Attribute.gas_meter_volume, @@ -178,6 +258,7 @@ CAPABILITY_TO_SENSORS = { VOLUME_CUBIC_METERS, None, STATE_CLASS_MEASUREMENT, + None, ), ], Capability.illuminance_measurement: [ @@ -187,6 +268,7 @@ CAPABILITY_TO_SENSORS = { LIGHT_LUX, DEVICE_CLASS_ILLUMINANCE, STATE_CLASS_MEASUREMENT, + None, ) ], Capability.infrared_level: [ @@ -196,31 +278,50 @@ CAPABILITY_TO_SENSORS = { PERCENTAGE, None, STATE_CLASS_MEASUREMENT, + None, ) ], Capability.media_input_source: [ - Map(Attribute.input_source, "Media Input Source", None, None, None) + Map(Attribute.input_source, "Media Input Source", None, None, None, None) ], Capability.media_playback_repeat: [ - Map(Attribute.playback_repeat_mode, "Media Playback Repeat", None, None, None) + Map( + Attribute.playback_repeat_mode, + "Media Playback Repeat", + None, + None, + None, + None, + ) ], Capability.media_playback_shuffle: [ - Map(Attribute.playback_shuffle, "Media Playback Shuffle", None, None, None) + Map( + Attribute.playback_shuffle, "Media Playback Shuffle", None, None, None, None + ) ], Capability.media_playback: [ - Map(Attribute.playback_status, "Media Playback Status", None, None, None) + Map(Attribute.playback_status, "Media Playback Status", None, None, None, None) ], Capability.odor_sensor: [ - Map(Attribute.odor_level, "Odor Sensor", None, None, None) + Map(Attribute.odor_level, "Odor Sensor", None, None, None, None) + ], + Capability.oven_mode: [ + Map( + Attribute.oven_mode, + "Oven Mode", + None, + None, + None, + ENTITY_CATEGORY_CONFIG, + ) ], - Capability.oven_mode: [Map(Attribute.oven_mode, "Oven Mode", None, None, None)], Capability.oven_operating_state: [ - Map(Attribute.machine_state, "Oven Machine State", None, None, None), - Map(Attribute.oven_job_state, "Oven Job State", None, None, None), - Map(Attribute.completion_time, "Oven Completion Time", None, None, None), + Map(Attribute.machine_state, "Oven Machine State", None, None, None, None), + Map(Attribute.oven_job_state, "Oven Job State", None, None, None, None), + Map(Attribute.completion_time, "Oven Completion Time", None, None, None, None), ], Capability.oven_setpoint: [ - Map(Attribute.oven_setpoint, "Oven Set Point", None, None, None) + Map(Attribute.oven_setpoint, "Oven Set Point", None, None, None, None) ], Capability.power_consumption_report: [], Capability.power_meter: [ @@ -230,10 +331,18 @@ CAPABILITY_TO_SENSORS = { POWER_WATT, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT, + None, ) ], Capability.power_source: [ - Map(Attribute.power_source, "Power Source", None, None, None) + Map( + Attribute.power_source, + "Power Source", + None, + None, + None, + ENTITY_CATEGORY_DIAGNOSTIC, + ) ], Capability.refrigeration_setpoint: [ Map( @@ -242,6 +351,7 @@ CAPABILITY_TO_SENSORS = { None, DEVICE_CLASS_TEMPERATURE, None, + None, ) ], Capability.relative_humidity_measurement: [ @@ -251,6 +361,7 @@ CAPABILITY_TO_SENSORS = { PERCENTAGE, DEVICE_CLASS_HUMIDITY, STATE_CLASS_MEASUREMENT, + None, ) ], Capability.robot_cleaner_cleaning_mode: [ @@ -260,11 +371,17 @@ CAPABILITY_TO_SENSORS = { None, None, None, + ENTITY_CATEGORY_CONFIG, ) ], Capability.robot_cleaner_movement: [ Map( - Attribute.robot_cleaner_movement, "Robot Cleaner Movement", None, None, None + Attribute.robot_cleaner_movement, + "Robot Cleaner Movement", + None, + None, + None, + None, ) ], Capability.robot_cleaner_turbo_mode: [ @@ -274,20 +391,29 @@ CAPABILITY_TO_SENSORS = { None, None, None, + ENTITY_CATEGORY_CONFIG, ) ], Capability.signal_strength: [ - Map(Attribute.lqi, "LQI Signal Strength", None, None, STATE_CLASS_MEASUREMENT), + Map( + Attribute.lqi, + "LQI Signal Strength", + None, + None, + STATE_CLASS_MEASUREMENT, + ENTITY_CATEGORY_DIAGNOSTIC, + ), Map( Attribute.rssi, "RSSI Signal Strength", None, DEVICE_CLASS_SIGNAL_STRENGTH, STATE_CLASS_MEASUREMENT, + ENTITY_CATEGORY_DIAGNOSTIC, ), ], Capability.smoke_detector: [ - Map(Attribute.smoke, "Smoke Detector", None, None, None) + Map(Attribute.smoke, "Smoke Detector", None, None, None, None) ], Capability.temperature_measurement: [ Map( @@ -296,6 +422,7 @@ CAPABILITY_TO_SENSORS = { None, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, + None, ) ], Capability.thermostat_cooling_setpoint: [ @@ -305,10 +432,18 @@ CAPABILITY_TO_SENSORS = { None, DEVICE_CLASS_TEMPERATURE, None, + None, ) ], Capability.thermostat_fan_mode: [ - Map(Attribute.thermostat_fan_mode, "Thermostat Fan Mode", None, None, None) + Map( + Attribute.thermostat_fan_mode, + "Thermostat Fan Mode", + None, + None, + None, + ENTITY_CATEGORY_CONFIG, + ) ], Capability.thermostat_heating_setpoint: [ Map( @@ -317,10 +452,18 @@ CAPABILITY_TO_SENSORS = { None, DEVICE_CLASS_TEMPERATURE, None, + ENTITY_CATEGORY_CONFIG, ) ], Capability.thermostat_mode: [ - Map(Attribute.thermostat_mode, "Thermostat Mode", None, None, None) + Map( + Attribute.thermostat_mode, + "Thermostat Mode", + None, + None, + None, + ENTITY_CATEGORY_CONFIG, + ) ], Capability.thermostat_operating_state: [ Map( @@ -329,6 +472,7 @@ CAPABILITY_TO_SENSORS = { None, None, None, + None, ) ], Capability.thermostat_setpoint: [ @@ -338,12 +482,13 @@ CAPABILITY_TO_SENSORS = { None, DEVICE_CLASS_TEMPERATURE, None, + ENTITY_CATEGORY_CONFIG, ) ], Capability.three_axis: [], Capability.tv_channel: [ - Map(Attribute.tv_channel, "Tv Channel", None, None, None), - Map(Attribute.tv_channel_name, "Tv Channel Name", None, None, None), + Map(Attribute.tv_channel, "Tv Channel", None, None, None, None), + Map(Attribute.tv_channel_name, "Tv Channel Name", None, None, None, None), ], Capability.tvoc_measurement: [ Map( @@ -352,6 +497,7 @@ CAPABILITY_TO_SENSORS = { CONCENTRATION_PARTS_PER_MILLION, None, STATE_CLASS_MEASUREMENT, + None, ) ], Capability.ultraviolet_index: [ @@ -361,6 +507,7 @@ CAPABILITY_TO_SENSORS = { None, None, STATE_CLASS_MEASUREMENT, + None, ) ], Capability.voltage_measurement: [ @@ -370,20 +517,29 @@ CAPABILITY_TO_SENSORS = { ELECTRIC_POTENTIAL_VOLT, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT, + None, ) ], Capability.washer_mode: [ - Map(Attribute.washer_mode, "Washer Mode", None, None, None) + Map( + Attribute.washer_mode, + "Washer Mode", + None, + None, + None, + ENTITY_CATEGORY_CONFIG, + ) ], Capability.washer_operating_state: [ - Map(Attribute.machine_state, "Washer Machine State", None, None, None), - Map(Attribute.washer_job_state, "Washer Job State", None, None, None), + Map(Attribute.machine_state, "Washer Machine State", None, None, None, None), + Map(Attribute.washer_job_state, "Washer Job State", None, None, None, None), Map( Attribute.completion_time, "Washer Completion Time", None, DEVICE_CLASS_TIMESTAMP, None, + None, ), ], } @@ -431,6 +587,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): m.default_unit, m.device_class, m.state_class, + m.entity_category, ) for m in maps ] @@ -448,6 +605,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): m.default_unit, m.device_class, m.state_class, + m.entity_category, ) for m in maps ] @@ -474,6 +632,7 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity): default_unit: str, device_class: str, state_class: str | None, + entity_category: str | None, ) -> None: """Init the class.""" super().__init__(device) @@ -482,6 +641,7 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity): self._device_class = device_class self._default_unit = default_unit self._attr_state_class = state_class + self._attr_entity_category = entity_category @property def name(self) -> str: diff --git a/tests/components/smartthings/test_binary_sensor.py b/tests/components/smartthings/test_binary_sensor.py index f3d548c1e39..efc34424ae0 100644 --- a/tests/components/smartthings/test_binary_sensor.py +++ b/tests/components/smartthings/test_binary_sensor.py @@ -13,7 +13,11 @@ from homeassistant.components.binary_sensor import ( from homeassistant.components.smartthings import binary_sensor from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_UNAVAILABLE +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_UNAVAILABLE, +) from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -100,3 +104,23 @@ async def test_unload_config_entry(hass, device_factory): hass.states.get("binary_sensor.motion_sensor_1_motion").state == STATE_UNAVAILABLE ) + + +async def test_entity_category(hass, device_factory): + """Tests the state attributes properly match the light types.""" + device1 = device_factory( + "Motion Sensor 1", [Capability.motion_sensor], {Attribute.motion: "inactive"} + ) + device2 = device_factory( + "Tamper Sensor 2", [Capability.tamper_alert], {Attribute.tamper: "inactive"} + ) + await setup_platform(hass, BINARY_SENSOR_DOMAIN, devices=[device1, device2]) + + entity_registry = er.async_get(hass) + entry = entity_registry.async_get("binary_sensor.motion_sensor_1_motion") + assert entry + assert entry.entity_category is None + + entry = entity_registry.async_get("binary_sensor.tamper_sensor_2_tamper") + assert entry + assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index f36f05616d6..049666baf99 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -17,6 +17,7 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, + ENTITY_CATEGORY_DIAGNOSTIC, PERCENTAGE, STATE_UNAVAILABLE, STATE_UNKNOWN, @@ -94,6 +95,7 @@ async def test_entity_and_device_attributes(hass, device_factory): entry = entity_registry.async_get("sensor.sensor_1_battery") assert entry assert entry.unique_id == f"{device.device_id}.{Attribute.battery}" + assert entry.entity_category == ENTITY_CATEGORY_DIAGNOSTIC entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) assert entry assert entry.name == device.label @@ -120,6 +122,7 @@ async def test_energy_sensors_for_switch_device(hass, device_factory): entry = entity_registry.async_get("sensor.switch_1_energy_meter") assert entry assert entry.unique_id == f"{device.device_id}.{Attribute.energy}" + assert entry.entity_category is None entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) assert entry assert entry.name == device.label @@ -132,6 +135,7 @@ async def test_energy_sensors_for_switch_device(hass, device_factory): entry = entity_registry.async_get("sensor.switch_1_power_meter") assert entry assert entry.unique_id == f"{device.device_id}.{Attribute.power}" + assert entry.entity_category is None entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) assert entry assert entry.name == device.label From c9966a3b043402c4aea68bd29b0ead73173dd50e Mon Sep 17 00:00:00 2001 From: Robert Meijers Date: Tue, 26 Oct 2021 15:52:43 +0200 Subject: [PATCH 0871/1038] Add offset support to time trigger (#56838) --- .../components/homeassistant/triggers/time.py | 51 +++-- .../homeassistant/triggers/test_time.py | 184 ++++++++++++++++++ 2 files changed, 224 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/homeassistant/triggers/time.py b/homeassistant/components/homeassistant/triggers/time.py index 90780489d7b..be7ded37dc2 100644 --- a/homeassistant/components/homeassistant/triggers/time.py +++ b/homeassistant/components/homeassistant/triggers/time.py @@ -1,5 +1,5 @@ """Offer time listening automation rules.""" -from datetime import datetime +from datetime import datetime, timedelta from functools import partial import voluptuous as vol @@ -8,6 +8,8 @@ from homeassistant.components import sensor from homeassistant.const import ( ATTR_DEVICE_CLASS, CONF_AT, + CONF_ENTITY_ID, + CONF_OFFSET, CONF_PLATFORM, STATE_UNAVAILABLE, STATE_UNKNOWN, @@ -23,9 +25,21 @@ import homeassistant.util.dt as dt_util # mypy: allow-untyped-defs, no-check-untyped-defs +_TIME_TRIGGER_ENTITY_REFERENCE = vol.All( + str, cv.entity_domain(["input_datetime", "sensor"]) +) + +_TIME_TRIGGER_WITH_OFFSET_SCHEMA = vol.Schema( + { + vol.Required(CONF_ENTITY_ID): _TIME_TRIGGER_ENTITY_REFERENCE, + vol.Required(CONF_OFFSET): cv.time_period, + } +) + _TIME_TRIGGER_SCHEMA = vol.Any( cv.time, - vol.All(str, cv.entity_domain(["input_datetime", "sensor"])), + _TIME_TRIGGER_ENTITY_REFERENCE, + _TIME_TRIGGER_WITH_OFFSET_SCHEMA, msg="Expected HH:MM, HH:MM:SS or Entity ID with domain 'input_datetime' or 'sensor'", ) @@ -43,6 +57,7 @@ async def async_attach_trigger(hass, config, action, automation_info): entities = {} removes = [] job = HassJob(action) + offsets = {} @callback def time_automation_listener(description, now, *, entity_id=None): @@ -77,6 +92,8 @@ async def async_attach_trigger(hass, config, action, automation_info): if not new_state: return + offset = offsets[entity_id] if entity_id in offsets else timedelta(0) + # Check state of entity. If valid, set up a listener. if new_state.domain == "input_datetime": if has_date := new_state.attributes["has_date"]: @@ -93,14 +110,17 @@ async def async_attach_trigger(hass, config, action, automation_info): if has_date: # If input_datetime has date, then track point in time. - trigger_dt = datetime( - year, - month, - day, - hour, - minute, - second, - tzinfo=dt_util.DEFAULT_TIME_ZONE, + trigger_dt = ( + datetime( + year, + month, + day, + hour, + minute, + second, + tzinfo=dt_util.DEFAULT_TIME_ZONE, + ) + + offset ) # Only set up listener if time is now or in the future. if trigger_dt >= dt_util.now(): @@ -132,7 +152,7 @@ async def async_attach_trigger(hass, config, action, automation_info): == sensor.DEVICE_CLASS_TIMESTAMP and new_state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN) ): - trigger_dt = dt_util.parse_datetime(new_state.state) + trigger_dt = dt_util.parse_datetime(new_state.state) + offset if trigger_dt is not None and trigger_dt > dt_util.utcnow(): remove = async_track_point_in_time( @@ -156,6 +176,15 @@ async def async_attach_trigger(hass, config, action, automation_info): # entity to_track.append(at_time) update_entity_trigger(at_time, new_state=hass.states.get(at_time)) + elif isinstance(at_time, dict) and CONF_OFFSET in at_time: + # entity with offset + entity_id = at_time.get(CONF_ENTITY_ID) + to_track.append(entity_id) + offsets[entity_id] = at_time.get(CONF_OFFSET) + update_entity_trigger( + entity_id, + new_state=hass.states.get(entity_id), + ) else: # datetime.time removes.append( diff --git a/tests/components/homeassistant/triggers/test_time.py b/tests/components/homeassistant/triggers/test_time.py index 499fcf8611e..7961ce25026 100644 --- a/tests/components/homeassistant/triggers/test_time.py +++ b/tests/components/homeassistant/triggers/test_time.py @@ -150,6 +150,96 @@ async def test_if_fires_using_at_input_datetime(hass, calls, has_date, has_time) ) +@pytest.mark.parametrize( + "offset,delta", + [ + ("00:00:10", timedelta(seconds=10)), + ("-00:00:10", timedelta(seconds=-10)), + ({"minutes": 5}, timedelta(minutes=5)), + ], +) +async def test_if_fires_using_at_input_datetime_with_offset(hass, calls, offset, delta): + """Test for firing at input_datetime.""" + await async_setup_component( + hass, + "input_datetime", + {"input_datetime": {"trigger": {"has_date": True, "has_time": True}}}, + ) + now = dt_util.now() + + set_dt = now.replace(hour=5, minute=0, second=0, microsecond=0) + timedelta(2) + trigger_dt = set_dt + delta + + await hass.services.async_call( + "input_datetime", + "set_datetime", + { + ATTR_ENTITY_ID: "input_datetime.trigger", + "datetime": str(set_dt.replace(tzinfo=None)), + }, + blocking=True, + ) + + time_that_will_not_match_right_away = trigger_dt - timedelta(minutes=1) + + some_data = "{{ trigger.platform }}-{{ trigger.now.day }}-{{ trigger.now.hour }}-{{ trigger.now.minute }}-{{ trigger.now.second }}-{{trigger.entity_id}}" + with patch( + "homeassistant.util.dt.utcnow", + return_value=dt_util.as_utc(time_that_will_not_match_right_away), + ): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "time", + "at": { + "entity_id": "input_datetime.trigger", + "offset": offset, + }, + }, + "action": { + "service": "test.automation", + "data_template": {"some": some_data}, + }, + } + }, + ) + await hass.async_block_till_done() + + async_fire_time_changed(hass, trigger_dt + timedelta(seconds=1)) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert ( + calls[0].data["some"] + == f"time-{trigger_dt.day}-{trigger_dt.hour}-{trigger_dt.minute}-{trigger_dt.second}-input_datetime.trigger" + ) + + set_dt += timedelta(days=1, hours=1) + trigger_dt += timedelta(days=1, hours=1) + + await hass.services.async_call( + "input_datetime", + "set_datetime", + { + ATTR_ENTITY_ID: "input_datetime.trigger", + "datetime": str(set_dt.replace(tzinfo=None)), + }, + blocking=True, + ) + + async_fire_time_changed(hass, trigger_dt + timedelta(seconds=1)) + await hass.async_block_till_done() + + assert len(calls) == 2 + assert ( + calls[1].data["some"] + == f"time-{trigger_dt.day}-{trigger_dt.hour}-{trigger_dt.minute}-{trigger_dt.second}-input_datetime.trigger" + ) + + async def test_if_fires_using_multiple_at(hass, calls): """Test for firing at.""" @@ -498,12 +588,103 @@ async def test_if_fires_using_at_sensor(hass, calls): assert len(calls) == 2 +@pytest.mark.parametrize( + "offset,delta", + [ + ("00:00:10", timedelta(seconds=10)), + ("-00:00:10", timedelta(seconds=-10)), + ({"minutes": 5}, timedelta(minutes=5)), + ], +) +async def test_if_fires_using_at_sensor_with_offset(hass, calls, offset, delta): + """Test for firing at sensor time.""" + now = dt_util.now() + + start_dt = now.replace(hour=5, minute=0, second=0, microsecond=0) + timedelta(2) + trigger_dt = start_dt + delta + + hass.states.async_set( + "sensor.next_alarm", + start_dt.isoformat(), + {ATTR_DEVICE_CLASS: sensor.DEVICE_CLASS_TIMESTAMP}, + ) + + time_that_will_not_match_right_away = trigger_dt - timedelta(minutes=1) + + some_data = "{{ trigger.platform }}-{{ trigger.now.day }}-{{ trigger.now.hour }}-{{ trigger.now.minute }}-{{ trigger.now.second }}-{{trigger.entity_id}}" + with patch( + "homeassistant.util.dt.utcnow", + return_value=dt_util.as_utc(time_that_will_not_match_right_away), + ): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "time", + "at": { + "entity_id": "sensor.next_alarm", + "offset": offset, + }, + }, + "action": { + "service": "test.automation", + "data_template": {"some": some_data}, + }, + } + }, + ) + await hass.async_block_till_done() + + async_fire_time_changed(hass, trigger_dt + timedelta(seconds=1)) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert ( + calls[0].data["some"] + == f"time-{trigger_dt.day}-{trigger_dt.hour}-{trigger_dt.minute}-{trigger_dt.second}-sensor.next_alarm" + ) + + start_dt += timedelta(days=1, hours=1) + trigger_dt += timedelta(days=1, hours=1) + + hass.states.async_set( + "sensor.next_alarm", + start_dt.isoformat(), + {ATTR_DEVICE_CLASS: sensor.DEVICE_CLASS_TIMESTAMP}, + ) + await hass.async_block_till_done() + + async_fire_time_changed(hass, trigger_dt + timedelta(seconds=1)) + await hass.async_block_till_done() + + assert len(calls) == 2 + assert ( + calls[1].data["some"] + == f"time-{trigger_dt.day}-{trigger_dt.hour}-{trigger_dt.minute}-{trigger_dt.second}-sensor.next_alarm" + ) + + @pytest.mark.parametrize( "conf", [ {"platform": "time", "at": "input_datetime.bla"}, {"platform": "time", "at": "sensor.bla"}, {"platform": "time", "at": "12:34"}, + { + "platform": "time", + "at": {"entity_id": "input_datetime.bla", "offset": "00:01"}, + }, + {"platform": "time", "at": {"entity_id": "sensor.bla", "offset": "-00:01"}}, + { + "platform": "time", + "at": [{"entity_id": "input_datetime.bla", "offset": "01:00:00"}], + }, + { + "platform": "time", + "at": [{"entity_id": "sensor.bla", "offset": "-01:00:00"}], + }, ], ) def test_schema_valid(conf): @@ -517,6 +698,9 @@ def test_schema_valid(conf): {"platform": "time", "at": "binary_sensor.bla"}, {"platform": "time", "at": 745}, {"platform": "time", "at": "25:00"}, + {"platform": "time", "at": {"entity_id": "input_datetime.bla", "offset": "0:"}}, + {"platform": "time", "at": {"entity_id": "input_datetime.bla", "offset": "a"}}, + {"platform": "time", "at": {"entity_id": "13:00:00", "offset": "0:10"}}, ], ) def test_schema_invalid(conf): From 3970a50553eb32803fc0b56f4efe08321d3dadde Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 26 Oct 2021 16:09:10 +0200 Subject: [PATCH 0872/1038] Add reauth for Netatmo when token or token scope is invalid (#57487) --- homeassistant/components/netatmo/__init__.py | 30 +++- .../components/netatmo/config_flow.py | 50 ++++-- homeassistant/components/netatmo/const.py | 14 ++ homeassistant/components/netatmo/light.py | 6 - homeassistant/components/netatmo/strings.json | 9 +- .../components/netatmo/translations/en.json | 25 +-- tests/components/netatmo/conftest.py | 4 +- tests/components/netatmo/test_config_flow.py | 125 ++++++++++++-- tests/components/netatmo/test_init.py | 156 ++++++++++++------ 9 files changed, 313 insertions(+), 106 deletions(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index bb522291d19..e6ea2819eb4 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -1,9 +1,11 @@ """The Netatmo integration.""" from __future__ import annotations +from http import HTTPStatus import logging import secrets +import aiohttp import pyatmo import voluptuous as vol @@ -21,6 +23,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import CoreState, HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import ( aiohttp_client, config_entry_oauth2_flow, @@ -45,6 +48,7 @@ from .const import ( DATA_PERSONS, DATA_SCHEDULES, DOMAIN, + NETATMO_SCOPES, OAUTH2_AUTHORIZE, OAUTH2_TOKEN, PLATFORMS, @@ -112,6 +116,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_update_entry(entry, unique_id=DOMAIN) session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) + try: + await session.async_ensure_token_valid() + except aiohttp.ClientResponseError as ex: + _LOGGER.debug("API error: %s (%s)", ex.code, ex.message) + if ex.code in ( + HTTPStatus.BAD_REQUEST, + HTTPStatus.UNAUTHORIZED, + HTTPStatus.FORBIDDEN, + ): + raise ConfigEntryAuthFailed("Token not valid, trigger renewal") from ex + raise ConfigEntryNotReady from ex + + if sorted(session.token["scope"]) != sorted(NETATMO_SCOPES): + _LOGGER.debug( + "Scope is invalid: %s != %s", session.token["scope"], NETATMO_SCOPES + ) + raise ConfigEntryAuthFailed("Token scope not valid, trigger renewal") + hass.data[DOMAIN][entry.entry_id] = { AUTH: api.AsyncConfigEntryNetatmoAuth( aiohttp_client.async_get_clientsession(hass), session @@ -224,15 +246,17 @@ async def async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) -> async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" + data = hass.data[DOMAIN] + if CONF_WEBHOOK_ID in entry.data: webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID]) - await hass.data[DOMAIN][entry.entry_id][AUTH].async_dropwebhook() + await data[entry.entry_id][AUTH].async_dropwebhook() _LOGGER.info("Unregister Netatmo webhook") unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) + if unload_ok and entry.entry_id in data: + data.pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/netatmo/config_flow.py b/homeassistant/components/netatmo/config_flow.py index bb6a034b19f..ad8f75f5d45 100644 --- a/homeassistant/components/netatmo/config_flow.py +++ b/homeassistant/components/netatmo/config_flow.py @@ -23,8 +23,11 @@ from .const import ( CONF_UUID, CONF_WEATHER_AREAS, DOMAIN, + NETATMO_SCOPES, ) +_LOGGER = logging.getLogger(__name__) + class NetatmoFlowHandler( config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN @@ -49,31 +52,46 @@ class NetatmoFlowHandler( @property def extra_authorize_data(self) -> dict: """Extra data that needs to be appended to the authorize url.""" - scopes = [ - "access_camera", - "access_presence", - "read_camera", - "read_homecoach", - "read_presence", - "read_smokedetector", - "read_station", - "read_thermostat", - "write_camera", - "write_presence", - "write_thermostat", - ] - - return {"scope": " ".join(scopes)} + return {"scope": " ".join(NETATMO_SCOPES)} async def async_step_user(self, user_input: dict | None = None) -> FlowResult: """Handle a flow start.""" await self.async_set_unique_id(DOMAIN) - if self._async_current_entries(): + if ( + self.source != config_entries.SOURCE_REAUTH + and self._async_current_entries() + ): return self.async_abort(reason="single_instance_allowed") return await super().async_step_user(user_input) + async def async_step_reauth(self, user_input: dict | None = None) -> FlowResult: + """Perform reauth upon an API authentication error.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict | None = None + ) -> FlowResult: + """Dialog that informs the user that reauth is required.""" + if user_input is None: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema({}), + ) + + return await self.async_step_user() + + async def async_oauth_create_entry(self, data: dict) -> FlowResult: + """Create an oauth config entry or update existing entry for reauth.""" + existing_entry = await self.async_set_unique_id(DOMAIN) + if existing_entry: + self.hass.config_entries.async_update_entry(existing_entry, data=data) + await self.hass.config_entries.async_reload(existing_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return await super().async_oauth_create_entry(data) + class NetatmoOptionsFlowHandler(config_entries.OptionsFlow): """Handle Netatmo options.""" diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 07651d982a5..14e165b5cb4 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -13,6 +13,20 @@ DEFAULT_ATTRIBUTION = f"Data provided by {MANUFACTURER}" PLATFORMS = [CAMERA_DOMAIN, CLIMATE_DOMAIN, LIGHT_DOMAIN, SELECT_DOMAIN, SENSOR_DOMAIN] +NETATMO_SCOPES = [ + "access_camera", + "access_presence", + "read_camera", + "read_homecoach", + "read_presence", + "read_smokedetector", + "read_station", + "read_thermostat", + "write_camera", + "write_presence", + "write_thermostat", +] + MODEL_NAPLUG = "Relay" MODEL_NATHERM1 = "Smart Thermostat" MODEL_NRV = "Smart Radiator Valves" diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index 9a03a3fa848..9d83aa02977 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -33,12 +33,6 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Netatmo camera light platform.""" - if "access_camera" not in entry.data["token"]["scope"]: - _LOGGER.info( - "Cameras are currently not supported with this authentication method" - ) - return - data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] await data_handler.register_data_class( diff --git a/homeassistant/components/netatmo/strings.json b/homeassistant/components/netatmo/strings.json index c65001b2e8f..f58daadcf7f 100644 --- a/homeassistant/components/netatmo/strings.json +++ b/homeassistant/components/netatmo/strings.json @@ -3,13 +3,18 @@ "step": { "pick_implementation": { "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Netatmo integration needs to re-authenticate your account" } }, "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", - "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" @@ -62,4 +67,4 @@ "therm_mode": "{entity_name} switched to \"{subtype}\"" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/netatmo/translations/en.json b/homeassistant/components/netatmo/translations/en.json index 7e230374720..5d089120697 100644 --- a/homeassistant/components/netatmo/translations/en.json +++ b/homeassistant/components/netatmo/translations/en.json @@ -1,18 +1,23 @@ { "config": { + "step": { + "pick_implementation": { + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Netatmo integration needs to re-authenticate your account" + } + }, "abort": { - "authorize_url_timeout": "Timeout generating authorize URL.", - "missing_configuration": "The component is not configured. Please follow the documentation.", - "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", - "single_instance_allowed": "Already configured. Only a single configuration possible." + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", + "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "create_entry": { - "default": "Successfully authenticated" - }, - "step": { - "pick_implementation": { - "title": "Pick Authentication Method" - } + "default": "[%key:common::config_flow::create_entry::authenticated%]" } }, "device_automation": { diff --git a/tests/components/netatmo/conftest.py b/tests/components/netatmo/conftest.py index 4d6bbb752f3..808e477e053 100644 --- a/tests/components/netatmo/conftest.py +++ b/tests/components/netatmo/conftest.py @@ -22,7 +22,7 @@ def mock_config_entry_fixture(hass): "type": "Bearer", "expires_in": 60, "expires_at": time() + 1000, - "scope": " ".join(ALL_SCOPES), + "scope": ALL_SCOPES, }, }, options={ @@ -53,7 +53,7 @@ def mock_config_entry_fixture(hass): return mock_entry -@pytest.fixture +@pytest.fixture(name="netatmo_auth") def netatmo_auth(): """Restrict loaded platforms to list given.""" with patch( diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py index 8f18ae1410a..fbc1c62c0b1 100644 --- a/tests/components/netatmo/test_config_flow.py +++ b/tests/components/netatmo/test_config_flow.py @@ -13,6 +13,8 @@ from homeassistant.components.netatmo.const import ( from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow +from .common import ALL_SCOPES + from tests.common import MockConfigEntry CLIENT_ID = "1234" @@ -67,21 +69,7 @@ async def test_full_flow( }, ) - scope = "+".join( - [ - "access_camera", - "access_presence", - "read_camera", - "read_homecoach", - "read_presence", - "read_smokedetector", - "read_station", - "read_thermostat", - "write_camera", - "write_presence", - "write_thermostat", - ] - ) + scope = "+".join(sorted(ALL_SCOPES)) assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" @@ -227,3 +215,110 @@ async def test_option_flow_wrong_coordinates(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY for k, v in expected_result.items(): assert config_entry.options[CONF_WEATHER_AREAS]["Home"][k] == v + + +async def test_reauth( + hass, hass_client_no_auth, aioclient_mock, current_request_with_host +): + """Test initialization of the reauth flow.""" + assert await setup.async_setup_component( + hass, + "netatmo", + { + "netatmo": {CONF_CLIENT_ID: CLIENT_ID, CONF_CLIENT_SECRET: CLIENT_SECRET}, + "http": {"base_url": "https://example.com"}, + }, + ) + + result = await hass.config_entries.flow.async_init( + "netatmo", context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + scope = "+".join(sorted(ALL_SCOPES)) + + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope={scope}" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch( + "homeassistant.components.netatmo.async_setup_entry", return_value=True + ) as mock_setup: + await hass.config_entries.flow.async_configure(result["flow_id"]) + await hass.async_block_till_done() + + new_entry = hass.config_entries.async_entries(DOMAIN)[0] + + assert new_entry.state == config_entries.ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert len(mock_setup.mock_calls) == 1 + + # Should show form + result = await hass.config_entries.flow.async_init( + "netatmo", context={"source": config_entries.SOURCE_REAUTH} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + + # Confirm reauth flow + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + # Update entry + with patch( + "homeassistant.components.netatmo.async_setup_entry", return_value=True + ) as mock_setup: + result3 = await hass.config_entries.flow.async_configure(result2["flow_id"]) + await hass.async_block_till_done() + + new_entry2 = hass.config_entries.async_entries(DOMAIN)[0] + + assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["reason"] == "reauth_successful" + assert new_entry2.state == config_entries.ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/netatmo/test_init.py b/tests/components/netatmo/test_init.py index fba85d9d45c..2d0c43ac3f6 100644 --- a/tests/components/netatmo/test_init.py +++ b/tests/components/netatmo/test_init.py @@ -4,6 +4,7 @@ from datetime import timedelta from time import time from unittest.mock import AsyncMock, patch +import aiohttp import pyatmo from homeassistant import config_entries @@ -14,6 +15,7 @@ from homeassistant.setup import async_setup_component from homeassistant.util import dt from .common import ( + ALL_SCOPES, FAKE_WEBHOOK_ACTIVATION, fake_post_request, selected_platforms, @@ -49,24 +51,8 @@ FAKE_WEBHOOK = { } -async def test_setup_component(hass): +async def test_setup_component(hass, config_entry): """Test setup and teardown of the netatmo component.""" - config_entry = MockConfigEntry( - domain="netatmo", - data={ - "auth_implementation": "cloud", - "token": { - "refresh_token": "mock-refresh-token", - "access_token": "mock-access-token", - "type": "Bearer", - "expires_in": 60, - "expires_at": time() + 1000, - "scope": "read_station", - }, - }, - ) - config_entry.add_to_hass(hass) - with patch( "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth", ) as mock_auth, patch( @@ -248,7 +234,7 @@ async def test_setup_with_cloudhook(hass): "type": "Bearer", "expires_in": 60, "expires_at": time() + 1000, - "scope": "read_station", + "scope": ALL_SCOPES, }, }, ) @@ -298,24 +284,8 @@ async def test_setup_with_cloudhook(hass): assert not hass.config_entries.async_entries(DOMAIN) -async def test_setup_component_api_error(hass): +async def test_setup_component_api_error(hass, config_entry): """Test error on setup of the netatmo component.""" - config_entry = MockConfigEntry( - domain="netatmo", - data={ - "auth_implementation": "cloud", - "token": { - "refresh_token": "mock-refresh-token", - "access_token": "mock-access-token", - "type": "Bearer", - "expires_in": 60, - "expires_at": time() + 1000, - "scope": "read_station", - }, - }, - ) - config_entry.add_to_hass(hass) - with patch( "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth", ) as mock_auth, patch( @@ -337,24 +307,8 @@ async def test_setup_component_api_error(hass): mock_impl.assert_called_once() -async def test_setup_component_api_timeout(hass): +async def test_setup_component_api_timeout(hass, config_entry): """Test timeout on setup of the netatmo component.""" - config_entry = MockConfigEntry( - domain="netatmo", - data={ - "auth_implementation": "cloud", - "token": { - "refresh_token": "mock-refresh-token", - "access_token": "mock-access-token", - "type": "Bearer", - "expires_in": 60, - "expires_at": time() + 1000, - "scope": "read_station", - }, - }, - ) - config_entry.add_to_hass(hass) - with patch( "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth", ) as mock_auth, patch( @@ -429,3 +383,101 @@ async def test_setup_component_with_delay(hass, config_entry): await hass.async_stop() mock_dropwebhook.assert_called_once() + + +async def test_setup_component_invalid_token_scope(hass): + """Test handling of invalid token scope.""" + config_entry = MockConfigEntry( + domain="netatmo", + data={ + "auth_implementation": "cloud", + "token": { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + "expires_at": time() + 1000, + "scope": " ".join( + [ + "read_smokedetector", + "read_thermostat", + "write_thermostat", + ] + ), + }, + }, + options={}, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth", + ) as mock_auth, patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ) as mock_impl, patch( + "homeassistant.components.webhook.async_generate_url" + ) as mock_webhook: + mock_auth.return_value.async_post_request.side_effect = fake_post_request + mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() + mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() + assert await async_setup_component(hass, "netatmo", {}) + + await hass.async_block_till_done() + + mock_auth.assert_not_called() + mock_impl.assert_called_once() + mock_webhook.assert_not_called() + + assert config_entry.state is config_entries.ConfigEntryState.SETUP_ERROR + assert hass.config_entries.async_entries(DOMAIN) + assert len(hass.states.async_all()) > 0 + + for config_entry in hass.config_entries.async_entries("netatmo"): + await hass.config_entries.async_remove(config_entry.entry_id) + + +async def test_setup_component_invalid_token(hass, config_entry): + """Test handling of invalid token.""" + + async def fake_ensure_valid_token(*args, **kwargs): + print("fake_ensure_valid_token") + raise aiohttp.ClientResponseError( + request_info=aiohttp.client.RequestInfo( + url="http://example.com", + method="GET", + headers={}, + real_url="http://example.com", + ), + code=400, + history=(), + ) + + with patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth", + ) as mock_auth, patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ) as mock_impl, patch( + "homeassistant.components.webhook.async_generate_url" + ) as mock_webhook, patch( + "homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session" + ) as mock_session: + mock_auth.return_value.async_post_request.side_effect = fake_post_request + mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() + mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() + mock_session.return_value.async_ensure_token_valid.side_effect = ( + fake_ensure_valid_token + ) + assert await async_setup_component(hass, "netatmo", {}) + + await hass.async_block_till_done() + + mock_auth.assert_not_called() + mock_impl.assert_called_once() + mock_webhook.assert_not_called() + + assert config_entry.state is config_entries.ConfigEntryState.SETUP_ERROR + assert hass.config_entries.async_entries(DOMAIN) + assert len(hass.states.async_all()) > 0 + + for config_entry in hass.config_entries.async_entries("netatmo"): + await hass.config_entries.async_remove(config_entry.entry_id) From 11d8bcf0e210dedb99eaa59c3529bce96b6cac9c Mon Sep 17 00:00:00 2001 From: Tom Brien Date: Tue, 26 Oct 2021 15:39:46 +0100 Subject: [PATCH 0873/1038] Register Coinbase service in Device Registry and provide configuration URL (#58472) --- homeassistant/components/coinbase/sensor.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py index f37af04065e..b4ef4bb8e35 100644 --- a/homeassistant/components/coinbase/sensor.py +++ b/homeassistant/components/coinbase/sensor.py @@ -3,6 +3,7 @@ import logging from homeassistant.components.sensor import SensorEntity from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.helpers.entity import DeviceInfo from .const import ( API_ACCOUNT_AMOUNT, @@ -104,6 +105,13 @@ class AccountSensor(SensorEntity): API_ACCOUNT_CURRENCY ] break + self._attr_device_info = DeviceInfo( + configuration_url="https://www.coinbase.com/settings/api", + entry_type="service", + identifiers={(DOMAIN, self._coinbase_data.user_id)}, + manufacturer="Coinbase.com", + name=f"Coinbase {self._coinbase_data.user_id[-4:]}", + ) @property def name(self): @@ -169,6 +177,13 @@ class ExchangeRateSensor(SensorEntity): 1 / float(self._coinbase_data.exchange_rates[API_RATES][self.currency]), 2 ) self._unit_of_measurement = exchange_base + self._attr_device_info = DeviceInfo( + configuration_url="https://www.coinbase.com/settings/api", + entry_type="service", + identifiers={(DOMAIN, self._coinbase_data.user_id)}, + manufacturer="Coinbase.com", + name=f"Coinbase {self._coinbase_data.user_id[-4:]}", + ) @property def name(self): From f1b082a369b2339ef056ad2f998337cbaef14dc0 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 26 Oct 2021 08:14:12 -0700 Subject: [PATCH 0874/1038] Publish nest event ids in camera related events (#58299) --- homeassistant/components/nest/__init__.py | 5 +++-- homeassistant/components/nest/events.py | 16 +++++++++++++--- tests/components/nest/test_events.py | 8 +++++++- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 87682c7043e..fce475e1788 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -122,13 +122,14 @@ class SignalUpdateCallback: device_entry = device_registry.async_get_device({(DOMAIN, device_id)}) if not device_entry: return - for event in events: - if not (event_type := EVENT_NAME_MAP.get(event)): + for api_event_type, image_event in events.items(): + if not (event_type := EVENT_NAME_MAP.get(api_event_type)): continue message = { "device_id": device_entry.id, "type": event_type, "timestamp": event_message.timestamp, + "nest_event_id": image_event.event_id, } self._hass.bus.async_fire(NEST_EVENT, message) diff --git a/homeassistant/components/nest/events.py b/homeassistant/components/nest/events.py index abfe688c71c..6802a98cc40 100644 --- a/homeassistant/components/nest/events.py +++ b/homeassistant/components/nest/events.py @@ -17,12 +17,22 @@ NEST_EVENT = "nest_event" # The nest_event namespace will fire events that are triggered from messages # received via the Pub/Sub subscriber. # -# An example event data payload: +# An example event payload: +# # { -# "device_id": "enterprises/some/device/identifier" -# "event_type": "camera_motion" +# "event_type": "nest_event" +# "data": { +# "device_id": "my-device-id", +# "type": "camera_motion", +# "timestamp": "2021-10-24T19:42:43.304000+00:00", +# "nest_event_id": "KcO1HIR9sPKQ2bqby_vTcCcEov..." +# }, +# ... # } # +# The nest_event_id corresponds to the event id in the SDM API used to retrieve +# snapshots. +# # The following event types are fired: EVENT_DOORBELL_CHIME = "doorbell_chime" EVENT_CAMERA_MOTION = "camera_motion" diff --git a/tests/components/nest/test_events.py b/tests/components/nest/test_events.py index df459a35f71..6e9dd7dd40d 100644 --- a/tests/components/nest/test_events.py +++ b/tests/components/nest/test_events.py @@ -1,4 +1,4 @@ -"""Test for Nest binary sensor platform for the Smart Device Management API. +"""Test for Nest events for the Smart Device Management API. These tests fake out the subscriber/devicemanager, and are not using a real pubsub subscriber. @@ -117,6 +117,7 @@ async def test_doorbell_chime_event(hass): "device_id": entry.device_id, "type": "doorbell_chime", "timestamp": event_time, + "nest_event_id": EVENT_ID, } @@ -144,6 +145,7 @@ async def test_camera_motion_event(hass): "device_id": entry.device_id, "type": "camera_motion", "timestamp": event_time, + "nest_event_id": EVENT_ID, } @@ -171,6 +173,7 @@ async def test_camera_sound_event(hass): "device_id": entry.device_id, "type": "camera_sound", "timestamp": event_time, + "nest_event_id": EVENT_ID, } @@ -198,6 +201,7 @@ async def test_camera_person_event(hass): "device_id": entry.device_id, "type": "camera_person", "timestamp": event_time, + "nest_event_id": EVENT_ID, } @@ -234,11 +238,13 @@ async def test_camera_multiple_event(hass): "device_id": entry.device_id, "type": "camera_motion", "timestamp": event_time, + "nest_event_id": EVENT_ID, } assert events[1].data == { "device_id": entry.device_id, "type": "camera_person", "timestamp": event_time, + "nest_event_id": EVENT_ID, } From 5deeeed672d552c9908125e23d03351b7663aead Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 26 Oct 2021 08:25:15 -0700 Subject: [PATCH 0875/1038] Add some more required/optional tags to condition schemas (#58424) --- homeassistant/helpers/config_validation.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 8f0c93f5c9f..e6c2792304b 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1029,13 +1029,13 @@ TIME_CONDITION_SCHEMA = vol.All( { **CONDITION_BASE_SCHEMA, vol.Required(CONF_CONDITION): "time", - "before": vol.Any( + vol.Optional("before"): vol.Any( time, vol.All(str, entity_domain(["input_datetime", "sensor"])) ), - "after": vol.Any( + vol.Optional("after"): vol.Any( time, vol.All(str, entity_domain(["input_datetime", "sensor"])) ), - "weekday": weekdays, + vol.Optional("weekday"): weekdays, } ), has_at_least_one_key("before", "after", "weekday"), @@ -1054,7 +1054,7 @@ ZONE_CONDITION_SCHEMA = vol.Schema( **CONDITION_BASE_SCHEMA, vol.Required(CONF_CONDITION): "zone", vol.Required(CONF_ENTITY_ID): entity_ids, - "zone": entity_ids, + vol.Required("zone"): entity_ids, # To support use_trigger_value in automation # Deprecated 2016/04/25 vol.Optional("event"): vol.Any("enter", "leave"), From 6b1b8c9880172a440430eab3446118066c6f5013 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 26 Oct 2021 17:53:13 +0200 Subject: [PATCH 0876/1038] Fjaraskupan number entity for periodic venting (#58179) --- .coveragerc | 1 + .../components/fjaraskupan/__init__.py | 2 +- homeassistant/components/fjaraskupan/fan.py | 20 +++++- .../components/fjaraskupan/number.py | 70 +++++++++++++++++++ 4 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/fjaraskupan/number.py diff --git a/.coveragerc b/.coveragerc index 74da8acf8b8..fe268171b33 100644 --- a/.coveragerc +++ b/.coveragerc @@ -331,6 +331,7 @@ omit = homeassistant/components/fjaraskupan/const.py homeassistant/components/fjaraskupan/fan.py homeassistant/components/fjaraskupan/light.py + homeassistant/components/fjaraskupan/number.py homeassistant/components/fjaraskupan/sensor.py homeassistant/components/fleetgo/device_tracker.py homeassistant/components/flexit/climate.py diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index 56a67a14a02..f5cedad243d 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -23,7 +23,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DISPATCH_DETECTION, DOMAIN -PLATFORMS = ["binary_sensor", "fan", "light", "sensor"] +PLATFORMS = ["binary_sensor", "fan", "light", "sensor", "number"] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/fjaraskupan/fan.py b/homeassistant/components/fjaraskupan/fan.py index 4a81e70b848..7cb7c7cd18e 100644 --- a/homeassistant/components/fjaraskupan/fan.py +++ b/homeassistant/components/fjaraskupan/fan.py @@ -35,10 +35,12 @@ 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 = { @@ -48,6 +50,10 @@ PRESET_TO_COMMAND = { } +class UnsupportedPreset(Exception): + """The preset is unsupported.""" + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -112,7 +118,10 @@ class Fan(CoordinatorEntity[State], FanEntity): async with self._device: if preset_mode != self._preset_mode: - await self._device.send_command(PRESET_TO_COMMAND[preset_mode]) + if command := PRESET_TO_COMMAND.get(preset_mode): + await self._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)) @@ -125,8 +134,11 @@ class Fan(CoordinatorEntity[State], FanEntity): async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - await self._device.send_command(PRESET_TO_COMMAND[preset_mode]) - self.coordinator.async_set_updated_data(self._device.state) + if command := PRESET_TO_COMMAND.get(preset_mode): + await self._device.send_command(command) + self.coordinator.async_set_updated_data(self._device.state) + else: + raise UnsupportedPreset(f"The preset {preset_mode} is unsupported") async def async_turn_off(self, **kwargs) -> None: """Turn the entity off.""" @@ -181,6 +193,8 @@ class Fan(CoordinatorEntity[State], 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 diff --git a/homeassistant/components/fjaraskupan/number.py b/homeassistant/components/fjaraskupan/number.py new file mode 100644 index 00000000000..d5862bf2e7f --- /dev/null +++ b/homeassistant/components/fjaraskupan/number.py @@ -0,0 +1,70 @@ +"""Support for sensors.""" +from __future__ import annotations + +from fjaraskupan import Device, State + +from homeassistant.components.number import NumberEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENTITY_CATEGORY_CONFIG, TIME_MINUTES +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from . import DeviceState, async_setup_entry_platform + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up sensors dynamically through discovery.""" + + def _constructor(device_state: DeviceState) -> list[Entity]: + return [ + PeriodicVentingTime( + device_state.coordinator, device_state.device, device_state.device_info + ), + ] + + async_setup_entry_platform(hass, config_entry, async_add_entities, _constructor) + + +class PeriodicVentingTime(CoordinatorEntity[State], NumberEntity): + """Periodic Venting.""" + + _attr_max_value: float = 59 + _attr_min_value: float = 0 + _attr_step: float = 1 + _attr_entity_registry_enabled_default = True + _attr_entity_category = ENTITY_CATEGORY_CONFIG + _attr_unit_of_measurement = TIME_MINUTES + + def __init__( + self, + coordinator: DataUpdateCoordinator[State], + device: Device, + device_info: DeviceInfo, + ) -> None: + """Init sensor.""" + super().__init__(coordinator) + self._device = device + self._attr_unique_id = f"{device.address}-periodic-venting" + self._attr_device_info = device_info + self._attr_name = f"{device_info['name']} Periodic Venting" + + @property + def value(self) -> float | None: + """Return the entity value to represent the entity state.""" + if data := self.coordinator.data: + return data.periodic_venting + return None + + async def async_set_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) From 777589cdccf359e99abe40aeb710adc64e3bee47 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 26 Oct 2021 18:43:33 +0200 Subject: [PATCH 0877/1038] Add vlc telnet error handler decorator (#58468) * Add vlc telnet error handler decorator * Delint * Update stale docstring --- .../components/vlc_telnet/media_player.py | 98 ++++++++++++------- 1 file changed, 62 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index ad88cfc2627..624234ce712 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -2,7 +2,8 @@ from __future__ import annotations from datetime import datetime -from typing import Any +from functools import wraps +from typing import Any, Callable, TypeVar, cast from aiovlc.client import Client from aiovlc.exceptions import AuthError, CommandError, ConnectError @@ -65,6 +66,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) +Func = TypeVar("Func", bound=Callable[..., Any]) + async def async_setup_platform( hass: HomeAssistant, @@ -96,6 +99,25 @@ async def async_setup_entry( async_add_entities([VlcDevice(entry, vlc, name, available)], True) +def catch_vlc_errors(func: Func) -> Func: + """Catch VLC errors.""" + + @wraps(func) + async def wrapper(self, *args: Any, **kwargs: Any) -> Any: + """Catch VLC errors and modify availability.""" + try: + await func(self, *args, **kwargs) + except CommandError as err: + LOGGER.error("Command error: %s", err) + except ConnectError as err: + # pylint: disable=protected-access + if self._available: + LOGGER.error("Connection error: %s", err) + self._available = False + + return cast(Func, wrapper) + + class VlcDevice(MediaPlayerEntity): """Representation of a vlc player.""" @@ -125,6 +147,7 @@ class VlcDevice(MediaPlayerEntity): "entry_type": "service", } + @catch_vlc_errors async def async_update(self) -> None: """Get the latest details from the device.""" if not self._available: @@ -147,47 +170,39 @@ class VlcDevice(MediaPlayerEntity): self._available = True LOGGER.info("Connected to vlc host: %s", self._vlc.host) - try: - status = await self._vlc.status() - LOGGER.debug("Status: %s", status) + status = await self._vlc.status() + LOGGER.debug("Status: %s", status) - self._volume = status.audio_volume / MAX_VOLUME - state = status.state - if state == "playing": - self._state = STATE_PLAYING - elif state == "paused": - self._state = STATE_PAUSED - else: - self._state = STATE_IDLE + self._volume = status.audio_volume / MAX_VOLUME + state = status.state + if state == "playing": + self._state = STATE_PLAYING + elif state == "paused": + self._state = STATE_PAUSED + else: + self._state = STATE_IDLE - if self._state != STATE_IDLE: - self._media_duration = (await self._vlc.get_length()).length - time_output = await self._vlc.get_time() - vlc_position = time_output.time + if self._state != STATE_IDLE: + self._media_duration = (await self._vlc.get_length()).length + time_output = await self._vlc.get_time() + vlc_position = time_output.time - # Check if current position is stale. - if vlc_position != self._media_position: - self._media_position_updated_at = dt_util.utcnow() - self._media_position = vlc_position + # Check if current position is stale. + if vlc_position != self._media_position: + self._media_position_updated_at = dt_util.utcnow() + self._media_position = vlc_position - info = await self._vlc.info() - data = info.data - LOGGER.debug("Info data: %s", data) + info = await self._vlc.info() + data = info.data + LOGGER.debug("Info data: %s", data) - self._media_artist = data.get(0, {}).get("artist") - self._media_title = data.get(0, {}).get("title") + self._media_artist = data.get(0, {}).get("artist") + self._media_title = data.get(0, {}).get("title") - if not self._media_title: - # Fall back to filename. - if data_info := data.get("data"): - self._media_title = data_info["filename"] - - except CommandError as err: - LOGGER.error("Command error: %s", err) - except ConnectError as err: - if self._available: - LOGGER.error("Connection error: %s", err) - self._available = False + if not self._media_title: + # Fall back to filename. + if data_info := data.get("data"): + self._media_title = data_info["filename"] @property def name(self): @@ -249,10 +264,12 @@ class VlcDevice(MediaPlayerEntity): """Artist of current playing media, music track only.""" return self._media_artist + @catch_vlc_errors async def async_media_seek(self, position: float) -> None: """Seek the media to a specific location.""" await self._vlc.seek(round(position)) + @catch_vlc_errors async def async_mute_volume(self, mute: bool) -> None: """Mute the volume.""" assert self._volume is not None @@ -264,6 +281,7 @@ class VlcDevice(MediaPlayerEntity): self._muted = mute + @catch_vlc_errors async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" await self._vlc.set_volume(round(volume * MAX_VOLUME)) @@ -273,11 +291,13 @@ class VlcDevice(MediaPlayerEntity): # This can happen if we were muted and then see a volume_up. self._muted = False + @catch_vlc_errors async def async_media_play(self) -> None: """Send play command.""" await self._vlc.play() self._state = STATE_PLAYING + @catch_vlc_errors async def async_media_pause(self) -> None: """Send pause command.""" status = await self._vlc.status() @@ -288,11 +308,13 @@ class VlcDevice(MediaPlayerEntity): self._state = STATE_PAUSED + @catch_vlc_errors async def async_media_stop(self) -> None: """Send stop command.""" await self._vlc.stop() self._state = STATE_IDLE + @catch_vlc_errors async def async_play_media( self, media_type: str, media_id: str, **kwargs: Any ) -> None: @@ -308,18 +330,22 @@ class VlcDevice(MediaPlayerEntity): await self._vlc.add(media_id) self._state = STATE_PLAYING + @catch_vlc_errors async def async_media_previous_track(self) -> None: """Send previous track command.""" await self._vlc.prev() + @catch_vlc_errors async def async_media_next_track(self) -> None: """Send next track command.""" await self._vlc.next() + @catch_vlc_errors async def async_clear_playlist(self) -> None: """Clear players playlist.""" await self._vlc.clear() + @catch_vlc_errors async def async_set_shuffle(self, shuffle: bool) -> None: """Enable/disable shuffle mode.""" shuffle_command = "on" if shuffle else "off" From 8b021ea06befb48026f001a9a7437908c60a37e9 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 26 Oct 2021 18:47:10 +0200 Subject: [PATCH 0878/1038] Fix mysensors metric/non-metric gateway (#58476) --- homeassistant/components/mysensors/gateway.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index 1f9b96e6825..e7f97792493 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -232,6 +232,8 @@ async def _get_gateway( protocol_version=version, ) gateway.event_callback = event_callback + gateway.metric = hass.config.units.is_metric + if persistence: await gateway.start_persistence() From f15840e7ff1ed94c488ce9c9dddb93e409536ff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 26 Oct 2021 19:58:17 +0200 Subject: [PATCH 0879/1038] Limit add-on stats to add-ons that are running (#58479) --- homeassistant/components/hassio/__init__.py | 9 +++++--- .../components/hassio/binary_sensor.py | 10 +++++++-- homeassistant/components/hassio/const.py | 1 + homeassistant/components/hassio/entity.py | 19 +++++++++++++++- tests/components/hassio/test_init.py | 5 +++++ tests/components/hassio/test_sensor.py | 22 ++++--------------- 6 files changed, 42 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 0f8ff5441f3..6ad1c67f1f3 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -46,6 +46,8 @@ from .const import ( ATTR_PASSWORD, ATTR_REPOSITORY, ATTR_SLUG, + ATTR_STARTED, + ATTR_STATE, ATTR_URL, ATTR_VERSION, DATA_KEY_ADDONS, @@ -539,12 +541,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: hassio.get_os_info(), ) - addon_slugs = [ - addon[ATTR_SLUG] + addons = [ + addon for addon in hass.data[DATA_SUPERVISOR_INFO].get("addons", []) + if addon[ATTR_STATE] == ATTR_STARTED ] stats_data = await asyncio.gather( - *[update_addon_stats(slug) for slug in addon_slugs] + *[update_addon_stats(addon[ATTR_SLUG]) for addon in addons] ) hass.data[DATA_ADDONS_STATS] = dict(stats_data) diff --git a/homeassistant/components/hassio/binary_sensor.py b/homeassistant/components/hassio/binary_sensor.py index 8953bd47942..5078e11c26e 100644 --- a/homeassistant/components/hassio/binary_sensor.py +++ b/homeassistant/components/hassio/binary_sensor.py @@ -14,7 +14,13 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ADDONS_COORDINATOR -from .const import ATTR_STATE, ATTR_UPDATE_AVAILABLE, DATA_KEY_ADDONS, DATA_KEY_OS +from .const import ( + ATTR_STARTED, + ATTR_STATE, + ATTR_UPDATE_AVAILABLE, + DATA_KEY_ADDONS, + DATA_KEY_OS, +) from .entity import HassioAddonEntity, HassioOSEntity @@ -37,7 +43,7 @@ ENTITY_DESCRIPTIONS = ( entity_registry_enabled_default=False, key=ATTR_STATE, name="Running", - target="started", + target=ATTR_STARTED, ), ) diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index 540d00b4906..7cdc87708ae 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -46,6 +46,7 @@ ATTR_CPU_PERCENT = "cpu_percent" ATTR_MEMORY_PERCENT = "memory_percent" ATTR_SLUG = "slug" ATTR_STATE = "state" +ATTR_STARTED = "started" ATTR_URL = "url" ATTR_REPOSITORY = "repository" diff --git a/homeassistant/components/hassio/entity.py b/homeassistant/components/hassio/entity.py index 1fd2926fe56..5dd41166c32 100644 --- a/homeassistant/components/hassio/entity.py +++ b/homeassistant/components/hassio/entity.py @@ -8,7 +8,7 @@ from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import DOMAIN, HassioDataUpdateCoordinator -from .const import ATTR_SLUG +from .const import ATTR_SLUG, DATA_KEY_ADDONS, DATA_KEY_OS class HassioAddonEntity(CoordinatorEntity): @@ -28,6 +28,15 @@ class HassioAddonEntity(CoordinatorEntity): self._attr_unique_id = f"{addon[ATTR_SLUG]}_{entity_description.key}" self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, addon[ATTR_SLUG])}) + @property + def available(self) -> bool: + """Return True if entity is available.""" + return ( + super().available + and self.entity_description.key + in self.coordinator.data[DATA_KEY_ADDONS][self._addon_slug] + ) + class HassioOSEntity(CoordinatorEntity): """Base Entity for Hass.io OS.""" @@ -43,3 +52,11 @@ class HassioOSEntity(CoordinatorEntity): self._attr_name = f"Home Assistant Operating System: {entity_description.name}" self._attr_unique_id = f"home_assistant_os_{entity_description.key}" self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, "OS")}) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return ( + super().available + and self.entity_description.key in self.coordinator.data[DATA_KEY_OS] + ) diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 62d2cb67d0d..f5214b563b3 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -493,6 +493,7 @@ async def test_device_registry_calls(hass): "addons": [ { "name": "test", + "state": "started", "slug": "test", "installed": True, "update_available": False, @@ -503,6 +504,7 @@ async def test_device_registry_calls(hass): }, { "name": "test2", + "state": "started", "slug": "test2", "installed": True, "update_available": False, @@ -537,6 +539,7 @@ async def test_device_registry_calls(hass): "addons": [ { "name": "test2", + "state": "started", "slug": "test2", "installed": True, "update_available": False, @@ -568,6 +571,7 @@ async def test_device_registry_calls(hass): { "name": "test2", "slug": "test2", + "state": "started", "installed": True, "update_available": False, "version": "1.0.0", @@ -577,6 +581,7 @@ async def test_device_registry_calls(hass): { "name": "test3", "slug": "test3", + "state": "stopped", "installed": True, "update_available": False, "version": "1.0.0", diff --git a/tests/components/hassio/test_sensor.py b/tests/components/hassio/test_sensor.py index 9039d302640..00d2c32c520 100644 --- a/tests/components/hassio/test_sensor.py +++ b/tests/components/hassio/test_sensor.py @@ -66,6 +66,7 @@ def mock_all(aioclient_mock, request): "addons": [ { "name": "test", + "state": "started", "slug": "test", "installed": True, "update_available": False, @@ -76,6 +77,7 @@ def mock_all(aioclient_mock, request): }, { "name": "test2", + "state": "stopped", "slug": "test2", "installed": True, "update_available": False, @@ -104,22 +106,6 @@ def mock_all(aioclient_mock, request): }, }, ) - aioclient_mock.get( - "http://127.0.0.1/addons/test2/stats", - json={ - "result": "ok", - "data": { - "cpu_percent": 0.8, - "memory_usage": 51941376, - "memory_limit": 3977146368, - "memory_percent": 1.31, - "network_rx": 31338284, - "network_tx": 15692900, - "blk_read": 740077568, - "blk_write": 6004736, - }, - }, - ) aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) @@ -149,9 +135,9 @@ async def test_sensors(hass, aioclient_mock): "sensor.test2_version": "3.1.0", "sensor.test2_newest_version": "3.2.0", "sensor.test_cpu_percent": "0.99", - "sensor.test2_cpu_percent": "0.8", + "sensor.test2_cpu_percent": "unavailable", "sensor.test_memory_percent": "4.59", - "sensor.test2_memory_percent": "1.31", + "sensor.test2_memory_percent": "unavailable", } """Check that entities are disabled by default.""" From c5cf69dd9b15cda1594eecd283b18ecfbdaa5b19 Mon Sep 17 00:00:00 2001 From: FlavorFx Date: Tue, 26 Oct 2021 20:01:42 +0200 Subject: [PATCH 0880/1038] Support Energy Sensor and Statistics in Homematic IP Cloud Integration (#57734) * Update sensor.py * Update test_device.py --- .../components/homematicip_cloud/sensor.py | 44 ++++++++++++++++++- .../homematicip_cloud/test_device.py | 2 +- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index ae2bb9f0c6d..ae866bb42e2 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -26,13 +26,19 @@ from homematicip.aio.device import ( ) from homematicip.base.enums import ValveState -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + SensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + ENERGY_KILO_WATT_HOUR, LENGTH_MILLIMETERS, LIGHT_LUX, PERCENTAGE, @@ -111,6 +117,7 @@ async def async_setup_entry( ), ): entities.append(HomematicipPowerSensor(hap, device)) + entities.append(HomematicipEnergySensor(hap, device)) if isinstance( device, (AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro) ): @@ -127,6 +134,8 @@ async def async_setup_entry( class HomematicipAccesspointDutyCycle(HomematicipGenericEntity, SensorEntity): """Representation of then HomeMaticIP access point.""" + _attr_state_class = STATE_CLASS_MEASUREMENT + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize access point status entity.""" super().__init__(hap, device, post="Duty Cycle") @@ -179,6 +188,8 @@ class HomematicipHeatingThermostat(HomematicipGenericEntity, SensorEntity): class HomematicipHumiditySensor(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP humidity sensor.""" + _attr_state_class = STATE_CLASS_MEASUREMENT + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the thermometer device.""" super().__init__(hap, device, post="Humidity") @@ -202,6 +213,8 @@ class HomematicipHumiditySensor(HomematicipGenericEntity, SensorEntity): class HomematicipTemperatureSensor(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP thermometer.""" + _attr_state_class = STATE_CLASS_MEASUREMENT + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the thermometer device.""" super().__init__(hap, device, post="Temperature") @@ -239,6 +252,8 @@ class HomematicipTemperatureSensor(HomematicipGenericEntity, SensorEntity): class HomematicipIlluminanceSensor(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP Illuminance sensor.""" + _attr_state_class = STATE_CLASS_MEASUREMENT + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the device.""" super().__init__(hap, device, post="Illuminance") @@ -277,6 +292,8 @@ class HomematicipIlluminanceSensor(HomematicipGenericEntity, SensorEntity): class HomematicipPowerSensor(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP power measuring sensor.""" + _attr_state_class = STATE_CLASS_MEASUREMENT + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the device.""" super().__init__(hap, device, post="Power") @@ -297,6 +314,31 @@ class HomematicipPowerSensor(HomematicipGenericEntity, SensorEntity): return POWER_WATT +class HomematicipEnergySensor(HomematicipGenericEntity, SensorEntity): + """Representation of the HomematicIP energy measuring sensor.""" + + _attr_state_class = STATE_CLASS_TOTAL_INCREASING + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the device.""" + super().__init__(hap, device, post="Energy") + + @property + def device_class(self) -> str: + """Return the device class of the sensor.""" + return DEVICE_CLASS_ENERGY + + @property + def native_value(self) -> float: + """Return the energy counter value.""" + return self._device.energyCounter + + @property + def native_unit_of_measurement(self) -> str: + """Return the unit this state is expressed in.""" + return ENERGY_KILO_WATT_HOUR + + class HomematicipWindspeedSensor(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP wind speed sensor.""" diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 6f1d3071714..8e3d80ca839 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -22,7 +22,7 @@ async def test_hmip_load_all_supported_devices(hass, default_mock_hap_factory): test_devices=None, test_groups=None ) - assert len(mock_hap.hmip_device_by_entity_id) == 253 + assert len(mock_hap.hmip_device_by_entity_id) == 258 async def test_hmip_remove_device(hass, default_mock_hap_factory): From 2ea537e1a61afba929a9756a7072b6d6bf677f27 Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Wed, 27 Oct 2021 05:19:18 +1100 Subject: [PATCH 0881/1038] dlna_dmr will gracefully handle device's rejection of subscription attempt (#58451) --- .../components/dlna_dmr/media_player.py | 7 +++- .../components/dlna_dmr/test_media_player.py | 33 ++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index e91ea1e830e..2835117e57c 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -7,8 +7,9 @@ from datetime import datetime, timedelta import functools from typing import Any, Callable, TypeVar, cast -from async_upnp_client import UpnpError, UpnpService, UpnpStateVariable +from async_upnp_client import UpnpService, UpnpStateVariable from async_upnp_client.const import NotificationSubType +from async_upnp_client.exceptions import UpnpError, UpnpResponseError from async_upnp_client.profiles.dlna import DmrDevice, TransportState from async_upnp_client.utils import async_get_local_ip import voluptuous as vol @@ -347,6 +348,10 @@ class DlnaDmrEntity(MediaPlayerEntity): try: self._device.on_event = self._on_event await self._device.async_subscribe_services(auto_resubscribe=True) + except UpnpResponseError as err: + # Device rejected subscription request. This is OK, variables + # will be polled instead. + _LOGGER.debug("Device rejected subscription: %r", err) except UpnpError as err: # Don't leave the device half-constructed self._device.on_event = None diff --git a/tests/components/dlna_dmr/test_media_player.py b/tests/components/dlna_dmr/test_media_player.py index 2d02c8f1a8f..e12c23535fa 100644 --- a/tests/components/dlna_dmr/test_media_player.py +++ b/tests/components/dlna_dmr/test_media_player.py @@ -8,7 +8,11 @@ from types import MappingProxyType from typing import Any from unittest.mock import ANY, DEFAULT, Mock, patch -from async_upnp_client.exceptions import UpnpConnectionError, UpnpError +from async_upnp_client.exceptions import ( + UpnpConnectionError, + UpnpError, + UpnpResponseError, +) from async_upnp_client.profiles.dlna import TransportState import pytest @@ -357,6 +361,33 @@ async def test_event_subscribe_failure( } +async def test_event_subscribe_rejected( + hass: HomeAssistant, + config_entry_mock: MockConfigEntry, + dmr_device_mock: Mock, +) -> None: + """Test _device_connect continues when the device rejects a subscription. + + Device state will instead be obtained via polling in async_update. + """ + dmr_device_mock.async_subscribe_services.side_effect = UpnpResponseError(501) + + mock_entity_id = await setup_mock_component(hass, config_entry_mock) + mock_state = hass.states.get(mock_entity_id) + assert mock_state is not None + + # Device should be connected + assert mock_state.state == ha_const.STATE_IDLE + + # Device should not be unsubscribed + dmr_device_mock.async_unsubscribe_services.assert_not_awaited() + + # Unload config entry to clean up + assert await hass.config_entries.async_remove(config_entry_mock.entry_id) == { + "require_restart": False + } + + async def test_available_device( hass: HomeAssistant, dmr_device_mock: Mock, From dd1154ad0827a21e6c3b6a7c16766b1528affc92 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 26 Oct 2021 14:22:18 -0400 Subject: [PATCH 0882/1038] Bump ZHA quirks version to 0.0.63 (#58478) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zha/test_climate.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index acb5651de1f..a4cae4686bc 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.28.0", "pyserial==3.5", "pyserial-asyncio==0.5", - "zha-quirks==0.0.62", + "zha-quirks==0.0.63", "zigpy-deconz==0.13.0", "zigpy==0.39.0", "zigpy-xbee==0.14.0", diff --git a/requirements_all.txt b/requirements_all.txt index 125337ccf19..16084510e96 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2468,7 +2468,7 @@ zengge==0.2 zeroconf==0.36.9 # homeassistant.components.zha -zha-quirks==0.0.62 +zha-quirks==0.0.63 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 13ddf117d4f..e2628d00576 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1433,7 +1433,7 @@ youless-api==0.15 zeroconf==0.36.9 # homeassistant.components.zha -zha-quirks==0.0.62 +zha-quirks==0.0.63 # homeassistant.components.zha zigpy-deconz==0.13.0 diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py index 1784813250f..09da644bd29 100644 --- a/tests/components/zha/test_climate.py +++ b/tests/components/zha/test_climate.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest import zhaquirks.sinope.thermostat -import zhaquirks.tuya.valve +import zhaquirks.tuya.ts0601_trv import zigpy.profiles import zigpy.zcl.clusters from zigpy.zcl.clusters.hvac import Thermostat @@ -227,7 +227,7 @@ async def device_climate_moes(device_climate_mock): """MOES thermostat.""" return await device_climate_mock( - CLIMATE_MOES, manuf=MANUF_MOES, quirk=zhaquirks.tuya.valve.MoesHY368_Type1 + CLIMATE_MOES, manuf=MANUF_MOES, quirk=zhaquirks.tuya.ts0601_trv.MoesHY368_Type1 ) From 47f6313e5bd53dda97f1d1c99daa8f6dfbc93833 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 26 Oct 2021 20:23:20 +0200 Subject: [PATCH 0883/1038] Add entity category to UniFi sensors and switches (#58484) --- homeassistant/components/unifi/sensor.py | 4 +++- homeassistant/components/unifi/switch.py | 7 +++++++ tests/components/unifi/test_sensor.py | 14 ++++++++++++++ tests/components/unifi/test_switch.py | 9 +++++++++ 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 5cbf61d9635..a9e89876459 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -7,7 +7,7 @@ Support for uptime sensors of network clients. from datetime import datetime, timedelta from homeassistant.components.sensor import DEVICE_CLASS_TIMESTAMP, DOMAIN, SensorEntity -from homeassistant.const import DATA_MEGABYTES +from homeassistant.const import DATA_MEGABYTES, ENTITY_CATEGORY_DIAGNOSTIC from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.dt as dt_util @@ -86,6 +86,7 @@ class UniFiBandwidthSensor(UniFiClient, SensorEntity): DOMAIN = DOMAIN + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC _attr_native_unit_of_measurement = DATA_MEGABYTES @property @@ -132,6 +133,7 @@ class UniFiUpTimeSensor(UniFiClient, SensorEntity): TYPE = UPTIME_SENSOR _attr_device_class = DEVICE_CLASS_TIMESTAMP + _attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC def __init__(self, client, controller): """Set up tracked client.""" diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index eebd5014cb5..03cd9056830 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -15,6 +15,7 @@ from aiounifi.events import ( ) from homeassistant.components.switch import DOMAIN, SwitchEntity +from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo @@ -183,6 +184,8 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchEntity, RestoreEntity): DOMAIN = DOMAIN TYPE = POE_SWITCH + _attr_entity_category = ENTITY_CATEGORY_CONFIG + def __init__(self, client, controller): """Set up POE switch.""" super().__init__(client, controller) @@ -270,6 +273,8 @@ class UniFiBlockClientSwitch(UniFiClient, SwitchEntity): DOMAIN = DOMAIN TYPE = BLOCK_SWITCH + _attr_entity_category = ENTITY_CATEGORY_CONFIG + def __init__(self, client, controller): """Set up block switch.""" super().__init__(client, controller) @@ -319,6 +324,8 @@ class UniFiDPIRestrictionSwitch(UniFiBase, SwitchEntity): DOMAIN = DOMAIN TYPE = DPI_SWITCH + _attr_entity_category = ENTITY_CATEGORY_CONFIG + @property def key(self) -> Any: """Return item key.""" diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index fbf697e295f..2df279090f3 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -15,6 +15,8 @@ from homeassistant.components.unifi.const import ( CONF_TRACK_DEVICES, DOMAIN as UNIFI_DOMAIN, ) +from homeassistant.const import ENTITY_CATEGORY_DIAGNOSTIC +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send import homeassistant.util.dt as dt_util @@ -74,6 +76,12 @@ async def test_bandwidth_sensors(hass, aioclient_mock, mock_unifi_websocket): assert hass.states.get("sensor.wireless_client_rx").state == "2345.0" assert hass.states.get("sensor.wireless_client_tx").state == "6789.0" + ent_reg = er.async_get(hass) + assert ( + ent_reg.async_get("sensor.wired_client_rx").entity_category + == ENTITY_CATEGORY_DIAGNOSTIC + ) + # Verify state update wireless_client["rx_bytes"] = 3456000000 @@ -179,6 +187,12 @@ async def test_uptime_sensors( assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 1 assert hass.states.get("sensor.client1_uptime").state == "2021-01-01T01:00:00+00:00" + ent_reg = er.async_get(hass) + assert ( + ent_reg.async_get("sensor.client1_uptime").entity_category + == ENTITY_CATEGORY_DIAGNOSTIC + ) + # Verify normal new event doesn't change uptime # 4 seconds has passed diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index ad277f18a8d..b870465e2bc 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -16,6 +16,7 @@ from homeassistant.components.unifi.const import ( DOMAIN as UNIFI_DOMAIN, ) from homeassistant.components.unifi.switch import POE_SWITCH +from homeassistant.const import ENTITY_CATEGORY_CONFIG from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -373,6 +374,14 @@ async def test_switches(hass, aioclient_mock): assert dpi_switch.state == "on" assert dpi_switch.attributes["icon"] == "mdi:network" + ent_reg = er.async_get(hass) + for entry_id in ( + "switch.poe_client_1", + "switch.block_client_1", + "switch.block_media_streaming", + ): + assert ent_reg.async_get(entry_id).entity_category == ENTITY_CATEGORY_CONFIG + # Block and unblock client aioclient_mock.post( From b60934b10db745762294c9f455482fc4f88657f5 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 26 Oct 2021 20:27:26 +0200 Subject: [PATCH 0884/1038] Enable type checking - bmw_connected_drive (#58310) --- .../bmw_connected_drive/__init__.py | 5 ++-- .../components/bmw_connected_drive/sensor.py | 28 ++++++++++++++++--- mypy.ini | 3 -- script/hassfest/mypy_config.py | 1 - 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 722809dff5c..0dad3e1fce1 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -1,6 +1,7 @@ """Reads vehicle status from BMW connected drive portal.""" from __future__ import annotations +from collections.abc import Callable import logging from bimmer_connected.account import ConnectedDriveAccount @@ -282,7 +283,7 @@ class BMWConnectedDriveAccount: self.read_only = read_only self.account = ConnectedDriveAccount(username, password, region) self.name = name - self._update_listeners = [] + self._update_listeners: list[Callable[[], None]] = [] # Set observer position once for older cars to be in range for # GPS position (pre-7/2014, <2km) and get new data from API @@ -311,7 +312,7 @@ class BMWConnectedDriveAccount: ) _LOGGER.exception(exception) - def add_update_listener(self, listener): + def add_update_listener(self, listener: Callable[[], None]) -> None: """Add a listener for update notifications.""" self._update_listeners.append(listener) diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index daf7569bc77..d2b434f96cd 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -388,12 +388,18 @@ async def async_setup_entry( if service == SERVICE_LAST_TRIP: entities.extend( [ + # mypy issues will be fixed in next release + # https://github.com/python/mypy/issues/9096 BMWConnectedDriveSensor( - account, vehicle, description, unit_system, service + account, + vehicle, + description, # type: ignore[arg-type] + unit_system, + service, ) for attribute_name in vehicle.state.last_trip.available_attributes if attribute_name != "date" - and (description := SENSOR_TYPES.get(attribute_name)) + and (description := SENSOR_TYPES.get(attribute_name)) # type: ignore[no-redef] ] ) if "date" in vehicle.state.last_trip.available_attributes: @@ -534,7 +540,14 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): vehicle_last_trip = self._vehicle.state.last_trip if sensor_key == "date_utc": date_str = getattr(vehicle_last_trip, "date") - self._attr_native_value = dt_util.parse_datetime(date_str).isoformat() + if parsed_date := dt_util.parse_datetime(date_str): + self._attr_native_value = parsed_date.isoformat() + else: + _LOGGER.debug( + "Could not parse date string for 'date_utc' sensor: %s", + date_str, + ) + self._attr_native_value = None else: self._attr_native_value = getattr(vehicle_last_trip, sensor_key) elif self._service == SERVICE_ALL_TRIPS: @@ -553,7 +566,14 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): return if sensor_key == "reset_date_utc": date_str = getattr(vehicle_all_trips, "reset_date") - self._attr_native_value = dt_util.parse_datetime(date_str).isoformat() + if parsed_date := dt_util.parse_datetime(date_str): + self._attr_native_value = parsed_date.isoformat() + else: + _LOGGER.debug( + "Could not parse date string for 'reset_date_utc' sensor: %s", + date_str, + ) + self._attr_native_value = None else: self._attr_native_value = getattr(vehicle_all_trips, sensor_key) diff --git a/mypy.ini b/mypy.ini index 058d587ec22..315ee094b40 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1546,9 +1546,6 @@ ignore_errors = true [mypy-homeassistant.components.blueprint.*] ignore_errors = true -[mypy-homeassistant.components.bmw_connected_drive.*] -ignore_errors = true - [mypy-homeassistant.components.climacell.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 6ffd2e0da42..7d289984335 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -16,7 +16,6 @@ from .model import Config, Integration IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.awair.*", "homeassistant.components.blueprint.*", - "homeassistant.components.bmw_connected_drive.*", "homeassistant.components.climacell.*", "homeassistant.components.cloud.*", "homeassistant.components.config.*", From 4db743d01f30fef66d655d7e898fb3c201cfad3d Mon Sep 17 00:00:00 2001 From: Yuval Aboulafia Date: Tue, 26 Oct 2021 20:32:22 +0200 Subject: [PATCH 0885/1038] Remove Huawei Router (ADR-0004) (#57136) --- .coveragerc | 1 - CODEOWNERS | 1 - .../components/huawei_router/__init__.py | 1 - .../huawei_router/device_tracker.py | 156 ------------------ .../components/huawei_router/manifest.json | 7 - 5 files changed, 166 deletions(-) delete mode 100644 homeassistant/components/huawei_router/__init__.py delete mode 100644 homeassistant/components/huawei_router/device_tracker.py delete mode 100644 homeassistant/components/huawei_router/manifest.json diff --git a/.coveragerc b/.coveragerc index fe268171b33..eb8b643d1b6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -457,7 +457,6 @@ omit = homeassistant/components/hp_ilo/sensor.py homeassistant/components/htu21d/sensor.py homeassistant/components/huawei_lte/* - homeassistant/components/huawei_router/device_tracker.py homeassistant/components/hue/light.py homeassistant/components/hunterdouglas_powerview/__init__.py homeassistant/components/hunterdouglas_powerview/scene.py diff --git a/CODEOWNERS b/CODEOWNERS index 12ab4d6b472..a1eb8373f21 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -227,7 +227,6 @@ homeassistant/components/homematic/* @pvizeli @danielperna84 homeassistant/components/honeywell/* @rdfurman homeassistant/components/http/* @home-assistant/core homeassistant/components/huawei_lte/* @scop @fphammerle -homeassistant/components/huawei_router/* @abmantis homeassistant/components/hue/* @balloob @frenck homeassistant/components/huisbaasje/* @dennisschroer homeassistant/components/humidifier/* @home-assistant/core @Shulyaka diff --git a/homeassistant/components/huawei_router/__init__.py b/homeassistant/components/huawei_router/__init__.py deleted file mode 100644 index 861809992c6..00000000000 --- a/homeassistant/components/huawei_router/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The huawei_router component.""" diff --git a/homeassistant/components/huawei_router/device_tracker.py b/homeassistant/components/huawei_router/device_tracker.py deleted file mode 100644 index d4882f0a499..00000000000 --- a/homeassistant/components/huawei_router/device_tracker.py +++ /dev/null @@ -1,156 +0,0 @@ -"""Support for HUAWEI routers.""" -import base64 -from collections import namedtuple -import logging -import re - -import requests -import voluptuous as vol - -from homeassistant.components.device_tracker import ( - DOMAIN, - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, - DeviceScanner, -) -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - } -) - - -def get_scanner(hass, config): - """Validate the configuration and return a HUAWEI scanner.""" - scanner = HuaweiDeviceScanner(config[DOMAIN]) - - return scanner - - -Device = namedtuple("Device", ["name", "ip", "mac", "state"]) - - -class HuaweiDeviceScanner(DeviceScanner): - """This class queries a router running HUAWEI firmware.""" - - ARRAY_REGEX = re.compile(r"var UserDevinfo = new Array\((.*)null\);") - DEVICE_REGEX = re.compile(r"new USERDevice\((.*?)\),") - DEVICE_ATTR_REGEX = re.compile( - '"(?P.*?)","(?P.*?)",' - '"(?P.*?)","(?P.*?)",' - '"(?P.*?)","(?P.*?)",' - '"(?P.*?)","(?P.*?)",' - '"(?P